diff --git a/src/SwaggerProvider.Runtime/RuntimeHelpers.fs b/src/SwaggerProvider.Runtime/RuntimeHelpers.fs index 459367c5..0e84deeb 100644 --- a/src/SwaggerProvider.Runtime/RuntimeHelpers.fs +++ b/src/SwaggerProvider.Runtime/RuntimeHelpers.fs @@ -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, 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, false) :?> JsonPropertyNameAttribute + + let name = if isNull attr then prop.Name else attr.Name (name, prop)) ) diff --git a/tests/SwaggerProvider.Tests/RuntimeHelpersTests.fs b/tests/SwaggerProvider.Tests/RuntimeHelpersTests.fs index c86ba985..325f3c48 100644 --- a/tests/SwaggerProvider.Tests/RuntimeHelpersTests.fs +++ b/tests/SwaggerProvider.Tests/RuntimeHelpersTests.fs @@ -599,6 +599,50 @@ module CreateHttpRequestTests = use req = createHttpRequest "GET" "/pets/42" [] req.RequestUri.ToString() |> shouldEqual "pets/42" + [] + let ``createHttpRequest creates PATCH request``() = + use req = createHttpRequest "PATCH" "v1/pets/1" [] + req.Method.Method |> shouldEqual "PATCH" + + [] + let ``createHttpRequest creates HEAD request``() = + use req = createHttpRequest "HEAD" "v1/ping" [] + req.Method |> shouldEqual HttpMethod.Head + + [] + let ``createHttpRequest creates OPTIONS request``() = + use req = createHttpRequest "OPTIONS" "v1/resource" [] + req.Method |> shouldEqual HttpMethod.Options + + [] + let ``createHttpRequest creates TRACE request``() = + use req = createHttpRequest "TRACE" "v1/resource" [] + req.Method |> shouldEqual HttpMethod.Trace + + [] + 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" + + [] + 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 + + [] + 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 = diff --git a/tests/SwaggerProvider.Tests/Schema.V3SchemaCompilationTests.fs b/tests/SwaggerProvider.Tests/Schema.V3SchemaCompilationTests.fs new file mode 100644 index 00000000..89e23356 --- /dev/null +++ b/tests/SwaggerProvider.Tests/Schema.V3SchemaCompilationTests.fs @@ -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" } ] + } + } + } +}""" + +[] +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 + +[] +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 + +[] +let ``oneOf single $ref resolves to the referenced type``() = + let types = compileV3Schema oneOfSingleRefSchema false + types |> List.exists(fun t -> t.Name = "Dog") |> shouldEqual true + +[] +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 + +[] +let ``anyOf single $ref resolves to the referenced type``() = + let types = compileV3Schema anyOfSingleRefSchema false + types |> List.exists(fun t -> t.Name = "Cat") |> shouldEqual true + +[] +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 diff --git a/tests/SwaggerProvider.Tests/SwaggerProvider.Tests.fsproj b/tests/SwaggerProvider.Tests/SwaggerProvider.Tests.fsproj index 40e96abd..e039d519 100644 --- a/tests/SwaggerProvider.Tests/SwaggerProvider.Tests.fsproj +++ b/tests/SwaggerProvider.Tests/SwaggerProvider.Tests.fsproj @@ -16,6 +16,7 @@ +