Skip to content

Commit

Permalink
Added subRoutef http handler
Browse files Browse the repository at this point in the history
Fixes #223
  • Loading branch information
dustinmoris committed Feb 11, 2018
1 parent 6c5f255 commit 224f4a3
Show file tree
Hide file tree
Showing 5 changed files with 226 additions and 2 deletions.
32 changes: 31 additions & 1 deletion DOCUMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
6 changes: 6 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
42 changes: 41 additions & 1 deletion src/Giraffe/Routing.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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
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
1 change: 1 addition & 0 deletions tests/Giraffe.Tests/Giraffe.Tests.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
<Compile Include="Helpers.fs" />
<Compile Include="FormatExpressionTests.fs" />
<Compile Include="HttpHandlerTests.fs" />
<Compile Include="RoutingTests.fs" />
<Compile Include="AuthTests.fs" />
<Compile Include="GiraffeViewEngineTests.fs" />
<Compile Include="HttpContextExtensionsTests.fs" />
Expand Down
147 changes: 147 additions & 0 deletions tests/Giraffe.Tests/RoutingTests.fs
Original file line number Diff line number Diff line change
@@ -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
// ---------------------------------

[<Fact>]
let ``subRoutef: GET "/" returns "Not found"`` () =
let ctx = Substitute.For<HttpContext>()
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<obj,obj>() :> IDictionary<obj,obj>) |> 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)
}

[<Fact>]
let ``subRoutef: GET "/bar" returns "foo"`` () =
let ctx = Substitute.For<HttpContext>()
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<obj,obj>() :> IDictionary<obj,obj>) |> 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)
}

[<Fact>]
let ``subRoutef: GET "/John/5/foo" returns "bar"`` () =
let ctx = Substitute.For<HttpContext>()
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<obj,obj>() :> IDictionary<obj,obj>) |> 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)
}

[<Fact>]
let ``subRoutef: GET "/en/10/Julia" returns "Hello Julia! Lang: en, Version: 10"`` () =
let ctx = Substitute.For<HttpContext>()
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<obj,obj>() :> IDictionary<obj,obj>) |> 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)
}

[<Fact>]
let ``subRoutef: GET "/en/10/api/Julia" returns "Hello Julia! Lang: en, Version: 10"`` () =
let ctx = Substitute.For<HttpContext>()
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<obj,obj>() :> IDictionary<obj,obj>) |> 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)
}

0 comments on commit 224f4a3

Please sign in to comment.