Skip to content

Commit 49f120d

Browse files
feat: add routeBind support to Giraffe.EndpointRouting (#709)
* feat: add routeBind support to Giraffe.EndpointRouting * docs: update routeBind use * Apply suggestions from code review * format: run fantomas on EnpointRoutingTests.fs
1 parent b304625 commit 49f120d

3 files changed

Lines changed: 81 additions & 1 deletion

File tree

DOCUMENTATION.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1175,7 +1175,7 @@ let webApp =
11751175
]
11761176
```
11771177

1178-
The `routeBind<'T>` http handler can also contain valid `Regex` code to match a variety of different routes.
1178+
The `routeBind<'T>` http handler from the `Giraffe.Routing` module can also contain valid `Regex` code to match a variety of different routes.
11791179

11801180
For example by definition (according to the spec) a route with a trailing slash **is not** the same as the equivalent route without a trailing slash. Therefore it is perfectly valid if a web server doesn't serve (or serves a different response) for the following two routes:
11811181

@@ -1214,6 +1214,8 @@ routeBind<Blah> "/p/{foo}/{bar}(/*)" blahHandler
12141214

12151215
For a complete list of valid `Regex` codes please visit the official [Regular Expression Language Reference](https://docs.microsoft.com/en-us/dotnet/standard/base-types/regular-expression-language-quick-reference).
12161216

1217+
In case you are using `Giraffe.EndpointRouting`, the request path is handled by ASP.NET Core’s Endpoint Routing infrastructure. We therefore recommend reviewing the following sections of the official documentation: [url matching](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/routing?view=aspnetcore-10.0#url-matching) and [route constraints](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/routing?view=aspnetcore-10.0#route-constraints).
1218+
12171219
#### routeStartsWith
12181220

12191221
Sometimes it can be useful to pre-filter a route in order to enable certain functionality which should only be applied to a specific collection of routes.

src/Giraffe/EndpointRouting.fs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,8 @@ module private RequestDelegateBuilder =
150150

151151
[<AutoOpen>]
152152
module Routers =
153+
open System.Collections.Generic
154+
open Microsoft.Extensions.Primitives
153155

154156
type HttpVerb =
155157
| GET
@@ -274,6 +276,30 @@ module Routers =
274276
let routef (path: PrintfFormat<_, _, _, _, 'T>) (routeHandler: 'T -> HttpHandler) : Endpoint =
275277
routefWithExtensions (id) (path) (routeHandler)
276278

279+
let routeBindWithExtensions<'T>
280+
(configureEndpoint: ConfigureEndpoint)
281+
(path: string)
282+
(routeHandler: 'T -> HttpHandler)
283+
: Endpoint =
284+
285+
let bindRouteHandler (handler: 'T -> HttpHandler) : HttpHandler =
286+
fun next ctx ->
287+
let routeData =
288+
ctx.GetRouteData().Values
289+
|> Seq.map (fun kvp -> KeyValuePair(kvp.Key, StringValues(kvp.Value :?> string)))
290+
|> Dictionary<string, StringValues>
291+
292+
match ModelParser.tryParse<'T> None routeData with
293+
| Ok model -> handler model next ctx
294+
| Error _ -> skipPipeline
295+
296+
let newHandler = (bindRouteHandler routeHandler)
297+
298+
SimpleEndpoint(HttpVerb.NotSpecified, path, newHandler, configureEndpoint)
299+
300+
let routeBind<'T> (path: string) (routeHandler: 'T -> HttpHandler) : Endpoint =
301+
routeBindWithExtensions<'T> (id) (path) (routeHandler)
302+
277303
let subRouteWithExtensions
278304
(configureEndpoint: ConfigureEndpoint)
279305
(path: string)

tests/Giraffe.Tests/EndpointRoutingTests.fs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ open Xunit
77
open Giraffe
88
open Giraffe.EndpointRouting
99
open System.Net.Http
10+
open System.Net
1011

1112
// ---------------------------------
1213
// routef Tests
@@ -253,3 +254,54 @@ let ``routef: GET "/foo/%i:fooId/bar/%i/baz/%s" returns named and unnamed parame
253254
let! content = response |> readText
254255
content |> shouldEqual expected
255256
}
257+
258+
[<CLIMutable>]
259+
type Name = { First: string; Last: string }
260+
261+
[<CLIMutable>]
262+
type Person = { Name: Name; Age: int }
263+
264+
[<Theory>]
265+
[<InlineData("/p/John/Doe/32", HttpStatusCode.OK, "Name.First: John, Name.Last: Doe, Age: 32")>]
266+
[<InlineData("/p/John%20Paul/Doe/32", HttpStatusCode.OK, "Name.First: John Paul, Name.Last: Doe, Age: 32")>]
267+
[<InlineData("/p/John%20Paul/Doe/32/", HttpStatusCode.OK, "Name.First: John Paul, Name.Last: Doe, Age: 32")>]
268+
[<InlineData("/p/John/Doe/9111222333", HttpStatusCode.UnprocessableEntity, "")>]
269+
[<InlineData("/p/John/Doe/not-a-number", HttpStatusCode.UnprocessableEntity, "")>]
270+
[<InlineData("/p/John/Doe//", HttpStatusCode.NotFound, "Not Found")>]
271+
let ``routebind: GET "/p/{Name.First}/{Name.Last}/{Age}" returns person object``
272+
(path: string, expectedStatus: HttpStatusCode, expectedContent: string)
273+
=
274+
task {
275+
let endpoints: Endpoint list =
276+
[
277+
GET [
278+
routeBindWithExtensions<Person>
279+
(fun eb -> eb.WithOrder 1)
280+
"/p/{Name.First}/{Name.Last}/{Age}"
281+
(fun (person: Person) ->
282+
text ($"Name.First: {person.Name.First}, Name.Last: {person.Name.Last}, Age: {person.Age}")
283+
)
284+
routefWithExtensions
285+
(fun eb -> eb.WithOrder 2)
286+
"/p/%s:firstName/%s:lastName/%d:age"
287+
(fun (firstName: string, lastName: string, age: int64) ->
288+
text ($"firstName: {firstName}, lastName: {lastName}, age: {age}")
289+
)
290+
]
291+
]
292+
293+
let notFoundHandler = "Not Found" |> text |> RequestErrors.notFound
294+
295+
let configureApp (app: IApplicationBuilder) =
296+
app.UseRouting().UseGiraffe(endpoints).UseGiraffe(notFoundHandler)
297+
298+
let configureServices (services: IServiceCollection) =
299+
services.AddRouting().AddGiraffe() |> ignore
300+
301+
let request = createRequest HttpMethod.Get path
302+
303+
let! response = makeRequest (fun () -> configureApp) configureServices () request
304+
let! content = response |> isStatus expectedStatus |> readText
305+
306+
content |> shouldEqual expectedContent
307+
}

0 commit comments

Comments
 (0)