Skip to content

Commit ba7f34b

Browse files
RJSonnenberg64J0
andauthored
Enhance routef support for named parameters and improve documentation (#656)
* fix: enhance getConstraint function to handle optional name parameter * docs: add handler for petId endpoint to sample * test: add routef test for named parameter in pet endpoint * docs: add support for named parameters in routef documentation * Apply fantomas * Add more unit tests * Fix formatting * Update tests/Giraffe.Tests/EndpointRoutingTests.fs * Fix formatting and add new test scenarios --------- Co-authored-by: Vinícius Gajo <[email protected]> Co-authored-by: 64J0 <[email protected]>
1 parent e548019 commit ba7f34b

File tree

4 files changed

+137
-6
lines changed

4 files changed

+137
-6
lines changed

DOCUMENTATION.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1089,6 +1089,9 @@ let fooHandler (first : string,
10891089
10901090
let webApp =
10911091
choose [
1092+
// Named parameter example:
1093+
routef "/pet/%i:petId" (fun (petId: int) -> text (sprintf "PetId: %i" petId))
1094+
// Classic usage:
10921095
routef "/foo/%s/%s/%i" fooHandler
10931096
routef "/bar/%O" (fun guid -> text (guid.ToString()))
10941097
@@ -1099,7 +1102,7 @@ let webApp =
10991102

11001103
The `routef` http handler takes two parameters - a format string and an `HttpHandler` function.
11011104

1102-
The format string supports the following format chars:
1105+
The format string supports the following format chars, and now also supports **named parameters** using the syntax `%c:name` (e.g. `%i:petId`):
11031106

11041107
| Format Char | Type |
11051108
| ----------- | ---- |
@@ -1112,6 +1115,8 @@ The format string supports the following format chars:
11121115
| `%O` | `Guid` (including short GUIDs*) |
11131116
| `%u` | `uint64` (formatted as a short ID*) |
11141117

1118+
**Named parameters**: You can use `%c:name` to assign a name to a route parameter, which is especially useful for OpenAPI/Swagger documentation and for clarity. For example, `routef "/pet/%i:petId"` will match `/pet/42` and bind `petId` to `42`.
1119+
11151120
*) Please note that the `%O` and `%u` format characters also support URL friendly short GUIDs and IDs.
11161121

11171122
The `%O` format character supports GUIDs in the format of:

samples/EndpointRoutingApp/Program.fs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,17 @@ let handler2 (firstName: string, age: int) : HttpHandler =
1818
let handler3 (a: string, b: string, c: string, d: int) : HttpHandler =
1919
fun (_: HttpFunc) (ctx: HttpContext) -> sprintf "Hello %s %s %s %i" a b c d |> ctx.WriteTextAsync
2020

21+
let handlerNamed (petId: int) : HttpHandler =
22+
fun (_: HttpFunc) (ctx: HttpContext) -> sprintf "PetId: %i" petId |> ctx.WriteTextAsync
23+
2124
let endpoints =
2225
[
2326
subRoute "/foo" [ GET [ route "/bar" (text "Aloha!") ] ]
2427
GET [
2528
route "/" (text "Hello World")
2629
routef "/%s/%i" handler2
2730
routef "/%s/%s/%s/%i" handler3
31+
routef "/pet/%i:petId" handlerNamed
2832
]
2933
GET_HEAD [
3034
route "/foo" (text "Bar")

src/Giraffe/EndpointRouting.fs

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ module RouteTemplateBuilder =
2121

2222
let private shortIdPattern = "([-_0-9A-Za-z]{{10}}[048AEIMQUYcgkosw])"
2323

24-
let private getConstraint (i: int) (c: char) =
25-
let name = sprintf "%c%i" c i
24+
let private getConstraint (i: int) (c: char) (name: string option) =
25+
let name = Option.defaultValue (sprintf "%c%i" c i) name
2626

2727
match c with
2828
| 'b' -> name, sprintf "{%s:bool}" name // bool
@@ -42,9 +42,29 @@ module RouteTemplateBuilder =
4242
let template, mappings = convert i tail
4343
"%" + template, mappings
4444
| '%' :: c :: tail ->
45-
let template, mappings = convert (i + 1) tail
46-
let placeholderName, placeholderTemplate = getConstraint i c
47-
placeholderTemplate + template, (placeholderName, c) :: mappings
45+
match tail with
46+
| ':' :: stail ->
47+
let splitIndex = stail |> List.tryFindIndex (fun c -> c = '/')
48+
49+
match splitIndex with
50+
| Some splitIndex ->
51+
let name, newTail = stail |> List.splitAt splitIndex
52+
53+
let placeholderName, placeholderTemplate =
54+
getConstraint i c (Some(System.String.Concat(Array.ofList (name))))
55+
56+
let template, mappings = convert (i + 1) newTail
57+
placeholderTemplate + template, (placeholderName, c) :: mappings
58+
| None ->
59+
let placeholderName, placeholderTemplate =
60+
getConstraint i c (Some(System.String.Concat(Array.ofList (stail))))
61+
62+
let template, mappings = convert (i + 1) []
63+
placeholderTemplate + template, (placeholderName, c) :: mappings
64+
| _ ->
65+
let placeholderName, placeholderTemplate = getConstraint i c None
66+
let template, mappings = convert (i + 1) tail
67+
placeholderTemplate + template, (placeholderName, c) :: mappings
4868
| c :: tail ->
4969
let template, mappings = convert i tail
5070
c.ToString() + template, mappings

tests/Giraffe.Tests/EndpointRoutingTests.fs

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,3 +151,105 @@ let ``subRouteWithExtensions: GET request returns expected result`` (path: strin
151151

152152
content |> shouldEqual expected
153153
}
154+
155+
[<Theory>]
156+
[<InlineData("/pet/42", "PetId: 42")>]
157+
[<InlineData("/pet/0", "PetId: 0")>]
158+
[<InlineData("/pet/123", "PetId: 123")>]
159+
[<InlineData("/pet/-1", "PetId: -1")>]
160+
[<InlineData("/pet/abc", "Not Found")>]
161+
[<InlineData("/pet/123abc", "Not Found")>]
162+
[<InlineData("/pet/123.456", "Not Found")>]
163+
[<InlineData("/pet/123-456", "Not Found")>]
164+
let ``routef: GET "/pet/%i:petId" returns named parameter`` (path: string, expected: string) =
165+
task {
166+
let endpoints: Endpoint list =
167+
[
168+
GET [ routef "/pet/%i:petId" (fun (petId: int) -> text ($"PetId: {petId}")) ]
169+
GET [
170+
routef
171+
"/foo/%i/bar/%i:barId"
172+
(fun (fooId: int, barId: int) -> text ($"FooId: {fooId}, BarId: {barId}"))
173+
]
174+
]
175+
176+
let notFoundHandler = "Not Found" |> text |> RequestErrors.notFound
177+
178+
let configureApp (app: IApplicationBuilder) =
179+
app.UseRouting().UseGiraffe(endpoints).UseGiraffe(notFoundHandler)
180+
181+
let configureServices (services: IServiceCollection) =
182+
services.AddRouting().AddGiraffe() |> ignore
183+
184+
let request = createRequest HttpMethod.Get path
185+
186+
let! response = makeRequest (fun () -> configureApp) configureServices () request
187+
let! content = response |> readText
188+
content |> shouldEqual expected
189+
}
190+
191+
[<Theory>]
192+
[<InlineData("/foo/123/bar/abc", "FooId: 123, BarId: abc")>]
193+
[<InlineData("/foo/999/bar/789", "FooId: 999, BarId: 789")>]
194+
[<InlineData("/foo/-1/bar/123", "FooId: -1, BarId: 123")>]
195+
[<InlineData("/foo/abc/bar/def", "Not Found")>]
196+
let ``routef: GET "/foo/%i:fooId/bar/%i" returns named and unnamed parameters`` (path: string, expected: string) =
197+
task {
198+
let endpoints: Endpoint list =
199+
[
200+
GET [
201+
routef
202+
"/foo/%i:fooId/bar/%s:barId"
203+
(fun (fooId: int, barId: string) -> text ($"FooId: {fooId}, BarId: {barId}"))
204+
]
205+
]
206+
207+
let notFoundHandler = "Not Found" |> text |> RequestErrors.notFound
208+
209+
let configureApp (app: IApplicationBuilder) =
210+
app.UseRouting().UseGiraffe(endpoints).UseGiraffe(notFoundHandler)
211+
212+
let configureServices (services: IServiceCollection) =
213+
services.AddRouting().AddGiraffe() |> ignore
214+
215+
let request = createRequest HttpMethod.Get path
216+
217+
let! response = makeRequest (fun () -> configureApp) configureServices () request
218+
let! content = response |> readText
219+
content |> shouldEqual expected
220+
}
221+
222+
[<Theory>]
223+
[<InlineData("/foo/123/bar/000/baz/aaa", "FooId: 123, BarId: 0, BazName: aaa")>]
224+
[<InlineData("/foo/999/bar/789/baz/bbb", "FooId: 999, BarId: 789, BazName: bbb")>]
225+
[<InlineData("/foo/-1/bar/123/baz/ccc", "FooId: -1, BarId: 123, BazName: ccc")>]
226+
[<InlineData("/foo/abc/bar/v01/baz/ddd", "Not Found")>]
227+
let ``routef: GET "/foo/%i:fooId/bar/%i/baz/%s" returns named and unnamed parameters``
228+
(path: string, expected: string)
229+
=
230+
task {
231+
let endpoints: Endpoint list =
232+
[
233+
GET [
234+
routef
235+
"/foo/%i:fooId/bar/%i/baz/%s"
236+
(fun (fooId: int, barId: int, bazName: string) ->
237+
text ($"FooId: {fooId}, BarId: {barId}, BazName: {bazName}")
238+
)
239+
]
240+
]
241+
242+
let notFoundHandler = "Not Found" |> text |> RequestErrors.notFound
243+
244+
let configureApp (app: IApplicationBuilder) =
245+
app.UseRouting().UseGiraffe(endpoints).UseGiraffe(notFoundHandler)
246+
247+
let configureServices (services: IServiceCollection) =
248+
services.AddRouting().AddGiraffe() |> ignore
249+
250+
let request = createRequest HttpMethod.Get path
251+
252+
let! response = makeRequest (fun () -> configureApp) configureServices () request
253+
let! content = response |> readText
254+
content |> shouldEqual expected
255+
}

0 commit comments

Comments
 (0)