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
18 changes: 11 additions & 7 deletions src/SwaggerProvider.DesignTime/DefinitionCompiler.fs
Original file line number Diff line number Diff line change
Expand Up @@ -281,22 +281,26 @@ type DefinitionCompiler(schema: OpenApiDocument, provideNullable, useDateOnly: b
ty.AddXmlDoc schemaObj.Description

// Combine composite schemas
// Cache the isEmpty check to avoid iterating `allOf` twice (once per field/required block).
let allOfEmpty = Seq.isEmpty allOf

let schemaObjProperties =
let getProps(s: IOpenApiSchema) =
s.Properties |> toSeq

match Seq.isEmpty allOf with
| false -> allOf |> Seq.append [ schemaObj ] |> Seq.collect getProps
| true -> getProps schemaObj

if allOfEmpty then
getProps schemaObj
else
allOf |> Seq.append [ schemaObj ] |> Seq.collect getProps

let schemaObjRequired =
let getReq(s: IOpenApiSchema) =
s.Required |> toSeq

match Seq.isEmpty allOf with
| false -> allOf |> Seq.append [ schemaObj ] |> Seq.collect getReq
| true -> getReq schemaObj
if allOfEmpty then
getReq schemaObj
else
allOf |> Seq.append [ schemaObj ] |> Seq.collect getReq
|> Set.ofSeq

// Helper to check if a schema has the Null type flag (OpenAPI 3.0 nullable)
Expand Down
10 changes: 6 additions & 4 deletions src/SwaggerProvider.Runtime/ProvidedApiClientBase.fs
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,14 @@ type ProvidedApiClientBase(httpClient: HttpClient, options: JsonSerializerOption
return ""
}

match errorCodes |> Array.tryFindIndex((=) codeStr) with
| Some idx ->
let desc = errorDescriptions[idx]
// Use Array.IndexOf to avoid allocating a partial-application closure on every error response.
let errorIdx = System.Array.IndexOf(errorCodes, codeStr)

if errorIdx >= 0 then
let desc = errorDescriptions[errorIdx]
let! body = readBody()
return raise(OpenApiException(code, desc, response.Headers, response.Content, body))
| None ->
else
let! body = readBody()

let desc =
Expand Down
134 changes: 134 additions & 0 deletions tests/SwaggerProvider.Tests/Schema.V3SchemaCompilationTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,137 @@ let ``anyOf single $ref resolves to the referenced type``() =
let ``anyOf single $ref does not produce a separate wrapper type``() =
let types = compileV3Schema anyOfSingleRefSchema false
types |> List.exists(fun t -> t.Name = "CatRef") |> shouldEqual false

// ── Required vs optional properties ──────────────────────────────────────────

[<Fact>]
let ``required property compiles to non-option type``() =
let schema =
"""{
"openapi": "3.0.0",
"info": { "title": "Test", "version": "1.0.0" },
"paths": {},
"components": {
"schemas": {
"Order": {
"type": "object",
"required": ["id"],
"properties": {
"id": { "type": "integer" },
"note": { "type": "string" }
}
}
}
}
}"""

let types = compileV3Schema schema false
let orderType = types |> List.find(fun t -> t.Name = "Order")
// 'id' is required β†’ int32 (not option)
orderType.GetDeclaredProperty("Id").PropertyType
|> shouldEqual typeof<int32>

[<Fact>]
let ``optional property compiles to option type``() =
let schema =
"""{
"openapi": "3.0.0",
"info": { "title": "Test", "version": "1.0.0" },
"paths": {},
"components": {
"schemas": {
"Order": {
"type": "object",
"required": ["id"],
"properties": {
"id": { "type": "integer" },
"note": { "type": "string" }
}
}
}
}
}"""

let types = compileV3Schema schema false
let orderType = types |> List.find(fun t -> t.Name = "Order")
// 'note' is not required β†’ string option
orderType.GetDeclaredProperty("Note").PropertyType
|> shouldEqual typeof<string option>

// ── String enum compilation ────────────────────────────────────────────────────

[<Fact>]
let ``string enum schema compiles to a named enum type``() =
let schema =
"""{
"openapi": "3.0.0",
"info": { "title": "Test", "version": "1.0.0" },
"paths": {},
"components": {
"schemas": {
"Status": {
"type": "string",
"enum": ["active", "inactive", "pending"]
}
}
}
}"""

let types = compileV3Schema schema false
types |> List.exists(fun t -> t.Name = "Status") |> shouldEqual true

[<Fact>]
let ``string enum type is an enum``() =
let schema =
"""{
"openapi": "3.0.0",
"info": { "title": "Test", "version": "1.0.0" },
"paths": {},
"components": {
"schemas": {
"Status": {
"type": "string",
"enum": ["active", "inactive", "pending"]
}
}
}
}"""

let types = compileV3Schema schema false
let statusType = types |> List.find(fun t -> t.Name = "Status")
statusType.IsEnum |> shouldEqual true

// ── Schema description as XmlDoc ─────────────────────────────────────────────

[<Fact>]
let ``object schema description is surfaced as XmlDoc``() =
let schema =
"""{
"openapi": "3.0.0",
"info": { "title": "Test", "version": "1.0.0" },
"paths": {},
"components": {
"schemas": {
"Widget": {
"type": "object",
"description": "A widget with a name",
"properties": {
"name": { "type": "string" }
}
}
}
}
}"""

let types = compileV3Schema schema false
let widgetType = types |> List.find(fun t -> t.Name = "Widget")
// XmlDoc is accessible via GetCustomAttributesData on the provided type
let doc = widgetType.GetCustomAttributesData()

doc
|> Seq.exists(fun a ->
a.AttributeType.Name = "TypeProviderXmlDocAttribute"
&& a.ConstructorArguments.Count > 0
&& a.ConstructorArguments.[0].Value :? string
&& (a.ConstructorArguments.[0].Value :?> string).Contains("A widget with a name"))
|> shouldEqual true
Loading