Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions src/SwaggerProvider.Runtime/RuntimeHelpers.fs
Original file line number Diff line number Diff line change
Expand Up @@ -327,11 +327,15 @@ module RuntimeHelpers =
fun ty ->
ty.GetProperties(Reflection.BindingFlags.Public ||| Reflection.BindingFlags.Instance)
|> Array.map(fun prop ->
let name =
match prop.GetCustomAttributes(typeof<JsonPropertyNameAttribute>, false) with
| [| x |] -> (x :?> JsonPropertyNameAttribute).Name
| _ -> prop.Name

// Use the single-attribute overload to avoid allocating an obj[] on each property.
// JsonPropertyNameAttribute has AllowMultiple=false (AttributeUsage), so at most one
// instance can ever be present; Attribute.GetCustomAttribute cannot throw
// AmbiguousMatchException for this attribute type.
// Pass inherit=false to match the original GetCustomAttributes(..., false) behaviour.
let attr =
Attribute.GetCustomAttribute(prop, typeof<JsonPropertyNameAttribute>, false) :?> JsonPropertyNameAttribute

let name = if isNull attr then prop.Name else attr.Name
(name, prop))
)

Expand Down
44 changes: 44 additions & 0 deletions tests/SwaggerProvider.Tests/RuntimeHelpersTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -599,6 +599,50 @@ module CreateHttpRequestTests =
use req = createHttpRequest "GET" "/pets/42" []
req.RequestUri.ToString() |> shouldEqual "pets/42"

[<Fact>]
let ``createHttpRequest creates PATCH request``() =
use req = createHttpRequest "PATCH" "v1/pets/1" []
req.Method.Method |> shouldEqual "PATCH"

[<Fact>]
let ``createHttpRequest creates HEAD request``() =
use req = createHttpRequest "HEAD" "v1/ping" []
req.Method |> shouldEqual HttpMethod.Head

[<Fact>]
let ``createHttpRequest creates OPTIONS request``() =
use req = createHttpRequest "OPTIONS" "v1/resource" []
req.Method |> shouldEqual HttpMethod.Options

[<Fact>]
let ``createHttpRequest creates TRACE request``() =
use req = createHttpRequest "TRACE" "v1/resource" []
req.Method |> shouldEqual HttpMethod.Trace

[<Fact>]
let ``createHttpRequest creates custom method uppercased``() =
// Non-standard methods are normalised to upper-case.
use req = createHttpRequest "purge" "v1/cache" []
req.Method.Method |> shouldEqual "PURGE"

[<Fact>]
let ``createHttpRequest resolves 'PATCH' and 'patch' to the same cached HttpMethod instance``() =
// The standardHttpMethods dictionary uses OrdinalIgnoreCase, so both "PATCH" and "patch"
// should return the same cached HttpMethod object reference - not just equal string values.
use req1 = createHttpRequest "PATCH" "v1/pets/1" []
use req2 = createHttpRequest "patch" "v1/pets/1" []
obj.ReferenceEquals(req1.Method, req2.Method) |> shouldEqual true

[<Fact>]
let ``createHttpRequest with multiple query params encodes all``() =
use req =
createHttpRequest "GET" "v1/search" [ ("q", "cat"); ("page", "1"); ("size", "20") ]

let uri = req.RequestUri.ToString()
uri |> shouldContainText "q=cat"
uri |> shouldContainText "page=1"
uri |> shouldContainText "size=20"


module FillHeadersTests =

Expand Down
109 changes: 109 additions & 0 deletions tests/SwaggerProvider.Tests/Schema.V3SchemaCompilationTests.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
module SwaggerProvider.Tests.Schema_V3SchemaCompilationTests

/// Tests for OpenAPI 3.0-specific schema compilation behaviour in the v3
/// DefinitionCompiler pipeline.

open Xunit
open FsUnitTyped

// ── allOf/oneOf/anyOf single-$ref wrapper collapse ────────────────────────────

