From b63814c4202e3dcaff1a2fde58922b582f2bb573 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 23 Apr 2026 22:06:31 +0000 Subject: [PATCH 1/3] =?UTF-8?q?improve:=20surface=20enum=20allowed-values?= =?UTF-8?q?=20in=20operation=20parameter=20XmlDocs=20(+3=20tests,=20324?= =?UTF-8?q?=E2=86=92327)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extract the enum-value formatter from DefinitionCompiler into a shared XmlDoc.buildEnumDoc helper in Utils.fs, then use it in both places: - DefinitionCompiler already added 'Allowed values: ...' to object property XmlDocs; this PR refactors the local formatEnumValue function out to the shared module (no behaviour change for properties). - OperationCompiler now also adds enum value hints to the tags in generated method XmlDocs, so IntelliSense shows valid values for query/path/header/cookie parameters that have an enum schema. Before: /// List items /// Filter by status After: /// List items /// Filter by status /// Allowed values: active, inactive, pending Also refactors Schema.XmlDocTests.fs: extract parseSchema / getXmlDocAttr helpers to eliminate duplication and add a getMethodXmlDoc helper used by 3 new tests covering the operation-parameter enum doc feature. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../DefinitionCompiler.fs | 22 +-- .../OperationCompiler.fs | 20 ++- src/SwaggerProvider.DesignTime/Utils.fs | 23 +++ .../Schema.XmlDocTests.fs | 149 +++++++++++++++--- 4 files changed, 167 insertions(+), 47 deletions(-) diff --git a/src/SwaggerProvider.DesignTime/DefinitionCompiler.fs b/src/SwaggerProvider.DesignTime/DefinitionCompiler.fs index 394c018a..31ac440a 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 938dbf7e..d82de838 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 ff3890c6..6922593e 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 13d71e98..75246086 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,69 @@ 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" + // There is a summary, so XmlDoc should be set, but must NOT contain "Allowed values" + match doc with + | None -> () // no XmlDoc at all — also fine + | Some d -> d |> shouldNotContainText "Allowed values:" From ff4b33021655be184ac29fb79f49c367286886fe Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 23 Apr 2026 22:06:34 +0000 Subject: [PATCH 2/3] ci: trigger checks From 9be7bbcc684db3df260b859eca11b63fcb84fa95 Mon Sep 17 00:00:00 2001 From: Sergey Tihon Date: Fri, 24 Apr 2026 17:00:14 +0200 Subject: [PATCH 3/3] Update tests/SwaggerProvider.Tests/Schema.XmlDocTests.fs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tests/SwaggerProvider.Tests/Schema.XmlDocTests.fs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/SwaggerProvider.Tests/Schema.XmlDocTests.fs b/tests/SwaggerProvider.Tests/Schema.XmlDocTests.fs index 75246086..910abce1 100644 --- a/tests/SwaggerProvider.Tests/Schema.XmlDocTests.fs +++ b/tests/SwaggerProvider.Tests/Schema.XmlDocTests.fs @@ -205,7 +205,6 @@ let private noEnumParamSchema = [] let ``non-enum query parameter does not add Allowed values to XmlDoc``() = let doc = getMethodXmlDoc noEnumParamSchema "GetHealth" - // There is a summary, so XmlDoc should be set, but must NOT contain "Allowed values" - match doc with - | None -> () // no XmlDoc at all — also fine - | Some d -> d |> shouldNotContainText "Allowed values:" + doc.IsSome |> shouldEqual true + doc.Value |> shouldContainText "Health check" + doc.Value |> shouldNotContainText "Allowed values:"