Tuesday, February 8, 2011

Validation in formlets

Last time, we broke up formlets into primitive applicative functors, then composed these primitives to produce formlets.

So far, if someone entered an unexpected value in our little implementation of formlets (e.g. "abc" in an int Formlet), we'd get a nasty exception and there wasn't much we could do about it because we couldn't trace the exception to the formlet that was producing it. What we want, instead of an exception, is something that accumulates errors without interrupting the computation flow. Applicative functors, being oblivious, are perfect for that. More concretely, we want to:

  1. Know if the formlet was able to collect the value or not, and 
  2. if it couldn't, get the formlet rendered with the values the user entered and error messages, so we can re-display this second form, giving the user a chance to correct his mistakes.

The first item is a job for the option type. We'll return Some v if the formlet was successful and None if it wasn't.

For the second, we already have a XmlWriter applicative, so we could just reuse it to build this "error form".

Recall the type of formlets as we defined it last time:

type 'a Formlet = 'a Environ XmlWriter NameGen

Adding the two concerns we just mentioned, the signature becomes:

type 'a Formlet = 'a Error XmlWriter Environ XmlWriter NameGen

where Error is the Maybe monad in applicative form (remember that every monad is also an applicative functor).

This is what I was talking about when I mentioned formlets being extensible: you can extend formlet features by composing additional applicatives.

By the way, here's the same formlet type expressed using C# bracket style (which F# also supports):

type Formlet<'a> = NameGen<XmlWriter<Environ<XmlWriter<Error<'a>>>>>

See now why I prefer ML syntax? Bracket style looks quite messy once you start nesting many types.

Anyway, it's not enough to compose these applicatives, we also need some way to define how to validate things and how to show error messages. Let's define a validator type:

/// Validator type. 
/// Fst determines if value is valid 
/// Snd builds an error message 
type 'a Validator = ('a -> bool) * ('a -> xml_item list -> xml_item list)

and a function that attaches a validator to a formlet:

satisfies : 'a Validator -> 'a Formlet -> 'a Formlet

(I'm not going to bore you with implementation details in this post)
So we can now write:

let isInt = Int32.TryParse >> fst 
let intErrorMsg a xml = xml @ [ Tag("span", ["class","error"], [Text(sprintf "'%s' is not a valid number" a)]) ] 
let inputInt = input |> satisfies (isInt, intErrorMsg) |> lift int

When rendered the first time, this formlet doesn't show anything different:

printfn "%s" (render inputInt)

<input name="input_0" value=""/>

If we enter an invalid value, validation kicks in:

let env = EnvDict.fromValueSeq ["input_0","abc"] 
match run inputInt env with 
| _, Some v -> printfn "Formlet successful, value %d" v 
| errorForm, None -> printfn "Formlet unsuccessful, error form: \n%A" (XmlWriter.render errorForm)

This will print:

Formlet unsuccessful, error form: 
<div> 
    <input name="input_0" value="abc" /> 
    <span class="error">'abc' is not a valid number</span> 
</div>

We might want to wrap all that XML/HTML manipulation in order to make defining validators easier, for example:

/// <summary> 
/// Constructs a validator 
/// </summary> 
/// <param name="isValid">Determines if value is valid</param> 
/// <param name="errorMsg">Builds the error message</param> 
let err (isValid: 'a -> bool) (errorMsg: 'a -> string) : 'a Validator = 
    let addError value xml = 
        [ Tag("span", ["class","errorinput"], xml) 
          Tag("span", ["class","error"], [Text(errorMsg value)]) ]
    isValid, addError

Now we can code inputInt as:

let inputInt = input |> satisfies (err isInt (sprintf "'%s' is not a valid number")) |> lift int

We can chain as many validators as we want for any formlet. For example, here's a formlet that checks that the submitted value falls within a specified range:

let isInRange min max n = n >= min && n <= max
let inputRange min max = inputInt |> satisfies (err (isInRange min max) (fun _ -> sprintf "Value must be between %d and %d" min max))
let input10to40 = inputRange 10 40

Note how I used inputInt as the starting point for inputRange. inputInt is already validated for integers, so any non-integers values fed to inputRange will fail with the same error message as before. inputRange adds further validation on top of inputInt's validation. This is another example of the composability of formlets.

I put up a fully-fledged implementation of formlets, including validation, on github. There are some minor differences with what I've written so far about formlets. I'll blog about these differences soon.

As with all my previous articles on formlets, I have to give credit to the Links team; these blog posts and code are mostly just a rehash of their papers, which I can't recommend enough.

No comments:

Post a Comment