From e9c0acbd2767e4a57d361f7eb6cfbaa83d15a81a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 10 Apr 2026 22:17:42 +0000 Subject: [PATCH 1/4] test: add DefinitionPath.Parse unit tests and provideNullable coverage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add 21 new unit tests: - v3/Schema.DefinitionPathTests.fs (16 tests): unit-tests for DefinitionPath.Parse covering simple names, one- and multi-level namespaces, PascalCase candidate generation, names with hyphens, and error cases (wrong prefix). DefinitionPath.Parse had no tests at all. - v3/Schema.TypeMappingTests.fs (+5 tests): PreferNullable mode tests verifying that optional value types produce Nullable instead of Option when provideNullable=true, and that required types and reference types are unaffected. - v3/Schema.TestHelpers.fs: add compilePropertyTypeWith helper that exposes the provideNullable parameter to enable the above tests. Test count: 295 → 316 (all pass). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../SwaggerProvider.Tests.fsproj | 1 + .../v3/Schema.DefinitionPathTests.fs | 109 ++++++++++++++++++ .../v3/Schema.TestHelpers.fs | 48 ++++++++ .../v3/Schema.TypeMappingTests.fs | 38 ++++++ 4 files changed, 196 insertions(+) create mode 100644 tests/SwaggerProvider.Tests/v3/Schema.DefinitionPathTests.fs diff --git a/tests/SwaggerProvider.Tests/SwaggerProvider.Tests.fsproj b/tests/SwaggerProvider.Tests/SwaggerProvider.Tests.fsproj index d3e74335..f1e24e3d 100644 --- a/tests/SwaggerProvider.Tests/SwaggerProvider.Tests.fsproj +++ b/tests/SwaggerProvider.Tests/SwaggerProvider.Tests.fsproj @@ -20,6 +20,7 @@ + diff --git a/tests/SwaggerProvider.Tests/v3/Schema.DefinitionPathTests.fs b/tests/SwaggerProvider.Tests/v3/Schema.DefinitionPathTests.fs new file mode 100644 index 00000000..ff0aa503 --- /dev/null +++ b/tests/SwaggerProvider.Tests/v3/Schema.DefinitionPathTests.fs @@ -0,0 +1,109 @@ +module SwaggerProvider.Tests.v3_Schema_DefinitionPathTests + +/// Unit tests for DefinitionPath.Parse — the function that splits a JSON Reference +/// path (e.g. "#/components/schemas/My.Namespace.TypeName") into its namespace list, +/// requested type name, and PascalCase candidate name. + +open SwaggerProvider.Internal.v3.Compilers +open Xunit +open FsUnitTyped + +// ── Prefix constant ─────────────────────────────────────────────────────────── + +[] +let ``DefinitionPrefix is the OpenAPI component schema reference prefix``() = + DefinitionPath.DefinitionPrefix |> shouldEqual "#/components/schemas/" + +// ── Simple (un-namespaced) names ────────────────────────────────────────────── + +[] +let ``simple name has empty namespace``() = + let result = DefinitionPath.Parse "#/components/schemas/Pet" + result.Namespace |> shouldEqual [] + +[] +let ``simple name preserves RequestedTypeName exactly``() = + let result = DefinitionPath.Parse "#/components/schemas/Pet" + result.RequestedTypeName |> shouldEqual "Pet" + +[] +let ``simple PascalCase name has matching ProvidedTypeNameCandidate``() = + let result = DefinitionPath.Parse "#/components/schemas/Pet" + result.ProvidedTypeNameCandidate |> shouldEqual "Pet" + +[] +let ``simple camelCase name is PascalCased in ProvidedTypeNameCandidate``() = + let result = DefinitionPath.Parse "#/components/schemas/petModel" + result.ProvidedTypeNameCandidate |> shouldEqual "PetModel" + +[] +let ``simple camelCase name preserves original casing in RequestedTypeName``() = + let result = DefinitionPath.Parse "#/components/schemas/petModel" + result.RequestedTypeName |> shouldEqual "petModel" + +// ── One-level namespaced names ──────────────────────────────────────────────── + +[] +let ``one-level namespace is extracted``() = + let result = DefinitionPath.Parse "#/components/schemas/My.Pet" + result.Namespace |> shouldEqual [ "My" ] + +[] +let ``one-level namespace leaves type name after the dot``() = + let result = DefinitionPath.Parse "#/components/schemas/My.Pet" + result.RequestedTypeName |> shouldEqual "Pet" + +[] +let ``one-level namespace applies PascalCase to ProvidedTypeNameCandidate``() = + let result = DefinitionPath.Parse "#/components/schemas/my.petModel" + result.ProvidedTypeNameCandidate |> shouldEqual "PetModel" + +// ── Multi-level namespaced names ────────────────────────────────────────────── + +[] +let ``two-level namespace is fully extracted``() = + let result = DefinitionPath.Parse "#/components/schemas/A.B.TypeName" + result.Namespace |> shouldEqual [ "A"; "B" ] + result.RequestedTypeName |> shouldEqual "TypeName" + +[] +let ``three-level namespace is fully extracted``() = + let result = DefinitionPath.Parse "#/components/schemas/A.B.C.TypeName" + result.Namespace |> shouldEqual [ "A"; "B"; "C" ] + result.RequestedTypeName |> shouldEqual "TypeName" + +[] +let ``deep namespace preserves all namespace segments``() = + let result = + DefinitionPath.Parse "#/components/schemas/Com.Example.Api.Models.Response" + + result.Namespace |> shouldEqual [ "Com"; "Example"; "Api"; "Models" ] + result.RequestedTypeName |> shouldEqual "Response" + +// ── Names containing non-alphanumeric / non-dot characters ─────────────────── +// Hyphens and underscores are valid in JSON schema names but are NOT dot-separators, +// so the function should find no namespace when no dot precedes them. + +[] +let ``name containing only a hyphen has no namespace``() = + let result = DefinitionPath.Parse "#/components/schemas/my-type" + result.Namespace |> shouldEqual [] + +[] +let ``name with hyphen does not extract a spurious namespace``() = + let result = DefinitionPath.Parse "#/components/schemas/Api.my-type" + // The dot before "my-type" is within the valid prefix; hyphen stops the scan, + // so LastIndexOf('.') finds the dot before "my-type". + result.Namespace |> shouldEqual [ "Api" ] + +// ── Error handling ──────────────────────────────────────────────────────────── + +[] +let ``definition not starting with prefix throws``() = + let act = fun () -> DefinitionPath.Parse "notADefinitionPath" |> ignore + act |> shouldFail + +[] +let ``swagger 2 definitions path does not start with v3 prefix and throws``() = + let act = fun () -> DefinitionPath.Parse "#/definitions/Pet" |> ignore + act |> shouldFail diff --git a/tests/SwaggerProvider.Tests/v3/Schema.TestHelpers.fs b/tests/SwaggerProvider.Tests/v3/Schema.TestHelpers.fs index 18b03162..4e003f00 100644 --- a/tests/SwaggerProvider.Tests/v3/Schema.TestHelpers.fs +++ b/tests/SwaggerProvider.Tests/v3/Schema.TestHelpers.fs @@ -71,3 +71,51 @@ components: propYaml compileSchemaAndGetValueType schemaStr + +/// Compile a minimal v3 schema with configurable DefinitionCompiler options. +/// Returns the .NET type of the `Value` property on `TestType`. +let compilePropertyTypeWith (provideNullable: bool) (propYaml: string) (required: bool) : Type = + let settings = Microsoft.OpenApi.Reader.OpenApiReaderSettings() + settings.AddYamlReader() + + let requiredBlock = + if required then + " required:\n - Value\n" + else + "" + + let schemaStr = + sprintf + """openapi: "3.0.0" +info: + title: TypeMappingTest + version: "1.0.0" +paths: {} +components: + schemas: + TestType: + type: object +%s properties: + Value: +%s""" + requiredBlock + propYaml + + let readResult = + Microsoft.OpenApi.OpenApiDocument.Parse(schemaStr, settings = settings) + + let schema = + match readResult.Document with + | null -> failwith "Failed to parse schema." + | doc -> doc + + let defCompiler = DefinitionCompiler(schema, provideNullable, false) + let opCompiler = OperationCompiler(schema, defCompiler, true, false, false) + opCompiler.CompileProvidedClients(defCompiler.Namespace) + + let types = defCompiler.Namespace.GetProvidedTypes() + let testType = types |> List.find(fun t -> t.Name = "TestType") + + match testType.GetDeclaredProperty("Value") with + | null -> failwith "Property 'Value' not found on TestType" + | prop -> prop.PropertyType diff --git a/tests/SwaggerProvider.Tests/v3/Schema.TypeMappingTests.fs b/tests/SwaggerProvider.Tests/v3/Schema.TypeMappingTests.fs index 443bdc0a..b7bf1170 100644 --- a/tests/SwaggerProvider.Tests/v3/Schema.TypeMappingTests.fs +++ b/tests/SwaggerProvider.Tests/v3/Schema.TypeMappingTests.fs @@ -279,3 +279,41 @@ let ``optional allOf $ref to integer alias resolves to Option``() = let ``optional allOf $ref to int64 alias resolves to Option``() = let ty = compileAllOfRefType " type: integer\n format: int64\n" false ty |> shouldEqual typeof + +// ── PreferNullable=true: optional value types use Nullable ───────────────── +// When provideNullable=true, the DefinitionCompiler wraps optional value types +// in Nullable instead of Option. + +[] +let ``PreferNullable: optional boolean maps to Nullable``() = + let ty = compilePropertyTypeWith true " type: boolean\n" false + + ty + |> shouldEqual(typedefof>.MakeGenericType(typeof)) + +[] +let ``PreferNullable: optional integer maps to Nullable``() = + let ty = compilePropertyTypeWith true " type: integer\n" false + + ty + |> shouldEqual(typedefof>.MakeGenericType(typeof)) + +[] +let ``PreferNullable: optional int64 maps to Nullable``() = + let ty = + compilePropertyTypeWith true " type: integer\n format: int64\n" false + + ty + |> shouldEqual(typedefof>.MakeGenericType(typeof)) + +[] +let ``PreferNullable: required integer is not wrapped (Nullable only for optional)``() = + let ty = compilePropertyTypeWith true " type: integer\n" true + ty |> shouldEqual typeof + +[] +let ``PreferNullable: optional string is not wrapped (reference type)``() = + // Reference types like string are not wrapped in Nullable since they are + // already nullable by nature — same behaviour as Option mode. + let ty = compilePropertyTypeWith true " type: string\n" false + ty |> shouldEqual typeof From 442ba4ab940ba624f2a2cd713513db88b25888f8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 10 Apr 2026 22:17:44 +0000 Subject: [PATCH 2/4] ci: trigger checks From 9c889585ff75c98af43e685559b6b781d493325a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 11 Apr 2026 08:18:47 +0000 Subject: [PATCH 3/4] test: address inline review comments on compilePropertyTypeWith, Nullable assertions, and comment wording Agent-Logs-Url: https://github.com/fsprojects/SwaggerProvider/sessions/39b1a730-3011-42c1-84c3-eb4a4ce202c3 Co-authored-by: sergey-tihon <1197905+sergey-tihon@users.noreply.github.com> --- .../v3/Schema.DefinitionPathTests.fs | 4 ++-- .../SwaggerProvider.Tests/v3/Schema.TestHelpers.fs | 13 ++++++++++++- .../v3/Schema.TypeMappingTests.fs | 9 +++------ 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/tests/SwaggerProvider.Tests/v3/Schema.DefinitionPathTests.fs b/tests/SwaggerProvider.Tests/v3/Schema.DefinitionPathTests.fs index ff0aa503..e2a72f14 100644 --- a/tests/SwaggerProvider.Tests/v3/Schema.DefinitionPathTests.fs +++ b/tests/SwaggerProvider.Tests/v3/Schema.DefinitionPathTests.fs @@ -92,8 +92,8 @@ let ``name containing only a hyphen has no namespace``() = [] let ``name with hyphen does not extract a spurious namespace``() = let result = DefinitionPath.Parse "#/components/schemas/Api.my-type" - // The dot before "my-type" is within the valid prefix; hyphen stops the scan, - // so LastIndexOf('.') finds the dot before "my-type". + // The dot before "my-type" is in the scanned definition-name segment after + // the prefix; the hyphen stops the scan, so LastIndexOf('.') finds that dot. result.Namespace |> shouldEqual [ "Api" ] // ── Error handling ──────────────────────────────────────────────────────────── diff --git a/tests/SwaggerProvider.Tests/v3/Schema.TestHelpers.fs b/tests/SwaggerProvider.Tests/v3/Schema.TestHelpers.fs index 4e003f00..fc93855a 100644 --- a/tests/SwaggerProvider.Tests/v3/Schema.TestHelpers.fs +++ b/tests/SwaggerProvider.Tests/v3/Schema.TestHelpers.fs @@ -104,9 +104,20 @@ components: let readResult = Microsoft.OpenApi.OpenApiDocument.Parse(schemaStr, 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 OpenAPI schema:%s%s" Environment.NewLine errorText + | _ -> () + let schema = match readResult.Document with - | null -> failwith "Failed to parse schema." + | null -> failwith "Failed to parse OpenAPI schema: Document is null." | doc -> doc let defCompiler = DefinitionCompiler(schema, provideNullable, false) diff --git a/tests/SwaggerProvider.Tests/v3/Schema.TypeMappingTests.fs b/tests/SwaggerProvider.Tests/v3/Schema.TypeMappingTests.fs index b7bf1170..072937f7 100644 --- a/tests/SwaggerProvider.Tests/v3/Schema.TypeMappingTests.fs +++ b/tests/SwaggerProvider.Tests/v3/Schema.TypeMappingTests.fs @@ -288,23 +288,20 @@ let ``optional allOf $ref to int64 alias resolves to Option``() = let ``PreferNullable: optional boolean maps to Nullable``() = let ty = compilePropertyTypeWith true " type: boolean\n" false - ty - |> shouldEqual(typedefof>.MakeGenericType(typeof)) + ty |> shouldEqual typeof> [] let ``PreferNullable: optional integer maps to Nullable``() = let ty = compilePropertyTypeWith true " type: integer\n" false - ty - |> shouldEqual(typedefof>.MakeGenericType(typeof)) + ty |> shouldEqual typeof> [] let ``PreferNullable: optional int64 maps to Nullable``() = let ty = compilePropertyTypeWith true " type: integer\n format: int64\n" false - ty - |> shouldEqual(typedefof>.MakeGenericType(typeof)) + ty |> shouldEqual typeof> [] let ``PreferNullable: required integer is not wrapped (Nullable only for optional)``() = From fd8d69ae5f4594f8b2e6499a622b96d87f72c14d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 11 Apr 2026 09:10:33 +0000 Subject: [PATCH 4/4] refactor: eliminate pipeline duplication in compilePropertyTypeWith via private core helper Agent-Logs-Url: https://github.com/fsprojects/SwaggerProvider/sessions/2592f70e-57ed-4977-9913-daaaccdd91e4 Co-authored-by: sergey-tihon <1197905+sergey-tihon@users.noreply.github.com> --- .../v3/Schema.TestHelpers.fs | 82 +++++-------------- 1 file changed, 21 insertions(+), 61 deletions(-) diff --git a/tests/SwaggerProvider.Tests/v3/Schema.TestHelpers.fs b/tests/SwaggerProvider.Tests/v3/Schema.TestHelpers.fs index fc93855a..0550fe28 100644 --- a/tests/SwaggerProvider.Tests/v3/Schema.TestHelpers.fs +++ b/tests/SwaggerProvider.Tests/v3/Schema.TestHelpers.fs @@ -5,9 +5,10 @@ open System open Microsoft.OpenApi.Reader open SwaggerProvider.Internal.v3.Compilers -/// Parse and compile a full OpenAPI v3 schema string, then return all provided types. -/// Pass asAsync=true to generate Async<'T> operation return types, or false for Task<'T>. -let compileV3Schema (schemaStr: string) (asAsync: bool) = +/// Core: parse, validate, and compile an OpenAPI v3 schema string. +/// `provideNullable` controls whether optional value-type properties use Nullable. +/// `asAsync` controls whether operation return types are Async<'T> or Task<'T>. +let private compileV3SchemaCore (schemaStr: string) (provideNullable: bool) (asAsync: bool) = let settings = OpenApiReaderSettings() settings.AddYamlReader() @@ -30,11 +31,16 @@ let compileV3Schema (schemaStr: string) (asAsync: bool) = | null -> failwith "Failed to parse OpenAPI schema: Document is null." | doc -> doc - let defCompiler = DefinitionCompiler(schema, false, false) + let defCompiler = DefinitionCompiler(schema, provideNullable, false) let opCompiler = OperationCompiler(schema, defCompiler, true, false, asAsync) opCompiler.CompileProvidedClients(defCompiler.Namespace) defCompiler.Namespace.GetProvidedTypes() +/// Parse and compile a full OpenAPI v3 schema string, then return all provided types. +/// Pass asAsync=true to generate Async<'T> operation return types, or false for Task<'T>. +let compileV3Schema (schemaStr: string) (asAsync: bool) = + compileV3SchemaCore schemaStr false asAsync + /// Parse and compile a full OpenAPI v3 schema string, then return the .NET type of /// the `Value` property on the `TestType` component schema. let compileSchemaAndGetValueType(schemaStr: string) : Type = @@ -45,17 +51,16 @@ let compileSchemaAndGetValueType(schemaStr: string) : Type = | null -> failwith "Property 'Value' not found on TestType" | prop -> prop.PropertyType -/// Compile a minimal v3 schema where TestType.Value is defined by `propYaml`. -let compilePropertyType (propYaml: string) (required: bool) : Type = +/// Build the minimal v3 schema string for a TestType.Value property. +let private buildPropertySchema (propYaml: string) (required: bool) = let requiredBlock = if required then " required:\n - Value\n" else "" - let schemaStr = - sprintf - """openapi: "3.0.0" + sprintf + """openapi: "3.0.0" info: title: TypeMappingTest version: "1.0.0" @@ -67,64 +72,19 @@ components: %s properties: Value: %s""" - requiredBlock - propYaml + requiredBlock + propYaml - compileSchemaAndGetValueType schemaStr +/// Compile a minimal v3 schema where TestType.Value is defined by `propYaml`. +let compilePropertyType (propYaml: string) (required: bool) : Type = + compileSchemaAndGetValueType(buildPropertySchema propYaml required) /// Compile a minimal v3 schema with configurable DefinitionCompiler options. /// Returns the .NET type of the `Value` property on `TestType`. let compilePropertyTypeWith (provideNullable: bool) (propYaml: string) (required: bool) : Type = - let settings = Microsoft.OpenApi.Reader.OpenApiReaderSettings() - settings.AddYamlReader() - - let requiredBlock = - if required then - " required:\n - Value\n" - else - "" - - let schemaStr = - sprintf - """openapi: "3.0.0" -info: - title: TypeMappingTest - version: "1.0.0" -paths: {} -components: - schemas: - TestType: - type: object -%s properties: - Value: -%s""" - requiredBlock - propYaml - - let readResult = - Microsoft.OpenApi.OpenApiDocument.Parse(schemaStr, 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 OpenAPI schema:%s%s" Environment.NewLine errorText - | _ -> () - - let schema = - match readResult.Document with - | null -> failwith "Failed to parse OpenAPI schema: Document is null." - | doc -> doc - - let defCompiler = DefinitionCompiler(schema, provideNullable, false) - let opCompiler = OperationCompiler(schema, defCompiler, true, false, false) - opCompiler.CompileProvidedClients(defCompiler.Namespace) + let types = + compileV3SchemaCore (buildPropertySchema propYaml required) provideNullable false - let types = defCompiler.Namespace.GetProvidedTypes() let testType = types |> List.find(fun t -> t.Name = "TestType") match testType.GetDeclaredProperty("Value") with