Skip to content

Commit fe6bc74

Browse files
NinoFlorisKrzysztof-Cieslak
authored andcommitted
Removes stringConvert, we now completely rely on giraffe to convert our route segments, which means ShortID and ShortGuid automatically work as they should, also adds quite some tests for (sub) routing
1 parent e76b284 commit fe6bc74

File tree

3 files changed

+95
-74
lines changed

3 files changed

+95
-74
lines changed

src/Saturn/Controller.fs

Lines changed: 18 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -173,22 +173,22 @@ module Controller =
173173
member this.Run (state: ControllerState<'Key, 'IndexOutput, 'ShowOutput, 'AddOutput, 'EditOutput, 'CreateOutput, 'UpdateOutput, 'PatchOutput, 'DeleteOutput, 'DeleteAllOutput>) : HttpHandler =
174174
let siteMap = HandlerMap()
175175
let addToSiteMap v p = siteMap.AddPath p v
176-
let keyFormat, stringConvert =
176+
let keyFormat =
177177
match state with
178-
| { Show = None; Edit = None; Update = None; Delete = None; SubControllers = [] } -> None, None
178+
| { Show = None; Edit = None; Update = None; Delete = None; SubControllers = [] } -> None
179179
| _ ->
180-
// Parameterizing `string` here so we don't constrain 'Key to SRTP ^Key when we would have used it later on
181180
match typeof<'Key> with
182-
| k when k = typeof<bool> -> "/%b", string<bool> :> obj
183-
| k when k = typeof<char> -> "/%c", string<char> :> obj
184-
| k when k = typeof<string> -> "/%s", string<string> :> obj
185-
| k when k = typeof<int32> -> "/%i", string<int32> :> obj
186-
| k when k = typeof<int64> -> "/%d", string<int64> :> obj
187-
| k when k = typeof<float> -> "/%f", string<float> :> obj
188-
| k when k = typeof<Guid> -> "/%O", string<Guid> :> obj
181+
| k when k = typeof<bool> -> "/%b"
182+
| k when k = typeof<char> -> "/%c"
183+
| k when k = typeof<string> -> "/%s"
184+
| k when k = typeof<int32> -> "/%i"
185+
| k when k = typeof<int64> -> "/%d"
186+
| k when k = typeof<float> -> "/%f"
187+
| k when k = typeof<Guid> -> "/%O"
188+
| k when k = typeof<uint64> -> "/%u"
189189
| k -> failwithf
190190
"Type %A is not a supported type for controller<'T>. Supported types include bool, char, float, guid int32, int64, and string" k
191-
|> fun (keyFormat, stringConvert) -> (Some keyFormat, Some (unbox<'Key -> string> stringConvert))
191+
|> Some
192192

193193
let initialController =
194194
let trailingSlashHandler : HttpHandler =
@@ -261,6 +261,7 @@ module Controller =
261261
let addToSiteMap = addToSiteMap "DELETE"
262262

263263
if state.DeleteAll.IsSome then
264+
addToSiteMap "/"
264265
yield this.AddHandlerWithRoute state DeleteAll state.DeleteAll.Value trailingSlashHandler
265266

266267
if keyFormat.IsSome then
@@ -285,20 +286,14 @@ module Controller =
285286
let controllerWithSubs =
286287
choose [
287288
if keyFormat.IsSome then
288-
let stringConvert = stringConvert.Value
289-
for (sPath, sCs) in state.SubControllers do
290-
if not (sPath.StartsWith("/")) then
291-
failwith (sprintf "Subcontroller route '%s' is not valid, these routes should start with a '/'." sPath)
292-
293-
let path = keyFormat.Value
294-
let dummy = sCs (unbox<'Key> Unchecked.defaultof<'Key>)
295-
siteMap.Forward (path + sPath) "" dummy
289+
for (subPath, sCs) in state.SubControllers do
290+
if not (subPath.StartsWith("/")) then
291+
failwith (sprintf "Subcontroller route '%s' is not valid, these routes should start with a '/'." subPath)
296292

297-
yield routef (PrintfFormat<'Key -> obj,_,_,_,'Key> (path + sPath))
298-
(fun input -> subRoute ("/" + (stringConvert input) + sPath) (sCs (unbox<'Key> input)))
293+
let fullPath = keyFormat.Value + subPath
299294

300-
yield routef (PrintfFormat<'Key -> string -> obj,_,_,_,'Key * string> (path + sPath + "%s"))
301-
(fun (input, _) -> subRoute ("/" + (stringConvert input) + sPath) (sCs (unbox<'Key> input)))
295+
siteMap.Forward fullPath "" (sCs (Unchecked.defaultof<'Key>))
296+
yield subRoutef (PrintfFormat<'Key -> obj,_,_,_,'Key> fullPath) (unbox<'Key> >> sCs)
302297

303298
yield controllerWithErrorHandler
304299
]

tests/Saturn.UnitTests/ControllerTests.fs

Lines changed: 52 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -2,32 +2,43 @@ module ControllerTests
22

33
open Expecto
44
open Saturn
5-
open Giraffe
65
open Giraffe.GiraffeViewEngine
76
open FSharp.Control.Tasks.V2.ContextInsensitive
87
open Microsoft.Extensions.Primitives
98
open Microsoft.AspNetCore.Http
109
open System
1110

12-
let createAction =
13-
fun ctx ->
14-
"Create" |> Controller.text ctx
15-
16-
let updateAction =
17-
fun ctx id -> (sprintf "Update %i" id) |> Controller.text ctx
11+
let createAction id ctx =
12+
match id with
13+
| Some id -> sprintf "Create %i" id
14+
| None -> "Create"
15+
|> Controller.text ctx
16+
17+
let updateAction id ctx subId =
18+
match id with
19+
| Some id -> sprintf "Update %i %i" id subId
20+
| None -> sprintf "Update %i" subId
21+
|> Controller.text ctx
22+
23+
let testSubController (id: int) = controller {
24+
create (createAction (Some id))
25+
update (updateAction (Some id))
26+
}
1827

1928
let testController = controller {
20-
create createAction
21-
update updateAction
29+
subController "/sub" testSubController
30+
31+
create (createAction None)
32+
update (updateAction None)
2233
}
2334

2435
let basicTemplate =
25-
html [] [
26-
head [] []
27-
body [] [
28-
h1 [] [ encodedText "Hello, world!" ]
29-
]
30-
]
36+
html [] [
37+
head [] []
38+
body [] [
39+
h1 [] [ encodedText "Hello, world!" ]
40+
]
41+
]
3142

3243
let implicitNodeToHtmlTestController = controller {
3344
index (fun _ -> task { return basicTemplate })
@@ -38,44 +49,42 @@ let explicitNodeToHtmlTestController = controller {
3849
}
3950

4051
let implicitStringToHtmlTestController = controller {
41-
index (fun _ -> task { return GiraffeViewEngine.renderHtmlNode basicTemplate})
52+
index (fun _ -> task { return renderHtmlNode basicTemplate})
4253
}
4354

55+
let responseTestCase = responseTestCase testController
56+
4457
[<Tests>]
4558
let tests =
4659
testList "Controller Tests" [
47-
testCase "create works" <| fun _ ->
48-
let ctx = getEmptyContext "POST" ""
49-
let expected = "Create"
60+
testCase "subController Update works" <|
61+
responseTestCase "PUT" "/1/sub/2" "Update 1 2"
5062

51-
try
52-
let result = testController next ctx |> runTask
53-
match result with
54-
| None -> failtestf "Result was expected to be %s, but was %A" expected result
55-
| Some ctx ->
56-
Expect.equal (getBody ctx) expected "Result should be equal"
57-
with ex -> failtestf "failed because %A" ex
63+
testCase "subController Create trailing slash works" <|
64+
responseTestCase "POST" "/1/sub/" "Create 1"
5865

59-
testCase "update works" <| fun _ ->
60-
let ctx = getEmptyContext "PUT" "/1"
61-
let expected = "Update 1"
66+
testCase "subController Create no trailing slash works" <|
67+
responseTestCase "POST" "/1/sub" "Create 1"
6268

63-
try
64-
let result = testController next ctx |> runTask
65-
match result with
66-
| None -> failtestf "Result was expected to be %s, but was %A" expected result
67-
| Some ctx ->
68-
Expect.equal (getBody ctx) expected "Result should be equal"
69+
testCase "Create trailing slash works" <|
70+
responseTestCase "POST" "/" "Create"
6971

70-
with ex -> failtestf "failed because %A" ex
72+
testCase "Create no trailing slash works" <|
73+
responseTestCase "POST" "" "Create"
74+
75+
testCase "Update POST works" <|
76+
responseTestCase "POST" "/1" "Update 1"
77+
78+
testCase "Update PUT works" <|
79+
responseTestCase "PUT" "/1" "Update 1"
7180

7281
testCase "deleteAll works" <| fun _ ->
7382
let expectedStatusCode = 204
7483
let expectedString = "deleted"
7584
let mutable plugged = ""
7685
let deleteAll = fun (ctx: HttpContext) ->
7786
task {
78-
do ctx.SetStatusCode 204
87+
ctx.Response.StatusCode <- 204
7988
return (Some ctx)
8089
}
8190
let deleteController = controller {
@@ -98,7 +107,7 @@ let tests =
98107
let mutable plugged = ""
99108
let deleteAll = fun (ctx: HttpContext) ->
100109
task {
101-
do ctx.SetStatusCode 204
110+
ctx.Response.StatusCode <- 204
102111
return (Some ctx)
103112
}
104113
let deleteController = controller {
@@ -118,33 +127,27 @@ let tests =
118127
testCase "plugs should only fire once" <| fun _ ->
119128
let deleteAll = fun (ctx: HttpContext) ->
120129
task {
121-
do ctx.SetStatusCode 204
130+
ctx.Response.StatusCode <- 204
122131
return (Some ctx)
123132
}
124133
let mutable count = 0
125134
let controllerWithPlugs =
126135
controller {
127-
create createAction
128-
update updateAction
136+
create (createAction None)
137+
update (updateAction None)
129138
delete_all deleteAll
130139
plug [All] (fun next ctx -> count <- count + 1; next ctx)
131140
}
132141
try
133142
let postEmpty = getEmptyContext "POST" "" |> controllerWithPlugs next |> runTask
134143
Expect.equal count 1 "Count should be 1"
135-
match postEmpty with
136-
| None -> failtestf "Result was expected to be %s, but was %A" "Create" postEmpty
137-
| Some ctx ->
138-
Expect.equal (getBody ctx) "Create" "Result should be equal"
144+
expectResponse "Create" postEmpty
139145
getEmptyContext "POST" "/" |> controllerWithPlugs next |> runTask |> ignore
140146
Expect.equal count 2 "Count should be 2"
141147
getEmptyContext "POST" "/1" |> controllerWithPlugs next |> runTask |> ignore
142148
Expect.equal count 3 "Count should be 3"
143149
let putResult = getEmptyContext "PUT" "/1" |> controllerWithPlugs next |> runTask
144-
match putResult with
145-
| None -> failtestf "Result was expected to be %s, but was %A" "Create" postEmpty
146-
| Some ctx ->
147-
Expect.equal (getBody ctx) "Update 1" "Result should be equal"
150+
expectResponse "Update 1" putResult
148151
Expect.equal count 4 "Count should be 4"
149152
let deleteAllResult = getEmptyContext "DELETE" "" |> controllerWithPlugs next |> runTask
150153
match deleteAllResult with

tests/Saturn.UnitTests/Helpers.fs

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ open System.IO
88
open System.Net.Http
99
open System.Text
1010
open NSubstitute
11-
11+
open System.Collections.Generic
12+
open Microsoft.AspNetCore.Http.Features
13+
open Expecto.Tests
14+
open Expecto
1215

1316
let getEmptyContext (method: string) (path : string) =
1417
let ctx = Substitute.For<HttpContext>()
@@ -20,6 +23,12 @@ let getEmptyContext (method: string) (path : string) =
2023
ctx.Request.Path.ReturnsForAnyArgs (PathString(path)) |> ignore
2124
ctx.Request.QueryString.ReturnsForAnyArgs (QueryString "") |>ignore
2225

26+
// Essential for Giraffe subrouting work.
27+
ctx.Items.ReturnsForAnyArgs(Dictionary<_,_>()) |> ignore
28+
29+
// For posterity, not needed, yet.
30+
ctx.Features.ReturnsForAnyArgs(FeatureCollection()) |> ignore
31+
2332
ctx.Response.Body <- new MemoryStream()
2433
ctx
2534

@@ -47,4 +56,18 @@ let readText (response : HttpResponseMessage) =
4756

4857
let readBytes (response : HttpResponseMessage) =
4958
response.Content.ReadAsByteArrayAsync()
50-
|> runTask
59+
|> runTask
60+
61+
let expectResponse expected actual =
62+
match actual with
63+
| None -> failtestf "Result was expected to be %s, but was %A" expected actual
64+
| Some ctx ->
65+
Expect.equal (getBody ctx) expected "Result should be equal"
66+
67+
let responseTestCase handler method path expected () =
68+
let ctx = getEmptyContext method path
69+
70+
try
71+
let result = handler next ctx |> runTask
72+
expectResponse expected result
73+
with ex -> failtestf "failed because %A" ex

0 commit comments

Comments
 (0)