From e9541e4d9388b087c8c468a2975b7b6755b957a0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 19 May 2026 20:14:11 +0000 Subject: [PATCH 1/5] =?UTF-8?q?perf/test:=20avoid=20obj[]=20in=20getProper?= =?UTF-8?q?tyNamesAndInfos;=20add=20HTTP=20method=20and=20schema=20compila?= =?UTF-8?q?tion=20tests=20(+13=20tests,=20425=E2=86=92438)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task 8 (perf): replace GetCustomAttributes(type, bool) array-pattern in getPropertyNamesAndInfos with Attribute.GetCustomAttribute(prop, type), avoiding the obj[] allocation on each property during cache-fill. Task 9 (test): add 8 tests for HTTP method handling in CreateHttpRequestTests covering PATCH, HEAD, OPTIONS, TRACE, custom method normalisation, and multiple query parameter encoding. Task 10 (forward): add 6 tests for tryResolveSingle (allOf/oneOf/anyOf single-$ref resolution) in Schema.V2SchemaCompilationTests, verifying that wrapper schemas with a single reference collapse into the referenced type and do not produce spurious empty object types. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/SwaggerProvider.Runtime/RuntimeHelpers.fs | 9 +- .../RuntimeHelpersTests.fs | 41 ++++++ .../Schema.V2SchemaCompilationTests.fs | 131 ++++++++++++++++++ 3 files changed, 177 insertions(+), 4 deletions(-) diff --git a/src/SwaggerProvider.Runtime/RuntimeHelpers.fs b/src/SwaggerProvider.Runtime/RuntimeHelpers.fs index 459367c5..6cdbdecf 100644 --- a/src/SwaggerProvider.Runtime/RuntimeHelpers.fs +++ b/src/SwaggerProvider.Runtime/RuntimeHelpers.fs @@ -327,11 +327,12 @@ 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 so at most one is present. + let attr = + Attribute.GetCustomAttribute(prop, typeof) :?> 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..5f5319e9 100644 --- a/tests/SwaggerProvider.Tests/RuntimeHelpersTests.fs +++ b/tests/SwaggerProvider.Tests/RuntimeHelpersTests.fs @@ -599,6 +599,47 @@ 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 is case-insensitive for PATCH``() = + use req = createHttpRequest "patch" "v1/pets/1" [] + req.Method.Method |> shouldEqual "PATCH" + + [] + 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.V2SchemaCompilationTests.fs b/tests/SwaggerProvider.Tests/Schema.V2SchemaCompilationTests.fs index cb285465..3e776dd8 100644 --- a/tests/SwaggerProvider.Tests/Schema.V2SchemaCompilationTests.fs +++ b/tests/SwaggerProvider.Tests/Schema.V2SchemaCompilationTests.fs @@ -240,3 +240,134 @@ let ``v2 compiled object type ToString invokeCode does not throw for concrete pr let body = invokeCode [ thisExpr ] // Expr is a value type; just verifying invokeCode did not throw is sufficient body.Type |> shouldEqual typeof + +// ── tryResolveSingle: allOf/oneOf/anyOf single-schema resolution ───────────── + +/// Tests for the DefinitionCompiler's tryResolveSingle logic, which resolves +/// a schema that has no explicit Type but carries a single allOf/oneOf/anyOf entry +/// to the underlying type of that entry. This matters for nullable wrappers +/// common in both Swagger 2 (converted allOf) and OpenAPI 3. + +let private compileV3Schema(jsonSchema: string) : ProviderImplementation.ProvidedTypes.ProvidedTypeDefinition list = + let settings = Microsoft.OpenApi.Reader.OpenApiReaderSettings() + settings.AddYamlReader() + + let readResult = + Microsoft.OpenApi.OpenApiDocument.Parse(jsonSchema, settings = settings) + + match readResult.Diagnostic with + | null -> () + | diagnostic when diagnostic.Errors |> Seq.isEmpty |> not -> + let errorText = + diagnostic.Errors + |> Seq.map string + |> String.concat Environment.NewLine + + failwithf "Failed to parse v3 schema:%s%s" Environment.NewLine errorText + | _ -> () + + let schema = + match readResult.Document with + | null -> failwith "Failed to parse v3 schema: Document is null." + | doc -> doc + + let defCompiler = DefinitionCompiler(schema, false, false) + let opCompiler = OperationCompiler(schema, defCompiler, true, false, false) + opCompiler.CompileProvidedClients(defCompiler.Namespace) + defCompiler.Namespace.GetProvidedTypes() + +/// 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 resolve PetRef to 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 + // PetRef should be resolved to Pet; the compiler calls ReleaseNameReservation + // for PetRef and returns the already-compiled Pet type. + // Result: only one compiled type (Pet) in the namespace, not two. + 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 + // PetRef collapses into Pet; there must be no extra empty type named "PetRef". + types |> List.exists(fun t -> t.Name = "PetRef") |> shouldEqual false + +[] +let ``oneOf single $ref resolves to the referenced type``() = + let types = compileV3Schema oneOfSingleRefSchema + 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 + types |> List.exists(fun t -> t.Name = "DogRef") |> shouldEqual false + +[] +let ``anyOf single $ref resolves to the referenced type``() = + let types = compileV3Schema anyOfSingleRefSchema + 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 + types |> List.exists(fun t -> t.Name = "CatRef") |> shouldEqual false From 004d20480edb3bf68952e94ce65d6476b3172980 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 19 May 2026 20:14:16 +0000 Subject: [PATCH 2/5] ci: trigger checks From 7fb59db6ff7446fe0468476ec5133238ff7f5041 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 20 May 2026 06:26:35 +0000 Subject: [PATCH 3/5] Address reviewer comments: inherit=false, remove local compileV3Schema, fix misleading comment Agent-Logs-Url: https://github.com/fsprojects/SwaggerProvider/sessions/07dbe28c-84b0-4c45-a1c2-da0960998b6a Co-authored-by: sergey-tihon <1197905+sergey-tihon@users.noreply.github.com> --- src/SwaggerProvider.Runtime/RuntimeHelpers.fs | 3 +- .../Schema.V2SchemaCompilationTests.fs | 43 +++---------------- 2 files changed, 9 insertions(+), 37 deletions(-) diff --git a/src/SwaggerProvider.Runtime/RuntimeHelpers.fs b/src/SwaggerProvider.Runtime/RuntimeHelpers.fs index 6cdbdecf..41d751e3 100644 --- a/src/SwaggerProvider.Runtime/RuntimeHelpers.fs +++ b/src/SwaggerProvider.Runtime/RuntimeHelpers.fs @@ -329,8 +329,9 @@ module RuntimeHelpers = |> Array.map(fun prop -> // Use the single-attribute overload to avoid allocating an obj[] on each property. // JsonPropertyNameAttribute has AllowMultiple=false so at most one is present. + // Pass inherit=false to match the original GetCustomAttributes(..., false) behaviour. let attr = - Attribute.GetCustomAttribute(prop, typeof) :?> JsonPropertyNameAttribute + 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/Schema.V2SchemaCompilationTests.fs b/tests/SwaggerProvider.Tests/Schema.V2SchemaCompilationTests.fs index 3e776dd8..e6bff76b 100644 --- a/tests/SwaggerProvider.Tests/Schema.V2SchemaCompilationTests.fs +++ b/tests/SwaggerProvider.Tests/Schema.V2SchemaCompilationTests.fs @@ -247,35 +247,6 @@ let ``v2 compiled object type ToString invokeCode does not throw for concrete pr /// a schema that has no explicit Type but carries a single allOf/oneOf/anyOf entry /// to the underlying type of that entry. This matters for nullable wrappers /// common in both Swagger 2 (converted allOf) and OpenAPI 3. - -let private compileV3Schema(jsonSchema: string) : ProviderImplementation.ProvidedTypes.ProvidedTypeDefinition list = - let settings = Microsoft.OpenApi.Reader.OpenApiReaderSettings() - settings.AddYamlReader() - - let readResult = - Microsoft.OpenApi.OpenApiDocument.Parse(jsonSchema, settings = settings) - - match readResult.Diagnostic with - | null -> () - | diagnostic when diagnostic.Errors |> Seq.isEmpty |> not -> - let errorText = - diagnostic.Errors - |> Seq.map string - |> String.concat Environment.NewLine - - failwithf "Failed to parse v3 schema:%s%s" Environment.NewLine errorText - | _ -> () - - let schema = - match readResult.Document with - | null -> failwith "Failed to parse v3 schema: Document is null." - | doc -> doc - - let defCompiler = DefinitionCompiler(schema, false, false) - let opCompiler = OperationCompiler(schema, defCompiler, true, false, false) - opCompiler.CompileProvidedClients(defCompiler.Namespace) - defCompiler.Namespace.GetProvidedTypes() - /// 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 resolve PetRef to Pet /// rather than creating a new empty object type. @@ -340,34 +311,34 @@ let private anyOfSingleRefSchema = [] let ``allOf single $ref resolves to the referenced type without creating a new object type``() = - let types = compileV3Schema allOfSingleRefSchema + let types = compileV3Schema allOfSingleRefSchema false // PetRef should be resolved to Pet; the compiler calls ReleaseNameReservation // for PetRef and returns the already-compiled Pet type. - // Result: only one compiled type (Pet) in the namespace, not two. + // Pet is present; PetRef is not added as a separate type. 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 + let types = compileV3Schema allOfSingleRefSchema false // PetRef collapses into Pet; there must be no extra empty type named "PetRef". types |> List.exists(fun t -> t.Name = "PetRef") |> shouldEqual false [] let ``oneOf single $ref resolves to the referenced type``() = - let types = compileV3Schema oneOfSingleRefSchema + 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 + 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 + 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 + let types = compileV3Schema anyOfSingleRefSchema false types |> List.exists(fun t -> t.Name = "CatRef") |> shouldEqual false From 6acbaa235bc6aa9b7f4e4660fcfa6c7e1cd36bf1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 20 May 2026 07:36:32 +0000 Subject: [PATCH 4/5] Address reviewer comments: ReferenceEquals cache test, move v3 tests to Schema.V3SchemaCompilationTests.fs, fix section comments Agent-Logs-Url: https://github.com/fsprojects/SwaggerProvider/sessions/4f4edf09-c80c-4daf-8348-e079cf17f3e4 Co-authored-by: sergey-tihon <1197905+sergey-tihon@users.noreply.github.com> --- .../RuntimeHelpersTests.fs | 9 +- .../Schema.V2SchemaCompilationTests.fs | 102 ---------------- .../Schema.V3SchemaCompilationTests.fs | 109 ++++++++++++++++++ .../SwaggerProvider.Tests.fsproj | 1 + 4 files changed, 116 insertions(+), 105 deletions(-) create mode 100644 tests/SwaggerProvider.Tests/Schema.V3SchemaCompilationTests.fs diff --git a/tests/SwaggerProvider.Tests/RuntimeHelpersTests.fs b/tests/SwaggerProvider.Tests/RuntimeHelpersTests.fs index 5f5319e9..325f3c48 100644 --- a/tests/SwaggerProvider.Tests/RuntimeHelpersTests.fs +++ b/tests/SwaggerProvider.Tests/RuntimeHelpersTests.fs @@ -626,9 +626,12 @@ module CreateHttpRequestTests = req.Method.Method |> shouldEqual "PURGE" [] - let ``createHttpRequest is case-insensitive for PATCH``() = - use req = createHttpRequest "patch" "v1/pets/1" [] - req.Method.Method |> shouldEqual "PATCH" + 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``() = diff --git a/tests/SwaggerProvider.Tests/Schema.V2SchemaCompilationTests.fs b/tests/SwaggerProvider.Tests/Schema.V2SchemaCompilationTests.fs index e6bff76b..cb285465 100644 --- a/tests/SwaggerProvider.Tests/Schema.V2SchemaCompilationTests.fs +++ b/tests/SwaggerProvider.Tests/Schema.V2SchemaCompilationTests.fs @@ -240,105 +240,3 @@ let ``v2 compiled object type ToString invokeCode does not throw for concrete pr let body = invokeCode [ thisExpr ] // Expr is a value type; just verifying invokeCode did not throw is sufficient body.Type |> shouldEqual typeof - -// ── tryResolveSingle: allOf/oneOf/anyOf single-schema resolution ───────────── - -/// Tests for the DefinitionCompiler's tryResolveSingle logic, which resolves -/// a schema that has no explicit Type but carries a single allOf/oneOf/anyOf entry -/// to the underlying type of that entry. This matters for nullable wrappers -/// common in both Swagger 2 (converted allOf) and OpenAPI 3. -/// 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 resolve PetRef to 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 should be resolved to Pet; the compiler calls ReleaseNameReservation - // for PetRef and returns the already-compiled Pet type. - // Pet is present; PetRef is not added as a separate type. - 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 collapses into Pet; there must be no extra empty type named "PetRef". - 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/Schema.V3SchemaCompilationTests.fs b/tests/SwaggerProvider.Tests/Schema.V3SchemaCompilationTests.fs new file mode 100644 index 00000000..fab5dc8d --- /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 (DefinitionCompiler.fs lines 470-502), which release the name reservation +// for the wrapper schema and return the already-compiled referenced type directly. +// 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 @@ + From f69c6a770df254336d6643dd5c9b040db1904586 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 20 May 2026 13:39:44 +0000 Subject: [PATCH 5/5] Address reviewer comments: clarify AmbiguousMatchException safety, remove stale line numbers from test comments Agent-Logs-Url: https://github.com/fsprojects/SwaggerProvider/sessions/fd30f0f3-2e82-4986-acf3-53eab5c7b9fc Co-authored-by: sergey-tihon <1197905+sergey-tihon@users.noreply.github.com> --- src/SwaggerProvider.Runtime/RuntimeHelpers.fs | 4 +++- .../SwaggerProvider.Tests/Schema.V3SchemaCompilationTests.fs | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/SwaggerProvider.Runtime/RuntimeHelpers.fs b/src/SwaggerProvider.Runtime/RuntimeHelpers.fs index 41d751e3..0e84deeb 100644 --- a/src/SwaggerProvider.Runtime/RuntimeHelpers.fs +++ b/src/SwaggerProvider.Runtime/RuntimeHelpers.fs @@ -328,7 +328,9 @@ module RuntimeHelpers = ty.GetProperties(Reflection.BindingFlags.Public ||| Reflection.BindingFlags.Instance) |> Array.map(fun prop -> // Use the single-attribute overload to avoid allocating an obj[] on each property. - // JsonPropertyNameAttribute has AllowMultiple=false so at most one is present. + // 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 diff --git a/tests/SwaggerProvider.Tests/Schema.V3SchemaCompilationTests.fs b/tests/SwaggerProvider.Tests/Schema.V3SchemaCompilationTests.fs index fab5dc8d..89e23356 100644 --- a/tests/SwaggerProvider.Tests/Schema.V3SchemaCompilationTests.fs +++ b/tests/SwaggerProvider.Tests/Schema.V3SchemaCompilationTests.fs @@ -9,8 +9,8 @@ open FsUnitTyped // ── allOf/oneOf/anyOf single-$ref wrapper collapse ──────────────────────────── // Tests for the DefinitionCompiler's explicit allOf/oneOf/anyOf single-$ref collapse -// branches (DefinitionCompiler.fs lines 470-502), which release the name reservation -// for the wrapper schema and return the already-compiled referenced type directly. +// 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.