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
12 changes: 9 additions & 3 deletions src/SwaggerProvider.DesignTime/Provider.OpenApiClient.fs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ type public OpenApiClientTypeProvider(cfg: TypeProviderConfig) as this =
ProvidedStaticParameter("PreferNullable", typeof<bool>, false)
ProvidedStaticParameter("PreferAsync", typeof<bool>, false)
ProvidedStaticParameter("SsrfProtection", typeof<bool>, true)
ProvidedStaticParameter("IgnoreParseErrors", typeof<bool>, false) ]
ProvidedStaticParameter("IgnoreParseErrors", typeof<bool>, false)
ProvidedStaticParameter("WrapNullableStrings", typeof<bool>, false) ]

t.AddXmlDoc
"""<summary>Statically typed OpenAPI provider.</summary>
Expand All @@ -59,7 +60,8 @@ type public OpenApiClientTypeProvider(cfg: TypeProviderConfig) as this =
<param name='PreferNullable'>Provide `Nullable<_>` for not required properties, instead of `Option<_>`. Defaults value `false`.</param>
<param name='PreferAsync'>Generate async actions of type `Async<'T>` instead of `Task<'T>`. Defaults value `false`.</param>
<param name='SsrfProtection'>Enable SSRF protection (blocks HTTP and localhost). Set to false for development/testing. Default value `true`.</param>
<param name='IgnoreParseErrors'>Continue generating the provider even when the OpenAPI parser reports validation errors (e.g. vendor extensions or non-strictly-compliant schemas). Warnings are printed to stderr. Default value `false`.</param>"""
<param name='IgnoreParseErrors'>Continue generating the provider even when the OpenAPI parser reports validation errors (e.g. vendor extensions or non-strictly-compliant schemas). Warnings are printed to stderr. Default value `false`.</param>
<param name='WrapNullableStrings'>Wrap optional/nullable string fields as `Option&lt;string&gt;` instead of plain `string`. Matches the treatment of other optional types. Default value `false`.</param>"""

t.DefineStaticParameters(
staticParams,
Expand All @@ -71,6 +73,7 @@ type public OpenApiClientTypeProvider(cfg: TypeProviderConfig) as this =
let preferAsync = unbox<bool> args.[4]
let ssrfProtection = unbox<bool> args.[5]
let ignoreParseErrors = unbox<bool> args.[6]
let wrapNullableStrings = unbox<bool> args.[7]

// Cache key includes cfg.RuntimeAssembly, cfg.ResolutionFolder, and cfg.SystemRuntimeAssemblyVersion
// to differentiate between different TFM builds (same approach as FSharp.Data)
Expand All @@ -83,6 +86,7 @@ type public OpenApiClientTypeProvider(cfg: TypeProviderConfig) as this =
preferAsync,
ssrfProtection,
ignoreParseErrors,
wrapNullableStrings,
cfg.RuntimeAssembly,
cfg.ResolutionFolder,
cfg.SystemRuntimeAssemblyVersion)
Expand Down Expand Up @@ -119,7 +123,9 @@ type public OpenApiClientTypeProvider(cfg: TypeProviderConfig) as this =
|> Seq.toList

let useDateOnly = cfg.SystemRuntimeAssemblyVersion.Major >= 6
let defCompiler = DefinitionCompiler(schema, preferNullable, useDateOnly)

let defCompiler =
DefinitionCompiler(schema, preferNullable, useDateOnly, wrapNullableStrings)

let opCompiler =
OperationCompiler(schema, defCompiler, ignoreControllerPrefix, ignoreOperationId, preferAsync)
Expand Down
6 changes: 5 additions & 1 deletion src/SwaggerProvider.DesignTime/v3/DefinitionCompiler.fs
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,9 @@ and NamespaceAbstraction(name: string) =
Some ty)

/// Object for compiling definitions.
type DefinitionCompiler(schema: OpenApiDocument, provideNullable, useDateOnly: bool) as this =
type DefinitionCompiler(schema: OpenApiDocument, provideNullable, useDateOnly: bool, ?wrapNullableStrings: bool) as this =
let wrapNullableStrings = defaultArg wrapNullableStrings false

let pathToSchema =
let dict = Collections.Generic.Dictionary<string, IOpenApiSchema>()

Expand Down Expand Up @@ -538,6 +540,8 @@ type DefinitionCompiler(schema: OpenApiDocument, provideNullable, useDateOnly: b
typedefof<Option<obj>>

ProvidedTypeBuilder.MakeGenericType(baseGenTy, [ tyType ])
else if wrapNullableStrings && tyType = typeof<string> then
ProvidedTypeBuilder.MakeGenericType(typedefof<Option<obj>>, [ tyType ])
else
tyType

Expand Down
49 changes: 49 additions & 0 deletions tests/SwaggerProvider.Tests/v3/Schema.TestHelpers.fs
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,52 @@ components:
propYaml

compileSchemaAndGetValueType schemaStr

/// Compile a minimal v3 schema using the given DefinitionCompiler options.
let compilePropertyTypeWith (propYaml: string) (required: bool) (provideNullable: bool) (wrapNullableStrings: 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)

let schema =
match readResult.Document with
| null -> failwith "Failed to parse OpenAPI schema: Document is null."
| doc -> doc

let defCompiler =
DefinitionCompiler(schema, provideNullable, false, wrapNullableStrings)

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
30 changes: 30 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,33 @@ 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>

// ── WrapNullableStrings option ───────────────────────────────────────────────

[<Fact>]
let ``optional string without WrapNullableStrings maps to plain string``() =
let ty = compilePropertyType " type: string\n" false
ty |> shouldEqual typeof<string>

[<Fact>]
let ``optional string with WrapNullableStrings maps to string option``() =
let ty = compilePropertyTypeWith " type: string\n" false false true
ty |> shouldEqual typeof<string option>

[<Fact>]
let ``required string with WrapNullableStrings still maps to plain string``() =
let ty = compilePropertyTypeWith " type: string\n" true false true
ty |> shouldEqual typeof<string>

[<Fact>]
let ``optional string date-time with WrapNullableStrings maps to DateTimeOffset option``() =
// Non-string reference types (DateTimeOffset is a value type) unaffected by WrapNullableStrings
let ty =
compilePropertyTypeWith " type: string\n format: date-time\n" false false true

ty |> shouldEqual typeof<DateTimeOffset option>

[<Fact>]
let ``optional integer with WrapNullableStrings still maps to int32 option``() =
let ty = compilePropertyTypeWith " type: integer\n" false false true
ty |> shouldEqual typeof<int32 option>
Loading