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
5 changes: 1 addition & 4 deletions src/SwaggerProvider.DesignTime/OperationCompiler.fs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ open System
open System.Collections.Generic
open System.Net.Http
open System.Text.Json
open System.Text.RegularExpressions

open Microsoft.FSharp.Quotations
open Microsoft.FSharp.Quotations.ExprShape
Expand Down Expand Up @@ -339,9 +338,7 @@ type OperationCompiler(schema: OpenApiDocument, defCompiler: DefinitionCompiler,
| ParameterLocation.Path ->
let value = coerceString valueExpr
let pattern = $"{{%s{name}}}"
// Escape $ in the replacement to avoid regex back-reference interpretation ($0, $& etc.)
let escaped = <@ (%value).Replace("$", "$$") @>
let path' = <@ Regex.Replace(%path, pattern, %escaped) @>
let path' = <@ (%path).Replace(pattern, %value) @>
(path', query, headers, cookies)
| ParameterLocation.Query ->
let listValues = coerceQueryString name valueExpr
Expand Down
166 changes: 166 additions & 0 deletions tests/SwaggerProvider.Tests/Schema.OperationCompilationTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -640,3 +640,169 @@ let ``default response is used as return type when no 2xx response is defined``(
// The string schema from the default response should produce Task<string>
method.ReturnType.GetGenericArguments()[0]
|> shouldEqual typeof<string>

// ── Multiple path parameters ───────────────────────────────────────────────────

let private multiplePathParamsSchema =
"""openapi: "3.0.0"
info:
title: MultiplePathParamsTest
version: "1.0.0"
paths:
/users/{userId}/posts/{postId}:
get:
operationId: getUserPost
parameters:
- name: userId
in: path
required: true
schema:
type: integer
- name: postId
in: path
required: true
schema:
type: integer
responses:
"200":
description: OK
content:
application/json:
schema:
type: string
components:
schemas: {}
"""

[<Fact>]
let ``both path parameters appear as required parameters``() =
let types = compileTaskSchema multiplePathParamsSchema
let method = (findMethod types "GetUserPost").Value
let parameters = method.GetParameters()
let paramNames = parameters |> Array.map(fun p -> p.Name)
paramNames |> shouldContain "userId"
paramNames |> shouldContain "postId"

[<Fact>]
let ``path parameters in nested path are required (not optional)``() =
let types = compileTaskSchema multiplePathParamsSchema
let method = (findMethod types "GetUserPost").Value
let parameters = method.GetParameters()

let userIdParam = parameters |> Array.find(fun p -> p.Name = "userId")
userIdParam.IsOptional |> shouldEqual false

let postIdParam = parameters |> Array.find(fun p -> p.Name = "postId")
postIdParam.IsOptional |> shouldEqual false

[<Fact>]
let ``multiple path params appear before CancellationToken``() =
let types = compileTaskSchema multiplePathParamsSchema
let method = (findMethod types "GetUserPost").Value
let parameters = method.GetParameters()
let lastParam = parameters |> Array.last

lastParam.ParameterType |> shouldEqual typeof<CancellationToken>

parameters.Length |> shouldEqual 3 // userId, postId, CancellationToken

// ── PATCH operation ────────────────────────────────────────────────────────────

let private patchSchema =
"""openapi: "3.0.0"
info:
title: PatchTest
version: "1.0.0"
paths:
/items/{id}:
patch:
operationId: updateItem
parameters:
- name: id
in: path
required: true
schema:
type: integer
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
name:
type: string
responses:
"200":
description: OK
components:
schemas: {}
"""

[<Fact>]
let ``PATCH endpoint generates a method``() =
let types = compileTaskSchema patchSchema
let method = findMethod types "UpdateItem"
method.IsSome |> shouldEqual true

[<Fact>]
let ``PATCH endpoint has path param, body param, and CancellationToken``() =
let types = compileTaskSchema patchSchema
let method = (findMethod types "UpdateItem").Value
let parameters = method.GetParameters()
let paramNames = parameters |> Array.map(fun p -> p.Name)
paramNames |> shouldContain "id"
paramNames |> shouldContain "json"
let lastParam = parameters |> Array.last

lastParam.ParameterType |> shouldEqual typeof<CancellationToken>

// ── Auto-generated operation name (no operationId) ─────────────────────────────

let private noOperationIdSchema =
"""openapi: "3.0.0"
info:
title: NoOperationIdTest
version: "1.0.0"
paths:
/categories/{categoryId}/items:
get:
parameters:
- name: categoryId
in: path
required: true
schema:
type: integer
responses:
"200":
description: OK
content:
application/json:
schema:
type: string
components:
schemas: {}
"""

[<Fact>]
let ``operation without operationId generates a method from path and HTTP method``() =
let types = compileTaskSchema noOperationIdSchema
let method = findMethod types "GetCategoryItems"
method.IsSome |> shouldEqual true

[<Fact>]
let ``operation without operationId has correct parameter count``() =
let types = compileTaskSchema noOperationIdSchema
// findMethod searches all methods on all types; just verify we find exactly one
// method that has the expected signature (categoryId + CancellationToken)
let allMethods =
types
|> List.collect(fun t -> t.GetMethods() |> Array.toList)
|> List.filter(fun m ->
let ps = m.GetParameters()

ps.Length = 2
&& ps[0].Name = "categoryId"
&& ps[1].ParameterType = typeof<CancellationToken>)

allMethods.Length |> shouldEqual 1