Skip to content

Instantly share code, notes, and snippets.

@panesofglass
Last active December 29, 2015 17:59
Show Gist options
  • Save panesofglass/7708181 to your computer and use it in GitHub Desktop.
Save panesofglass/7708181 to your computer and use it in GitHub Desktop.
Schema for a routing type provider
# Use the following example to formulate a schema to generate types for routes.
# Generate either IObservables or MailboxProcessors to which to subscribe or provide a handler.
# Current thought is to use Frank as the handler implementation.
# Remaining question is whether to model "routes" or HTTP resources. I prefer the latter.
/ GET,POST # RootR
/about GET # AboutR
/customers GET,POST
/{id: int} GET,PUT,DELETE
type App = Routes<"path to spec">
App.RootR.Get(fun request -> async { return! process request })
// This sets the implementation for the GET on the root.
// The MbP contains the logic to write the response to the underlying socket stream.
type App = Routes<"path to spec">
let rootGet = App.RootR.Get.Subscribe(fun (request, out) -> let response = process request in writeResponse response out)
let rootPost = App.RootR.Post.Subscribe(fun (request, out) -> let response = process request in writeResponse response out)
// See https://github.com/panesofglass/frank/blob/master/src/Frank.fs#L323
/// Alias `MailboxProcessor<'T>` as `Agent<'T>`.
type Agent<'T> = MailboxProcessor<'T>
/// Messages used by the HTTP resource agent.
type internal ResourceMessage =
| Request of HttpRequestMessage * Stream
| SetHandler of HttpMethod * HttpApplication
| Error of exn
| Shutdown
/// An HTTP resource agent.
type Resource private (uriTemplate, allowedMethods, handlers) =
let onError = new Event<exn>()
let agent = Agent<ResourceMessage>.Start(fun inbox ->
let rec loop handlers = async {
let! msg = inbox.Receive()
match msg with
| Request(request, out) ->
let! response =
match handlers |> List.tryFind (fun (m, _) -> m = request.Method) with
| Some (_, h) -> h request
| None -> ``405 Method Not Allowed`` allowedMethods request
do out.WriteByte(0uy) // TODO: write the response to the out stream
return! loop handlers
| SetHandler(httpMethod, handler) ->
let handlers' =
match allowedMethods |> List.tryFind (fun m -> m = httpMethod) with
| None -> handlers
| Some _ -> (httpMethod, handler)::(List.filter (fun (m,h) -> m <> httpMethod) handlers)
return! loop handlers'
| Error exn ->
onError.Trigger(exn)
return! loop handlers
| Shutdown -> ()
}
loop handlers
)
new (uriTemplate, handlers) =
let allowedMethods = handlers |> List.map fst
Resource(uriTemplate, allowedMethods, handlers)
new (uriTemplate, allowedMethods) =
Resource(uriTemplate, allowedMethods, [])
/// Connect the resource to the request event stream.
/// This method applies a default filter to subscribe only to events
/// matching the `Resource`'s `uriTemplate`.
// NOTE: This should be internal if used in a type provider.
abstract Connect : IObservable<HttpRequestMessage * Stream> -> IDisposable
default x.Connect(observable) =
(observable
|> Observable.filter (fun (r: HttpRequestMessage, _) -> r.RequestUri.AbsolutePath = uriTemplate)
).Subscribe(x)
/// Sets the handler for the specified `HttpMethod`.
/// Ideally, we would expose methods matching the allowed methods.
member x.SetHandler(httpMethod, handler) =
agent.Post <| SetHandler(httpMethod, handler)
/// Provide stream of `exn` for logging purposes.
[<CLIEvent>]
member x.Error = onError.Publish
/// Implement `IObserver` to allow the `Resource` to subscribe to the request event stream.
interface IObserver<HttpRequestMessage * Stream> with
member x.OnNext(value) = agent.Post <| Request value
member x.OnError(exn) = agent.Post <| Error exn
member x.OnCompleted() = agent.Post Shutdown
// TODO: Create a ResourceManager or some form of Supervisor to serve as the App.
// Example:
type App () as x =
// Should this also be an Agent<'T>?
let onRequest = new Event<HttpRequestMessage * Stream>()
let onError = new Event<exn>()
// This shows that strings are used, but they should be hidden behind generated types.
let rootR = Resource("/", [ HttpMethod.Get ])
let aboutR = Resource("/about", [ HttpMethod.Get ])
let customersR = Resource("/customers", [ HttpMethod.Get; HttpMethod.Post ])
let customerR = Resource("/customers/{id:int}", [ HttpMethod.Get; HttpMethod.Put; HttpMethod.Delete ])
let subscriptions = [
rootR.Connect(x :> IObservable<_>)
aboutR.Connect(x :> IObservable<_>)
customersR.Connect(x :> IObservable<_>)
customerR.Connect(x :> IObservable<_>) ]
member x.RootR = rootR
member x.AboutR = aboutR
member x.CustomersR = customersR
member x.CustomerR = customerR
member x.Dispose() =
for disposable in subscriptions do disposable.Dispose()
[<CLIEvent>]
member x.Error = onError.Publish
interface IObservable<HttpRequestMessage * Stream> with
member x.Subscribe(observer) = onRequest.Publish.Subscribe(observer)
interface IObserver<HttpRequestMessage * Stream> with
member x.OnNext(value) = onRequest.Trigger(value)
member x.OnError(exn) = onError.Trigger(exn)
member x.OnCompleted() = () // dispose the resources
interface IDisposable with
member x.Dispose() = x.Dispose()
@panesofglass
Copy link
Author

@ademar yes, you can route any number of ways. What I am attempting here is to provide type safety to HTTP (per spec, not per "real use?). This certainly won't be what everyone wants, especially the ability to replace implementation at runtime. It is also missing examples of providing querystring/form parameters in handler functions. Lastly, this should target OWIN, not System.Net.Http types.

I would argue that the routing mechanism is the reason devs pick a web framework. I certainly don't think this is the only way; it is just what I wish I always had. I'll post a better sample tomorrow.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment