Tuesday, April 17, 2012

A Haskell Newbies' guide to Snap, Part 1 (updated for Snap 0.8)

[This tutorial is now updated for Snap 0.8. It could really use a more thorough overhaul -- the code does a poor job of separating view from model. But I think it still has value in demonstrating the API]  

I've spent a bit of time working up a simple application in the Snap web framework.  This proved to be a bit of a challenge: the documentation for Snap is definitely a work in progress, and doesn't cater well to a Haskell newbie such as myself.  This account is intended to help others in my position get up to speed on Snap, by filling in some of the information that the tutorial's authors may have taken for granted.  It is not intended as a standalone introduction to Snap. If you want to use this document to help you learn Snap, I would suggest the following steps:
  • Read the first four Snap/Heist documents in the TUTORIAL section
  • Read through this document
  • Build an example application

Snap was absolutely easy to install and get running.  The API introduction walks you through creating a hello-snap app from scratch.  The app has a built-in HTTP server that serves a few simple web pages.  My goal was to create a web page that integrated data from a Sqlite3 database.  

A standard Snap application will consist of a few source files in /src and static resource files in /static and some other overhead files.  The only one of the overhead files I had to touch was the cabal file, and GHC told me when I needed to tweak that.  Assuming you also use Heist, you will also need /snaplets/heist/templates for your HTML templates.  The static resource files that go in /static might consist both of straight HTML or CSS files.  This all should be pretty obvious if you read the Snap intro.

Heist is a pretty slick templating system.  It lets you define your own tags which it expands when it renders a template.  A tag's definition can be either static text or a simple substitution, or you can call Haskell code via a Splice to get the text for a tag. 

I followed the instructions in the Heist tutorial for building some static templates, and all was fine.  When I tried to build a simple Splice, though, I had difficulty figuring out how to bind the splice tag into the template's list of tags.  The method used in the example application wasn't covered anywhere in the Heist tutorial, and the tutorial showed you how to set up a Heist state object containing the Splice but not how to use it.  I spent a lot of time trying the tutorial's method, when just a little more effort on the example's method got me success. Even there, I had one significant obstacle -- I got mentally stuck on the fact that the tutorial's way of binding the Splice returned a templateState value, while the method in the example code wants a function that takes a templateState and returns a template.  The API docs have more information about bindSplice and bindSplices.

A Splice that uses tag attributes

My first Splice was grabSQLwhereAttr, which used tag attributes to pass information from the Heist template to the Splice. The Splice then uses those attribute values in building an SQL query.  The result of that query is then used to build the Splice's return value, which Heist uses in rendering the template. 

The code to bind the Splice is

  my_template :: Handler App App ()
  my_template= heistLocal templateSt $ render "cheese_template"
        templateSt ts = bindSplices sqlSplices ts
        sqlSplices = [ ("get_sql_where_attr", grabSQLwhereAttr) ]

The new template gets routed properly by adding the bolded code below

  routes :: [(ByteString, Handler App App ())]
  routes = [ ("/",            index)
           , ("/echo/:stuff", echo)
           , ("/cheese_template", my_template)
           , ("", with heist heistServe)
           , ("", serveDirectory "static")

And the Splice itself is defined via

  import qualified Data.Text as DT
  import qualified Text.XmlHtml as X

  grabSQLwhereAttr :: Splice AppHandler
  grabSQLwhereAttr = do
    contents <- getParamNode 

    let column = getAttr contents "column" 
        key    = getAttr contents "key" 
        value  = getAttr contents "value" 
        table  = getAttr contents "table" 
    qval <- liftIO $ runQuery 
            $ bldQueryWhere column table key value 
    return $ [X.TextNode $ DT.pack $ qval] 

  getAttr :: X.Node -> DT.Text -> String
  getAttr x y = DT.unpack $ maybe (DT.pack "") id 

                                  (X.getAttribute y x)

getParamNode grabs the data from your Heist element; it is returned as  a Node object from Text.XmlHtml*. XmlHtml gives you a lot of ability to work with HTML; the docs have the details.  Here, we are grabbing the attributes from the get_sql_where_attr key.  The runQuery and bldQueryWhere code can be seen at Site.hs on github (the project is here). 

The Heist template that uses this Splice looks like

  <get_sql_where_attr column="price" key="name" 

                      value="'dubliner'" table="cheese">

The Splice code pulls out the attributes from the <get_sql_where_attr> tag, builds a query with them, runs the query, and takes the result text and substitutes it into the template in place of the tag.

A Splice that uses contents of child elements

The code above works fine, but the case-insensitive nature of HTML attributes limits it.  Let's refactor to use the contents of child elements instead (and allow any where clause in our SQL query):

  grabSQLwhere :: Splice Application
  grabSQLwhere = do
    contents <- getParamNode 

    let column = getChildText contents "column" 
        clause = getChildText contents "where_clause" 
        table  = getChildText contents "table" 
    qval <- liftIO $ runQuery 
            $ bldQueryWhereNew column table clause 
    return $ [X.TextNode $ DT.pack $ qval]
  getChildText :: X.Node -> DT.Text -> String
  getChildText x y = DT.unpack $ maybe (DT.pack "") id 

                                       (liftM X.nodeText tag)
      where tag = X.childElementTag y x

The template then becomes

    <where_clause>name = 'dubliner'</where_clause>

Click here for Part 2 of this guide

*This took me some work to find out, as getAttribute comes up empty in Hoogle and the tutorial doesn't mention the return type.  A more experienced Haskeller would have dug this up quickly; for me it was good experience in navigating the Haskell information web.