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:
Consider the following sample program that tries to serve the following URLs:
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:
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>
Consider a function with the following form:
func :: a -> m b
Because Haskell is pure and has a strong static type system, data used in this function can only come from three places: global symbols that are in scope or imported, the parameters (the 'a'), and the monad context 'm'. So the problem you describe isn't unique to Heist, it's a fact of using Haskell.
This suggests a couple ways of solving your problem. One is to pass the data you need as arguments to your splice functions. Something like this:
factorialSplice :: Int -> TemplateMonad (RouteT Sitemap (ServerPartT IO)) [X.Node]
factorialSplice n = return [X.TextNode . T.pack . show $ product [1 .. n]]
In Snap we have a function called renderWithSplices that lets you bind some splices right before you render the template. You could use a function like this to bind the right splice on the line where you currently have "render templates".
The second approach is using the underlying monad. You say that "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." In my mind, having access to the "application monad" is exactly what you need to get this stuff inside splices. So my second suggestion is to use that. If the application monad you're using doesn't have that data, then it's a deficiency of that monad, not a Heist problem.
As you can see in the type signature above, TemplateMonad is a monad transformer where the underlying monad is (RouteT Sitemap (ServerPartT IO)). This gives the splice access to everything in the underlying monad via a simple lift. I've never used web-routes, but it seems to me that there should be a RouteT function to get at that Sitemap. Let's assume the following function exists:
getUrlData :: RouteT url m url
Then you should be able to write:
factorialSplice :: TemplateMonad (RouteT Sitemap (ServerPartT IO)) [X.Node]
factorialSplice = do
url <- lift getUrlData
return $ case url of
Factorial n -> [X.TextNode . T.pack . show $ product [1 .. n]]
_ -> []
Or to generalize it a bit, you could do this:
factorialArgSplice :: TemplateMonad (RouteT Sitemap (ServerPartT IO)) [X.Node]
factorialArgSplice = do
url <- lift getUrlData
return $ case url of
Factorial n -> [X.TextNode . T.pack . show $ n]
_ -> []
Then you could bind that to the <factorialArg> tag and do the following in your template.
<p>The factorial of <factorialArg> is <factorial><factorialArg/></factorial>.</p>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With