Skip to content

Commit deb9d9f

Browse files
github-actions[bot]Copilotsergey-tihonCopilot
authored
[Repo Assist] improve: surface enum allowed-values in operation parameter XmlDocs (+3 tests, 324→327) (#398)
* improve: surface enum allowed-values in operation parameter XmlDocs (+3 tests, 324→327) 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 <param> tags in generated method XmlDocs, so IntelliSense shows valid values for query/path/header/cookie parameters that have an enum schema. Before: /// <summary>List items</summary> /// <param name="status">Filter by status</param> After: /// <summary>List items</summary> /// <param name="status">Filter by status /// Allowed values: active, inactive, pending</param> 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> * ci: trigger checks * Update tests/SwaggerProvider.Tests/Schema.XmlDocTests.fs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Sergey Tihon <sergey.tihon@gmail.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent dec1bdd commit deb9d9f

4 files changed

Lines changed: 166 additions & 47 deletions

File tree

src/SwaggerProvider.DesignTime/DefinitionCompiler.fs

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -305,27 +305,7 @@ type DefinitionCompiler(schema: OpenApiDocument, provideNullable, useDateOnly: b
305305

306306
let pField, pProp = generateProperty propName pTy
307307

308-
let formatEnumValue(v: System.Text.Json.Nodes.JsonNode) =
309-
if isNull v then
310-
"null"
311-
else
312-
// Format known JsonNode scalar types directly so documentation does not depend
313-
// on JSON serialization/escaping or specific ToString() implementations.
314-
match v with
315-
| :? System.Text.Json.Nodes.JsonValue as jv ->
316-
match jv.GetValueKind() with
317-
| System.Text.Json.JsonValueKind.String -> jv.GetValue<string>()
318-
| System.Text.Json.JsonValueKind.Null -> "null"
319-
| _ -> jv.ToString()
320-
| _ -> v.ToString()
321-
322-
let enumValuesDoc =
323-
if not(isNull propSchema.Enum) && propSchema.Enum.Count > 0 then
324-
let values = propSchema.Enum |> Seq.map formatEnumValue |> String.concat ", "
325-
326-
Some $"Allowed values: {values}"
327-
else
328-
None
308+
let enumValuesDoc = XmlDoc.buildEnumDoc propSchema.Enum
329309

330310
let propDoc =
331311
match

src/SwaggerProvider.DesignTime/OperationCompiler.fs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -495,8 +495,26 @@ type OperationCompiler(schema: OpenApiDocument, defCompiler: DefinitionCompiler,
495495
)
496496

497497
let xmlDoc =
498+
let buildParamDesc(p: IOpenApiParameter) =
499+
let enumDoc =
500+
if not(isNull p.Schema) then
501+
XmlDoc.buildEnumDoc p.Schema.Enum
502+
else
503+
None
504+
505+
match
506+
p.Description
507+
|> Option.ofObj
508+
|> Option.filter(String.IsNullOrWhiteSpace >> not),
509+
enumDoc
510+
with
511+
| None, None -> null
512+
| Some d, None -> d
513+
| None, Some ev -> ev
514+
| Some d, Some ev -> $"{d}\n{ev}"
515+
498516
let paramDescriptions =
499-
[ for p in openApiParameters -> niceCamelName p.Name, p.Description
517+
[ for p in openApiParameters -> niceCamelName p.Name, buildParamDesc p
500518
if not(isNull operation.RequestBody) then
501519
yield niceCamelName(payloadTy.ToString()), operation.RequestBody.Description ]
502520

src/SwaggerProvider.DesignTime/Utils.fs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,10 +334,33 @@ module SchemaReader =
334334

335335
module XmlDoc =
336336
open System
337+
open System.Collections.Generic
338+
open System.Text.Json
339+
open System.Text.Json.Nodes
337340

338341
let private escapeXml(s: string) =
339342
s.Replace("&", "&amp;").Replace("<", "&lt;").Replace(">", "&gt;")
340343

344+
let private formatEnumValue(v: JsonNode) =
345+
if isNull v then
346+
"null"
347+
else
348+
match v with
349+
| :? JsonValue as jv ->
350+
match jv.GetValueKind() with
351+
| JsonValueKind.String -> jv.GetValue<string>()
352+
| JsonValueKind.Null -> "null"
353+
| _ -> jv.ToString()
354+
| _ -> v.ToString()
355+
356+
/// Returns "Allowed values: x, y, z" if the schema has enum values, otherwise None.
357+
let buildEnumDoc(enumValues: IList<JsonNode>) =
358+
if isNull enumValues || enumValues.Count = 0 then
359+
None
360+
else
361+
let values = enumValues |> Seq.map formatEnumValue |> String.concat ", "
362+
Some $"Allowed values: {values}"
363+
341364
/// Builds a structured XML doc string from summary, description, and parameter descriptions.
342365
/// paramDescriptions is a sequence of (camelCaseName, description) pairs.
343366
let buildXmlDoc (summary: string) (description: string) (paramDescriptions: (string * string) seq) =

tests/SwaggerProvider.Tests/Schema.XmlDocTests.fs

Lines changed: 123 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,33 @@ open SwaggerProvider.Internal.Compilers
66
open Xunit
77
open FsUnitTyped
88

9+
let private parseSchema(schemaStr: string) =
10+
let settings = OpenApiReaderSettings()
11+
settings.AddYamlReader()
12+
13+
let readResult =
14+
Microsoft.OpenApi.OpenApiDocument.Parse(schemaStr, settings = settings)
15+
16+
match readResult.Diagnostic with
17+
| null -> ()
18+
| diagnostic when diagnostic.Errors |> Seq.isEmpty |> not ->
19+
let errorText =
20+
diagnostic.Errors
21+
|> Seq.map string
22+
|> String.concat Environment.NewLine
23+
24+
failwithf "Failed to parse OpenAPI schema:%s%s" Environment.NewLine errorText
25+
| _ -> ()
26+
27+
match readResult.Document with
28+
| null -> failwith "Failed to parse OpenAPI schema: Document is null."
29+
| doc -> doc
30+
31+
let private getXmlDocAttr(m: System.Reflection.MemberInfo) =
32+
m.GetCustomAttributesData()
33+
|> Seq.tryFind(fun a -> a.AttributeType.Name = "TypeProviderXmlDocAttribute")
34+
|> Option.map(fun a -> a.ConstructorArguments.[0].Value :?> string)
35+
936
/// Compile a minimal OpenAPI v3 schema and return the XmlDoc string for the "Value" property
1037
/// of "TestType", or None if no XmlDoc was added.
1138
let private getPropertyXmlDoc(propYaml: string) : string option =
@@ -25,41 +52,47 @@ components:
2552
%s"""
2653
propYaml
2754

28-
let settings = OpenApiReaderSettings()
29-
settings.AddYamlReader()
55+
let schema = parseSchema schemaStr
3056

31-
let readResult =
32-
Microsoft.OpenApi.OpenApiDocument.Parse(schemaStr, settings = settings)
57+
let defCompiler = DefinitionCompiler(schema, false, false)
58+
let opCompiler = OperationCompiler(schema, defCompiler, true, false, true)
59+
opCompiler.CompileProvidedClients(defCompiler.Namespace)
3360

34-
match readResult.Diagnostic with
35-
| null -> ()
36-
| diagnostic when diagnostic.Errors |> Seq.isEmpty |> not ->
37-
let errorText =
38-
diagnostic.Errors
39-
|> Seq.map string
40-
|> String.concat Environment.NewLine
61+
let types = defCompiler.Namespace.GetProvidedTypes()
62+
let testType = types |> List.find(fun t -> t.Name = "TestType")
4163

42-
failwithf "Failed to parse OpenAPI schema:%s%s" Environment.NewLine errorText
43-
| _ -> ()
64+
match testType.GetDeclaredProperty("Value") with
65+
| null -> failwith "Property 'Value' not found on TestType"
66+
| prop -> getXmlDocAttr prop
67+
68+
/// Compile a minimal OpenAPI v3 schema and return the XmlDoc string for the generated
69+
/// operation method, or None if no XmlDoc was added.
70+
let private getMethodXmlDoc (pathsYaml: string) (operationId: string) : string option =
71+
let schemaStr =
72+
sprintf
73+
"""openapi: "3.0.0"
74+
info:
75+
title: XmlDocMethodTest
76+
version: "1.0.0"
77+
paths:
78+
%s
79+
components:
80+
schemas: {}
81+
"""
82+
pathsYaml
4483

45-
let schema =
46-
match readResult.Document with
47-
| null -> failwith "Failed to parse OpenAPI schema: Document is null."
48-
| doc -> doc
84+
let schema = parseSchema schemaStr
4985

5086
let defCompiler = DefinitionCompiler(schema, false, false)
5187
let opCompiler = OperationCompiler(schema, defCompiler, true, false, true)
5288
opCompiler.CompileProvidedClients(defCompiler.Namespace)
5389

5490
let types = defCompiler.Namespace.GetProvidedTypes()
55-
let testType = types |> List.find(fun t -> t.Name = "TestType")
5691

57-
match testType.GetDeclaredProperty("Value") with
58-
| null -> failwith "Property 'Value' not found on TestType"
59-
| prop ->
60-
prop.GetCustomAttributesData()
61-
|> Seq.tryFind(fun a -> a.AttributeType.Name = "TypeProviderXmlDocAttribute")
62-
|> Option.map(fun a -> a.ConstructorArguments.[0].Value :?> string)
92+
types
93+
|> List.collect(fun t -> t.GetMethods() |> Array.toList)
94+
|> List.tryFind(fun m -> m.Name.Equals(operationId, StringComparison.OrdinalIgnoreCase))
95+
|> Option.bind getXmlDocAttr
6396

6497
// ── Property description ─────────────────────────────────────────────────────
6598

@@ -75,7 +108,7 @@ let ``no XmlDoc added when no description and no enum``() =
75108
let doc = getPropertyXmlDoc " type: string\n"
76109
doc |> shouldEqual None
77110

78-
// ── Enum values in XmlDoc ────────────────────────────────────────────────────
111+
// ── Enum values in property XmlDoc ────────────────────────────────────────────
79112

80113
[<Fact>]
81114
let ``string enum values appear in property XmlDoc``() =
@@ -110,3 +143,68 @@ let ``description is preserved alongside enum values``() =
110143
doc.Value |> shouldContainText "Allowed values:"
111144
doc.Value |> shouldContainText "active"
112145
doc.Value |> shouldContainText "inactive"
146+
147+
// ── Enum values in operation parameter XmlDoc ─────────────────────────────────
148+
149+
let private statusEnumParamSchema =
150+
""" /items:
151+
get:
152+
operationId: listItems
153+
summary: List items
154+
parameters:
155+
- name: status
156+
in: query
157+
description: "Filter by status"
158+
schema:
159+
type: string
160+
enum:
161+
- active
162+
- inactive
163+
- pending
164+
responses:
165+
"200":
166+
description: OK
167+
content:
168+
application/json:
169+
schema:
170+
type: string
171+
"""
172+
173+
[<Fact>]
174+
let ``enum query parameter values appear in method XmlDoc param tag``() =
175+
let doc = getMethodXmlDoc statusEnumParamSchema "ListItems"
176+
doc.IsSome |> shouldEqual true
177+
doc.Value |> shouldContainText "Allowed values:"
178+
doc.Value |> shouldContainText "active"
179+
doc.Value |> shouldContainText "inactive"
180+
doc.Value |> shouldContainText "pending"
181+
182+
[<Fact>]
183+
let ``enum parameter description and allowed values are both preserved in method XmlDoc``() =
184+
let doc = getMethodXmlDoc statusEnumParamSchema "ListItems"
185+
doc.IsSome |> shouldEqual true
186+
doc.Value |> shouldContainText "Filter by status"
187+
doc.Value |> shouldContainText "Allowed values:"
188+
189+
let private noEnumParamSchema =
190+
""" /health:
191+
get:
192+
operationId: getHealth
193+
summary: Health check
194+
parameters:
195+
- name: verbose
196+
in: query
197+
description: "Verbose output"
198+
schema:
199+
type: boolean
200+
responses:
201+
"200":
202+
description: OK
203+
"""
204+
205+
[<Fact>]
206+
let ``non-enum query parameter does not add Allowed values to XmlDoc``() =
207+
let doc = getMethodXmlDoc noEnumParamSchema "GetHealth"
208+
doc.IsSome |> shouldEqual true
209+
doc.Value |> shouldContainText "Health check"
210+
doc.Value |> shouldNotContainText "Allowed values:"

0 commit comments

Comments
 (0)