Posted
over 13 years
ago
by
[email protected] (Mauricio Scheffer)
A couple of posts ago I introduced FsConneg, a stand-alone HTTP content negotiation library written in F#. One of my goals in making it stand-alone is that it could be reused across projects, maybe eventually getting integrated into Frank or
... [More]
MonoRail3. Here I will show some potential integrations with Figment. Let's start with a trivial function: let connegAction _ = "hello world"
We want to bind this to an URL (i.e. GETting the URL would return "hello world" obviously), and negotiate the response media type with the user agent. The server will support XML and JSON.
We can build a dispatch table with the supported media types and corresponding actions:
let writers = [
["text/xml"; "application/xml"], Result.xml
["application/json"], Result.json
]
Just as a reminder, Result.xml and Result.json are of type 'a -> FAction, that is, they take some value and return an action where the value is serialized as XML or JSON respectively.
Wrapping actions
Now with FsConneg and this table, we write a generic function that wraps the action with content negotiation (this is all framework-level code):
let internal accepted (ctx: ControllerContext) =
ctx.Request.Headers.["Accept"]
let negotiateActionMediaType writers action =
let servedMedia = List.collect fst writers
let bestOf = accepted >> FsConneg.bestMediaType servedMedia >> Option.map fst
let findWriterFor mediaType = List.find (fst >> List.exists ((=)mediaType)) >> snd
fun ctx ->
let a =
match bestOf ctx with
| Some mediaType ->
let writer = writers |> findWriterFor mediaType
action >>= writer >>. vary "Accept"
| _ -> Result.notAcceptable
a ctx
Briefly, this function takes a table of acceptable media types and associated writers (just like the table we created above) and a "partial" action, and returns an action where the media type is negotiated with the user agent.
Armed with this function, let's bind the negotiated action to an URL:
get "conneg1" (negotiateActionMediaType writers connegAction)
For a second URL we'd also like to offer text/html. Here's a simple parameterized Wing Beats page:
let wbpage title =
[e.Html [
e.Head [
e.Title [ &title ]
]
e.Body [
e.H1 [ &title ]
]
]]
We want to make this an action:
let html = wbpage >> wbview
I defined wbview in a previous article, it's not instrumental to this post. What's important is that html is a function string -> FAction so we can now add it to our dispatch table:
let conneg2writers = (["text/html"], html)::writers
and bind it to an URL:
get "conneg2" (negotiateActionMediaType conneg2writers connegAction)
Using routing
An entirely different approach is to use routing to select the appropriate 'writer' (or serializer, or formatter, whatever you want to call it)
let ifConneg3Get = ifMethodIsGet &&. ifPathIs "conneg3"
action (ifConneg3Get &&. ifAcceptsAny ["application/xml"; "text/xml"]) (connegAction >>= Result.xml)
I think this snippet is pretty intuitive, even if you're not familiar with Figment or functional programming. I'll explain anyway:
ifMethodIsGet and ifPathIs are routing functions built into Figment. The &&. operator composes these routing functions as expected, i.e. the resulting routing function must satisfy both conditions. This is explained in more detail in my introduction to Figment.
The >>= operator is a monadic bind. The function
connegAction >>= Result.xml
is equivalent to:
result {
let! result = connegAction
return! Result.xml result }
or:
fun ctx ->
let result = connegAction ctx
Result.xml result ctx
Except the first one is evidently more concise. I explained this in more detail in my last post.
ifAcceptsAny uses FsConneg to determine if any of the media types in the list is acceptable to the client. Its definition is quite simple:
let ifAcceptsAny media =
fun (ctx: HttpContextBase, _) ->
let acceptable = FsConneg.negotiateMediaType media ctx.Request.Headers.["Accept"]
acceptable.Length > 0
Similarly, let's add JSON and HTML support:
action (ifConneg3Get &&. ifAcceptsAny ["application/json"]) (connegAction >>= Result.json)
action (ifConneg3Get &&. ifAcceptsAny ["text/html"]) (connegAction >>= html)
We close by stating that all other media types are not acceptable:
action ifConneg3Get Result.notAcceptable
The HTTP RFC says it's ok to respond with some non-acceptable media type, so you could also use this to define a default media type instead of a "not acceptable".
The important thing to notice about this last example is that using routing like this doesn't yield proper content negotiation. If a user-agent requests with "Accept: application/json, application/xml;q=0.8" (i.e. prefers application/json), the code above will respond with application/xml, disregarding the client's preferences, simply because the action for application/xml was defined before application/json.
Many frameworks don't handle this properly. If you're planning to build a RESTful application I recommend testing the framework you'll use for this. For example, OpenRasta does the right thing, but Lift had issues until some months ago, and WCF Web API doesn't handle it correctly at the moment.
Using extensions
A common way to select a response media type is using extensions in the URL. Twitter used to do this, until they scrapped XML support altogether. MySpace still does it. Similarly, others use a query string parameter to select the media type, like last.fm.
This isn't really content negotiation as defined by HTTP, but some people do it in the name of simplicity, or to work around client issues. It could be considered as part of a client-driven negotiation, though.
At any rate, implementing extension-driven media types is quite easy. Similarly to the first example, we can build a dispatch table of extensions and then act on it:
let extensions = [
"xml", Result.xml
"json", Result.json
"html", html
]
for ext,writer in extensions do
let ifConneg4 = ifPathIsf "conneg4.%s" ext
action (ifMethodIsGet &&. ifConneg4) (connegAction >>= writer)
Using extensions + conneg
Some prefer a compromise between conneg and extensions, by implementing an extensionless URL that supports content negotiation and then the same URL with extensions as a means to override conneg and work around possible client issues, or just selecting a media type without messing with headers.
For our example we'd want an URL /conneg5 that supports content negotiation, plus /conneg5.xml, /conneg5.json and /conneg5.html to force a particular media type.
As with the previous approaches, let's build a table:
let writers = [
"xml", ["application/xml"; "text/xml"], Result.xml
"json", ["application/json"], Result.json
"html", ["text/html"], html
]
Now let's map the extensions, just as in the last example:
let basePath = "conneg5"
for ext,_,writer in writers do
let ifBasePath = ifPathIsf "%s.%s" basePath ext
action (ifMethodIsGet &&. ifBasePath) (connegAction >>= writer)
Finally, the conneg'd URL:
let mediaTypes = List.map (fun (_,a,b) -> a,b) writers
let ifBasePath = ifPathIs basePath
action (ifMethodIsGet &&. ifBasePath) (negotiateActionMediaType mediaTypes connegAction)
If you do this for all your actions you could easily extract this to a reusable function.
Final words
As I said before, these are all potential integrations. I specifically want these libraries to be as non-opinionated as possible. But at the same time, I want to provide all the tools to let the developer easily create her own opinions/conventions to fit her project, using library calls and standard language constructs instead of having to learn framework extension points. For example, notice that the dispatch tables I explained are all regular lists of strings and functions, which are then mapped and iterated just like any other list. More on this in a future post.
All code posted here is part of the FigmentPlayground repository. [Less]
|
Posted
over 13 years
ago
by
[email protected] (Mauricio Scheffer)
A couple of posts ago I introduced FsConneg, a stand-alone HTTP content negotiation library written in F#. One of my goals in making it stand-alone is that it could be reused across projects, maybe eventually getting integrated into Frank or
... [More]
MonoRail3. Here I will show some potential integrations with Figment. Let's start with a trivial function: let connegAction _ = "hello world"
We want to bind this to an URL (i.e. GETting the URL would return "hello world" obviously), and negotiate the response media type with the user agent. The server will support XML and JSON.
We can build a dispatch table with the supported media types and corresponding actions:
let writers = [
["text/xml"; "application/xml"], Result.xml
["application/json"], Result.json
]
Just as a reminder, Result.xml and Result.json are of type 'a -> FAction, that is, they take some value and return an action where the value is serialized as XML or JSON respectively.
Wrapping actions
Now with FsConneg and this table, we write a generic function that wraps the action with content negotiation (this is all framework-level code):
let internal accepted (ctx: ControllerContext) =
ctx.Request.Headers.["Accept"]
let negotiateActionMediaType writers action =
let servedMedia = List.collect fst writers
let bestOf = accepted >> FsConneg.bestMediaType servedMedia >> Option.map fst
let findWriterFor mediaType = List.find (fst >> List.exists ((=)mediaType)) >> snd
fun ctx ->
let a =
match bestOf ctx with
| Some mediaType ->
let writer = writers |> findWriterFor mediaType
action >>= writer >>. vary "Accept"
| _ -> Result.notAcceptable
a ctx
Briefly, this function takes a table of acceptable media types and associated writers (just like the table we created above) and a "partial" action, and returns an action where the media type is negotiated with the user agent.
Armed with this function, let's bind the negotiated action to an URL:
get "conneg1" (negotiateActionMediaType writers connegAction)
For a second URL we'd also like to offer text/html. Here's a simple parameterized Wing Beats page:
let wbpage title =
[e.Html [
e.Head [
e.Title [ &title ]
]
e.Body [
e.H1 [ &title ]
]
]]
We want to make this an action:
let html = wbpage >> wbview
I defined wbview in a previous article, it's not instrumental to this post. What's important is that html is a function string -> FAction so we can now add it to our dispatch table:
let conneg2writers = (["text/html"], html)::writers
and bind it to an URL:
get "conneg2" (negotiateActionMediaType conneg2writers connegAction)
Using routing
An entirely different approach is to use routing to select the appropriate 'writer' (or serializer, or formatter, whatever you want to call it)
let ifConneg3Get = ifMethodIsGet &&. ifPathIs "conneg3"
action (ifConneg3Get &&. ifAcceptsAny ["application/xml"; "text/xml"]) (connegAction >>= Result.xml)
I think this snippet is pretty intuitive, even if you're not familiar with Figment or functional programming. I'll explain anyway:
ifMethodIsGet and ifPathIs are routing functions built into Figment. The &&. operator composes these routing functions as expected, i.e. the resulting routing function must satisfy both conditions. This is explained in more detail in my introduction to Figment.
The >>= operator is a monadic bind. The function
connegAction >>= Result.xml
is equivalent to:
result {
let! result = connegAction
return! Result.xml result }
or:
fun ctx ->
let result = connegAction ctx
Result.xml result ctx
Except the first one is evidently more concise. I explained this in more detail in my last post.
ifAcceptsAny uses FsConneg to determine if any of the media types in the list is acceptable to the client. Its definition is quite simple:
let ifAcceptsAny media =
fun (ctx: HttpContextBase, _) ->
let acceptable = FsConneg.negotiateMediaType media ctx.Request.Headers.["Accept"]
acceptable.Length > 0
Similarly, let's add JSON and HTML support:
action (ifConneg3Get &&. ifAcceptsAny ["application/json"]) (connegAction >>= Result.json)
action (ifConneg3Get &&. ifAcceptsAny ["text/html"]) (connegAction >>= html)
We close by stating that all other media types are not acceptable:
action ifConneg3Get Result.notAcceptable
The HTTP RFC says it's ok to respond with some non-acceptable media type, so you could also use this to define a default media type instead of a "not acceptable".
The important thing to notice about this last example is that using routing like this doesn't yield proper content negotiation. If a user-agent requests with "Accept: application/json, application/xml;q=0.8" (i.e. prefers application/json), the code above will respond with application/xml, disregarding the client's preferences, simply because the action for application/xml was defined before application/json.
Many frameworks don't handle this properly. If you're planning to build a RESTful application I recommend testing the framework you'll use for this. For example, OpenRasta does the right thing, but Lift had issues until some months ago, and WCF Web API doesn't handle it correctly at the moment.
Using extensions
A common way to select a response media type is using extensions in the URL. Twitter used to do this, until they scrapped XML support altogether. MySpace still does it. Similarly, others use a query string parameter to select the media type, like last.fm.
This isn't really content negotiation as defined by HTTP, but some people do it in the name of simplicity, or to work around client issues. It could be considered as part of a client-driven negotiation, though.
At any rate, implementing extension-driven media types is quite easy. Similarly to the first example, we can build a dispatch table of extensions and then act on it:
let extensions = [
"xml", Result.xml
"json", Result.json
"html", html
]
for ext,writer in extensions do
let ifConneg4 = ifPathIsf "conneg4.%s" ext
action (ifMethodIsGet &&. ifConneg4) (connegAction >>= writer)
Using extensions + conneg
Some prefer a compromise between conneg and extensions, by implementing an extensionless URL that supports content negotiation and then the same URL with extensions as a means to override conneg and work around possible client issues, or just selecting a media type without messing with headers.
For our example we'd want an URL /conneg5 that supports content negotiation, plus /conneg5.xml, /conneg5.json and /conneg5.html to force a particular media type.
As with the previous approaches, let's build a table:
let writers = [
"xml", ["application/xml"; "text/xml"], Result.xml
"json", ["application/json"], Result.json
"html", ["text/html"], html
]
Now let's map the extensions, just as in the last example:
let basePath = "conneg5"
for ext,_,writer in writers do
let ifBasePath = ifPathIsf "%s.%s" basePath ext
action (ifMethodIsGet &&. ifBasePath) (connegAction >>= writer)
Finally, the conneg'd URL:
let mediaTypes = List.map (fun (_,a,b) -> a,b) writers
let ifBasePath = ifPathIs basePath
action (ifMethodIsGet &&. ifBasePath) (negotiateActionMediaType mediaTypes connegAction)
If you do this for all your actions you could easily extract this to a reusable function.
Final words
As I said before, these are all potential integrations. I specifically want these libraries to be as non-opinionated as possible. But at the same time, I want to provide all the tools to let the developer easily create her own opinions/conventions to fit her project, using library calls and standard language constructs instead of having to learn framework extension points. For example, notice that the dispatch tables I explained are all regular lists of strings and functions, which are then mapped and iterated just like any other list. More on this in a future post.
All code posted here is part of the FigmentPlayground repository. [Less]
|
Posted
over 13 years
ago
by
[email protected] (Mauricio Scheffer)
A couple of posts ago I introduced FsConneg, a stand-alone HTTP content negotiation library written in F#. One of my goals in making it stand-alone is that it could be reused across projects, maybe eventually getting integrated into Frank or
... [More]
MonoRail3. Here I will show some potential integrations with Figment. Let's start with a trivial function: let connegAction _ = "hello world"
We want to bind this to an URL (i.e. GETting the URL would return "hello world" obviously), and negotiate the response media type with the user agent. The server will support XML and JSON.
We can build a dispatch table with the supported media types and corresponding actions:
let writers = [
["text/xml"; "application/xml"], Result.xml
["application/json"], Result.json
]
Just as a reminder, Result.xml and Result.json are of type 'a -> FAction, that is, they take some value and return an action where the value is serialized as XML or JSON respectively.
Wrapping actions
Now with FsConneg and this table, we write a generic function that wraps the action with content negotiation (this is all framework-level code):
let internal accepted (ctx: ControllerContext) =
ctx.Request.Headers.["Accept"]
let negotiateActionMediaType writers action =
let servedMedia = List.collect fst writers
let bestOf = accepted >> FsConneg.bestMediaType servedMedia >> Option.map fst
let findWriterFor mediaType = List.find (fst >> List.exists ((=)mediaType)) >> snd
fun ctx ->
let a =
match bestOf ctx with
| Some mediaType ->
let writer = writers |> findWriterFor mediaType
action >>= writer >>. vary "Accept"
| _ -> Result.notAcceptable
a ctx
Briefly, this function takes a table of acceptable media types and associated writers (just like the table we created above) and a "partial" action, and returns an action where the media type is negotiated with the user agent.
Armed with this function, let's bind the negotiated action to an URL:
get "conneg1" (negotiateActionMediaType writers connegAction)
For a second URL we'd also like to offer text/html. Here's a simple parameterized Wing Beats page:
let wbpage title =
[e.Html [
e.Head [
e.Title [ &title ]
]
e.Body [
e.H1 [ &title ]
]
]]
We want to make this an action:
let html = wbpage >> wbview
I defined wbview in a previous article, it's not instrumental to this post. What's important is that html is a function string -> FAction so we can now add it to our dispatch table:
let conneg2writers = (["text/html"], html)::writers
and bind it to an URL:
get "conneg2" (negotiateActionMediaType conneg2writers connegAction)
Using routing
An entirely different approach is to use routing to select the appropriate 'writer' (or serializer, or formatter, whatever you want to call it)
let ifConneg3Get = ifMethodIsGet &&. ifPathIs "conneg3"
action (ifConneg3Get &&. ifAcceptsAny ["application/xml"; "text/xml"]) (connegAction >>= Result.xml)
I think this snippet is pretty intuitive, even if you're not familiar with Figment or functional programming. I'll explain anyway:
ifMethodIsGet and ifPathIs are routing functions built into Figment. The &&. operator composes these routing functions as expected, i.e. the resulting routing function must satisfy both conditions. This is explained in more detail in my introduction to Figment.
The >>= operator is a monadic bind. The function
connegAction >>= Result.xml
is equivalent to:
result {
let! result = connegAction
return! Result.xml result }
or:
fun ctx ->
let result = connegAction ctx
Result.xml result ctx
Except the first one is evidently more concise. I explained this in more detail in my last post.
ifAcceptsAny uses FsConneg to determine if any of the media types in the list is acceptable to the client. Its definition is quite simple:
let ifAcceptsAny media =
fun (ctx: HttpContextBase, _) ->
let acceptable = FsConneg.negotiateMediaType media ctx.Request.Headers.["Accept"]
acceptable.Length > 0
Similarly, let's add JSON and HTML support:
action (ifConneg3Get &&. ifAcceptsAny ["application/json"]) (connegAction >>= Result.json)
action (ifConneg3Get &&. ifAcceptsAny ["text/html"]) (connegAction >>= html)
We close by stating that all other media types are not acceptable:
action ifConneg3Get Result.notAcceptable
The HTTP RFC says it's ok to respond with some non-acceptable media type, so you could also use this to define a default media type instead of a "not acceptable".
The important thing to notice about this last example is that using routing like this doesn't yield proper content negotiation. If a user-agent requests with "Accept: application/json, application/xml;q=0.8" (i.e. prefers application/json), the code above will respond with application/xml, disregarding the client's preferences, simply because the action for application/xml was defined before application/json.
Many frameworks don't handle this properly. If you're planning to build a RESTful application I recommend testing the framework you'll use for this. For example, OpenRasta does the right thing, but Lift had issues until some months ago, and WCF Web API doesn't handle it correctly at the moment.
Using extensions
A common way to select a response media type is using extensions in the URL. Twitter used to do this, until they scrapped XML support altogether. MySpace still does it. Similarly, others use a query string parameter to select the media type, like last.fm.
This isn't really content negotiation as defined by HTTP, but some people do it in the name of simplicity, or to work around client issues. It could be considered as part of a client-driven negotiation, though.
At any rate, implementing extension-driven media types is quite easy. Similarly to the first example, we can build a dispatch table of extensions and then act on it:
let extensions = [
"xml", Result.xml
"json", Result.json
"html", html
]
for ext,writer in extensions do
let ifConneg4 = ifPathIsf "conneg4.%s" ext
action (ifMethodIsGet &&. ifConneg4) (connegAction >>= writer)
Using extensions + conneg
Some prefer a compromise between conneg and extensions, by implementing an extensionless URL that supports content negotiation and then the same URL with extensions as a means to override conneg and work around possible client issues, or just selecting a media type without messing with headers.
For our example we'd want an URL /conneg5 that supports content negotiation, plus /conneg5.xml, /conneg5.json and /conneg5.html to force a particular media type.
As with the previous approaches, let's build a table:
let writers = [
"xml", ["application/xml"; "text/xml"], Result.xml
"json", ["application/json"], Result.json
"html", ["text/html"], html
]
Now let's map the extensions, just as in the last example:
let basePath = "conneg5"
for ext,_,writer in writers do
let ifBasePath = ifPathIsf "%s.%s" basePath ext
action (ifMethodIsGet &&. ifBasePath) (connegAction >>= writer)
Finally, the conneg'd URL:
let mediaTypes = List.map (fun (_,a,b) -> a,b) writers
let ifBasePath = ifPathIs basePath
action (ifMethodIsGet &&. ifBasePath) (negotiateActionMediaType mediaTypes connegAction)
If you do this for all your actions you could easily extract this to a reusable function.
Final words
As I said before, these are all potential integrations. I specifically want these libraries to be as non-opinionated as possible. But at the same time, I want to provide all the tools to let the developer easily create her own opinions/conventions to fit her project, using library calls and standard language constructs instead of having to learn framework extension points. For example, notice that the dispatch tables I explained are all regular lists of strings and functions, which are then mapped and iterated just like any other list. More on this in a future post.
All code posted here is part of the FigmentPlayground repository. [Less]
|
Posted
over 13 years
ago
by
[email protected] (Mauricio Scheffer)
This is a refactoring tale about Figment, the web framework I've been writing. As with most refactoring tales, I'll be extra verbose and explicit and maybe a little dramatic about each step and its rationale. Figment is, as I explained when I first
... [More]
wrote about it, based on ASP.NET MVC. As such, it uses many ASP.NET MVC types, like ControllerContext and ActionResult. Let's say we wanted to create a new ActionResult to model a HTTP "method not allowed" response. The RFC says that this response has a status code 405 and "the response MUST include an Allow header containing a list of valid methods for the requested resource". Such a response is appropriate when the client issues a request with a HTTP method that is not supported by the server, i.e. not every application can handle a DELETE method. But pretty much every application can handle GET and POST, so when receiving a DELETE request the server could respond with 405 and an Allow: GET, POST header (supporting HEAD is pretty common too, but for this post let's assume only GET and POST are supported). If we were working with objects and classes we would write a class inheriting ActionResult to encapsulate this, something like (using F#): type MethodNotAllowed(validMethods: string seq) =
inherit ActionResult() with
override x.ExecuteResult ctx =
ctx.Response.StatusCode <- 405
ctx.Response.AppendHeader("Allow", String.Join(", ", validMethods))
and we would use this in Figment like this:
action (ifMethodIs "DELETE") (fun _ -> MethodNotAllowed ["GET"; "POST"])
Thanks to object expressions in F#, we can get away without writing an explicit class:
let methodNotAllowed(validMethods: #seq<string>) =
{ new ActionResult() with
override x.ExecuteResult ctx =
ctx.Response.StatusCode <- 405
ctx.Response.AppendHeader("Allow", String.Join(", ", validMethods)) }
Now in Figment there's not much difference:
action (ifMethodIs "DELETE") (fun _ -> methodNotAllowed ["GET"; "POST"])
But we can be more concise and functional in the definition of this response.
The first thing to realize is that ActionResult has a single method ExecuteResult with signature ControllerContext -> unit. So we could easily represent it as a regular function ControllerContext -> unit and then build the actual ActionResult whenever we need it:
let inline result r =
{new ActionResult() with
override x.ExecuteResult ctx =
r ctx }
result here is (ControllerContext -> unit) -> ActionResult
This is a pretty common pattern in F# to "functionalize" a single-method interface or class.
Let's also write a little function to execute an ActionResult:
let inline exec ctx (r: ActionResult) =
r.ExecuteResult ctx
Setting a status code and header are pretty common things to do. We should encapsulate them into their own ActionResults, and then we can compose them:
let status code =
result (fun ctx -> ctx.Response.StatusCode <- code)
let header name value =
result (fun ctx -> ctx.Response.AppendHeader(name, value))
Now we'd like to define methodNotAllowed by composing these two ActionResults, for example:
let allow (methods: #seq<string>) = header "Allow" (String.Join(", ", methods))
let methodNotAllowed methods = status 405 >>. allow methods
Notice how the ControllerContext and Response are implicit now. We can define the >>. operator like this:
let concat a b =
let c ctx =
exec ctx a
exec ctx b
result c
let (>>.) = concat
That is, concat executes two ActionResults sequentially.
But wait a minute... "sequencing actions"... where have we seen this before? Yup, monads. We have just reinvented the Reader monad. Let's not reinvent it and instead make it explicit, but first we have to refactor Figment to use ControllerContext -> unit instead of ActionResult (it will be still used, but under the covers). This is a simple, mechanical, uninteresting refactor, so I won't show it. Just consider it done. Now we can define:
type ReaderBuilder() =
member x.Bind(m, f) = fun c -> f (m c) c
member x.Return a = fun _ -> a
member x.ReturnFrom a = a
let result = ReaderBuilder()
let (>>.) m f = r.Bind(m, fun _ -> f)
let (>>=) m f = r.Bind(m,f)
>>. is just like Haskell's >> operator, described like this:
Sequentially compose two actions, discarding any value produced by the first, like sequencing operators (such as the semicolon) in imperative languages.
We can still define methodNotAllowed as above, or using computation expression syntax:
let methodNotAllowed methods =
result {
do! status 405
do! allow methods
}
Or, if you don't want to use monads, you can just pass the context explicitly:
let methodNotAllowed allowedMethods =
fun ctx ->
status 405 ctx
allow allowedMethods ctx
They're all equivalent definitions.
Now, after the last refactor ("unpacking" ActionResult) we changed the Figment action type from
ControllerContext -> ActionResult
to
ControllerContext -> (ControllerContext -> unit)
which is a pretty silly type if you think about it... but that's a refactoring tale for another day ;-)
I hope this post served to contrast object-oriented and functional code in a familiar environment (ASP.NET MVC), and to show how monads can arise "naturally", it's just a matter of recognizing them.
I should say that this is of course not the only way to do it, and I don't claim this is the best way to do it.
In the next post I'll show more uses and conveniences of having the action as a Reader monad. [Less]
|
Posted
over 13 years
ago
by
[email protected] (Mauricio Scheffer)
This is a refactoring tale about Figment, the web framework I've been writing. As with most refactoring tales, I'll be extra verbose and explicit and maybe a little dramatic about each step and its rationale. Figment is, as I explained when I first
... [More]
wrote about it, based on ASP.NET MVC. As such, it uses many ASP.NET MVC types, like ControllerContext and ActionResult. Let's say we wanted to create a new ActionResult to model a HTTP "method not allowed" response. The RFC says that this response has a status code 405 and "the response MUST include an Allow header containing a list of valid methods for the requested resource". Such a response is appropriate when the client issues a request with a HTTP method that is not supported by the server, i.e. not every application can handle a DELETE method. But pretty much every application can handle GET and POST, so when receiving a DELETE request the server could respond with 405 and an Allow: GET, POST header (supporting HEAD is pretty common too, but for this post let's assume only GET and POST are supported). If we were working with objects and classes we would write a class inheriting ActionResult to encapsulate this, something like (using F#): type MethodNotAllowed(validMethods: string seq) =
inherit ActionResult() with
override x.ExecuteResult ctx =
ctx.Response.StatusCode <- 405
ctx.Response.AppendHeader("Allow", String.Join(", ", validMethods))
and we would use this in Figment like this:
action (ifMethodIs "DELETE") (fun _ -> MethodNotAllowed ["GET"; "POST"])
Thanks to object expressions in F#, we can get away without writing an explicit class:
let methodNotAllowed(validMethods: #seq<string>) =
{ new ActionResult() with
override x.ExecuteResult ctx =
ctx.Response.StatusCode <- 405
ctx.Response.AppendHeader("Allow", String.Join(", ", validMethods)) }
Now in Figment there's not much difference:
action (ifMethodIs "DELETE") (fun _ -> methodNotAllowed ["GET"; "POST"])
But we can be more concise and functional in the definition of this response.
The first thing to realize is that ActionResult has a single method ExecuteResult with signature ControllerContext -> unit. So we could easily represent it as a regular function ControllerContext -> unit and then build the actual ActionResult whenever we need it:
let inline result r =
{new ActionResult() with
override x.ExecuteResult ctx =
r ctx }
result here is (ControllerContext -> unit) -> ActionResult
This is a pretty common pattern in F# to "functionalize" a single-method interface or class.
Let's also write a little function to execute an ActionResult:
let inline exec ctx (r: ActionResult) =
r.ExecuteResult ctx
Setting a status code and header are pretty common things to do. We should encapsulate them into their own ActionResults, and then we can compose them:
let status code =
result (fun ctx -> ctx.Response.StatusCode <- code)
let header name value =
result (fun ctx -> ctx.Response.AppendHeader(name, value))
Now we'd like to define methodNotAllowed by composing these two ActionResults, for example:
let allow (methods: #seq<string>) = header "Allow" (String.Join(", ", methods))
let methodNotAllowed methods = status 405 >>. allow methods
Notice how the ControllerContext and Response are implicit now. We can define the >>. operator like this:
let concat a b =
let c ctx =
exec ctx a
exec ctx b
result c
let (>>.) = concat
That is, concat executes two ActionResults sequentially.
But wait a minute... "sequencing actions"... where have we seen this before? Yup, monads. We have just reinvented the Reader monad. Let's not reinvent it and instead make it explicit, but first we have to refactor Figment to use ControllerContext -> unit instead of ActionResult (it will be still used, but under the covers). This is a simple, mechanical, uninteresting refactor, so I won't show it. Just consider it done. Now we can define:
type ReaderBuilder() =
member x.Bind(m, f) = fun c -> f (m c) c
member x.Return a = fun _ -> a
member x.ReturnFrom a = a
let result = ReaderBuilder()
let (>>.) m f = r.Bind(m, fun _ -> f)
let (>>=) m f = r.Bind(m,f)
>>. is just like Haskell's >> operator, described like this:
Sequentially compose two actions, discarding any value produced by the first, like sequencing operators (such as the semicolon) in imperative languages.
We can still define methodNotAllowed as above, or using computation expression syntax:
let methodNotAllowed methods =
result {
do! status 405
do! allow methods
}
Or, if you don't want to use monads, you can just pass the context explicitly:
let methodNotAllowed allowedMethods =
fun ctx ->
status 405 ctx
allow allowedMethods ctx
They're all equivalent definitions.
Now, after the last refactor ("unpacking" ActionResult) we changed the Figment action type from
ControllerContext -> ActionResult
to
ControllerContext -> (ControllerContext -> unit)
which is a pretty silly type if you think about it... but that's a refactoring tale for another day ;-)
I hope this post served to contrast object-oriented and functional code in a familiar environment (ASP.NET MVC), and to show how monads can arise "naturally", it's just a matter of recognizing them.
I should say that this is of course not the only way to do it, and I don't claim this is the best way to do it.
In the next post I'll show more uses and conveniences of having the action as a Reader monad. [Less]
|
Posted
over 13 years
ago
by
[email protected] (Mauricio Scheffer)
This is a refactoring tale about Figment, the web framework I've been writing. As with most refactoring tales, I'll be extra verbose and explicit and maybe a little dramatic about each step and its rationale. Figment is, as I explained when I first
... [More]
wrote about it, based on ASP.NET MVC. As such, it uses many ASP.NET MVC types, like ControllerContext and ActionResult. Let's say we wanted to create a new ActionResult to model a HTTP "method not allowed" response. The RFC says that this response has a status code 405 and "the response MUST include an Allow header containing a list of valid methods for the requested resource". Such a response is appropriate when the client issues a request with a HTTP method that is not supported by the server, i.e. not every application can handle a DELETE method. But pretty much every application can handle GET and POST, so when receiving a DELETE request the server could respond with 405 and an Allow: GET, POST header (supporting HEAD is pretty common too, but for this post let's assume only GET and POST are supported). If we were working with objects and classes we would write a class inheriting ActionResult to encapsulate this, something like (using F#): type MethodNotAllowed(validMethods: string seq) =
inherit ActionResult() with
override x.ExecuteResult ctx =
ctx.Response.StatusCode <- 405
ctx.Response.AppendHeader("Allow", String.Join(", ", validMethods))
and we would use this in Figment like this:
action (ifMethodIs "DELETE") (fun _ -> MethodNotAllowed ["GET"; "POST"])
Thanks to object expressions in F#, we can get away without writing an explicit class:
let methodNotAllowed(validMethods: #seq) =
{ new ActionResult() with
override x.ExecuteResult ctx =
ctx.Response.StatusCode <- 405
ctx.Response.AppendHeader("Allow", String.Join(", ", validMethods)) }
Now in Figment there's not much difference:
action (ifMethodIs "DELETE") (fun _ -> methodNotAllowed ["GET"; "POST"])
But we can be more concise and functional in the definition of this response.
The first thing to realize is that ActionResult has a single method ExecuteResult with signature ControllerContext -> unit. So we could easily represent it as a regular function ControllerContext -> unit and then build the actual ActionResult whenever we need it:
let inline result r =
{new ActionResult() with
override x.ExecuteResult ctx =
r ctx }
result here is (ControllerContext -> unit) -> ActionResult
This is a pretty common pattern in F# to "functionalize" a single-method interface or class.
Let's also write a little function to execute an ActionResult:
let inline exec ctx (r: ActionResult) =
r.ExecuteResult ctx
Setting a status code and header are pretty common things to do. We should encapsulate them into their own ActionResults, and then we can compose them:
let status code =
result (fun ctx -> ctx.Response.StatusCode <- code)
let header name value =
result (fun ctx -> ctx.Response.AppendHeader(name, value))
Now we'd like to define methodNotAllowed by composing these two ActionResults, for example:
let allow (methods: #seq) = header "Allow" (String.Join(", ", methods))
let methodNotAllowed methods = status 405 >>. allow methods
Notice how the ControllerContext and Response are implicit now. We can define the >>. operator like this:
let concat a b =
let c ctx =
exec ctx a
exec ctx b
result c
let (>>.) = concat
That is, concat executes two ActionResults sequentially.
But wait a minute... "sequencing actions"... where have we seen this before? Yup, monads. We have just reinvented the Reader monad. Let's not reinvent it and instead make it explicit, but first we have to refactor Figment to use ControllerContext -> unit instead of ActionResult (it will be still used, but under the covers). This is a simple, mechanical, uninteresting refactor, so I won't show it. Just consider it done. Now we can define:
type ReaderBuilder() =
member x.Bind(m, f) = fun c -> f (m c) c
member x.Return a = fun _ -> a
member x.ReturnFrom a = a
let result = ReaderBuilder()
let (>>.) m f = r.Bind(m, fun _ -> f)
let (>>=) m f = r.Bind(m,f)
>>. is just like Haskell's >> operator, described like this:
Sequentially compose two actions, discarding any value produced by the first, like sequencing operators (such as the semicolon) in imperative languages.
We can still define methodNotAllowed as above, or using computation expression syntax:
let methodNotAllowed methods =
result {
do! status 405
do! allow methods
}
Or, if you don't want to use monads, you can just pass the context explicitly:
let methodNotAllowed allowedMethods =
fun ctx ->
status 405 ctx
allow allowedMethods ctx
They're all equivalent definitions.
Now, after the last refactor ("unpacking" ActionResult) we changed the Figment action type from
ControllerContext -> ActionResult
to
ControllerContext -> (ControllerContext -> unit)
which is a pretty silly type if you think about it... but that's a refactoring tale for another day ;-)
I hope this post served to contrast object-oriented and functional code in a familiar environment (ASP.NET MVC), and to show how monads can arise "naturally", it's just a matter of recognizing them.
I should say that this is of course not the only way to do it, and I don't claim this is the best way to do it.
In the next post I'll show more uses and conveniences of having the action as a Reader monad. [Less]
|
Posted
over 13 years
ago
by
[email protected] (Mauricio Scheffer)
I've been writing a HTTP content negotiation (server-driven) library in F# I called FsConneg (yeah, I've been pretty lazy lately about naming things). First, here's a little introduction to the topic:Content negotiation is briefly specified in
... [More]
section 12 of RFC2616, although the meat of it is really in the definitions of the Accept-* headers. There are four content characteristics that can be negotiated: media type, language, charset and encoding. Encoding refers to content compression, and is usually handled at the web server level, for example IIS static/dynamic compression or mod_deflate in Apache.Charset refers to UTF-8, ISO-8859-1, etc. The most interesting are media type and language which are the most commonly negotiated characteristics in user-level code. Language is self-explanatory, and media type negotiates whether the response should be XML, JSON, PDF, HTML, etc.FsConneg is inspired by clj-conneg and has a similar interface. clj-conneg currently negotiates media types only, but FsConneg can negotiate any content characteristic. Like clj-conneg, FsConneg doesn't assume any particular web framework, it works with raw header values, and so it can be easily integrated into any web server or framework.Let's say you have a server application that can respond with application/xml or application/json, but it prefers application/json:let serves = ["application/json"; "application/xml"]And you get a request from a user agent with an Accept header looking like this:let accepts = "text/html, application/xml;q=0.8, */*;q=0.5"
Which means: Hey server, I prefer a text/html response, but if you can't do that I'll take application/xml, or as a last resort give me whatever media type you have.Given these two constraints, the server wants to find out what media type it should use:match bestMediaType serves accepts with
| Some (mediaType, q) -> printfn "Negotiated media type %s, now the server formats its response with this media type" mediaType
| _ -> failwithf "Couldn't negotiate an acceptable media type with client: %s" acceptsIn this example, the negotiated media type is of course application/xml. In case of negotiation failure, the server should respond with status 406 Not Acceptable. There are similar functions bestEncoding, bestCharset and bestLanguage to negotiate the other content characteristics.At a lower level, you might want to use negotiate* functions. Instead of giving you a single best type, these give you a sorted list of acceptable types. For example, using the same serves and accepts as above:> negotiateMediaType serves accepts
val it : (string * float) list =
[("application/xml", 0.8); ("application/json", 0.5)]Even though server-driven content negotiation was defined back in 1997, it hasn't been used a lot, and with good reason. Every party involved (user agent, server and proxies) has to implement negotiation semantics right, or Bad Things could happen, like the user agent asking for a page in English and getting it in French because some proxy didn't honor the Vary header.
Until a few years ago, Internet Explorer didn't handle the Vary header all too well, and some proxies had issues as well. Until version 9, Internet Explorer used to send a mess of an Accept header, and WebKit preferred application/xml over text/html, which doesn't make much sense for a browser. Here's a spreadsheet with the Accept header some browsers send. Also, we developers and the frameworks we use sometimes get details wrong. Pure server-driven language negotiation is most of the time insufficient and has to be complemented with agent-driven negotiation. Even Roy Fielding, who defined REST, says that "a server is rarely able to make effective use of preemptive negotiation".
As a server app developer, some of these issues are out of your control, yet affect how content gets served to your clients. Many people argue that content negotiation is broken, or overly complex, or an ivory tower concept, or just don't agree with it.I think it still can work and has its time and place, in particular for APIs. But just like the rest of REST (no pun intended), content negotiation is not as simple as it might seem at first sight.In the next post I'll describe some ways to do concrete content negotiation with this library and Figment.The code for FsConneg is on github. [Less]
|
Posted
over 13 years
ago
by
[email protected] (Mauricio Scheffer)
I've been writing a HTTP content negotiation (server-driven) library in F# I called FsConneg (yeah, I've been pretty lazy lately about naming things). First, here's a little introduction to the topic:Content negotiation is briefly specified in
... [More]
section 12 of RFC2616, although the meat of it is really in the definitions of the Accept-* headers. There are four content characteristics that can be negotiated: media type, language, charset and encoding. Encoding refers to content compression, and is usually handled at the web server level, for example IIS static/dynamic compression or mod_deflate in Apache.Charset refers to UTF-8, ISO-8859-1, etc. The most interesting are media type and language which are the most commonly negotiated characteristics in user-level code. Language is self-explanatory, and media type negotiates whether the response should be XML, JSON, PDF, HTML, etc.FsConneg is inspired by clj-conneg and has a similar interface. clj-conneg currently negotiates media types only, but FsConneg can negotiate any content characteristic. Like clj-conneg, FsConneg doesn't assume any particular web framework, it works with raw header values, and so it can be easily integrated into any web server or framework.Let's say you have a server application that can respond with application/xml or application/json, but it prefers application/json:let serves = ["application/json"; "application/xml"]And you get a request from a user agent with an Accept header looking like this:let accepts = "text/html, application/xml;q=0.8, */*;q=0.5"
Which means: Hey server, I prefer a text/html response, but if you can't do that I'll take application/xml, or as a last resort give me whatever media type you have.Given these two constraints, the server wants to find out what media type it should use:match bestMediaType serves accepts with
| Some (mediaType, q) -> printfn "Negotiated media type %s, now the server formats its response with this media type" mediaType
| _ -> failwithf "Couldn't negotiate an acceptable media type with client: %s" acceptsIn this example, the negotiated media type is of course application/xml. In case of negotiation failure, the server should respond with status 406 Not Acceptable. There are similar functions bestEncoding, bestCharset and bestLanguage to negotiate the other content characteristics.At a lower level, you might want to use negotiate* functions. Instead of giving you a single best type, these give you a sorted list of acceptable types. For example, using the same serves and accepts as above:> negotiateMediaType serves accepts
val it : (string * float) list =
[("application/xml", 0.8); ("application/json", 0.5)]Even though server-driven content negotiation was defined back in 1997, it hasn't been used a lot, and with good reason. Every party involved (user agent, server and proxies) has to implement negotiation semantics right, or Bad Things could happen, like the user agent asking for a page in English and getting it in French because some proxy didn't honor the Vary header.
Until a few years ago, Internet Explorer didn't handle the Vary header all too well, and some proxies had issues as well. Until version 9, Internet Explorer used to send a mess of an Accept header, and WebKit preferred application/xml over text/html, which doesn't make much sense for a browser. Here's a spreadsheet with the Accept header some browsers send. Also, we developers and the frameworks we use sometimes get details wrong. Pure server-driven language negotiation is most of the time insufficient and has to be complemented with agent-driven negotiation. Even Roy Fielding, who defined REST, says that "a server is rarely able to make effective use of preemptive negotiation".
As a server app developer, some of these issues are out of your control, yet affect how content gets served to your clients. Many people argue that content negotiation is broken, or overly complex, or an ivory tower concept, or just don't agree with it.I think it still can work and has its time and place, in particular for APIs. But just like the rest of REST (no pun intended), content negotiation is not as simple as it might seem at first sight.In the next post I'll describe some ways to do concrete content negotiation with this library and Figment.The code for FsConneg is on github. [Less]
|
Posted
over 13 years
ago
by
[email protected] (mausch)
I've been writing a HTTP content negotiation (server-driven) library in F# I called FsConneg (yeah, I've been pretty lazy lately about naming things). First, here's a little introduction to the topic: Content negotiation is briefly specified in
... [More]
section 12 of RFC2616, although the meat of it is really in the definitions of the Accept-* headers. There are four content characteristics that can be negotiated: media type, language, charset and encoding. Encoding refers to content compression, and is usually handled at the web server level, for example IIS static/dynamic compression or mod_deflate in Apache. Charset refers to UTF-8, ISO-8859-1, etc. The most interesting are media type and language which are the most commonly negotiated characteristics in user-level code. Language is self-explanatory, and media type negotiates whether the response should be XML, JSON, PDF, HTML, etc. FsConneg is inspired by clj-conneg and has a similar interface. clj-conneg currently negotiates media types only, but FsConneg can negotiate any content characteristic. Like clj-conneg, FsConneg doesn't assume any particular web framework, it works with raw header values, and so it can be easily integrated into any web server or framework. Let's say you have a server application that can respond with application/xml or application/json, but it prefers application/json: let serves = ["application/json"; "application/xml"]
And you get a request from a user agent with an Accept header looking like this:
let accepts = "text/html, application/xml;q=0.8, */*;q=0.5"
Which means: Hey server, I prefer a text/html response, but if you can't do that I'll take application/xml, or as a last resort give me whatever media type you have.
Given these two constraints, the server wants to find out what media type it should use:
match bestMediaType serves accepts with
| Some (mediaType, q) -> printfn "Negotiated media type %s, now the server formats its response with this media type" mediaType
| _ -> failwithf "Couldn't negotiate an acceptable media type with client: %s" accepts
In this example, the negotiated media type is of course application/xml. In case of negotiation failure, the server should respond with status 406 Not Acceptable.
There are similar functions bestEncoding, bestCharset and bestLanguage to negotiate the other content characteristics.
At a lower level, you might want to use negotiate* functions. Instead of giving you a single best type, these give you a sorted list of acceptable types. For example, using the same serves and accepts as above:
> negotiateMediaType serves accepts
val it : (string * float) list =
[("application/xml", 0.8); ("application/json", 0.5)]
Even though server-driven content negotiation was defined back in 1997, it hasn't been used a lot, and with good reason. Every party involved (user agent, server and proxies) has to implement negotiation semantics right, or Bad Things could happen, like the user agent asking for a page in English and getting it in French because some proxy didn't honor the Vary header.
Until a few years ago, Internet Explorer didn't handle the Vary header all too well, and some proxies had issues as well. Until version 9, Internet Explorer used to send a mess of an Accept header, and WebKit preferred application/xml over text/html, which doesn't make much sense for a browser. Here's a spreadsheet with the Accept header some browsers send. Also, we developers and the frameworks we use sometimes get details wrong. Pure server-driven language negotiation is most of the time insufficient and has to be complemented with agent-driven negotiation. Even Roy Fielding, who defined REST, says that "a server is rarely able to make effective use of preemptive negotiation".
As a server app developer, some of these issues are out of your control, yet affect how content gets served to your clients. Many people argue that content negotiation is broken, or overly complex, or an ivory tower concept, or just don't agree with it.
I think it still can work and has its time and place, in particular for APIs. But just like the rest of REST (no pun intended), content negotiation is not as simple as it might seem at first sight.
In the next post I'll describe some ways to do concrete content negotiation with this library and Figment.
The code for FsConneg is on github. [Less]
|
Posted
almost 14 years
ago
by
[email protected] (Mauricio Scheffer)
I've blogged before about formlets, a nice abstraction of HTML forms. I started with a basic implementation, then showed validation. Now I'll present a non-toy implementation of formlets I called FsFormlets (really original name, I know). By
... [More]
"non-toy" I mean this implementation is not a proof of concept or just for didactic purposes, but is meant to be eventually production-quality. I don't like to explain things in a vacuum, so I'll use a typical registration form to show how it works: Even though FsFormlets can be used to generate HTML on its own, there's a much better tool for this in F#: Wing Beats. Wing Beats is an EDSL in F# to generate HTML, much like SharpDOM in C# or Blaze in Haskell, or Eliom's XHTML.M in OCaml (except that XHTML.M actually validates HTML statically). So I've added a module to integrate FsFormlets to Wing Beats. This integration is a separate assembly; FsFormlets is stand-alone (it only requires .NET 3.5 SP1 and the F# runtime). We'll use FsFormlets to express forms, and Wing Beats to express the rest of HTML. Also, we'll handle web requests with Figment, with the help of some more glue code to integrate it with FsFormlets and Wing Beats. Layout Let's start by defining a layout in Wing Beats: let e = XhtmlElement()
let layout title body =
[
e.DocTypeHTML5
e.Html [
e.Head [
e.Title [ &title ]
e.Style [
&".error {color:red;}"
&"body {font-family:Verdana,Geneva,sans-serif; line-height: 160%;}"
]
]
e.Body [
yield e.H1 [ &title ]
yield! body
]
]
]
No formlets so far, this is all pure HTML, expressed as a regular function. Now we'll build the form bottom-up.
ReCaptcha
FsFormlets already includes a reCaptcha formlet (I'll show its innards in a future post). We just have to configure it with a pair of public and private key (get it here) before using it:
let reCaptcha = reCaptcha {PublicKey = "your_public_key"; PrivateKey = "your_private_key"; MockedResult = None}
MockedResult lets you skip the actual validation web call and force a result, for testing purposes.
Date input
Now the date formlet, which is built from three inputs, plus labels and validation:
let f = e.Formlets
let dateFormlet : DateTime Formlet =
let baseFormlet =
yields t3
<*> (f.Text(maxlength = 2, attributes = ["type","number"; "min","1"; "max","12"; "required","required"; "size","3"]) |> f.WithLabel "Month: ")
<*> (f.Text(maxlength = 2, attributes = ["type","number"; "min","1"; "max","31"; "required","required"; "size","3"]) |> f.WithLabel "Day: ")
<*> (f.Text(maxlength = 4, attributes = ["type","number"; "min","1900"; "required","required"; "size","5"]) |> f.WithLabel "Year: ")
let isDate (month,day,year) =
let pad n (v: string) = v.PadLeft(n,'0')
let ymd = sprintf "%s%s%s" (pad 4 year) (pad 2 month) (pad 2 day)
DateTime.TryParseExact(ymd, "yyyyMMdd", CultureInfo.InvariantCulture, DateTimeStyles.None) |> fst
let dateValidator = err isDate (fun _ -> "Invalid date")
baseFormlet
|> satisfies dateValidator
|> map (fun (month,day,year) -> DateTime(int year,int month,int day))
Here, baseFormlet is of type (string * string * string) Formlet, that is, it collects values in their raw form. This baseFormlet is then validated to make sure it's a date, and finally mapped to a DateTime. Note the use of the 'number' input type (a HTML5 input) and also HTML5 validation attributes (required, min, max)
Password input
The password formlet is next:
let doublePassword =
let compressedLength (s: string) =
use buffer = new MemoryStream()
use comp = new DeflateStream(buffer, CompressionMode.Compress)
use w = new StreamWriter(comp)
w.Write(s)
w.Flush()
buffer.Length
let isStrong s = compressedLength s >= 106L
let f =
yields t2
<*> (f.Password(required = true) |> f.WithLabel "Password: ")
<+ e.Br()
<*> (f.Password(required = true) |> f.WithLabel "Repeat password: ")
let areEqual (a,b) = a = b
f
|> satisfies (err areEqual (fun _ -> "Passwords don't match"))
|> map fst
|> satisfies (err isStrong (fun _ -> "Password too weak"))
There are two things we validate here for passwords: first, the two entered passwords must match; and second, it must be strong enough. To measure password strength I use the compression technique I described in my last post.
The <+ operator is used to add pure markup to the formlet, using Wing Beats.
Also note the 'required = true' parameter. In the date formlet, we just used required=required as an HTML attribute. Here we used the optional argument required=true, which validates the formlet both at the client (with a required=required HTML attribute) and the server (it has a satisfies clause). In the date formlet we don't really care to validate server-side if the user filled out each input, we just want to know if it's a valid date or not.
The optional parameter 'maxlength' does a similar thing: it outputs a maxlength attribute, and also checks the maximum length of he POSTed value. Sure all browsers implement maxlength properly, but anyone can easily craft a request (e.g. with cUrl, no need to program at all) to circumvent it. I see malicious bots doing things like this every day. This makes sure the data is really valid before you start processing it.
Putting it all together
Moving on, let's write the final formlet and store the posted information in a record:
type PersonalInfo = {
Name: string
Email: string
Password: string
DateOfBirth: DateTime
}
let registrationFormlet ip =
yields (fun n e p d ->
{ Name = n; Email = e; Password = p; DateOfBirth = d })
<*> (f.Text(required = true) |> f.WithLabel "Name: ")
<+ e.Br()
<*> (f.Email(required = true) |> f.WithLabel "Email: ")
<+ e.Br()
<*> doublePassword
<+ e.Br()
<+ &"Date of birth: " <*> dateFormlet
<+ e.Br()
<+ &"Please read very carefully these terms and conditions before registering for this online program, blah blah blah"
<+ e.Br()
<* (f.Checkbox(false) |> satisfies (err id (fun _ -> "Please accept the terms and conditions")) |> f.WithLabel "I agree to the terms and conditions above")
<* reCaptcha ip
Some notes about this last snippet:
f.Email(required = true) generates an (again, HTML5 !), all server-validated.
Unlike previous formlets, this one is a function, because reCaptcha needs the client's IP address to validate.
If you've read my previous posts about formlets, you may be wondering why I'm using things like f.Text() and f.Checkbox() (which are members of an object) instead of the regular functions input and checkbox. Those functions are also present in FsFormlets and you may use them interchangeably with the object-style formlets, e.g. instead of f.CheckBox(false) you can write checkbox false []. The object-style formlets build on top of functional formlets, adding optional parameters for validation. They also integrate more seamlessly with Wing Beats.
And we're done with the form! Now let's build the actual page that contains it. Using the layout we wrote earlier:
let s = e.Shortcut
let registrationPage form =
layout "Registration" [
s.FormPost "" [
e.Fieldset [
yield e.Legend [ &"Please fill the fields below" ]
yield!!+form
yield e.Br()
yield s.Submit "Register!"
]
]
]
This is also pure Wing Beats markup, I think it doesn't need much explanation except for the "yield!!+form" which indicates where to render the formlet (yes, the operator doesn't look very friendly, I'll probably change it). Note how easy it is to compose pieces of potentially reusable HTML with Wing Beats as they're just functions.
Handling requests with Figment
Now all we need to do is bind the formlet to a URL to render the page:
get "register" (fun _ -> registrationFormlet "" |> renderToXml |> registrationPage |> Result.wbview)
and handle the form POST:
post "register" (fun ctx ->
let env = EnvDict.fromFormAndFiles ctx.Request
match run (registrationFormlet ctx.IP) env with
| Success v -> Result.redirectf "thankyou?n=%s" v.Name
| Failure(errorForm,_) -> errorForm |> registrationPage |> Result.wbview)
Oh, I almost forgot the little "thank you" after-registration page:
get "thankyou" (fun ctx -> Result.contentf "Thank you for registering, %s" ctx.QueryString.["n"])
Now, this workflow we've just modeled is pretty common:
Show form.
User submits form.
Validate form. If errors, show form again to user.
Process form data.
Redirect.
So it's worthy of abstraction. The only moving parts are the formlet, the page and what to do on successful validation, so instead of mapping get and post individually we can say:
formAction "register" {
Formlet = fun ctx -> registrationFormlet ctx.IP
Page = fun _ -> registrationPage
Success = fun _ v -> Result.redirectf "thankyou?n=%s" v.Name
}
HTML5 ready
Implementation of HTML5 in browsers has exploded in 2010 (see the beautiful html5readiness.com for reference). In particular, HTML5 forms are already implemented in Chrome 10, Opera and Firefox 4 (with caveats). Safari and IE haven't implemented it yet (at least not in versions 5.0.4 and 9 respectively), but WebKit already supports it so I guess Safari and other WebKit-based browsers will implement this in the short-term. So there's little reason not to use the new elements and attributes right now, as long as you also have server-side validation.
For example, here's how Chrome validates required fields when trying to submit:
Whereas in Safari 5.0.4/Windows the server-side validation kicks in:
If you want browser-side validation in Safari, IE, etc, you can easily use an unobtrusive polyfill. A thorough list of cross-browser HTML5 polyfills is available here.
For example, applying jQuery.tools is as easy as:
let jsValidation =
e.Div [
s.JavascriptFile "http://cdn.jquerytools.org/1.2.5/full/jquery.tools.min.js"
e.Script [ &"$('form').validator();" ]
]
and putting it at the bottom of the Wing Beats layout.
All code posted here is part of the sample Figment app.
FsFormlets source code is here. [Less]
|