diff --git a/src/SwaggerProvider.DesignTime/DefinitionCompiler.fs b/src/SwaggerProvider.DesignTime/DefinitionCompiler.fs index 394c018..31ac440 100644 --- a/src/SwaggerProvider.DesignTime/DefinitionCompiler.fs +++ b/src/SwaggerProvider.DesignTime/DefinitionCompiler.fs @@ -305,27 +305,7 @@ type DefinitionCompiler(schema: OpenApiDocument, provideNullable, useDateOnly: b let pField, pProp = generateProperty propName pTy - let formatEnumValue(v: System.Text.Json.Nodes.JsonNode) = - if isNull v then - "null" - else - // Format known JsonNode scalar types directly so documentation does not depend - // on JSON serialization/escaping or specific ToString() implementations. - match v with - | :? System.Text.Json.Nodes.JsonValue as jv -> - match jv.GetValueKind() with - | System.Text.Json.JsonValueKind.String -> jv.GetValue() - | System.Text.Json.JsonValueKind.Null -> "null" - | _ -> jv.ToString() - | _ -> v.ToString() - - let enumValuesDoc = - if not(isNull propSchema.Enum) && propSchema.Enum.Count > 0 then - let values = propSchema.Enum |> Seq.map formatEnumValue |> String.concat ", " - - Some $"Allowed values: {values}" - else - None + let enumValuesDoc = XmlDoc.buildEnumDoc propSchema.Enum let propDoc = match diff --git a/src/SwaggerProvider.DesignTime/OperationCompiler.fs b/src/SwaggerProvider.DesignTime/OperationCompiler.fs index 938dbf7..d82de83 100644 --- a/src/SwaggerProvider.DesignTime/OperationCompiler.fs +++ b/src/SwaggerProvider.DesignTime/OperationCompiler.fs @@ -495,8 +495,26 @@ type OperationCompiler(schema: OpenApiDocument, defCompiler: DefinitionCompiler, ) let xmlDoc = + let buildParamDesc(p: IOpenApiParameter) = + let enumDoc = + if not(isNull p.Schema) then + XmlDoc.buildEnumDoc p.Schema.Enum + else + None + + match + p.Description + |> Option.ofObj + |> Option.filter(String.IsNullOrWhiteSpace >> not), + enumDoc + with + | None, None -> null + | Some d, None -> d + | None, Some ev -> ev + | Some d, Some ev -> $"{d}\n{ev}" + let paramDescriptions = - [ for p in openApiParameters -> niceCamelName p.Name, p.Description + [ for p in openApiParameters -> niceCamelName p.Name, buildParamDesc p if not(isNull operation.RequestBody) then yield niceCamelName(payloadTy.ToString()), operation.RequestBody.Description ] diff --git a/src/SwaggerProvider.DesignTime/Utils.fs b/src/SwaggerProvider.DesignTime/Utils.fs index ff3890c..6922593 100644 --- a/src/SwaggerProvider.DesignTime/Utils.fs +++ b/src/SwaggerProvider.DesignTime/Utils.fs @@ -334,10 +334,33 @@ module SchemaReader = module XmlDoc = open System + open System.Collections.Generic + open System.Text.Json + open System.Text.Json.Nodes let private escapeXml(s: string) = s.Replace("&", "&").Replace("<", "<").Replace(">", ">") + let private formatEnumValue(v: JsonNode) = + if isNull v then + "null" + else + match v with + | :? JsonValue as jv -> + match jv.GetValueKind() with + | JsonValueKind.String -> jv.GetValue() + | JsonValueKind.Null -> "null" + | _ -> jv.ToString() + | _ -> v.ToString() + + /// Returns "Allowed values: x, y, z" if the schema has enum values, otherwise None. + let buildEnumDoc(enumValues: IList) = + if isNull enumValues || enumValues.Count = 0 then + None + else + let values = enumValues |> Seq.map formatEnumValue |> String.concat ", " + Some $"Allowed values: {values}" + /// Builds a structured XML doc string from summary, description, and parameter descriptions. /// paramDescriptions is a sequence of (camelCaseName, description) pairs. let buildXmlDoc (summary: string) (description: string) (paramDescriptions: (string * string) seq) = diff --git a/tests/SwaggerProvider.Tests/Schema.XmlDocTests.fs b/tests/SwaggerProvider.Tests/Schema.XmlDocTests.fs index 13d71e9..910abce 100644 --- a/tests/SwaggerProvider.Tests/Schema.XmlDocTests.fs +++ b/tests/SwaggerProvider.Tests/Schema.XmlDocTests.fs @@ -6,6 +6,33 @@ open SwaggerProvider.Internal.Compilers open Xunit open FsUnitTyped +let private parseSchema(schemaStr: string) = + let settings = OpenApiReaderSettings() + settings.AddYamlReader() + + let readResult = + Microsoft.OpenApi.OpenApiDocument.Parse(schemaStr, settings = settings) + + 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 + | _ -> () + + match readResult.Document with + | null -> failwith "Failed to parse OpenAPI schema: Document is null." + | doc -> doc + +let private getXmlDocAttr(m: System.Reflection.MemberInfo) = + m.GetCustomAttributesData() + |> Seq.tryFind(fun a -> a.AttributeType.Name = "TypeProviderXmlDocAttribute") + |> Option.map(fun a -> a.ConstructorArguments.[0].Value :?> string) + /// Compile a minimal OpenAPI v3 schema and return the XmlDoc string for the "Value" property /// of "TestType", or None if no XmlDoc was added. let private getPropertyXmlDoc(propYaml: string) : string option = @@ -25,41 +52,47 @@ components: %s""" propYaml - let settings = OpenApiReaderSettings() - settings.AddYamlReader() + let schema = parseSchema schemaStr - let readResult = - Microsoft.OpenApi.OpenApiDocument.Parse(schemaStr, settings = settings) + let defCompiler = DefinitionCompiler(schema, false, false) + let opCompiler = OperationCompiler(schema, defCompiler, true, false, true) + opCompiler.CompileProvidedClients(defCompiler.Namespace) - match readResult.Diagnostic with - | null -> () - | diagnostic when diagnostic.Errors |> Seq.isEmpty |> not -> - let errorText = - diagnostic.Errors - |> Seq.map string - |> String.concat Environment.NewLine + let types = defCompiler.Namespace.GetProvidedTypes() + let testType = types |> List.find(fun t -> t.Name = "TestType") - failwithf "Failed to parse OpenAPI schema:%s%s" Environment.NewLine errorText - | _ -> () + match testType.GetDeclaredProperty("Value") with + | null -> failwith "Property 'Value' not found on TestType" + | prop -> getXmlDocAttr prop + +/// Compile a minimal OpenAPI v3 schema and return the XmlDoc string for the generated +/// operation method, or None if no XmlDoc was added. +let private getMethodXmlDoc (pathsYaml: string) (operationId: string) : string option = + let schemaStr = + sprintf + """openapi: "3.0.0" +info: + title: XmlDocMethodTest + version: "1.0.0" +paths: +%s +components: + schemas: {} +""" + pathsYaml - let schema = - match readResult.Document with - | null -> failwith "Failed to parse OpenAPI schema: Document is null." - | doc -> doc + let schema = parseSchema schemaStr let defCompiler = DefinitionCompiler(schema, false, 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.GetCustomAttributesData() - |> Seq.tryFind(fun a -> a.AttributeType.Name = "TypeProviderXmlDocAttribute") - |> Option.map(fun a -> a.ConstructorArguments.[0].Value :?> string) + types + |> List.collect(fun t -> t.GetMethods() |> Array.toList) + |> List.tryFind(fun m -> m.Name.Equals(operationId, StringComparison.OrdinalIgnoreCase)) + |> Option.bind getXmlDocAttr // ── Property description ───────────────────────────────────────────────────── @@ -75,7 +108,7 @@ let ``no XmlDoc added when no description and no enum``() = let doc = getPropertyXmlDoc " type: string\n" doc |> shouldEqual None -// ── Enum values in XmlDoc ──────────────────────────────────────────────────── +// ── Enum values in property XmlDoc ──────────────────────────────────────────── [] let ``string enum values appear in property XmlDoc``() = @@ -110,3 +143,68 @@ let ``description is preserved alongside enum values``() = doc.Value |> shouldContainText "Allowed values:" doc.Value |> shouldContainText "active" doc.Value |> shouldContainText "inactive" + +// ── Enum values in operation parameter XmlDoc ───────────────────────────────── + +let private statusEnumParamSchema = + """ /items: + get: + operationId: listItems + summary: List items + parameters: + - name: status + in: query + description: "Filter by status" + schema: + type: string + enum: + - active + - inactive + - pending + responses: + "200": + description: OK + content: + application/json: + schema: + type: string +""" + +[] +let ``enum query parameter values appear in method XmlDoc param tag``() = + let doc = getMethodXmlDoc statusEnumParamSchema "ListItems" + doc.IsSome |> shouldEqual true + doc.Value |> shouldContainText "Allowed values:" + doc.Value |> shouldContainText "active" + doc.Value |> shouldContainText "inactive" + doc.Value |> shouldContainText "pending" + +[] +let ``enum parameter description and allowed values are both preserved in method XmlDoc``() = + let doc = getMethodXmlDoc statusEnumParamSchema "ListItems" + doc.IsSome |> shouldEqual true + doc.Value |> shouldContainText "Filter by status" + doc.Value |> shouldContainText "Allowed values:" + +let private noEnumParamSchema = + """ /health: + get: + operationId: getHealth + summary: Health check + parameters: + - name: verbose + in: query + description: "Verbose output" + schema: + type: boolean + responses: + "200": + description: OK +""" + +[] +let ``non-enum query parameter does not add Allowed values to XmlDoc``() = + let doc = getMethodXmlDoc noEnumParamSchema "GetHealth" + doc.IsSome |> shouldEqual true + doc.Value |> shouldContainText "Health check" + doc.Value |> shouldNotContainText "Allowed values:"