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/2] 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/2] ci: trigger checks