// Tests for the DefinitionCompiler's explicit allOf/oneOf/anyOf single-$ref collapse
// branches, which release the name reservation for the wrapper schema and return
// the already-compiled referenced type directly via ReleaseNameReservation.
// This avoids emitting an empty wrapper type when a schema has no explicit properties
// and carries a single $ref inside allOf/oneOf/anyOf.

/// Minimal OpenAPI 3.0 document with a Pet schema and a PetRef schema that wraps
/// it via allOf with a single $ref. The compiler should collapse PetRef into Pet
/// rather than creating a new empty object type.
let private allOfSingleRefSchema =
"""{
"openapi": "3.0.0",
"info": { "title": "Test", "version": "1.0.0" },
"paths": {},
"components": {
"schemas": {
"Pet": {
"type": "object",
"properties": {
"id": { "type": "integer" },
"name": { "type": "string" }
}
},
"PetRef": {
"allOf": [ { "$ref": "#/components/schemas/Pet" } ]
}
}
}
}"""

/// Same schema but using oneOf instead of allOf.
let private oneOfSingleRefSchema =
"""{
"openapi": "3.0.0",
"info": { "title": "Test", "version": "1.0.0" },
"paths": {},
"components": {
"schemas": {
"Dog": {
"type": "object",
"properties": { "breed": { "type": "string" } }
},
"DogRef": {
"oneOf": [ { "$ref": "#/components/schemas/Dog" } ]
}
}
}
}"""

/// Same schema but using anyOf instead of allOf.
let private anyOfSingleRefSchema =
"""{
"openapi": "3.0.0",
"info": { "title": "Test", "version": "1.0.0" },
"paths": {},
"components": {
"schemas": {
"Cat": {
"type": "object",
"properties": { "color": { "type": "string" } }
},
"CatRef": {
"anyOf": [ { "$ref": "#/components/schemas/Cat" } ]
}
}
}
}"""

[<Fact>]
let ``allOf single $ref resolves to the referenced type without creating a new object type``() =
let types = compileV3Schema allOfSingleRefSchema false
// PetRef collapses into Pet via ReleaseNameReservation; the referenced type is present.
types |> List.exists(fun t -> t.Name = "Pet") |> shouldEqual true

[<Fact>]
let ``allOf single $ref does not produce a separate wrapper type``() =
let types = compileV3Schema allOfSingleRefSchema false
// PetRef's name reservation is released; no separate empty type named "PetRef" is emitted.
types |> List.exists(fun t -> t.Name = "PetRef") |> shouldEqual false

[<Fact>]
let ``oneOf single $ref resolves to the referenced type``() =
let types = compileV3Schema oneOfSingleRefSchema false
types |> List.exists(fun t -> t.Name = "Dog") |> shouldEqual true

[<Fact>]
let ``oneOf single $ref does not produce a separate wrapper type``() =
let types = compileV3Schema oneOfSingleRefSchema false
types |> List.exists(fun t -> t.Name = "DogRef") |> shouldEqual false

[<Fact>]
let ``anyOf single $ref resolves to the referenced type``() =
let types = compileV3Schema anyOfSingleRefSchema false
types |> List.exists(fun t -> t.Name = "Cat") |> shouldEqual true

[<Fact>]
let ``anyOf single $ref does not produce a separate wrapper type``() =
let types = compileV3Schema anyOfSingleRefSchema false
types |> List.exists(fun t -> t.Name = "CatRef") |> shouldEqual false
1 change: 1 addition & 0 deletions tests/SwaggerProvider.Tests/SwaggerProvider.Tests.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
<Compile Include="Schema.TypeMappingTests.fs" />
<Compile Include="Schema.ArrayAndMapTypeMappingTests.fs" />
<Compile Include="Schema.V2SchemaCompilationTests.fs" />
<Compile Include="Schema.V3SchemaCompilationTests.fs" />
<Compile Include="Schema.DefinitionPathTests.fs" />
Comment thread
sergey-tihon marked this conversation as resolved.
<Compile Include="Schema.OperationCompilationTests.fs" />
<Compile Include="Schema.XmlDocTests.fs" />
Expand Down
Loading