Skip to content

Instantly share code, notes, and snippets.

@duairc
Created July 8, 2010 20:28
Show Gist options
  • Save duairc/468567 to your computer and use it in GitHub Desktop.
Save duairc/468567 to your computer and use it in GitHub Desktop.

Snaplets

Snaplet is a library containing a set of modular, reusable web-components for use with the Snap web framework. Each of these components is said to be a Snaplet. Snaplets can be used to easily add features like session management, user authentication, templating and database connection pooling to your application.

Every Snaplet has an interface and at least one implementation of that interface. For some Snaplets, like Heist, there is only ever going to be one implementation of the interface. In these cases, the interface and the implementation thereof are exported from the same module, Snaplets.Heist. For something like session management though, there could be multiple implementations, one using an HDBC backend, one using a CouchDB backend and one just using a flat-file backend. In these cases, the interface is exported from Snaplet.Session, and the implementations live in Snaplet.Session.HDBC, Snaplet.Session.CouchDB and Snaplet.Session.FlatFile.

Using Snaplets

Keeping this in mind, there are a number of things you need to do to use Snaplets in your application.

Define application State and Monad

First, we define a record type AppState for holding our application's state, including the state needed by the Snaplets we're using.

At the same time, we also define the monad for our application, App, as a type alias to Snaplet AppState. Snaplet is a MonadSnap and a MonadReader, whose environment is a user-supplied type; in our case, AppState.

module App where

import Snap.Types
import Snaplet
import Snaplet.Heist
import Snaplet.DBPool.Sqlite3

type App = Snaplet AppState

data AppState = AppState
    { heistState  :: HeistState App
    , dbPoolState :: DBPoolState
    }

An important thing to note is that the -State types that we use in the fields of AppState are specific to each implementation of a Snaplet interface. That is, Snaplet.DBPool.Postgres will export a different DBPoolState, whose internal representation might be complete different.

Provide instances for "HasState" classes

So, we have a datatype that contains all the internal state needed by our application and the Snaplets it uses. That's great, but when do we actually get to use this interface that these Snaplets export? Where do we use it? What do we actually do?

We use the interface provided by Snaplets from inside our application's monad. For example, the Heist Snaplet provides the function render :: MonadHeist m => ByteString -> m (). Is App a MonadHeist? Well, not quite yet. Any MonadReader which is also a MonadSnap whose environment contains a HeistState is a MonadSnap. That sounds a lot like App, doesn't it? We just have to tell the Heist Snaplet how to find the HeistState in our AppState.

{-# LANGUAGE MultiParamTypeClasses #-}

instance HasHeistState App AppState where
    getHeistState = heistState
    setHeistState hs as = as { heistState = hs }

And similarly for our DBPoolState:

instance HasDBPoolState AppState where
    getDBPoolState = dbPoolState
    setDBPoolState dbps as = as { dbPoolState = dbps }

With these instances, our application's monad App is now a MonadHeist and a MonadDBPool, giving it access to operations like render :: MonadHeist m => ByteString -> m () and withConnection :: MonadDBPool m => (Connection -> IO a) -> m a.

Define the runner function

Finally, we need to define the runner function for our application. The runner function for App constructs an AppState in terms of the runner functions for other Snaplets, which construct their respective State types. A Runner monad is provided to make it easy to construct such functions.

import Text.Templating.Heist

appRunner :: Runner AppState
appRunner = do
    hs <- heistRunner "resources/templates" emptyTemplateState
    db <- dbPoolRunner "resources/db.sqlite3"
    return $ AppState hs db

This Runner AppState can then be passed to runRunner, whose type signature is Runner s -> Snaplet s () -> IO (Snap (), Snap [Either String String], IO ()). The other arguments it takes is an App (), and the tuple it returns is a Snap action (which can be passed to httpServe), a "reload" action (which you may want to use in your handler for the path "admin/reload"; there is one element in the list returned for each Snaplet - each element is either a String containing an error message or a String containing a success message) and a cleanup action which you would run after httpServe.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment