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
