Using values not from the application monad with Heist templates
Asked Answered
D

1

7

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>
Depravity answered 5/11, 2011 at 20:52 Comment(0)
F
4

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>
Forras answered 7/11, 2011 at 17:36 Comment(2)
I've used custom happstack variations on the snap 'rendWithSplices' over here: github.com/aslatter/blog/blob/master/Blog/Templates.hs In this example, the 'appTemplates' function if of type 'App TemplateState'.Goldy
I don't understand how option 1 would work since initHeist says "We don’t provide functions to add either type of loadtime splices to your HeistState after initHeist because it doesn’t make any sense unless you re-initialize all templates with the new splices.". I'm not skilled enough to understand the source code of Snap. It would be great if this answer could provide actual code that shows how to apply option 1 when not using Snap. The comment above seems to use a very old Heist version. These types are no longer exported or no longer exist. I think the answer to OPs question is: don'tGley

© 2022 - 2024 — McMap. All rights reserved.