Snap Extensions is a library which makes it easy to extend your Snap application with modular chunks of functionality such as session management, user authentication, templating and database connection pooling.
Snap.Extension.DBPool
- Gives applications which need a HDBC `Connection` efficient pooling for
free. This module only contains an interface, implementations thereof are
in the modules listed below.
Snap.Extension.DBPool.MySQL
- A `DBPool` implementation for use with MySQL databases.
Snap.Extension.DBPool.ODBC
- A `DBPool` implementation for use with ODBC databases.
Snap.Extension.DBPool.Postgresql
- A `DBPool` implementation for use with PostgreSQL databases.
Snap.Extension.DBPool.Sqlite3
- A `DBPool` implementation for use with SQLite3 databases.
Snap.Extension.Heist
- Integrate Heist templating into your application.
Snap.Extension.Less
- Integrate Less stylesheets into your application.
Every extension has an interface and at least one implementation of that
interface. For some extensions, 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,
Snap.Extension.Heist
. For something like session management though, there
could be multiple implementations, one using a HDBC backend, one using a
CouchDB backend and one just using a flat-file backend. In these cases, the
interface is exported from Snap.Extension.Session
, and the implementations
live in Snap.Extension.Session.HDBC
, Snap.Extension.Session.CouchDB
and
Snap.Extension.Session.FlatFile
.
Keeping this in mind, there are a number of things you need to do to use Snap extensions in your application.
First, we define a record type AppState
for holding our application's state,
including the state needed by the extensions we're using.
At the same time, we also define the monad for our application, App
, as a
type alias to SnapExtend AppState
. SnapExtend
is a MonadSnap
and a
MonadReader
, whose environment is a user-supplied type; in our case,
AppState
.
module App where
import Snap.Extension
import Snap.Extension.DBPool.Postgresql
import Snap.Extension.Heist
import Snap.Types
type App = SnapExtend AppState
data AppState = AppState
{ dbPoolState :: DBPoolState
, heistState :: HeistState App
}
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 extension's
interface. That is, Snap.Extension.DBPool.Mysql
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 extensions it uses. Great! But when do we actually get to use this interface that these extensions export? What is actually being extended?
We use the interface provided by an extension inside our application's monad,
App
. Snap extensions extend our App
with new functionality. For example,
the Heist extension 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 extension how to find the
HeistState
in our AppState
.
instance HasHeistState 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
.
So, our monad is now a MonadHeist
and a MonadDBPool
, but how do we
actually construct our AppState
and turn an App ()
into a Snap ()
? Snap
extensions has a thing called a "Runner" that does these things. Each
implementation of a Snap extenion interface provides a Runner for its -State
type. We must construct a runner type for our -State
type, AppState
. A
Runner
monad is provided to make it easy to do this. For your convenience,
Runner
is an instance of MonadIO
.
import Text.Templating.Heist
appRunner :: Runner AppState
appRunner = do
db <- dbPoolRunner "user=dbuser pass=sekrit"
hs <- heistRunner "resources/templates" emptyTemplateState
return $ AppState db hs
In addition to constructing the AppState
, the Runner
monad also constructs
the init, destroy and reload functions for our application from the init,
reload and destroy functions for the extensions. Although it won't cause a
compile-time error, it is important to get the order of the runners correct as
much as possible, otherwise they may be reloaded and destroyed in the wrong
order. The "right" order is an order where every extension's dependencies are
initialised before that extension. For example,
Snap.Extension.Session.HDBC
would depend on something which would extend the
monad with MonadDBPool
, e.g., Snap.Extension.DBPool.Postgresql
. If you had
this configuration it would be important that you put the dbPoolRunner
before the sessionRunner
in your Runner AppState
.
This Runner AppState
can then be passed to runRunner
, whose type signature
is
Runner s -> SnapExtend s () -> IO (Snap (), IO (), IO [(String, Maybe String)])
.
The other arguments it takes is an App ()
, and the tuple it returns is a
Snap
action (which can be passed to httpServe
), a cleanup action (which
you run after httpServe
) and a "reload" action (which you may want to use
in your handler for the path "admin/reload"; the list it returns is for error
reporting - there is one tuple in the list for each Snap extension; the first
element of the tuple is the name of the Snap extension and the second is a
Maybe
which contains Nothing
if there was no error reloading that
extension and a Just
with the String
containing the error message if
there was) and a cleanup action which you would run after httpServe
. The
following is an example of how you might use this in main
:
main :: IO ()
main = do
(snap,cleanup,reload) <- runRunner appRunner site
quickHttpServe $ snap
<|> path "admin/reload" $ reloadHandler reload
cleanup
You'll notice we're using reloadHandler
. This is a function exported by
Snap.Extension
with the type signature
IO [(String, Maybe String)] -> Snap ()
. It takes the reload action returned
by runRunner
and returns a Snap
action which renders a simple page showing
how the reload went.
One of the big features of Snap 0.3 is the "development" mode which is
implemented using Hint. To use Hint in its current form, you need to call a
Template Haskell function loadSnapTH
which resembles an imaginary function
loadSnap :: IO AppState -> (AppState -> IO ()) -> (AppState -> Snap ()) -> IO (IO (), Snap ())
.
The tuple it returns contains a cleanup function and a Snap
action which can
be served with httpServe
.
To make it easier to use Snap extensions with the Hint project template in
0.3,
runRunnerHint :: Runner s -> SnapExtend s () -> (IO [(String, Maybe String)] -> Snap ()) -> IO (IO AppState, AppState -> IO (), AppState -> Snap ())
is provided. The first arguments are the same as in runRunner
. The
additional third argument is to do with reload handling; if you would like the
same behaviour as the above example using runRunner
, then you would pass
path "admin/reload" . reloadHandler
as that argument. If you didn't want to
use a reload handler at all, you could just pass const pass
as the argument.
The tuple it returns contains all the arguments you need to pass to
loadSnapTH
. An example of the use of runRunnerHint
is given below:
main :: IO ()
main = do
(state,mkCleanup,mkSnap) <-
runRunnerHint appRunner site $ path "admin/reload" . reloadHandler
(cleanup,snap) <- $(loadSnapTH 'state 'mkCleanup 'mkSnap)
quickHttpServer snap
cleanup
See src/Snap/Extensions/Heist.hs
for an example of a Snap extension.