diff --git a/src/SwaggerProvider.DesignTime/OperationCompiler.fs b/src/SwaggerProvider.DesignTime/OperationCompiler.fs index acde5a63..a2fbe083 100644 --- a/src/SwaggerProvider.DesignTime/OperationCompiler.fs +++ b/src/SwaggerProvider.DesignTime/OperationCompiler.fs @@ -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 @@ -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 diff --git a/tests/SwaggerProvider.Tests/Schema.OperationCompilationTests.fs b/tests/SwaggerProvider.Tests/Schema.OperationCompilationTests.fs index 5f4c2ce0..b985055e 100644 --- a/tests/SwaggerProvider.Tests/Schema.OperationCompilationTests.fs +++ b/tests/SwaggerProvider.Tests/Schema.OperationCompilationTests.fs @@ -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 method.ReturnType.GetGenericArguments()[0] |> shouldEqual typeof + +// ── 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: {} +""" + +[] +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" + +[] +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 + +[] +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 + + 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: {} +""" + +[] +let ``PATCH endpoint generates a method``() = + let types = compileTaskSchema patchSchema + let method = findMethod types "UpdateItem" + method.IsSome |> shouldEqual true + +[] +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 + +// ── 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: {} +""" + +[] +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 + +[] +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) + + allMethods.Length |> shouldEqual 1