The web-routes libraries provide a system for type-safe url routing. The basic concept behind type-safe urls is very simple. Instead of working directly with url strings, we create a type that represents all the possible urls in our web application. By using types instead of strings we benefit in several ways:
String
, "/hmoe" instead of "/home", the compiler will gleefully compile it. But if you mistype the constructor as Hmoe
instead of Home
you will get a compile time error.case
statement on the url type. If you forget to handle a route, the compiler will give you an Pattern match(es) are non-exhaustive warning.web-routes is designed to be very flexible. For example, it does not require that you use any particular mechanism for defining the mapping between the url type and the url string. Instead, we provide a variety of addon packages that provide different methods including, template-haskell, generics, parsec, quasi-quotation, and more. This means it is also easy to add your own custom mechanism. For example, you might still use template-haskell, but with a different set of rules for converting a type to a string.
web-routes is also not limited to use with any particular framework, templating system, database, etc. In fact, web-routes provides the foundation for type-safe url routing in yesod.
Let's start by looking at a simple example of using web-routes
. In this example we will use blaze for the html templates.
In order to run this demo you will need to install web-routes, web-routes-th and web-routes-happstack from hackage.
> {-# LANGUAGE DeriveDataTypeable, GeneralizedNewtypeDeriving, TemplateHaskell #-} > module Main where > > import Prelude hiding (head) > > import Control.Monad (msum) > import Data.Data (Data, Typeable) > import Data.Monoid (mconcat) > import Data.Text (pack) > import Happstack.Server ( Response, ServerPartT, ok, toResponse, simpleHTTP > , nullConf, seeOther, dir, notFound, seeOther) > import Text.Blaze.Html4.Strict ( (!), html, head, body, title, p, toHtml > , toValue, ol, li, a) > import Text.Blaze.Html4.Strict.Attributes (href) > import Web.Routes ( PathInfo(..), RouteT, showURL > , runRouteT, Site(..), setDefault, mkSitePI) > import Web.Routes.TH (derivePathInfo) > import Web.Routes.Happstack (implSite) >
First we need to define the type to represent our routes. In this site we will have a homepage and articles which can be retrieved by their id.
> newtype ArticleId > = ArticleId { unArticleId :: Int } > deriving (Eq, Ord, Enum, Read, Show, Data, Typeable, PathInfo) > > data Sitemap > = Home > | Article ArticleId > deriving (Eq, Ord, Read, Show, Data, Typeable) >
Next we use template-haskell to derive an instance of PathInfo
for the Sitemap
type.
> $(derivePathInfo ''Sitemap) >
The PathInfo
class is defined in Web.Routes
and looks like this:
> class PathInfo a where > toPathSegments :: a -> [String] > fromPathSegments :: URLParser a
It is basically a class that describes how to turn a type into a url and back. This class is semi-optional. Some conversion methods such as web-routes-th and web-routes-regular use it, but others do not.
Since ArticleId
is just a newtype
we were
able to just do deriving PathInfo
instead of having
to call derivePathInfo
.
Next we need a function that maps a route to the handlers:
> route :: Sitemap -> RouteT Sitemap (ServerPartT IO) Response > route url = > case url of > Home -> homePage > (Article articleId) -> articlePage articleId >
As you can see, mapping a url to a handler is just a straight-forward case statement. We do not need to do anything fancy here to extract the article id from the url, becuse that has already been done when the url was converted to a Sitemap
value.
You may be tempted to write the route
function like this instead of using the case statement:
> route :: Sitemap -> RouteT Sitemap (ServerPartT IO) Response > route Home = homePage > route (Article articleId) = articlePage articleId
But, I don't recommend it. In a real application, the route
function will likely take a number of extra arguments such as database handles. Sometimes those extra arguments are only used by some of the handlers. But every time you add a parameter, you have to update every pattern match to account for the extra argument. Using a case statement instead makes the code easier to maintain and more readable in my opinion.
The other thing you will notice is the RouteT
monad transformer in the type signature. The RouteT
monad transformer is another semi-optional feature of web-routes. RouteT
is basically a Reader
monad that holds the function which converts the url type into a string. At first, this seems unnecessary -- why not just call toPathInfo
directly and skip RouteT
entirely? But it turns out there are few advantages that RouteT
brings:
RouteT
is parametrized by the url type -- in this case Sitemap
. That will prevent us from accidentally trying to convert an ArticleId
into a url. An ArticleId
is a valid component of some urls, but it is not a valid URL by itself.RouteT
can also contain additional information needed to form a valid url, such as the hostname name, port, and path prefixRouteT
is also used when we want to embed a library/sub-site into a larger site.We will see examples of these benefits as we continue with the tutorial.
Next, we have the handler functions:
> homePage :: RouteT Sitemap (ServerPartT IO) Response > homePage = > do articles <- mapM mkArticle [(ArticleId 1) .. (ArticleId 10)] > ok $ toResponse $ > html $ do > head $ title $ (toHtml "Welcome Home!") > body $ do > ol $ mconcat articles > where > mkArticle articleId = > do url <- showURL (Article articleId) > return $ li $ a ! href (toValue url) $ > toHtml $ "Article " ++ (show $ unArticleId articleId) >
> articlePage :: ArticleId -> RouteT Sitemap (ServerPartT IO) Response > articlePage (ArticleId articleId) = > do homeURL <- showURL Home > ok $ toResponse $ > html $ do > head $ title $ (toHtml $ "Article " ++ show articleId) > body $ do > p $ toHtml $ "You are now reading article " ++ show articleId > p $ do toHtml "Click " > a ! href (toValue homeURL) $ toHtml "here" > toHtml " to return home." >
Even though we have the RouteT
in the type signature -- these functions look normal ServerPartT
functions -- we do not have to use lift
or anything else. That is because RouteT
is a instance of all the Happstack classes such as ServerMonad,
FilterMonad,
etc. Though you do need to make sure you have imported Web.Routes.Happstack
to get those instances.
The only new thing here is the showURL
function, which has the type:
> showURL :: ShowURL m => URL m -> m String
showURL
converts a url type, such as Sitemap
into a url String
that we can use an href, src, etc attribute.
URL m
is a type-function that calculates the url type based on the monad we are currently in. For RouteT url m a
, URL m
is going to be whatever url
is. In this example, url
is Sitemap
. If you are not familiar with type families and type functions, see this section.
Now we have:
Sitemap
PathInfo
route
We need to tie these three pieces together. That is what the Site
type does for us:
> data Site url a > = Site { > -- | function which routes the url to a handler > handleSite :: (url -> [(String, String)] -> String) -> url -> a > -- | This function must be the inverse of 'parsePathSegments'. > , formatPathSegments :: url -> ([String], [(String, String)]) > -- | This function must be the inverse of 'formatPathSegments'. > , parsePathSegments :: [String] -> Either String url > }
Looking at the type for Site
, we notice that it is very general -- it does not have any references to Happstack, PathInfo
, URLParser
, RouteT
, etc. That is because those are all addons to the core of web-routes. We can convert our route
to a Site
using some simple helper functions like this:
> site :: Site Sitemap (ServerPartT IO Response) > site = > setDefault Home $ mkSitePI (runRouteT route) >
runRouteT
removes the RouteT
wrapper from our routing function:
> runRouteT :: (url -> RouteT url m a) > -> ((url -> [(String, String)] -> String) -> url -> m a)
So if we have our routing function like:
> route :: Sitemap > -> RouteT Sitemap (ServerPartT IO) Response
runRouteT
will convert it to a function that takes a url showing function:
> (runRouteT route) :: (Sitemap -> [(String, String)] -> String) > -> Sitemap > -> ServerPartT IO Response
Since we created a PathInfo
instance for
Sitemap
we can use mkSitePI
to convert the
new function to a Site
. mkSitePI
has the type:
> mkSitePI :: (PathInfo url) => > ((url -> [(String, String)] -> String) -> url -> a) > -> Site url a
so applying it to runRouteT route
gives us:
> (mkSitePI (runRouteT route)) :: Site Sitemap (ServerPartT IO Response)
setDefault
allows you to map / to any route you want. In this example we map / to Home
.
> setDefault :: url -> Site url a -> Site url a
Next we use implSite
to embed the Site
into a normal Happstack route:
> main :: IO () > main = simpleHTTP nullConf $ > msum [ dir "favicon.ico" $ notFound (toResponse ()) > , implSite (pack "http://localhost:8000") (pack "/route") site > , seeOther "/route" (toResponse ()) > ] >
The type for implSite is straight-forward:
> implSite :: (Functor m, Monad m, MonadPlus m, ServerMonad m) => > String -- ^ "http://example.org" > -> FilePath -- ^ path to this handler, .e.g. "/route" > -> Site url (m a) -- ^ the 'Site' > -> m a
The first argument is the domain/port/etc that you want to add to the beginning of any URLs you show. The first argument is not used during the decoding/routing process -- it is merely prepended to any generated url strings.
The second argument is the path to this handler. This path is automatically used when routing the incoming request and when showing the URL. This path can be used to ensure that all routes generated by web-routes are unique because they will be in a separate sub-directory (aka, a separate namespace). If you do not want to put the routes in a separate sub-directory you can set this field to "".
The third argument is the Site
that does the routing.
If the URL decoding fails, then implSite
will call mzero
.
Sometimes you will want to know the exact parse error that caused the router to fail. You can get the error by using implSite_
instead. Here is an alternative main
that prints the route error to stdout.
> main :: IO () > main = simpleHTTP nullConf $ > msum [ dir "favicon.ico" $ notFound (toResponse ()) > , do r <- implSite_ (pack "http://localhost:8000") (pack "/route") site > case r of > (Left e) -> liftIO (print e) >> mzero > (Right m) -> return m > , seeOther "/route" (toResponse ()) > ] >
[Source code for the app is here.]
showURL
has the type:
> showURL :: ShowURL m => URL m -> m String
If you are not familiar with type families and type functions, the URL m
in that type signature might look a bit funny. But it is really very simple.
The showURL
function leverages the ShowURL
class:
> class ShowURL m where > type URL m > showURLParams :: (URL m) -> [(String, String)] -> m String
And here is the RouteT
instance for ShowURL
:
> instance (Monad m) => ShowURL (RouteT url m) where > type URL (RouteT url m) = url > showURLParams url params = > do showF <- askRouteT > return (showF url params)
Here URL
is a type function that is applied to a type and gives us another type. For example, writing URL (RouteT Sitemap (ServerPartT IO))
gives us the type Sitemap
. We can use the type function any place we would normally use a type.
In our example we had:
> homeURL <- showURL Home
So there, showURL is going to have the type:
> showURL :: URL (RouteT Sitemap (ServerPartT IO)) > -> RouteT Sitemap (ServerPartT IO) String
which can be simplified to:
> showURL :: Sitemap -> RouteT Sitemap (ServerPartT IO) String
So, we see that the url type we pass to showURL
is dictated by the monad we are currently in. This ensures that we only call showURL
on values of the right type.
While ShowURL
is generally used with the RouteT
type -- it is not actually a requirement. You can implement ShowURL
for any monad of your choosing.
In the previous example we used template haskell to automatically derive a mapping between the url type and the url string. This is very convenient early in the development process when the routes are changing a lot. But the resulting urls are not very attractive. One solution is to write the mappings from the url type to the url string by hand.
One way to do that would be to write one function to show the urls, and another function that uses parsec to parse the urls. But having to say the same thing twice is really annoying and error prone. What we really want is a way to write the mapping once, and automatically exact a parser and printer from the specification.
Fortunately, Sjoerd Visscher and Martijn van Steenbergen figured out exactly how to do that and published a proof of concept library know as Zwaluw. With permission, I have refactored their original library into two separate libraries: boomerang and web-routes-boomerang.
The technique behind Zwaluw and Boomerang is very cool. But in this tutorial we will skip the theory and get right to the practice.
In order to run this demo you will need to install web-routes, web-routes-boomerang and web-routes-happstack from hackage.
We will modify the previous demo to use boomerang in order to demonstrate how easy it is to change methods midstream. We will also add a few new routes to demonstrate some features of using boomerang.
> {-# LANGUAGE DeriveDataTypeable, GeneralizedNewtypeDeriving, TemplateHaskell, > TypeOperators, OverloadedStrings #-} > module Main where >
The first thing to notice is that we hide id
and (.)
from the Prelude
and import the versions from Control.Category
instead.
> import Prelude hiding (head, id, (.)) > import Control.Category (Category(id, (.))) > > import Control.Monad (msum) > import Data.Data (Data, Typeable) > import Data.Monoid (mconcat) > import Data.String (fromString) > import Data.Text (Text) > import Happstack.Server ( Response, ServerPartT, ok, toResponse, simpleHTTP > , nullConf, seeOther, dir, notFound, seeOther) > import Text.Blaze.Html4.Strict ( (!), html, head, body, title, p, toHtml > , toValue, ol, li, a) > import Text.Blaze.Html4.Strict.Attributes (href) > import Text.Boomerang.TH (derivePrinterParsers) > import Web.Routes ( PathInfo(..), RouteT, showURL > , runRouteT, Site(..), setDefault, mkSitePI) > import Web.Routes.TH (derivePathInfo) > import Web.Routes.Happstack (implSite) > import Web.Routes.Boomerang >
Next we have our Sitemap
types again. Sitemap
is similar to the previous example, except it also includes UserOverview
and UserDetail
.
> newtype ArticleId > = ArticleId { unArticleId :: Int } > deriving (Eq, Ord, Enum, Read, Show, Data, Typeable, PathInfo) > > data Sitemap > = Home > | Article ArticleId > | UserOverview > | UserDetail Int Text > deriving (Eq, Ord, Read, Show, Data, Typeable) >
Next we call derivePrinterParsers
:
> $(derivePrinterParsers ''Sitemap) >
That will create new combinators corresponding to the constructors
for Sitemap
. They will be named, rHome
, rArticle
, rUserOverview
, and rUserDetail
.
Now we can specify how the Sitemap
type is mapped to a url string and back:
> sitemap :: Router () (Sitemap :- ()) > sitemap = > ( rHome > <> rArticle . (lit "article" </> articleId) > <> lit "users" . users > ) > where > users = rUserOverview > <> rUserDetail </> int . lit "-" . anyText > > articleId :: Router () (ArticleId :- ()) > articleId = > xmaph ArticleId (Just . unArticleId) int
The mapping looks like this:
url | type | |
---|---|---|
/ | <=> | Home |
/article/int | <=> | Article int |
/users | <=> | UserOverview |
/users/int-string | <=> | UserDetail int string |
The sitemap
function looks like an ordinary parser. But, what makes it is exciting is that it also defines the pretty-printer at the same time.
By examining the mapping table and comparing it to the code, you should be able to get an intuitive feel for how boomerang works. The key boomerang features we see are:
<>
<>
is the choice operator. It chooses between the various paths..
.
is used to combine elements together.</>
lit
, int
, anyText
, operate on a single path segment. </>
matches on the / between path segments.lit
lit
matches on a string literal. If you enabled OverloadedStrings
then you do not need to explicitly use the lit
function. For example, you could just write, int . "-" . anyText
.int
int
matches on an Int
.anyText
anyText
matches on any string. It keeps going until it reaches the end of the current path segment.xmaph
xmaph
is a bit like fmap
, except instead of only needing a -> b
it also needs the other direction, b -> Maybe a
.
> xmaph :: (a -> b) > -> (b -> Maybe a) > -> PrinterParser e tok i (a :- o) > -> PrinterParser e tok i (b :- o)
xmaph
to convert int :: Router () (Int :- ())
into articleId :: Router () (ArticleId :- ())
.
/users
comes before /users/int-string
. Unlike parsec, the order of the parsers (usually) does not matter. We also do not have to use try to allow for backtracking. boomerang will find all valid parses and pick the best one. Here, that means the parser that consumed all the available input.Router
type is just a simple alias:
> type Router a b = PrinterParser TextsError [Text] a b
Looking at this line:
> <> rUserDetail </> int . lit "-" . anyText
and comparing it to the constructor
> UserDetail Int Text
we see that the constructor takes two arguments, but the mapping uses three combinators, int
, lit
, and anyText
. It turns out that some combinators produce/consume values from the url type, and some do not. We can find out which do and which don't by looking at the their types:
> int :: PrinterParser TextsError [Text] r (Int :- r) > anyText :: PrinterParser TextsError [Text] r (Text :- r) > lit :: Text -> PrinterParser TextsError [Text] r r
We see int
takes r
and produces (Int :- r)
and anyText
takes r
and produces (Text :- r)
. While lit
takes r
and returns r
.
Looking at the type of the all three composed together we get:
> int . lit "-" . anyText :: PrinterParser TextsError [Text] a (Int :- (Text :- a))
So there we see the Int
and Text
that are arguments to UserDetail
.
Looking at the type of rUserDetail
, we will see that it has the type:
> rUserDetail :: PrinterParser e tok (Int :- (Text :- r)) (Sitemap :- r)
So, it takes an Int
and Text
and produces a Sitemap
. That mirrors what the UserDetail
constructor itself does:
ghci> :t UserDetail UserDetail :: Int -> Text -> Sitemap
Next we need a function that maps a route to the handlers. This is the same exact function we used in the previous example extended with the additional routes:
> route :: Sitemap -> RouteT Sitemap (ServerPartT IO) Response > route url = > case url of > Home -> homePage > (Article articleId) -> articlePage articleId > UserOverview -> userOverviewPage > (UserDetail uid name) -> userDetailPage uid name >
Next, we have the handler functions. These are also exactly the same as the previous example, plus the new routes:
> homePage :: RouteT Sitemap (ServerPartT IO) Response > homePage = > do articles <- mapM mkArticle [(ArticleId 1) .. (ArticleId 10)] > userOverview <- showURL UserOverview > ok $ toResponse $ > html $ do > head $ title $ "Welcome Home!" > body $ do > a ! href (toValue userOverview) $ "User Overview" > ol $ mconcat articles > where > mkArticle articleId = > do url <- showURL (Article articleId) > return $ li $ a ! href (toValue url) $ > toHtml $ "Article " ++ (show $ unArticleId articleId) >
> articlePage :: ArticleId -> RouteT Sitemap (ServerPartT IO) Response > articlePage (ArticleId articleId) = > do homeURL <- showURL Home > ok $ toResponse $ > html $ do > head $ title $ (toHtml $ "Article " ++ show articleId) > body $ do > p $ toHtml $ "You are now reading article " ++ show articleId > p $ do "Click " > a ! href (toValue homeURL) $ "here" > " to return home." >
> userOverviewPage :: RouteT Sitemap (ServerPartT IO) Response > userOverviewPage = > do users <- mapM mkUser [1 .. 10] > ok $ toResponse $ > html $ do > head $ title $ "Our Users" > body $ do > ol $ mconcat users > where > mkUser userId = > do url <- showURL (UserDetail userId (fromString $ "user " ++ show userId)) > return $ li $ a ! href (toValue url) $ > toHtml $ "User " ++ (show $ userId) >
> userDetailPage :: Int -> Text -> RouteT Sitemap (ServerPartT IO) Response > userDetailPage userId userName = > do homeURL <- showURL Home > ok $ toResponse $ > html $ do > head $ title $ (toHtml $ "User " <> userName) > body $ do > p $ toHtml $ "You are now view user detail page for " <> userName > p $ do "Click " > a ! href (toValue homeURL) $ "here" > " to return home." >
Creating the Site
type is similar to the previous example. We still use runRouteT
to unwrap the RouteT
layer. But now we use boomerangSite
to convert the route
function into a Site
:
> site :: Site Sitemap (ServerPartT IO Response) > site = > setDefault Home $ boomerangSite (runRouteT route) sitemap >
The route function is essentially the same in this example and the previous example -- it did not have to be changed to work with boomerang
instead of PathInfo
. It is the formatPathSegments
and parsePathSegments
functions bundled up in the Site
that change. In the previous example, we used mkSitePI
, which leveraged the PathInfo
instances. Here we use boomerangSite
which uses the sitemap
mapping we defined above.
The practical result is that you can start by using derivePathInfo
and avoid having to think about how the urls will look. Later, once the routes have settled down, you can then easily switch to using boomerang
to create your route mapping.
Next we use implSite
to embed the Site
into a normal Happstack route:
> main :: IO () > main = simpleHTTP nullConf $ > msum [ dir "favicon.ico" $ notFound (toResponse ()) > , implSite "http://localhost:8000" "/route" site > , seeOther ("/route/" :: String) (toResponse ()) > ] >
[Source code for the app is here.]
In this example, we only used a few simple combinators. But boomerang provides a whole range of combinators such as many, some, chain, etc. For more information check out the haddock documentation for boomerang. Especially the Text.Boomerang.Combinators
and Text.Boomerang.Texts
modules.
You will need to install the optional web-routes, web-routes-th, web-routes-hsp and happstack-hsp packages for this section.
> {-# LANGUAGE TemplateHaskell #-} > {-# OPTIONS_GHC -F -pgmFtrhsx #-} > module Main where > > import Control.Applicative ((<$>)) > import Happstack.Server > import Happstack.Server.HSP.HTML > import qualified HSX.XMLGenerator as HSX > import Web.Routes > import Web.Routes.TH > import Web.Routes.XMLGenT > import Web.Routes.Happstack
If you are using web-routes and HSP then inserting URLs is especially clean and easy. If we have the URL:
> data SiteURL = Monkeys Int deriving (Eq, Ord, Read, Show) > > $(derivePathInfo ''SiteURL) >
Now we can define a template like this:
> monkeys :: Int -> RouteT SiteURL (ServerPartT IO) Response > monkeys n = > do html <- defaultTemplate "monkeys" () $ > <%> > <p>You have <% show n %> monkeys.</p> > <p>Click <a href=(Monkeys (succ n))>here</a> for more.</p> > </%> > ok $ (toResponse html)
Notice that in particular this bit:
> <a href=(Monkeys (succ n))>here</a>
We do not need showURL
, we just use the URL type directly. That works because Web.Routes.XMLGenT
provides an instance:
> instance (Functor m, Monad m) => EmbedAsAttr (RouteT url m) (Attr String url)
Here is the rest of the example:
> route :: SiteURL -> RouteT SiteURL (ServerPartT IO) Response > route url = > case url of > (Monkeys n) -> monkeys n > > site :: Site SiteURL (ServerPartT IO Response) > site = setDefault (Monkeys 0) $ mkSitePI (runRouteT route) > > main :: IO () > main = simpleHTTP nullConf $ > msum [ dir "favicon.ico" $ notFound (toResponse ()) > , implSite (pack "http://localhost:8000") empty site > ]
[Source code for the app is here.]
I am working on additional sections which will cover creating 'sub-sites' that can be embedded into larger sites, integration with HSP, and more.