Skip to content
Merged
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 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 ────────────────────────────────────────────────────────────

[<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
43 changes: 31 additions & 12 deletions tests/SwaggerProvider.Tests/v3/Schema.TestHelpers.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>.
/// `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()

Expand All @@ -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 =
Expand All @@ -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"
Expand All @@ -67,7 +72,21 @@ 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 types =
compileV3SchemaCore (buildPropertySchema propYaml required) provideNullable false

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
35 changes: 35 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,38 @@ 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 typeof<System.Nullable<bool>>

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

ty |> shouldEqual typeof<System.Nullable<int32>>

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

ty |> shouldEqual typeof<System.Nullable<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