tdammers / ginger

A Haskell implementation of the Jinja template language.
MIT License
77 stars 13 forks source link

Best way to deal with lists and maps as variables #63

Closed JivanRoquet closed 3 years ago

JivanRoquet commented 3 years ago

Hello,

First off, congrats for the work on Ginger. Being a long-time Jinja2 user I can definitely appreciate a similar template engine running on Haskell.

I managed to run the simple examples from the docs, but I'm having a hard time figuring out how to pass anything else than a HashMap Text Text as the source of input variables.

For instance, consider this template:

Hello {{ name }},
You have {% for item in items %}{{ item }}{% if not loop.last %}, {% endif %}{% endfor %}.

I'm trying to feed this template with the following elements:

import Data.HashMap.Strict (HashMap, fromList)

data Value =
    VText Text
  | VList [Text]
  deriving Show

context :: HashMap Text Value
context = fromList [
        ("name", VText "John"),
        ("items", VList ["car", "truck", "house", "phone"])
    ]

scopeLookup :: (Hashable k, Eq k, ToGVal m b) => k -> HashMap.HashMap k b -> GVal m
scopeLookup key context =
    toGVal $ HashMap.lookup key context

render :: Template SourcePos -> HashMap VarName Value -> Text
render template contextMap =
  let contextLookup = flip scopeLookup contextMap
      context = makeContextHtml contextLookup
  in htmlSource $ runGinger context template

With the code and the template above, I would expect the output to be:

Hello John,
You have car, truck, house, phone.

The problem is that Value doesn't derive ToGVal according to the compiler. I've tried different ways to fix it, but none has worked so far, which leads me to think that maybe I'm just doing it wrong entirely.

Is there any recommended way to inject a sum type like this, or any other way to deal with variables that can be either Text, or [Text], or HashMap Text Text?

tdammers commented 3 years ago

Ginger only understands GVals, so you can't inject sum types like this one directly - you have to convert them to GVal first.

Writing a ToGVal instance for your custom sum type isn't difficult though, it goes something like this:

instance ToGVal m Value where
  toGVal (VText t) = toGVal t
  toGVal (VList xs) = toGVal xs

However, you don't even need the sum type if you're only using it so that you can have heterogenous maps - instead, just convert all entries to GVal before putting them in the context object:

context :: HashMap Text (GVal m)
context = fromList [
        ("name", toGVal "John"),
        ("items", toGVal ["car", "truck", "house", "phone"])
    ]

And then you can ditch the toGVal from scopeLookup, because your entries are GVals already.

HTH.