I'm trying to write an application server using Happstack, Heist, and web-routes, but am having trouble figuring out how to let splices access values that don't originate from my application's monad stack.
There are two situations where this comes up:
- Parameters extracted from the URL path via web-routes. These come from pattern-matching on a type-safe URL when routing the request to the proper handler.
- Session information. If the request is for a brand-new session, I can't read the session identifier from a cookie in the request (since no such cookie exists yet), and I can't use the splices to create a new session if needed, since then if more than one splice tries to do it, I wind up creating multiple new sessions for a single request. But if I create the session before entering the web-routes stuff, the session exists outside the application monad.
Consider the following sample program that tries to serve the following URLs:
- /factorial/n outputs the factorial of n
- /reverse/str outputs str backwards
Since the parameter appears in the URL path instead of the query string, it gets extracted via web-routes instead of coming from the ServerPartT monad. From there, though, there's no clear way to put the parameter somewhere where the splices can see it, since they only have access to the application monad.
The obvious solution of sticking a ReaderT somewhere on the monad stack has two problems:
- Having a ReaderT above ServerPartT hides the Happstack parts of the monad stack, since ReaderT doesn't implement ServerMonad, FilterMonad, etc.
- It assumes that all the pages I'm serving take the same type of parameter, but in this example, /factorial wants an Int but /reverse wants a String. But for both page handlers to use the same TemplateDirectory, the ReaderT would need to be carrying a value of the same type.
From peeking at the Snap documentation, it looks like Snap handles parameters in the URL path by effectively copying them into the query string, which sidesteps the problem. But that's not an option with Happstack and web-routes, and besides, having two different ways for a URL to specify the same value strikes me as being a bad idea security-wise.
So, is there a "proper" way to expose non-application-monad request data to splices, or do I need to abandon Heist and use something like Blaze-HTML instead where this isn't an issue? I feel like I'm missing something obvious, but can't figure out what it might be.
Example code:
{-# LANGUAGE TemplateHaskell #-}
import Prelude hiding ((.))
import Control.Category ((.))
import Happstack.Server (Response, ServerPartT, nullConf, ok, simpleHTTP)
import Happstack.Server.Heist (render)
import Text.Boomerang.TH (derivePrinterParsers)
import Text.Templating.Heist (Splice, bindSplices, emptyTemplateState, getParamNode)
import Text.Templating.Heist.TemplateDirectory (TemplateDirectory, newTemplateDirectory')
import Web.Routes (RouteT, Site, runRouteT)
import Web.Routes.Boomerang (Router, anyString, boomerangSite, int, lit, (<>), (</>))
import Web.Routes.Happstack (implSite)
import qualified Data.ByteString.Char8 as C
import qualified Data.Text as T
import qualified Text.XmlHtml as X
data Sitemap = Factorial Int
| Reverse String
$(derivePrinterParsers ''Sitemap)
-- Conversion between type-safe URLs and URL strings.
sitemap :: Router Sitemap
sitemap = rFactorial . (lit "factorial" </> int)
<> rReverse . (lit "reverse" </> anyString)
-- Serve a page for each type-safe URL.
route :: TemplateDirectory (RouteT Sitemap (ServerPartT IO)) -> Sitemap -> RouteT Sitemap (ServerPartT IO) Response
route templates url = case url of
Factorial _num -> render templates (C.pack "factorial") >>= ok
Reverse _str -> render templates (C.pack "reverse") >>= ok
site :: TemplateDirectory (RouteT Sitemap (ServerPartT IO)) -> Site Sitemap (ServerPartT IO Response)
site templates = boomerangSite (runRouteT $ route templates) sitemap
-- <factorial>n</factorial> --> n!
factorialSplice :: (Monad m) => Splice m
factorialSplice = do input <- getParamNode
let n = read . T.unpack $ X.nodeText input :: Int
return [X.TextNode . T.pack . show $ product [1 .. n]]
-- <reverse>text</reverse> --> reversed text
reverseSplice :: (Monad m) => Splice m
reverseSplice = do input <- getParamNode
return [X.TextNode . T.reverse $ X.nodeText input]
main :: IO ()
main = do templates <- newTemplateDirectory' path . bindSplices splices $ emptyTemplateState path
simpleHTTP nullConf $ implSite "http://localhost:8000" "" $ site templates
where splices = [(T.pack "factorial", factorialSplice), (T.pack "reverse", reverseSplice)]
path = "."
factorial.tpl:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<title>Factorial</title>
</head>
<body>
<p>The factorial of 6 is <factorial>6</factorial>.</p>
<p>The factorial of ??? is ???.</p>
</body>
</html>
reverse.tpl:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<title>Reverse</title>
</head>
<body>
<p>The reverse of "<tt>hello world</tt>" is "<tt><reverse>hello world</reverse></tt>".</p>
<p>The reverse of "<tt>???</tt>" is "<tt>???</tt>".</p>
</body>
</html>