digestive-functors for Snap users

Uploads for Snap backend, and a Heist library!
Published on April 3, 2012 under the tag haskell

The past week there’s been a bit of work done on better integration between the digestive-functors library and the Snap framework.

Daniel Patterson has been so kind to implement file uploads for the Snap backend of digestive-functors – this was previously only implemented in the Happstack. The great thing is that this implementation still has all flexible options Snap allows for file uploads.

For now, digestive-functors enforced you to use the blaze-html library, simply because no other frontend library was available. Snap users will be delighted to hear we’ve written a Heist library now. Let’s take a look at an example. I’ll go over the forms rather quickly and focus on the Heist library. If this is the first time you read about digestive-functors, you might want to take a glance at this tutorial first.

First, we need some datatypes:

data Date = Date
    { dateDay   :: Int
    , dateMonth :: Int
    , dateYear  :: Int
    } deriving (Show)

data Sex = Female | Male
    deriving (Eq, Show)

data User = User
    { userName      :: Text
    , userPassword  :: Text
    , userSex       :: Sex
    , userBirthdate :: Date
    } deriving (Show)

And then some forms:

dateForm :: Monad m => Form Text m Date
dateForm = check "Not a valid date" validDate $ Date
    <$> "day"   .: stringRead "Not a number" (Just 16)
    <*> "month" .: stringRead "Not a number" (Just 6)
    <*> "year"  .: stringRead "Not a number" (Just 1990)
    validDate (Date day month _) =
        day   >= 1 && day   <= 31 &&
        month >= 1 && month <= 12

userForm :: Monad m => Form Text m User
userForm = User
    <$> "name"      .: text (Just "Jasper")
    <*> "password"  .: text Nothing
    <*> "sex"       .: choice [(Female, "Female"), (Male, "Male")] Nothing
    <*> "birthdate" .: dateForm

Because of the composable nature of digestive-functors, we will first write a template for the date form, and reuse that for the user form.

Let’s place the template for the date form in snaplets/heist/date-form.tpl. Almost all splices take a ref attribute, which tells the library what the field is for – by doing this, we automatically get the previous value if a form submission fails, etc. We can also easily pass arbitrary attributes to the input elements, like we do with size here.

<dfInputText ref="day" size="2" />
<dfInputText ref="month" size="2" />
<dfInputText ref="year" size="4" />

There’s a bit more in snaplets/heist/user-form.tpl.

<dfForm action="/">
    <dfChildErrorList ref="" />

    <dfLabel ref="name">Name: </dfLabel>
    <dfInputText ref="name" />

    <dfLabel ref="password">Password: </dfLabel>
    <dfInputPassword ref="password" />

    <dfLabel ref="sex">Sex: </dfLabel>
    <dfInputSelect ref="sex" />

    <dfSubView ref="birthdate">
        <apply template="date-form" />

    <dfInputSubmit value="Enter" />

The digestive-functors-heist library provides no Snaplet but integrates well with the concept. We just need to call runForm from the digestive-functors-snap library, and most is taken care of:

form :: Handler App App ()
form = do
    (view, result) <- runForm "form" userForm
    case result of
        Just x  -> -- do something with the user in x...
        Nothing -> heistLocal (bindDigestiveSplices view) $ render "user-form"

I hope this quick glance at the library was useful. Any comments and criticism are welcome, as always. The full source code for this example can be found here (snap-heist.hs, and the templates in snaplets/heist/templates).