diff --git a/src/SwaggerProvider.DesignTime/DefinitionCompiler.fs b/src/SwaggerProvider.DesignTime/DefinitionCompiler.fs index 24c4dca2..a9606bb0 100644 --- a/src/SwaggerProvider.DesignTime/DefinitionCompiler.fs +++ b/src/SwaggerProvider.DesignTime/DefinitionCompiler.fs @@ -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) diff --git a/src/SwaggerProvider.Runtime/ProvidedApiClientBase.fs b/src/SwaggerProvider.Runtime/ProvidedApiClientBase.fs index 4c45032c..b8913b0a 100644 --- a/src/SwaggerProvider.Runtime/ProvidedApiClientBase.fs +++ b/src/SwaggerProvider.Runtime/ProvidedApiClientBase.fs @@ -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 = diff --git a/tests/SwaggerProvider.Tests/Schema.V3SchemaCompilationTests.fs b/tests/SwaggerProvider.Tests/Schema.V3SchemaCompilationTests.fs index 89e23356..a38c9203 100644 --- a/tests/SwaggerProvider.Tests/Schema.V3SchemaCompilationTests.fs +++ b/tests/SwaggerProvider.Tests/Schema.V3SchemaCompilationTests.fs @@ -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 ────────────────────────────────────────── + +[] +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 + +[] +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 enum compilation ──────────────────────────────────────────────────── + +[] +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 + +[] +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 ───────────────────────────────────────────── + +[] +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