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
.
Keeping this in mind, there are a number of things you need to do to use Snaplets in your application.
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.
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
.
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
.