Snap web routes
provides type safe URLs for Snap using
web routes
.
The tutorial assumes you have a standard Snap app layout with an Application.hs and Site.hs. If your setup differs you'll need to adapt things accordingly.
To get going, you'll need to add a few things to Application.hs
.
This includes creating the URL data type and adding the routing
function to our App
data type.
-- Enable a few extensions
{-# LANGUAGE FlexibleInstances #-} -- Needed by
{-# LANGUAGE TypeFamilies #-} -- web-routes
{-# LANGUAGE DeriveGeneric #-} -- Needed to derive Generic
-- for our URL data type
-- Used in HasRouter instances
import Control.Monad.State (get)
-- Paths and params use Text.
import Data.Text (Text)
-- Snap.Snaplet.Router.Types exports everything you need to
-- define your PathInfo and HasRouter instances.
import Snap.Snaplet.Router.Types
-- Your URL data type. Deriving a `Generic` allows you to
-- get a free `PathInfo` instance.
data AppUrl
= Login
| Logout
| Count Int
| Echo Text
| Paths [Text]
deriving (Eq, Show, Read, Generic)
-- Extend your App type to include the router snaplet.
data App = App
{ _heist :: Snaplet (Heist App)
, _router :: Snaplet RouterState
}
-- Thanks to Generic, an empty instance definition is all
-- you need. Alternately, you can implement 'toPathSegments'
-- and 'fromPathSegments' yourself or use web-routes-th.
instance PathInfo AppUrl
-- You need to define a HasRouter instance for your app.
-- You must set type URL (Handler App App) to the URL
-- data type you defined above. The router in
-- `with router` is the lens for the @RouterState@ snaplet
-- you added to App.
instance HasRouter (Handler App App) where
type URL (Handler App App) = AppUrl
getRouterState = with router get
-- You also need to define a HasRouter instance for the
-- router snaplet. Once again, set type URL (Handler b
-- RouterState) to the data type you defined above.
instance HasRouter (Handler b RouterState) where
type URL (Handler b RouterState) = AppUrl
getRouterState = get
Moving on to Site.hs
, we'll setup handlers for each URL, as well as initialise
our app with the router snaplet..
-- Snap.Snaplet.Router provides routing functions
import Snap.Snaplet.Router
-- Add your new routes using routeWith
routes :: [(ByteString, Handler App App ())]
routes = [ ("", routeWith routeAppUrl)
, ("", serveDirectory "static")
]
-- Define handlers for each value constructor in your URL data type.
routeAppUrl :: AppUrl -> Handler App App ()
routeAppUrl appUrl =
case appUrl of
(Login) -> with auth handleLoginSubmit
(Logout) -> with auth handleLogout
(Count n) -> writeText $ ("Count = " `T.append` (T.pack $ show n))
(Echo text) -> echo text
(Paths ps) -> writeText $ T.intercalate " " ps
-- You'll note that these are normal Snap handlers, except they can take
-- values from the value constructor as arguments. This is a lot nicer than
-- having to use getParam.
echo :: T.Text -> Handler App App ()
echo msg = heistLocal (bindString "message" msg) $ render "echo"
-- Add the router snaplet to your app.
app :: SnapletInit App App
app = makeSnaplet "app" "An example snap-web-routes app." Nothing $ do
h <- nestSnaplet "" heist $ heistInit "templates"
r <- nestSnaplet "router" router $ initRouter ""
addRoutes routes
return $ App h r
The prefix you pass to the router snaplet must match the prefix you specified
in routes, e.g. if it was ("/prefix", routeWith routeAppUrl)
) then:
r <- nestSnaplet "router" router $ initRouter "/prefix"
If you are having trouble figuring out why a particular request isn't routing
as expected, try replacing routeWith
with routeWithDebug
. It'll display
the available routes, as well as any failed route parses. Just remember that
it's not suitable for production use, and only displays debugging
information for local requests.
Let's look at how you can use your newly defined URL data type in your app.
Firstly, you'll probably want to add links in Heist views. This is easily
accomplished with the urlSplice
and urlParamsSplice
functions.
linksHandler :: Handler App App ()
linksHandler = heistLocal (I.bindSplices linksSplices) $ render "links"
where
linksSplices = do
"loginUrl" ## urlSplice Login
"echoUrl" ## urlSplice (Echo "ping")
"countUrl" ## urlParamsSplice (Count 10) [("explanation", Just "true")]
As you can see, splicing URLs into Heist views is easily accomplished. You
will likely also want to redirect to the handler for a certain URL. To do
this we've got redirectURL
and redirectURLParams
. Let's look at an
example.
doSomethingHandler :: Handler App App ()
doSomethingHandler = doSomething >> redirectURL Logout
However, you will sometimes wish to redirect within a handler that runs in a snaplet other than the main app. With the router snaplet though, this is easily done:
handleLogout :: Handler App (AuthManager App) ()
handleLogout = logout >> (withTop router $ redirectURL (Echo "logged out"))
Lastly, you can render a URL as Text with urlPath
and urlPathParams
.
messageHandler :: Handler App App ()
messageHandler = do
pathText <- urlPath (Echo "hello")
heistLocal (I.bindSplices $ messageSplices path) $ render "message"
where
messageSplices path = do
"message" ## I.textSplice $ "The path is " `append` pathText
Remember, for each of urlSplice
, redirectURL
and urlPath
there is a
params version that takes a params list as an extra argument, and renders
the URL with the given params as a query string.