Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feedback requested] WIP: Add HEAD handling to router and controller #273

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 30 additions & 11 deletions src/Saturn/Controller.fs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ module Controller =
type Action =
| Index
| Show
| Exists
| Add
| Edit
| Create
Expand All @@ -32,13 +33,14 @@ module Controller =
let inputSet = Set actions
if inputSet |> Set.contains All then []
else
let allSet = Set [Index;Show;Add;Edit;Create;Update;Patch;Delete;DeleteAll]
let allSet = Set [Index;Show;Exists;Add;Edit;Create;Update;Patch;Delete;DeleteAll]
allSet - inputSet |> Set.toList

///Type representing internal state of the `controller` computation expression
type ControllerState<'Key, 'IndexOutput, 'ShowOutput, 'AddOutput, 'EditOutput, 'CreateOutput, 'UpdateOutput, 'PatchOutput, 'DeleteOutput, 'DeleteAllOutput> = {
type ControllerState<'Key, 'IndexOutput, 'ShowOutput, 'ExistsOutput, 'AddOutput, 'EditOutput, 'CreateOutput, 'UpdateOutput, 'PatchOutput, 'DeleteOutput, 'DeleteAllOutput> = {
Index: (HttpContext -> Task<'IndexOutput>) option
Show: (HttpContext -> 'Key -> Task<'ShowOutput>) option
Exists: (HttpContext -> 'Key -> Task<'ExistsOutput>) option
Add: (HttpContext -> Task<'AddOutput>) option
Edit: (HttpContext -> 'Key -> Task<'EditOutput>) option
Create: (HttpContext -> Task<'CreateOutput>) option
Expand Down Expand Up @@ -104,10 +106,10 @@ module Controller =
/// edit (fun (ctx, id) -> (sprintf "Edit handler no version - %i" id) |> Controller.text ctx)
/// }
/// ```
type ControllerBuilder<'Key, 'IndexOutput, 'ShowOutput, 'AddOutput, 'EditOutput, 'CreateOutput, 'UpdateOutput, 'PatchOutput, 'DeleteOutput, 'DeleteAllOutput> internal () =
type ControllerBuilder<'Key, 'IndexOutput, 'ShowOutput, 'ExistsOutput, 'AddOutput, 'EditOutput, 'CreateOutput, 'UpdateOutput, 'PatchOutput, 'DeleteOutput, 'DeleteAllOutput> internal () =

member __.Yield(_) : ControllerState<'Key, 'IndexOutput, 'ShowOutput, 'AddOutput, 'EditOutput, 'CreateOutput, 'UpdateOutput, 'PatchOutput, 'DeleteOutput, 'DeleteAllOutput> =
{ Index = None; Show = None; Add = None; Edit = None; Create = None; Update = None; Patch = None; Delete = None; DeleteAll = None; NotFoundHandler = None; Version = None; SubControllers = []; Plugs = Map.empty<_,_>; ErrorHandler = (fun _ ex -> raise ex); CaseInsensitive = false }
member __.Yield(_) : ControllerState<'Key, 'IndexOutput, 'ShowOutput, 'ExistsOutput, 'AddOutput, 'EditOutput, 'CreateOutput, 'UpdateOutput, 'PatchOutput, 'DeleteOutput, 'DeleteAllOutput> =
{ Index = None; Show = None; Exists = None; Add = None; Edit = None; Create = None; Update = None; Patch = None; Delete = None; DeleteAll = None; NotFoundHandler = None; Version = None; SubControllers = []; Plugs = Map.empty<_,_>; ErrorHandler = (fun _ ex -> raise ex); CaseInsensitive = false }

///Operation that should render (or return in case of API controllers) list of data
[<CustomOperation("index")>]
Expand All @@ -125,6 +127,14 @@ module Controller =
member x.Show (state, handler: HttpContext -> 'Dependency -> 'Key -> Task<'ShowOutput>) =
{state with Show = Some (x.MapDependencyHandlerToHandler' handler)}

///Operation that should handle a HEAD request and return a bodiless 200 OK or 404 NOT FOUND for a single entry
[<CustomOperation("exists")>]
member __.Exists (state, handler: HttpContext -> 'Key -> Task<'ExistsOutput>) =
{state with Exists = Some handler}

member x.Exists (state, handler: HttpContext -> 'Dependency -> 'Key -> Task<'ExistsOutput>) =
{state with Exists = Some (x.MapDependencyHandlerToHandler' handler)}

///Operation that should render form for adding new item
[<CustomOperation("add")>]
member __.Add (state, handler: HttpContext -> Task<'AddOutput>) =
Expand Down Expand Up @@ -183,7 +193,7 @@ module Controller =

///Define not-found handler for the controller
[<CustomOperation("not_found_handler")>]
member __.NotFoundHandler(state : ControllerState<_,_,_,_,_,_,_,_,_,_>, handler) =
member __.NotFoundHandler(state : ControllerState<_,_,_,_,_,_,_,_,_,_,_>, handler) =
{state with NotFoundHandler = Some handler}

///Define error for the controller
Expand All @@ -206,7 +216,7 @@ module Controller =

///Toggle case insensitve routing
[<CustomOperation("case_insensitive")>]
member __.CaseInsensitive (state : ControllerState<_,_,_,_,_,_,_,_,_,_> ) =
member __.CaseInsensitive (state : ControllerState<_,_,_,_,_,_,_,_,_,_,_> ) =
{state with CaseInsensitive = true}

///Inject a controller into the routing table rooted at a given route. All of that controller's actions will be anchored off of the route as a prefix.
Expand All @@ -226,7 +236,7 @@ module Controller =
{state with Plugs = newplugs}

if actions |> List.contains All then
[Index;Show;Add;Edit;Create;Update;Patch;Delete;DeleteAll] |> List.fold (fun acc e -> addPlug acc e handler) state
[Index;Show;Exists;Add;Edit;Create;Update;Patch;Delete;DeleteAll] |> List.fold (fun acc e -> addPlug acc e handler) state
else
actions |> List.fold (fun acc e -> addPlug acc e handler) state

Expand Down Expand Up @@ -324,12 +334,12 @@ module Controller =

| None -> routeHandler actionHandler

member this.Run (state: ControllerState<'Key, 'IndexOutput, 'ShowOutput, 'AddOutput, 'EditOutput, 'CreateOutput, 'UpdateOutput, 'PatchOutput, 'DeleteOutput, 'DeleteAllOutput>) : HttpHandler =
member this.Run (state: ControllerState<'Key, 'IndexOutput, 'ShowOutput, 'ExistsOutput, 'AddOutput, 'EditOutput, 'CreateOutput, 'UpdateOutput, 'PatchOutput, 'DeleteOutput, 'DeleteAllOutput>) : HttpHandler =
let siteMap = HandlerMap()
let addToSiteMap v p = siteMap.AddPath p v
let keyFormat =
match state with
| { Show = None; Edit = None; Update = None; Delete = None; Patch = None; SubControllers = [] } -> None
| { Show = None; Exists = None; Edit = None; Update = None; Delete = None; Patch = None; SubControllers = [] } -> None
| _ ->
match typeof<'Key> with
| k when k = typeof<bool> -> "/%b"
Expand Down Expand Up @@ -380,6 +390,15 @@ module Controller =
addToSiteMap route
yield this.AddKeyHandler state Show state.Show.Value route
]
yield HEAD >=> choose [
let addToSiteMap = addToSiteMap "HEAD"

if keyFormat.IsSome then
if state.Exists.IsSome then
let route = keyFormat.Value
addToSiteMap route
yield this.AddKeyHandler state Exists state.Exists.Value route
]
yield POST >=> choose [
let addToSiteMap = addToSiteMap "POST"

Expand Down Expand Up @@ -467,4 +486,4 @@ module Controller =
res

///Computation expression used to create controllers
let controller<'Key, 'IndexOutput, 'ShowOutput, 'AddOutput, 'EditOutput, 'CreateOutput, 'UpdateOutput, 'PatchOutput, 'DeleteOutput, 'DeleteAllOutput> = ControllerBuilder<'Key, 'IndexOutput, 'ShowOutput, 'AddOutput, 'EditOutput, 'CreateOutput, 'UpdateOutput, 'PatchOutput, 'DeleteOutput, 'DeleteAllOutput> ()
let controller<'Key, 'IndexOutput, 'ShowOutput, 'ExistsOutput, 'AddOutput, 'EditOutput, 'CreateOutput, 'UpdateOutput, 'PatchOutput, 'DeleteOutput, 'DeleteAllOutput> = ControllerBuilder<'Key, 'IndexOutput, 'ShowOutput, 'ExistsOutput, 'AddOutput, 'EditOutput, 'CreateOutput, 'UpdateOutput, 'PatchOutput, 'DeleteOutput, 'DeleteAllOutput> ()
17 changes: 9 additions & 8 deletions src/Saturn/ControllerEndpoint.fs
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,10 @@ module Controller =
allSet - inputSet |> Set.toList

///Type representing internal state of the `controller` computation expression
type ControllerState<'Key, 'IndexOutput, 'ShowOutput, 'AddOutput, 'EditOutput, 'CreateOutput, 'UpdateOutput, 'PatchOutput, 'DeleteOutput, 'DeleteAllOutput> = {
type ControllerState<'Key, 'IndexOutput, 'ShowOutput, 'ExistsOutput, 'AddOutput, 'EditOutput, 'CreateOutput, 'UpdateOutput, 'PatchOutput, 'DeleteOutput, 'DeleteAllOutput> = {
Index: (HttpContext -> Task<'IndexOutput>) option
Show: (HttpContext -> 'Key -> Task<'ShowOutput>) option
Exists: (HttpContext -> 'Key -> Task<'ExistsOutput>) option
Add: (HttpContext -> Task<'AddOutput>) option
Edit: (HttpContext -> 'Key -> Task<'EditOutput>) option
Create: (HttpContext -> Task<'CreateOutput>) option
Expand Down Expand Up @@ -104,10 +105,10 @@ module Controller =
/// edit (fun (ctx, id) -> (sprintf "Edit handler no version - %i" id) |> Controller.text ctx)
/// }
/// ```
type ControllerBuilder<'Key, 'IndexOutput, 'ShowOutput, 'AddOutput, 'EditOutput, 'CreateOutput, 'UpdateOutput, 'PatchOutput, 'DeleteOutput, 'DeleteAllOutput> internal () =
type ControllerBuilder<'Key, 'IndexOutput, 'ShowOutput, 'ExistsOutput, 'AddOutput, 'EditOutput, 'CreateOutput, 'UpdateOutput, 'PatchOutput, 'DeleteOutput, 'DeleteAllOutput> internal () =

member __.Yield(_) : ControllerState<'Key, 'IndexOutput, 'ShowOutput, 'AddOutput, 'EditOutput, 'CreateOutput, 'UpdateOutput, 'PatchOutput, 'DeleteOutput, 'DeleteAllOutput> =
{ Index = None; Show = None; Add = None; Edit = None; Create = None; Update = None; Patch = None; Delete = None; DeleteAll = None; NotFoundHandler = None; Version = None; SubControllers = []; Plugs = Map.empty<_,_>; ErrorHandler = (fun _ ex -> raise ex); }
member __.Yield(_) : ControllerState<'Key, 'IndexOutput, 'ShowOutput, 'ExistsOutput, 'AddOutput, 'EditOutput, 'CreateOutput, 'UpdateOutput, 'PatchOutput, 'DeleteOutput, 'DeleteAllOutput> =
{ Index = None; Show = None; Exists = None; Add = None; Edit = None; Create = None; Update = None; Patch = None; Delete = None; DeleteAll = None; NotFoundHandler = None; Version = None; SubControllers = []; Plugs = Map.empty<_,_>; ErrorHandler = (fun _ ex -> raise ex); }

///Operation that should render (or return in case of API controllers) list of data
[<CustomOperation("index")>]
Expand Down Expand Up @@ -183,7 +184,7 @@ module Controller =

///Define not-found handler for the controller
[<CustomOperation("not_found_handler")>]
member __.NotFoundHandler(state : ControllerState<_,_,_,_,_,_,_,_,_,_>, handler) =
member __.NotFoundHandler(state : ControllerState<_,_,_,_,_,_,_,_,_,_,_>, handler) =
{state with NotFoundHandler = Some handler}

///Define error for the controller
Expand Down Expand Up @@ -375,7 +376,7 @@ module Controller =

endpoint |> List.map (fun e -> e actionHandler)

member this.Run (state: ControllerState<'Key, 'IndexOutput, 'ShowOutput, 'AddOutput, 'EditOutput, 'CreateOutput, 'UpdateOutput, 'PatchOutput, 'DeleteOutput, 'DeleteAllOutput>) : Endpoint list =
member this.Run (state: ControllerState<'Key, 'IndexOutput, 'ShowOutput, 'ExistsOutput, 'AddOutput, 'EditOutput, 'CreateOutput, 'UpdateOutput, 'PatchOutput, 'DeleteOutput, 'DeleteAllOutput>) : Endpoint list =
let isKnownKey =
match state with
| { Show = None; Edit = None; Update = None; Delete = None; Patch = None; SubControllers = [] } -> false
Expand Down Expand Up @@ -469,7 +470,7 @@ module Controller =
]

///Computation expression used to create controllers
let controller<'Key, 'IndexOutput, 'ShowOutput, 'AddOutput, 'EditOutput, 'CreateOutput, 'UpdateOutput, 'PatchOutput, 'DeleteOutput, 'DeleteAllOutput> = ControllerBuilder<'Key, 'IndexOutput, 'ShowOutput, 'AddOutput, 'EditOutput, 'CreateOutput, 'UpdateOutput, 'PatchOutput, 'DeleteOutput, 'DeleteAllOutput> ()
let controller<'Key, 'IndexOutput, 'ShowOutput, 'ExistsOutput, 'AddOutput, 'EditOutput, 'CreateOutput, 'UpdateOutput, 'PatchOutput, 'DeleteOutput, 'DeleteAllOutput> = ControllerBuilder<'Key, 'IndexOutput, 'ShowOutput, 'ExistsOutput, 'AddOutput, 'EditOutput, 'CreateOutput, 'UpdateOutput, 'PatchOutput, 'DeleteOutput, 'DeleteAllOutput> ()

///Computation expression used to create HttpHandlers representing subcontrollers.
let subcontroller<'Key, 'IndexOutput, 'ShowOutput, 'AddOutput, 'EditOutput, 'CreateOutput, 'UpdateOutput, 'PatchOutput, 'DeleteOutput, 'DeleteAllOutput> = Saturn.Controller.ControllerBuilder<'Key, 'IndexOutput, 'ShowOutput, 'AddOutput, 'EditOutput, 'CreateOutput, 'UpdateOutput, 'PatchOutput, 'DeleteOutput, 'DeleteAllOutput> ()
let subcontroller<'Key, 'IndexOutput, 'ShowOutput, 'ExistsOutput, 'AddOutput, 'EditOutput, 'CreateOutput, 'UpdateOutput, 'PatchOutput, 'DeleteOutput, 'DeleteAllOutput> = Saturn.Controller.ControllerBuilder<'Key, 'IndexOutput, 'ShowOutput, 'ExistsOutput, 'AddOutput, 'EditOutput, 'CreateOutput, 'UpdateOutput, 'PatchOutput, 'DeleteOutput, 'DeleteAllOutput> ()
36 changes: 36 additions & 0 deletions src/Saturn/Router.fs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ module Router =
///Type representing route type, used in internal state of the `application` computation expression
type RouteType =
| Get
| Head
| GetOrHead
| Post
| Put
| Delete
Expand Down Expand Up @@ -145,6 +147,8 @@ module Router =
let v =
match typ with
| RouteType.Get -> "GET"
| RouteType.Head -> "HEAD"
| RouteType.GetOrHead -> "GET_HEAD"
| RouteType.Post -> "POST"
| RouteType.Put -> "PUT"
| RouteType.Patch -> "PATCH"
Expand Down Expand Up @@ -177,6 +181,8 @@ module Router =
routes, routesf

let gets, getsf = generateRoutes RouteType.Get
let heads, headsf = generateRoutes RouteType.Head
let getOrHeads, getOrHeadsf = generateRoutes RouteType.GetOrHead
let posts, postsf = generateRoutes RouteType.Post
let patches, patchesf = generateRoutes RouteType.Patch

Expand Down Expand Up @@ -212,6 +218,16 @@ module Router =
for e in getsf do
yield GET >=> e

for e in heads do
yield HEAD >=> e
for e in headsf do
yield HEAD >=> e

for e in getOrHeads do
yield GET_HEAD >=> e
for e in getOrHeadsf do
yield GET_HEAD >=> e

for e in posts do
yield POST >=> e
for e in postsf do
Expand Down Expand Up @@ -257,6 +273,26 @@ module Router =
member __.GetF(state, path : PrintfFormat<_,_,_,_,'f>, action) : RouterState =
addRouteF RouteType.Get state path action

///Adds handler for `HEAD` request.
[<CustomOperation("head")>]
member __.Head(state, path : string, action: HttpHandler) : RouterState =
addRoute RouteType.Head state path action

///Adds handler for `HEAD` request.
[<CustomOperation("headf")>]
member __.HeadF(state, path : PrintfFormat<_,_,_,_,'f>, action) : RouterState =
addRouteF RouteType.Head state path action

///Adds handler for either `GET` or `HEAD` request.
[<CustomOperation("get_head")>]
member __.GetOrHead(state, path : string, action: HttpHandler) : RouterState =
addRoute RouteType.GetOrHead state path action

///Adds handler for either `GET` or `HEAD` request.
[<CustomOperation("get_headf")>]
member __.GetOrHeadF(state, path : PrintfFormat<_,_,_,_,'f>, action) : RouterState =
addRouteF RouteType.GetOrHead state path action

///Adds handler for `POST` request.
[<CustomOperation("post")>]
member __.Post(state, path : string, action: HttpHandler) : RouterState =
Expand Down