Skip to content
Open
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
38 changes: 18 additions & 20 deletions src/SwaggerProvider.DesignTime/DefinitionCompiler.fs
Original file line number Diff line number Diff line change
Expand Up @@ -220,11 +220,11 @@ type DefinitionCompiler(schema: OpenApiDocument, provideNullable, useDateOnly: b
ty,
getterCode =
(function
| [ this ] -> Expr.FieldGetUnchecked(this, providedField)
| [ this ] -> Expr.FieldGet(this, providedField)
| _ -> failwith "invalid property getter params"),
setterCode =
(function
| [ this; v ] -> Expr.FieldSetUnchecked(this, providedField, v)
| [ this; v ] -> Expr.FieldSet(this, providedField, v)
| _ -> failwith "invalid property setter params")
)

Expand Down Expand Up @@ -441,6 +441,17 @@ type DefinitionCompiler(schema: OpenApiDocument, provideNullable, useDateOnly: b
else
DefinitionPath.DefinitionPrefix + refId

// Helper for the allOf/oneOf/anyOf single-ref collapse pattern.
// When `schemas` has exactly one entry that is a $ref and the outer schema has no
// own properties, collapse directly to the referenced type; otherwise fall back to
// compileNewObject so composite/inline schemas are handled as usual.
let compileSingleRefOrNewObject(schemas: System.Collections.Generic.IList<IOpenApiSchema>) =
match schemas.[0] with
| :? OpenApiSchemaReference as schemaRef when not(isNull schemaRef.Reference) ->
ns.ReleaseNameReservation tyName
compileByPath <| getFullPath schemaRef.Reference.Id
| _ -> compileNewObject()

let tyType =
match schemaObj with
| null -> failwithf $"Cannot compile object '%s{tyName}' when schema is 'null'"
Expand Down Expand Up @@ -472,39 +483,26 @@ type DefinitionCompiler(schema: OpenApiDocument, provideNullable, useDateOnly: b
compileBySchema ns (ns.ReserveUniqueName tyName "Item") elSchema true ns.RegisterType false

ProvidedTypeBuilder.MakeGenericType(typedefof<Map<string, obj>>, [ typeof<string>; elTy ])
// Handle allOf with single reference (e.g., nullable reference to another type)
// Handle allOf/oneOf/anyOf with a single $ref and no own properties:
// collapse the wrapper to the referenced type directly.
| _ when
not(isNull schemaObj.AllOf)
&& schemaObj.AllOf.Count = 1
&& (schemaObj.Properties |> isNull || schemaObj.Properties.Count = 0)
->
match schemaObj.AllOf.[0] with
| :? OpenApiSchemaReference as schemaRef when not(isNull schemaRef.Reference) ->
ns.ReleaseNameReservation tyName
compileByPath <| getFullPath schemaRef.Reference.Id
| _ -> compileNewObject()
// Handle oneOf with single reference (resolves to the referenced type)
compileSingleRefOrNewObject schemaObj.AllOf
| _ when
not(isNull schemaObj.OneOf)
&& schemaObj.OneOf.Count = 1
&& (schemaObj.Properties |> isNull || schemaObj.Properties.Count = 0)
->
match schemaObj.OneOf.[0] with
| :? OpenApiSchemaReference as schemaRef when not(isNull schemaRef.Reference) ->
ns.ReleaseNameReservation tyName
compileByPath <| getFullPath schemaRef.Reference.Id
| _ -> compileNewObject()
// Handle anyOf with single reference (resolves to the referenced type)
compileSingleRefOrNewObject schemaObj.OneOf
| _ when
not(isNull schemaObj.AnyOf)
&& schemaObj.AnyOf.Count = 1
&& (schemaObj.Properties |> isNull || schemaObj.Properties.Count = 0)
->
match schemaObj.AnyOf.[0] with
| :? OpenApiSchemaReference as schemaRef when not(isNull schemaRef.Reference) ->
ns.ReleaseNameReservation tyName
compileByPath <| getFullPath schemaRef.Reference.Id
| _ -> compileNewObject()
compileSingleRefOrNewObject schemaObj.AnyOf
| _ when
resolvedType.IsNone
|| resolvedType = Some JsonSchemaType.Object
Expand Down
156 changes: 156 additions & 0 deletions tests/SwaggerProvider.Tests/Schema.OperationCompilationTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -1112,3 +1112,159 @@ let ``ignoreOperationId=true does not generate the original operationId as metho
let methodNames = allMethods |> List.map(fun m -> m.Name)
methodNames |> shouldNotContain "ListAllPets"
methodNames |> shouldNotContain "GetPetById"

// ── text/plain request body ───────────────────────────────────────────────────

let private textPlainBodySchema =
"""openapi: "3.0.0"
info:
title: TextPlainBodyTest
version: "1.0.0"
paths:
/echo:
post:
operationId: echoText
requestBody:
required: true
content:
text/plain:
schema:
type: string
responses:
"200":
description: OK
components:
schemas: {}
"""

[<Fact>]
let ``text/plain request body generates a method``() =
let types = compileTaskSchema textPlainBodySchema
let method = findMethod types "EchoText"
method.IsSome |> shouldEqual true

[<Fact>]
let ``text/plain request body parameter is named textPlain``() =
let types = compileTaskSchema textPlainBodySchema
let method = (findMethod types "EchoText").Value
let paramNames = method.GetParameters() |> Array.map(fun p -> p.Name)
paramNames |> shouldContain "textPlain"

[<Fact>]
let ``text/plain request body has CancellationToken as last parameter``() =
let types = compileTaskSchema textPlainBodySchema
let method = (findMethod types "EchoText").Value
let lastParam = method.GetParameters() |> Array.last
lastParam.ParameterType |> shouldEqual typeof<CancellationToken>

// ── application/octet-stream response ────────────────────────────────────────

let private octetStreamResponseSchema =
"""openapi: "3.0.0"
info:
title: OctetStreamResponseTest
version: "1.0.0"
paths:
/file:
get:
operationId: downloadFile
responses:
"200":
description: File contents
content:
application/octet-stream:
schema:
type: string
format: binary
components:
schemas: {}
"""

[<Fact>]
let ``octet-stream response generates a method``() =
let types = compileTaskSchema octetStreamResponseSchema
let method = findMethod types "DownloadFile"
method.IsSome |> shouldEqual true

[<Fact>]
let ``octet-stream response produces Task<IO.Stream> return type``() =
let types = compileTaskSchema octetStreamResponseSchema
let method = (findMethod types "DownloadFile").Value
method.ReturnType.IsGenericType |> shouldEqual true

method.ReturnType.GetGenericTypeDefinition()
|> shouldEqual typedefof<Task<_>>

method.ReturnType.GetGenericArguments()[0]
|> shouldEqual typeof<IO.Stream>

[<Fact>]
let ``octet-stream response has CancellationToken as its only parameter``() =
let types = compileTaskSchema octetStreamResponseSchema
let method = (findMethod types "DownloadFile").Value
let parameters = method.GetParameters()
parameters.Length |> shouldEqual 1
parameters[0].ParameterType |> shouldEqual typeof<CancellationToken>

// ── Path-level parameters (inherited from PathItem) ───────────────────────────

let private pathLevelParamSchema =
"""openapi: "3.0.0"
info:
title: PathLevelParamTest
version: "1.0.0"
paths:
/users/{userId}/posts:
parameters:
- name: userId
in: path
required: true
schema:
type: integer
get:
operationId: getUserPosts
responses:
"200":
description: OK
content:
application/json:
schema:
type: array
items:
type: string
post:
operationId: createUserPost
requestBody:
required: true
content:
application/json:
schema:
type: string
responses:
"201":
description: Created
components:
schemas: {}
"""

[<Fact>]
let ``path-level parameter is inherited by GET operation``() =
let types = compileTaskSchema pathLevelParamSchema
let method = (findMethod types "GetUserPosts").Value
let paramNames = method.GetParameters() |> Array.map(fun p -> p.Name)
paramNames |> shouldContain "userId"

[<Fact>]
let ``path-level parameter is required with correct type``() =
let types = compileTaskSchema pathLevelParamSchema
let method = (findMethod types "GetUserPosts").Value
let userIdParam = method.GetParameters() |> Array.find(fun p -> p.Name = "userId")
userIdParam.ParameterType |> shouldEqual typeof<int32>
userIdParam.IsOptional |> shouldEqual false

[<Fact>]
let ``path-level parameter is inherited by POST operation``() =
let types = compileTaskSchema pathLevelParamSchema
let method = (findMethod types "CreateUserPost").Value
let paramNames = method.GetParameters() |> Array.map(fun p -> p.Name)
paramNames |> shouldContain "userId"
Loading