web-routes-boomerang-0.27.1: Library for maintaining correctness and composability of URLs within an application.

Safe HaskellNone



web-routes-boomerang makes it easy to use write custom pretty-printers and parsers for your URL types. Instead of writing a parser and a separate pretty-printer you can specify both at once by using the boomerang library:


This demo will show the basics of getting started.

First we need to enable some language extensions:

{-# LANGUAGE TemplateHaskell, TypeOperators, OverloadedStrings #-}
 module Main where

Note in the imports that we hide (id, (.)) from the Prelude and use the versions from Control.Category instead.

 import Prelude              hiding (id, (.))
 import Control.Category     (Category(id, (.)))
 import Control.Monad.Trans  (MonadIO(liftIO))
 import Text.Boomerang.TH    (derivePrinterParsers)
 import Web.Routes           (Site(..), RouteT(..), decodePathInfo, encodePathInfo, runSite, showURL)
 import Web.Routes.Boomerang (Router, (<>), (</>), int, parse1, boomerangSiteRouteT, anyString, parseStrings)

Next we define a data type that represents our sitemap.

 -- | the routes
 data Sitemap
    = Home
    | UserOverview
    | UserDetail Int
    | Article Int String
    deriving (Eq, Show)

To use the Sitemap type with boomerang we need to call derivePrinterParsers:

 $(derivePrinterParsers ''Sitemap)

That will create new combinators corresponding to the constructors for Sitemap. They will be named, rHome, rUserOverview, etc.

Now we can specify how the Sitemap type is mapped to a url and back:

 sitemap :: Router Sitemap
 sitemap =
     (  rHome
     <> "users" . users
     <> rArticle . ("article" </> int . "-" . anyString)
     users  =  rUserOverview
            <> rUserDetail </> int

The mapping looks like this:

 /                       <=> Home
 /users                  <=> UserOverview
 /users/<int>            <=> UserDetail <int>
 /article/<int>-<string> <=> Article <int> <string>

Next we have our function which maps a parsed route to the handler for that route. (There is nothing boomerang specific about this function):

 handle :: Sitemap -> RouteT Sitemap IO ()
 handle url =
     case url of
       _ -> do liftIO $ print url
               s <- showURL url
               liftIO $ putStrLn s

Normally the case statement would match on the different constructors and map them to different handlers. But in this case we use the same handler for all constructors. Also, instead of running in the IO monad, we would typically use a web framework monad like Happstack's ServerPartT.

The handler does two things:

  1. prints the parsed url
  2. unparses the url and prints it

We now have two pieces:

  1. sitemap - which converts urls to the Sitemap type and back
  2. handle - which maps Sitemap to handlers

We tie these two pieces together use boomerangSiteRouteT:

 site :: Site Sitemap (IO ())
 site = boomerangSiteRouteT handle sitemap

This gives as a standard Site value that we can use with runSite or with framework specific wrappers like implSite.

If we were not using RouteT then we could use boomerangSite instead.

Now we can create a simple test function that takes the path info part of a url and runs our site:

 test :: ByteString -- ^ path info of incoming url
      -> IO ()
 test path =
     case runSite "" site (decodePathInfo path) of
       (Left e)   -> putStrLn e
       (Right io) -> io

We can use it like this:

ghci> test users/1
UserDetail 1

Here is a simple wrapper to call test interactively:

 -- | interactively call 'test'
 main :: IO ()
 main = mapM_ test =<< fmap lines getContents

Here are two more helper functions you can use to experiment interactively:

 -- | a little function to test rendering a url
 showurl :: Sitemap -> String
 showurl url =
     let (ps, params) = formatPathSegments site url
     in (encodePathInfo ps params)
 -- | a little function to test parsing a url
 testParse :: String -> Either String Sitemap
 testParse pathInfo =
     case parsePathSegments site $ decodePathInfo pathInfo of
       (Left e)  -> Left (show e)
       (Right a) -> Right a



type Router a b = PrinterParser TextsError [Text] a bSource

'Router a b' is a simple type alias for 'PrinterParser TextsError [Text] a b'



:: ((url -> [(Text, Maybe Text)] -> Text) -> url -> a)

handler function

-> Router () (url :- ())

the router

-> Site url a 

function which creates a Site from a Router and a handler



:: (url -> RouteT url m a)

handler function

-> Router () (url :- ())

the router

-> Site url (m a) 

function which creates a Site from a Router and a RouteT handler

boomerangFromPathSegments :: PrinterParser TextsError [Text] () (url :- ()) -> URLParser urlSource

convert to a URLParser so we can create a PathInfo instance

boomerangToPathSegments :: PrinterParser TextsError [Text] () (url :- ()) -> url -> [Text]Source

convert to the type expected by toPathSegments from PathInfo