Last active
December 29, 2015 17:59
-
-
Save panesofglass/7708181 to your computer and use it in GitHub Desktop.
Schema for a routing type provider
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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 | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // 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() |
I just tried this without the provided types and found it quite nice, though the provided types would really help!
@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
I think I understand the idea but I'm not sure I would want to do this. We can already code routing very easily by other means.
The HTML/Razor Provider I do find attractive because allows to embed XML and compose it!
But then again I may be missing the point :)