Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Minor documentation and bug fixes.

  • Loading branch information...
commit 71887f49ff401a6ec01b1ec69d5a32253d2fa757 1 parent 1662e1d
Ryan Riley authored
Showing with 55 additions and 139 deletions.
  1. +35 −35 README.md
  2. +2 −1  samples/Echo.fsx
  3. +3 −1 samples/HelloWorld.fsx
  4. +15 −102 src/Frank.fs
View
70 README.md
@@ -17,11 +17,10 @@ Indeed, if you search the web, you're likely to find a large number of approache
When starting with Frank, I wanted to try to find a way to define an HTTP application
using pure functions and function composition. The closest I found was the following:
- type HttpRequestHandler = HttpContent -> Async<HttpResponseMessage>
- type HttpApplication = HttpRequestMessage -> HttpRequestHandler
+ type HttpApplication = HttpRequestMessage -> Async<HttpResponseMessage>
- let orElse right left = fun request -> Option.orElse (left request) (right request)
- let inline (<|>) left right = orElse right left
+ let orElse left right = fun request -> Option.orElse (left request) (right request)
+ let inline (<|>) left right = orElse left right
The last of these was a means for merging multiple applications together into a single
application. This allowed for a nice symmetry and elegance in that everything you composed
@@ -30,13 +29,13 @@ applications to specific methods or uri patterns.
A "Hello, world!" application using these signatures would look like the following:
- let helloWorld _ _ = respond HttpStatusCode.OK ignore <| Some "Hello, world!"
+ let helloWorld request =
+ async.Return <| HttpResponseMessage.ReplyTo(request, "Hello, world!")
A simple echo handler that returns the same input as it received might look like the following:
- let echo _ (content: HttpContent) =
- respond HttpStatusCode.OK (``Content-Type`` "text/plain")
- <| new StringContent(content.ReadAsStringAsync().Result)
+ let echo (request: HttpRequestMessage) =
+ async.Return <| HttpResponseMessage.ReplyTo(request, request.Content, ``Content-Type`` "text/plain")
### Define an HTTP Resource
@@ -67,7 +66,7 @@ type as a `Foo list`, which is where the typical MVC approach goes wrong.
type HttpResource =
{ Uri: string
Methods: string list
- Handler: HttpRequestMessage -> HttpRequestHandler option }
+ Handler: HttpRequestMessage -> Async<HttpResponseMessage> option }
with
member x.Invoke(request) =
match x.Handler request with
@@ -86,24 +85,29 @@ A compositional approach to type mapping and handler design.
Here, the actual function shows clearly that we are really using
the `id` function to return the very same result.
- let echo2Core = id
+ let echo2Transform = id
-The `echo2MapFrom` maps the incoming request to a value that can be used
-within the actual computation, or `echo2Core` in this example.
+The `echo2ReadRequest` maps the incoming request to a value that can be used
+within the actual computation, or `echo2Transform` in this example.
- let echo2MapFrom _ (content: HttpContent) = request.Content.ReadAsStringAsync().Result
+ let echo2ReadRequest (request: HttpRequestMessage) =
+ async { return! request.Content.AsyncReadAsString() }
-The `echo2MapTo` maps the outgoing message body to an HTTP response.
+The `echo2Respond` maps the outgoing message body to an HTTP response.
- let echo2MapTo body =
- respond HttpStatusCode.OK (``Content-Type`` "text/plain")
- <| new StringContent(body)
+ let echo2Respond request body =
+ async {
+ return HttpResponseMessage.ReplyTo(request, body, ``Content-Type`` "text/plain") }
This `echo2` is the same in principle as `echo` above, except that the
logic for the message transform deals only with the concrete types
about which it cares and isn't bothered by the transformations.
- let echo2 = echo2MapFrom >> echo2Core >> echo2MapTo
+ let echo2 request =
+ async {
+ let! content = echo2ReadRequest request
+ let body = echo2Transform content
+ return echo2Respond request body }
Create a `HttpResource` instance at the root of the site that responds to `POST`.
@@ -120,23 +124,19 @@ Check the samples for more examples of these combinators.
### Define a Middleware
-Middlewares follow a Russian-doll model for wrapping resource handlers with additional functionality.
-Frank middlewares take an `HttpApplication` and return an `HttpApplication`.
-The `Frank.Middleware` module defines several, simple middlewares, such as the `log` middleware that
-intercepts logs incoming requests and the time taken to respond:
-
- let log app = fun (request : HttpRequestMessage) content -> async {
- let sw = System.Diagnostics.Stopwatch.StartNew()
- let! response = app request content
- printfn "Received a %A request from %A. Responded in %i ms."
- request.Method.Method request.RequestUri.PathAndQuery sw.ElapsedMilliseconds
- sw.Reset()
- return response }
-
-The most likely place to insert middlewares is the outer edge of your application.
-However, since middlewares are themselves just `HttpApplication`s, you can compose them into a Frank application
-at any level. Want to support logging only on one troublesome resource? No problem. Expose the resource
-as an application, wrap it in the log middleware, and insert it into the larger application as you did before.
+Middlewares follow a Russian-doll model for wrapping resource handlers with additional functionality. Frank middlewares take an `HttpApplication` and return an `HttpApplication`.
+The `Frank.Middleware` module defines several, simple middlewares, such as the `log` middleware that intercepts logs incoming requests and the time taken to respond:
+
+ let log app = fun (request : HttpRequestMessage) ->
+ async {
+ let sw = System.Diagnostics.Stopwatch.StartNew()
+ let! response = app request
+ printfn "Received a %A request from %A. Responded in %i ms."
+ request.Method.Method request.RequestUri.PathAndQuery sw.ElapsedMilliseconds
+ sw.Reset()
+ return response }
+
+The most likely place to insert middlewares is the outer edge of your application. However, since middlewares are themselves just `HttpApplication`s, you can compose them into a Frank application at any level. Want to support logging only on one troublesome resource? No problem. Expose the resource as an application, wrap it in the log middleware, and insert it into the larger application as you did before.
## Hosting
View
3  samples/Echo.fsx
@@ -20,6 +20,7 @@ See LICENSE.txt for details.
#r @"..\packages\WebApi.0.6.0\lib\40-Full\Microsoft.Server.Common.dll"
#r @"..\packages\WebApi.0.6.0\lib\40-Full\Microsoft.ApplicationServer.Http.dll"
#r @"..\packages\WebApi.Enhancements.0.6.0\lib\40-Full\Microsoft.ApplicationServer.HttpEnhancements.dll"
+#load @"..\src\System.Net.Http.fs"
#load @"..\src\Frank.fs"
#load @"..\src\Hosting.fs"
@@ -30,7 +31,7 @@ open Frank.Hosting
// Respond with the request content, if any.
let echo (request: HttpRequestMessage) =
- respond HttpStatusCode.OK (``Content-Type`` "text/plain") <| new StringContent(request.Content.ReadAsStringAsync().Result)
+ async.Return <| HttpResponseMessage.ReplyTo(request, request.Content, ``Content-Type`` "text/plain")
// Create an application from an `HttpResource` that only responds to `POST` requests.
// Try sending a GET or other method to see a `405 Method Not Allowed` response.
View
4 samples/HelloWorld.fsx
@@ -20,6 +20,7 @@ See LICENSE.txt for details.
#r @"..\packages\WebApi.0.6.0\lib\40-Full\Microsoft.Server.Common.dll"
#r @"..\packages\WebApi.0.6.0\lib\40-Full\Microsoft.ApplicationServer.Http.dll"
#r @"..\packages\WebApi.Enhancements.0.6.0\lib\40-Full\Microsoft.ApplicationServer.HttpEnhancements.dll"
+#load @"..\src\System.Net.Http.fs"
#load @"..\src\Frank.fs"
#load @"..\src\Hosting.fs"
@@ -28,7 +29,8 @@ open System.Net.Http
open Frank
open Frank.Hosting
-let helloWorld _ = respond HttpStatusCode.OK ignore <| new StringContent("Hello, world!")
+let helloWorld request =
+ async.Return <| HttpResponseMessage.ReplyTo(request, "Hello, world!")
let config = WebApi.configure helloWorld
let baseUri = "http://localhost:1000/"
View
117 src/Frank.fs
@@ -33,48 +33,27 @@ open Swensen.Unquote.Assertions
// ## Define the web application interface
(*
-One may define a web application interface using a large variety of signatures.
-Indeed, if you search the web, you're likely to find a large number of approaches.
-When starting with `Frank`, I wanted to try to find a way to define an HTTP application
-using pure functions and function composition. The closest I found was the following:
+One may define a web application interface using a large variety of signatures. Indeed, if you search the web, you're likely to find a large number of approaches. When starting with `Frank`, I wanted to try to find a way to define an HTTP application using pure functions and function composition. The closest I found was the following:
type HttpApplication = HttpRequestMessage -> Async<HttpResponseMessage>
- let orElse right left = fun request -> Option.orElse (left request) (right request)
- let inline (<|>) left right = orElse right left
-
-The last of these was a means for merging multiple applications together into a single
-application. This allowed for a nice symmetry and elegance in that everything you composed
-would always have the same signature. Additional functions would allow you to map
-applications to specific methods or uri patterns.
-
-Alas, this approach works only so well. HTTP is a rich communication specification.
-The simplicity and elegance of a purely functional approach quickly loses the ability
-to communicate back options to the client. For instance, given the above, how do you
-return a meaningful `405 Method Not Allowed` response? The HTTP specification requires
-that you list the allowed methods, but if you merge all the logic for selecting an
-application into the functions, there is no easy way to recall all the allowed methods,
-short of trying them all. You could require that the developer add the list of used
-methods, but that, too, misses the point that the application should be collecting this
-and helping the developer by taking care of all of the nuts and bolts items
-
-The next approach I tried involved using a tuple of a list of allowed HTTP methods and
-the application handler, which used the merged function approach described above for
-actually executing the application. However, once again, there are limitations. This
-structure accurately represents a resource, but it does not allow for multiple resources
-to coexist side-by-side. Another tuple of uri pattern matching expressions could wrap
-a list of these method * handler tuples, but at this point I realized I would be better
-served by using real types and thus arrived at the signatures below.
-
-You'll see the signatures above are still mostly present, though they have been changed
-to better fit the signatures below.
+ let orElse left right = fun request -> Option.orElse (left request) (right request)
+ let inline (<|>) left right = orElse left right
+
+These signatures represent both the application signature and a means for merging multiple applications together into a single application. This allowed for a nice symmetry and elegance in that everything you composed would always have the same signature. Additional functions allow you to map applications to specific methods or uri patterns.
+
+Alas, this approach works only so well. HTTP is a rich communication specification. The simplicity and elegance of a purely functional approach quickly loses the ability to communicate back options to the client. For instance, given the above, how do you return a meaningful `405 Method Not Allowed` response? The HTTP specification requires that you list the allowed methods, but if you merge all the logic for selecting an application into the functions, there is no easy way to recall all the allowed methods, short of trying them all. You could require that the developer add the list of used methods, but that, too, misses the point that the application should be collecting this and helping the developer by taking care of all of the nuts and bolts items.
+
+The next approach I tried involved using a tuple of a list of allowed HTTP methods and the application handler, which used the merged function approach described above for actually executing the application. However, once again, there are limitations. This structure accurately represents a resource, but it does not allow for multiple resources to coexist side-by-side. Another tuple of uri pattern matching expressions could wrap a list of these method * handler tuples, but at this point I realized I would be better served by using real types and thus arrived at the signatures below.
+
+You'll see the signatures above are still mostly present, though they have been changed to better fit the signatures below.
*)
// `HttpApplication` defines the contract for processing any request.
-// An application takes an `HttpRequestMessage` and returns an `HttpRequestHandler`.
+// An application takes an `HttpRequestMessage` and returns an `HttpRequestHandler` asynchronously.
type HttpApplication = HttpRequestMessage -> Async<HttpResponseMessage>
-// ## HTTP Response Combinators
+// ## HTTP Response Header Combinators
// Headers are added using the `Reader` monad. If F# allows mutation, why do we need the monad?
// First of all, it allows for the explicit declaration of side effects. Second, a number
@@ -365,10 +344,10 @@ let delete handler = mapResourceHandler(HttpMethod.Delete.Method, handler)
// The intent here is to build a resource, with at most one handler per HTTP method. This goes
// against a lot of the "RESTful" approaches that just merge a bunch of method handlers at
// different URI addresses.
-let orElse right left =
+let orElse left right =
fst left @ fst right,
fun request -> Option.orElse (snd left request) (snd right request)
-let inline (<|>) left right = left |> orElse right
+let inline (<|>) left right = orElse left right
let route path handler =
{ Uri = path
@@ -471,69 +450,3 @@ type FrankHandler private () =
{ new FrankHandler() with
override this.SendAsync(request, cancelationToken) =
app(request, cancelationToken) } :> DelegatingHandler
-
-//let formatter = FormUrlEncodedMediaTypeFormatter() :> Formatting.MediaTypeFormatter
-//let testBody = dict [("foo", "bar");("bar", "baz")]
-//let createTestRequest() =
-// new HttpRequestMessage(
-// HttpMethod.Post, Uri("http://frankfs.net/"),
-// Version = Version(1,1),
-// Content = new FormUrlEncodedContent(testBody))
-//
-//// HttpRequestMessage -> HttpResponseMessage
-//let echo (request : HttpRequestMessage) =
-// let body = request.Content.ReadAs<JsonValue>(seq { yield formatter })
-// let response = new HttpResponseMessage<JsonValue>(body, HttpStatusCode.OK)
-// response.Content.Headers.ContentType <- Headers.MediaTypeHeaderValue.Parse("application/x-www-form-urlencoded")
-// response
-//
-//[<Test>]
-//let ``test echo should return a response of 200 OK``() =
-// let actual = echo <| createTestRequest()
-// test <@ actual.StatusCode = HttpStatusCode.OK @>
-//
-//[<Test>]
-//let ``test echo should return a response with one header for Content_Type of application/x-www-form-urlencoded``() =
-// let expected = Headers.MediaTypeHeaderValue.Parse("application/x-www-form-urlencoded")
-// let actual = echo <| createTestRequest()
-// test <@ actual.Content.Headers.ContentType = expected @>
-//
-//[<Test>]
-//let ``test echo should return a response with a body of echoing the request body``() =
-// let response = echo <| createTestRequest()
-// let actual = response.Content.ReadAs()
-// test <@ actual?foo = "bar" @>
-// test <@ actual?bar = "baz" @>
-//
-//let formatter = FormUrlEncodedMediaTypeFormatter() :> Formatting.MediaTypeFormatter
-//let testBody = dict [("foo", "bar");("bar", "baz")]
-//let createTestRequest() =
-// new HttpRequestMessage(
-// HttpMethod.Post, Uri("http://frankfs.net/"),
-// Version = Version(1,1),
-// Content = new FormUrlEncodedContent(testBody))
-//
-//// HttpRequestMessage -> HttpResponseMessage
-//let echo (request : HttpRequestMessage) =
-// let body = request.Content.ReadAs<JsonValue>(seq { yield formatter })
-// let response = new HttpResponseMessage<JsonValue>(body, HttpStatusCode.OK)
-// response.Content.Headers.ContentType <- Headers.MediaTypeHeaderValue.Parse("application/x-www-form-urlencoded")
-// response
-//
-//[<Test>]
-//let ``test echo should return a response of 200 OK``() =
-// let actual = echo <| createTestRequest()
-// test <@ actual.StatusCode = HttpStatusCode.OK @>
-//
-//[<Test>]
-//let ``test echo should return a response with one header for Content_Type of application/x-www-form-urlencoded``() =
-// let expected = Headers.MediaTypeHeaderValue.Parse("application/x-www-form-urlencoded")
-// let actual = echo <| createTestRequest()
-// test <@ actual.Content.Headers.ContentType = expected @>
-//
-//[<Test>]
-//let ``test echo should return a response with a body of echoing the request body``() =
-// let response = echo <| createTestRequest()
-// let actual = response.Content.ReadAs()
-// test <@ actual?foo = "bar" @>
-// test <@ actual?bar = "baz" @>
Please sign in to comment.
Something went wrong with that request. Please try again.