Skip to content
Draft
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
1 change: 1 addition & 0 deletions tests/SwaggerProvider.Tests/SwaggerProvider.Tests.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
<Compile Include="v3\Schema.TypeMappingTests.fs" />
<Compile Include="v3\Schema.ArrayAndMapTypeMappingTests.fs" />
<Compile Include="v3\Schema.V2SchemaCompilationTests.fs" />
<Compile Include="v3\Schema.DefinitionPathTests.fs" />
<Compile Include="v3\Schema.OperationCompilationTests.fs" />
<Compile Include="v3\Schema.XmlDocTests.fs" />
<Compile Include="PathResolutionTests.fs" />
Expand Down
109 changes: 109 additions & 0 deletions tests/SwaggerProvider.Tests/v3/Schema.DefinitionPathTests.fs
Original file line number Diff line number Diff line change
@@ -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 ───────────────────────────────────────────────────────────

[<Fact>]
let ``DefinitionPrefix is the OpenAPI component schema reference prefix``() =
DefinitionPath.DefinitionPrefix |> shouldEqual "#/components/schemas/"

// ── Simple (un-namespaced) names ──────────────────────────────────────────────

[<Fact>]
let ``simple name has empty namespace``() =
let result = DefinitionPath.Parse "#/components/schemas/Pet"
result.Namespace |> shouldEqual []

[<Fact>]
let ``simple name preserves RequestedTypeName exactly``() =
let result = DefinitionPath.Parse "#/components/schemas/Pet"
result.RequestedTypeName |> shouldEqual "Pet"

[<Fact>]
let ``simple PascalCase name has matching ProvidedTypeNameCandidate``() =
let result = DefinitionPath.Parse "#/components/schemas/Pet"
result.ProvidedTypeNameCandidate |> shouldEqual "Pet"

[<Fact>]
let ``simple camelCase name is PascalCased in ProvidedTypeNameCandidate``() =
let result = DefinitionPath.Parse "#/components/schemas/petModel"
result.ProvidedTypeNameCandidate |> shouldEqual "PetModel"

[<Fact>]
let ``simple camelCase name preserves original casing in RequestedTypeName``() =
let result = DefinitionPath.Parse "#/components/schemas/petModel"
result.RequestedTypeName |> shouldEqual "petModel"

// ── One-level namespaced names ────────────────────────────────────────────────

[<Fact>]
let ``one-level namespace is extracted``() =
let result = DefinitionPath.Parse "#/components/schemas/My.Pet"
result.Namespace |> shouldEqual [ "My" ]

[<Fact>]
let ``one-level namespace leaves type name after the dot``() =
let result = DefinitionPath.Parse "#/components/schemas/My.Pet"
result.RequestedTypeName |> shouldEqual "Pet"

[<Fact>]
let ``one-level namespace applies PascalCase to ProvidedTypeNameCandidate``() =
let result = DefinitionPath.Parse "#/components/schemas/my.petModel"
result.ProvidedTypeNameCandidate |> shouldEqual "PetModel"

// ── Multi-level namespaced names ──────────────────────────────────────────────

[<Fact>]
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"

[<Fact>]
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"

[<Fact>]
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.

[<Fact>]
let ``name containing only a hyphen has no namespace``() =
let result = DefinitionPath.Parse "#/components/schemas/my-type"
result.Namespace |> shouldEqual []

[<Fact>]
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 ────────────────────────────────────────────────────────────

[<Fact>]
let ``definition not starting with prefix throws``() =
let act = fun () -> DefinitionPath.Parse "notADefinitionPath" |> ignore
act |> shouldFail

[<Fact>]
let ``swagger 2 definitions path does not start with v3 prefix and throws``() =
let act = fun () -> DefinitionPath.Parse "#/definitions/Pet" |> ignore
act |> shouldFail
48 changes: 48 additions & 0 deletions tests/SwaggerProvider.Tests/v3/Schema.TestHelpers.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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
38 changes: 38 additions & 0 deletions tests/SwaggerProvider.Tests/v3/Schema.TypeMappingTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -279,3 +279,41 @@ let ``optional allOf $ref to integer alias resolves to Option<int32>``() =
let ``optional allOf $ref to int64 alias resolves to Option<int64>``() =
let ty = compileAllOfRefType " type: integer\n format: int64\n" false
ty |> shouldEqual typeof<int64 option>

// ── PreferNullable=true: optional value types use Nullable<T> ─────────────────
// When provideNullable=true, the DefinitionCompiler wraps optional value types
// in Nullable<T> instead of Option<T>.

[<Fact>]
let ``PreferNullable: optional boolean maps to Nullable<bool>``() =
let ty = compilePropertyTypeWith true " type: boolean\n" false

ty
|> shouldEqual(typedefof<System.Nullable<bool>>.MakeGenericType(typeof<bool>))

[<Fact>]
let ``PreferNullable: optional integer maps to Nullable<int32>``() =
let ty = compilePropertyTypeWith true " type: integer\n" false

ty
|> shouldEqual(typedefof<System.Nullable<int>>.MakeGenericType(typeof<int32>))

[<Fact>]
let ``PreferNullable: optional int64 maps to Nullable<int64>``() =
let ty =
compilePropertyTypeWith true " type: integer\n format: int64\n" false

ty
|> shouldEqual(typedefof<System.Nullable<int>>.MakeGenericType(typeof<int64>))

[<Fact>]
let ``PreferNullable: required integer is not wrapped (Nullable only for optional)``() =
let ty = compilePropertyTypeWith true " type: integer\n" true
ty |> shouldEqual typeof<int32>

[<Fact>]
let ``PreferNullable: optional string is not wrapped (reference type)``() =
// Reference types like string are not wrapped in Nullable<T> since they are
// already nullable by nature β€” same behaviour as Option mode.
let ty = compilePropertyTypeWith true " type: string\n" false
ty |> shouldEqual typeof<string>
Loading