diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index a7c81c54..3bb1daf7 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -871,7 +871,37 @@ let webApp = route "/bar" >=> text "Bar 2" ]) ]) ``` -**Note:** For both `subRoute` and `subRouteCi` if you wish to have a route that represents a default e.g. `/api/v1` (from the above example) then you need to specify the route as `route ""` not `route "/"` this will not match, as `api/v1/` is a fundamentally different route according to the HTTP specification. +Please note that only the path specified for `subRouteCi` is case insensitive. Nested routes after `subRouteCi` will be evaluated as per definition of each individual route. + +**Note:** If you wish to have a default route for any `subRoute` handler (e.g. `/api/v1` from the above example) then you need to specify the route as `route ""` and not as `route "/"`, because `/api/v1/` is a fundamentally different than `/api/v1` according to the HTTP specification. + +#### subRoutef + +The `subRoutef` http handler is a combination of the `routef` and the `subRoute` http handler: + +```fsharp +let app = + GET >=> choose [ + route "/" >=> text "index" + route "/foo" >=> text "bar" + + subRoutef "/%s/api" (fun lang -> + requiresAuthentication (challenge "Cookie") >=> + choose [ + route "/blah" >=> text "blah" + routef "/%s" (fun n -> text (sprintf "Hello %s! Lang: %s" n lang)) + ]) + setStatusCode 404 >=> text "Not found" ] +``` + +This can be useful when an application has dynamic parameters at the beginning of each route (e.g. language parameter): + +``` +https://example.org/en/users/John +https://example.org/de/users/Ryan +https://example.org/fr/users/Nicky +... +``` #### routePorts diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 76860f86..d9931a73 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,6 +1,12 @@ Release Notes ============= +## Development + +#### New features + +- Added `subRoutef` http handler (see [subRoutef](https://github.com/giraffe-fsharp/Giraffe/blob/master/DOCUMENTATION.md#subroutef)) + ## 1.0.0 First RTM release of Giraffe. diff --git a/src/Giraffe/Routing.fs b/src/Giraffe/Routing.fs index 77ce8745..13a44acf 100644 --- a/src/Giraffe/Routing.fs +++ b/src/Giraffe/Routing.fs @@ -212,4 +212,44 @@ let subRoute (path : string) (handler : HttpHandler) : HttpHandler = /// A Giraffe `HttpHandler` function which can be composed into a bigger web application. let subRouteCi (path : string) (handler : HttpHandler) : HttpHandler = routeStartsWithCi path >=> - handlerWithRootedPath path handler \ No newline at end of file + handlerWithRootedPath path handler + +/// ** Description ** +/// Filters an incoming HTTP request based on a part of the request path (case sensitive). +/// If the sub route matches the incoming HTTP request then the arguments from the `PrintfFormat<...>` will be automatically resolved and passed into the supplied `routeHandler`. +/// +/// ** Supported format chars ** +/// - `%b`: `bool` +/// - `%c`: `char` +/// - `%s`: `string` +/// - `%i`: `int` +/// - `%d`: `int64` +/// - `%f`: `float`/`double` +/// - `%O`: `Guid` +/// +/// Subsequent routing handlers inside the given `handler` function should omit the already validated path. +/// +/// ** Parameters ** +/// - `path`: A format string representing the expected request sub path. +/// - `routeHandler`: A function which accepts a tuple `'T` of the parsed arguments and returns a `HttpHandler` function which will subsequently deal with the request. +/// +/// ** Output ** +/// A Giraffe `HttpHandler` function which can be composed into a bigger web application. +let subRoutef (path : PrintfFormat<_,_,_,_, 'T>) (routeHandler : 'T -> HttpHandler) : HttpHandler = + validateFormat path + fun (next : HttpFunc) (ctx : HttpContext) -> + let paramCount = (path.Value.Split '/').Length + let subPathParts = (getPath ctx).Split '/' + if paramCount > subPathParts.Length then abort + else + let subPath = + subPathParts + |> Array.take paramCount + |> Array.fold (fun state elem -> + if String.IsNullOrEmpty elem + then state + else sprintf "%s/%s" state elem) "" + tryMatchInput path subPath false + |> function + | None -> abort + | Some args -> handlerWithRootedPath subPath (routeHandler args) next ctx \ No newline at end of file diff --git a/tests/Giraffe.Tests/Giraffe.Tests.fsproj b/tests/Giraffe.Tests/Giraffe.Tests.fsproj index 5d4f0bb4..a5780dd9 100644 --- a/tests/Giraffe.Tests/Giraffe.Tests.fsproj +++ b/tests/Giraffe.Tests/Giraffe.Tests.fsproj @@ -34,6 +34,7 @@ + diff --git a/tests/Giraffe.Tests/RoutingTests.fs b/tests/Giraffe.Tests/RoutingTests.fs new file mode 100644 index 00000000..ecb9db32 --- /dev/null +++ b/tests/Giraffe.Tests/RoutingTests.fs @@ -0,0 +1,147 @@ +module Giraffe.Tests.RoutingTests + +open System.IO +open System.Collections.Generic +open Microsoft.AspNetCore.Http +open Xunit +open NSubstitute +open Giraffe + +// --------------------------------- +// subRoutef Tests +// --------------------------------- + +[] +let ``subRoutef: GET "/" returns "Not found"`` () = + let ctx = Substitute.For() + let app = + GET >=> choose [ + subRoutef "/%s/%i" (fun (lang, version) -> + choose [ + route "/foo" >=> text "bar" + routef "/%s" (fun name -> text (sprintf "Hello %s! Lang: %s, Version: %i" name lang version)) + ]) + route "/bar" >=> text "foo" + setStatusCode 404 >=> text "Not found" ] + + ctx.Items.Returns (new Dictionary() :> IDictionary) |> ignore + ctx.Request.Method.ReturnsForAnyArgs "GET" |> ignore + ctx.Request.Path.ReturnsForAnyArgs (PathString("/")) |> ignore + ctx.Response.Body <- new MemoryStream() + let expected = "Not found" + + task { + let! result = app next ctx + + match result with + | None -> assertFailf "Result was expected to be %s" expected + | Some ctx -> Assert.Equal(expected, getBody ctx) + } + +[] +let ``subRoutef: GET "/bar" returns "foo"`` () = + let ctx = Substitute.For() + let app = + GET >=> choose [ + subRoutef "/%s/%i" (fun (lang, version) -> + choose [ + route "/foo" >=> text "bar" + routef "/%s" (fun name -> text (sprintf "Hello %s! Lang: %s, Version: %i" name lang version)) + ]) + route "/bar" >=> text "foo" + setStatusCode 404 >=> text "Not found" ] + + ctx.Items.Returns (new Dictionary() :> IDictionary) |> ignore + ctx.Request.Method.ReturnsForAnyArgs "GET" |> ignore + ctx.Request.Path.ReturnsForAnyArgs (PathString("/bar")) |> ignore + ctx.Response.Body <- new MemoryStream() + let expected = "foo" + + task { + let! result = app next ctx + + match result with + | None -> assertFailf "Result was expected to be %s" expected + | Some ctx -> Assert.Equal(expected, getBody ctx) + } + +[] +let ``subRoutef: GET "/John/5/foo" returns "bar"`` () = + let ctx = Substitute.For() + let app = + GET >=> choose [ + subRoutef "/%s/%i" (fun (lang, version) -> + choose [ + route "/foo" >=> text "bar" + routef "/%s" (fun name -> text (sprintf "Hello %s! Lang: %s, Version: %i" name lang version)) + ]) + route "/bar" >=> text "foo" + setStatusCode 404 >=> text "Not found" ] + + ctx.Items.Returns (new Dictionary() :> IDictionary) |> ignore + ctx.Request.Method.ReturnsForAnyArgs "GET" |> ignore + ctx.Request.Path.ReturnsForAnyArgs (PathString("/John/5/foo")) |> ignore + ctx.Response.Body <- new MemoryStream() + let expected = "bar" + + task { + let! result = app next ctx + + match result with + | None -> assertFailf "Result was expected to be %s" expected + | Some ctx -> Assert.Equal(expected, getBody ctx) + } + +[] +let ``subRoutef: GET "/en/10/Julia" returns "Hello Julia! Lang: en, Version: 10"`` () = + let ctx = Substitute.For() + let app = + GET >=> choose [ + subRoutef "/%s/%i" (fun (lang, version) -> + choose [ + route "/foo" >=> text "bar" + routef "/%s" (fun name -> text (sprintf "Hello %s! Lang: %s, Version: %i" name lang version)) + ]) + route "/bar" >=> text "foo" + setStatusCode 404 >=> text "Not found" ] + + ctx.Items.Returns (new Dictionary() :> IDictionary) |> ignore + ctx.Request.Method.ReturnsForAnyArgs "GET" |> ignore + ctx.Request.Path.ReturnsForAnyArgs (PathString("/en/10/Julia")) |> ignore + ctx.Response.Body <- new MemoryStream() + let expected = "Hello Julia! Lang: en, Version: 10" + + task { + let! result = app next ctx + + match result with + | None -> assertFailf "Result was expected to be %s" expected + | Some ctx -> Assert.Equal(expected, getBody ctx) + } + +[] +let ``subRoutef: GET "/en/10/api/Julia" returns "Hello Julia! Lang: en, Version: 10"`` () = + let ctx = Substitute.For() + let app = + GET >=> choose [ + subRoutef "/%s/%i/api" (fun (lang, version) -> + choose [ + route "/foo" >=> text "bar" + routef "/%s" (fun name -> text (sprintf "Hello %s! Lang: %s, Version: %i" name lang version)) + ]) + route "/bar" >=> text "foo" + setStatusCode 404 >=> text "Not found" ] + + ctx.Items.Returns (new Dictionary() :> IDictionary) |> ignore + ctx.Request.Method.ReturnsForAnyArgs "GET" |> ignore + ctx.Request.Path.ReturnsForAnyArgs (PathString("/en/10/api/Julia")) |> ignore + ctx.Response.Body <- new MemoryStream() + let expected = "Hello Julia! Lang: en, Version: 10" + + task { + let! result = app next ctx + + match result with + | None -> assertFailf "Result was expected to be %s" expected + | Some ctx -> Assert.Equal(expected, getBody ctx) + } \ No newline at end of file