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
22 changes: 1 addition & 21 deletions src/SwaggerProvider.DesignTime/DefinitionCompiler.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>()
| 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
Expand Down
20 changes: 19 additions & 1 deletion src/SwaggerProvider.DesignTime/OperationCompiler.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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 ]

Expand Down
23 changes: 23 additions & 0 deletions src/SwaggerProvider.DesignTime/Utils.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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("&", "&amp;").Replace("<", "&lt;").Replace(">", "&gt;")

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<string>()
| 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<JsonNode>) =
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) =
Expand Down
148 changes: 123 additions & 25 deletions tests/SwaggerProvider.Tests/Schema.XmlDocTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand All @@ -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 ─────────────────────────────────────────────────────

Expand All @@ -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 ────────────────────────────────────────────

[<Fact>]
let ``string enum values appear in property XmlDoc``() =
Expand Down Expand Up @@ -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
"""

[<Fact>]
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"

[<Fact>]
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
"""

[<Fact>]
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:"
Loading