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
3 changes: 0 additions & 3 deletions build.fsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,9 @@
#r "nuget: Fake.Core.ReleaseNotes"
#r "nuget: Fake.IO.FileSystem"
#r "nuget: Fake.DotNet.Cli"
#r "nuget: Fake.DotNet.MSBuild"
#r "nuget: Fake.DotNet.AssemblyInfoFile"
#r "nuget: Fake.DotNet.Paket"
#r "nuget: Fake.DotNet.FSFormatting"
#r "nuget: Fake.Tools.Git"
#r "nuget: Fake.Api.GitHub"

// Boilerplate - https://github.com/fsprojects/FAKE/issues/2719#issuecomment-1470687052
System.Environment.GetCommandLineArgs()
Expand Down
1 change: 1 addition & 0 deletions tests/SwaggerProvider.Tests/SwaggerProvider.Tests.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
<Compile Include="v2\Schema.Spec.Yaml.Tests.fs" />
<Compile Include="APIs.guru.fs" />
<Compile Include="Schema.Parser.Tests.fs" />
<Compile Include="v3\Schema.TypeMappingTests.fs" />
<Compile Include="PathResolutionTests.fs" />
<Compile Include="SsrfSecurityTests.fs" />
<Compile Include="RuntimeHelpersTests.fs" />
Expand Down
210 changes: 210 additions & 0 deletions tests/SwaggerProvider.Tests/v3/Schema.TypeMappingTests.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
module SwaggerProvider.Tests.v3_Schema_TypeMappingTests

open System
open Microsoft.OpenApi.Reader
open SwaggerProvider.Internal.v3.Compilers
open Xunit
open FsUnitTyped

/// Compile a minimal OpenAPI v3 schema containing one "TestType" object with a single
/// "Value" property defined by `propYaml`, and return that property's compiled .NET type.
let private compilePropertyType (propYaml: string) (required: bool) : Type =
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 settings = OpenApiReaderSettings()
settings.AddYamlReader()

let readResult =
Microsoft.OpenApi.OpenApiDocument.Parse(schemaStr, settings = settings)

// Ensure the schema was parsed successfully before using it
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, false)
let opCompiler = OperationCompiler(schema, defCompiler, true, false, true)
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

// ── Required primitive types ─────────────────────────────────────────────────

[<Fact>]
let ``required boolean maps to bool``() =
let ty = compilePropertyType " type: boolean\n" true

ty |> shouldEqual typeof<bool>

[<Fact>]
let ``required integer (no format) maps to int32``() =
let ty = compilePropertyType " type: integer\n" true

ty |> shouldEqual typeof<int32>

[<Fact>]
let ``required integer int64 format maps to int64``() =
let ty =
compilePropertyType " type: integer\n format: int64\n" true

ty |> shouldEqual typeof<int64>

[<Fact>]
let ``required number (no format) maps to float32``() =
let ty = compilePropertyType " type: number\n" true

ty |> shouldEqual typeof<float32>

[<Fact>]
let ``required number double format maps to double``() =
let ty =
compilePropertyType " type: number\n format: double\n" true

ty |> shouldEqual typeof<double>

// ── Required string formats ───────────────────────────────────────────────────

[<Fact>]
let ``required string (no format) maps to string``() =
let ty = compilePropertyType " type: string\n" true

ty |> shouldEqual typeof<string>

[<Fact>]
let ``required string date-time format maps to DateTimeOffset``() =
let ty =
compilePropertyType " type: string\n format: date-time\n" true

ty |> shouldEqual typeof<DateTimeOffset>

[<Fact>]
let ``required string date format maps to DateTimeOffset``() =
let ty = compilePropertyType " type: string\n format: date\n" true

ty |> shouldEqual typeof<DateTimeOffset>

[<Fact>]
let ``required string uuid format maps to Guid``() =
let ty = compilePropertyType " type: string\n format: uuid\n" true

ty |> shouldEqual typeof<Guid>

[<Fact>]
let ``required string byte format maps to byte array``() =
let ty = compilePropertyType " type: string\n format: byte\n" true

// DefinitionCompiler creates a rank-1 explicit array via MakeArrayType(1)
ty |> shouldEqual(typeof<byte>.MakeArrayType(1))

[<Fact>]
let ``required string binary format maps to Stream``() =
let ty =
compilePropertyType " type: string\n format: binary\n" true

ty |> shouldEqual typeof<IO.Stream>

// ── Optional (non-required) value types are wrapped in Option<T> ─────────────

[<Fact>]
let ``optional boolean maps to Option<bool>``() =
let ty = compilePropertyType " type: boolean\n" false

ty |> shouldEqual(typedefof<Option<_>>.MakeGenericType(typeof<bool>))

[<Fact>]
let ``optional integer maps to Option<int32>``() =
let ty = compilePropertyType " type: integer\n" false

ty |> shouldEqual(typedefof<Option<_>>.MakeGenericType(typeof<int32>))

[<Fact>]
let ``optional integer int64 maps to Option<int64>``() =
let ty =
compilePropertyType " type: integer\n format: int64\n" false

ty |> shouldEqual(typedefof<Option<_>>.MakeGenericType(typeof<int64>))

[<Fact>]
let ``optional number maps to Option<float32>``() =
let ty = compilePropertyType " type: number\n" false

ty
|> shouldEqual(typedefof<Option<_>>.MakeGenericType(typeof<float32>))

[<Fact>]
let ``optional number double maps to Option<double>``() =
let ty =
compilePropertyType " type: number\n format: double\n" false

ty
|> shouldEqual(typedefof<Option<_>>.MakeGenericType(typeof<double>))

[<Fact>]
let ``optional DateTimeOffset maps to Option<DateTimeOffset>``() =
let ty =
compilePropertyType " type: string\n format: date-time\n" false

ty
|> shouldEqual(typedefof<Option<_>>.MakeGenericType(typeof<DateTimeOffset>))

[<Fact>]
let ``optional Guid maps to Option<Guid>``() =
let ty =
compilePropertyType " type: string\n format: uuid\n" false

ty |> shouldEqual(typedefof<Option<_>>.MakeGenericType(typeof<Guid>))

// ── Optional reference types are NOT wrapped (they are already nullable) ─────

[<Fact>]
let ``optional string is not wrapped in Option``() =
let ty = compilePropertyType " type: string\n" false

// string is a reference type β€” not wrapped in Option<T> even when non-required
ty |> shouldEqual typeof<string>

[<Fact>]
let ``optional byte array is not wrapped in Option``() =
let ty =
compilePropertyType " type: string\n format: byte\n" false

// byte[*] is a reference type β€” not wrapped in Option<T>
ty |> shouldEqual(typeof<byte>.MakeArrayType(1))
Loading