An F# Type provider that generates types suitable for routing in a web application.
[<Literal>]
let routes = """
GET projects/{projectId} as getProject
PUT projects/{foo:string} as updateProject
POST projects/{projectId:int} as createProject
GET projects/{projectId}/comments/{commentId} as getProjectComments
"""
[<Literal>]
let outputPath = __SOURCE_DIRECTORY__ + "\MyRoutes.fs"
type Dummy = IsakSky.RouteProvider<
"MyRoutes", // name of generated type
routes, // string of routes to base routes on
false, // add a generic input type?
false, // add a generic output type?
outputPath>
open MyNamespace.MyModule
let router : MyRoutes =
{ getProject = fun p -> printfn "Hi project %d" p
updateProject = fun ps -> printfn "Hi project string %s" ps
getProjectComments = fun p c -> printfn "Hi project comment %d %d" p c
createProject = fun p -> printfn "Creating project %d" p
notFound = None }
// You can also use ```MyRoutes.Router``` to create the router, which may be more IDE friendly.
You can use int64
, int
, Guid
, or string
as type annotations. The default is int64
.
Now we can use the router like this:
router.DispatchRoute("GET", "projects/4321/comments/1234) // You can also pass a System.Uri
-> "You asked for project 4321 and comment 1234"
You can also build paths in a typed way like this:
let url = MyNamespace.MyModule.getProjectComments 123L 4L
-> "/projects/123/comments/4"
To integrate with the web library you are using, you can specify how the dispatch method should be generated by changing the values for inputType
and returnType
:
inputType | returnType | Dispatch method signature |
---|---|---|
false | false | member DispatchRoute : verb:string * uri:Uri -> unit |
false | true | member DispatchRoute : verb:string * uri:Uri -> 'TReturn |
true | true | member DispatchRoute : context:'TContext * verb:string * uri:Uri -> 'TReturn |
RouteProvider can be installed via Nuget:
Install-Package RouteProvider -Pre
Example using Suave, utilizing both input and return types:
Project | Route definition mechanism | Bidirectional? | Type safety |
---|---|---|---|
ASP.NET MVC | Reflection on attributes and method naming conventions | No | Limited |
Freya | Uri Templates | Yes | None |
Suave.IO | F# sprintf format string | No | Yes |
bidi (Clojure) | Data | Yes | None |
Ruby on Rails | Internal Ruby DSL | Yes | None |
Yesod (Haskell) | Types generated from Route DSL via Template Haskell | Yes | Full |
RouteProvider | Types generated from Route DSL via #F Type Provider | Yes | Full |
You can install it via Nuget:
Install-Package RouteProvider -Pre
It generates FSharp code and saves it to disk to the path you specify. For example, if you wanted to make a router for the following route definitions:
[<Literal>]
let routes = """
GET projects/{projectId} as getProject
GET projects/{projectId}/comments/{commentId} as getProjectComments
PUT projects/{projectId:int} as updateProject
GET projects/statistics
GET people/{name:string} as getPerson
"""
RouteProvider would then generate the following code:
namespace MyNamespace
open System
module MyModule =
let getProject (projectId:int64) =
"projects/" + projectId.ToString()
let getProjectComments (projectId:int64) (commentId:int64) =
"projects/" + projectId.ToString() + "comments/" + commentId.ToString()
let updateProject (projectId:int) =
"projects/" + projectId.ToString()
let GET__projects_statistics =
"projects/statistics/"
let getPerson (name:string) =
"people/" + name
module Internal =
let fakeBaseUri = new Uri("http://a.a")
exception RouteNotMatchedException of string * string
type MyRoutes<'TContext, 'TReturn> =
{ getProject: 'TContext->int64->'TReturn
getProjectComments: 'TContext->int64->int64->'TReturn
updateProject: 'TContext->int->'TReturn
GET__projects_statistics: 'TContext->'TReturn
getPerson: 'TContext->string->'TReturn
notFound: ('TContext->string->string->'TReturn) option }
member inline private this.HandleNotFound(context, verb, path) =
match this.notFound with
| None -> raise (Internal.RouteNotMatchedException (verb, path))
| Some(notFound) -> notFound context verb path
member this.DispatchRoute(context:'TContext, verb:string, path:string) : 'TReturn =
let parts = path.Split('/')
let start = if parts.[0] = "" then 1 else 0
let endOffset = if parts.Length > 0 && parts.[parts.Length - 1] = "" then 1 else 0
match parts.Length - start - endOffset with
| 4 ->
if String.Equals(parts.[0 + start],"projects") then
let mutable projectId = 0L
if Int64.TryParse(parts.[1 + start], &projectId) then
if String.Equals(parts.[2 + start],"comments") then
let mutable commentId = 0L
if Int64.TryParse(parts.[3 + start], &commentId) then
if verb = "GET" then this.getProjectComments context projectId commentId
else this.HandleNotFound(context, verb, path)
else this.HandleNotFound(context, verb, path)
else this.HandleNotFound(context, verb, path)
else this.HandleNotFound(context, verb, path)
else this.HandleNotFound(context, verb, path)
| 2 ->
if String.Equals(parts.[0 + start],"people") then
if verb = "GET" then this.getPerson context (parts.[1 + start])
else this.HandleNotFound(context, verb, path)
elif String.Equals(parts.[0 + start],"projects") then
let mutable int64ArgDepth_1 = 0L
let mutable intArgDepth_1 = 0
if String.Equals(parts.[1 + start],"statistics") then
if verb = "GET" then this.GET__projects_statistics context
else this.HandleNotFound(context, verb, path)
elif Int64.TryParse(parts.[1 + start], &int64ArgDepth_1) then
if verb = "GET" then this.getProject context int64ArgDepth_1
else this.HandleNotFound(context, verb, path)
elif Int32.TryParse(parts.[1 + start], &intArgDepth_1) then
if verb = "PUT" then this.updateProject context intArgDepth_1
else this.HandleNotFound(context, verb, path)
else this.HandleNotFound(context, verb, path)
else this.HandleNotFound(context, verb, path)
| _ ->
this.HandleNotFound(context, verb, path)
member this.DispatchRoute(context:'TContext, verb:string, uri:Uri) : 'TReturn =
// Ensure we have an Absolute Uri, or just about every method on Uri chokes
let uri = if uri.IsAbsoluteUri then uri else new Uri(Internal.fakeBaseUri, uri)
let path = uri.GetComponents(UriComponents.Path, UriFormat.Unescaped)
this.DispatchRoute(context, verb, path)
- @pezipink for the desperately needed new ideas around Type Providers. The RouteProvider rewrite is based on the Injecting Type provider discussed here: https://skillsmatter.com/skillscasts/6159-meta-programming-madness-with-the-mixin-type-provider