Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using values not from the application monad with Heist templates

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>
like image 648
Paul Kuliniewicz Avatar asked Nov 05 '11 20:11

Paul Kuliniewicz


1 Answers

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>
like image 88
mightybyte Avatar answered Sep 23 '22 09:09

mightybyte