zinza-0.2: src/Zinza.hs
{-# LANGUAGE ScopedTypeVariables #-}
-- |
-- License: GPL-2.0-or-later AND BSD-3-Clause
--
-- Zinza - a small jinja-syntax-inspired typed-template compiler.
--
-- Zinza typechecks and compiles a template.
-- We can compile either to Haskell function, or to verbatim Haskell module (planned).
--
-- Zinza is very minimalistic. Features are added when needed.
--
-- == Example usage
--
-- Given a template
--
-- @
-- {% for license in licenses %}
-- licenseName {{license.con}} = {{license.name}}
-- {% endfor %}
-- @
--
-- and data definitions like:
--
-- @
-- newtype Licenses = Licenses { licenses :: [License] }
-- deriving ('GHC.Generics.Generic')
--
-- data License = License
-- { licenseCon :: String
-- , licenseName :: String
-- }
-- deriving ('GHC.Generics.Generic')
-- @
--
-- We can (generically) derive 'Zinza' instances for @Licenses@ and @License@
--
-- @
-- instance 'Zinza' Licenses where
-- 'toType' = 'genericToType' id
-- 'toValue' = 'genericToValue' id
-- 'fromValue' = 'genericFromValue' id
--
-- instance 'Zinza' License where
-- 'toType' = 'genericToTypeSFP'
-- 'toValue' = 'genericToValueSFP'
-- 'fromValue' = 'genericFromValueSFP'
-- @
--
-- Then the example of run-time usage is
--
-- @
-- example :: IO String
-- example = do
-- -- this might fail, type errors!
-- run <- 'parseAndCompileTemplateIO' "fixtures/licenses.zinza"
-- -- this shouldn't fail (run-time errors are due bugs in zinza)
-- run $ Licenses
-- [ License \"Foo" (show "foo-1.0")
-- , License \"Bar" (show "bar-1.2")
-- ]
-- @
--
-- The result of running an @example@ is:
--
-- @
-- licenseName Foo = "foo-1.0"
-- licenseName Bar = "bar-1.2"
-- @
--
-- == Module generation
--
-- Zinza also supports standalone module generation.
--
-- @
-- 'parseAndCompileModuleIO' ('simpleConfig' \"DemoLicenses\" [\"Licenses\"] :: 'ModuleConfig' Licenses) "fixtures/licenses.zinza" >>= putStr
-- @
--
-- prints a Haskell module source code:
--
-- @
-- {-# OPTIONS_GHC -fno-warn-unused-imports #-}
-- module DemoLicenses (render) where
-- import Prelude (String, fst, snd, ($), not, return)
-- import Control.Monad (forM_)
-- import Licenses
-- type Writer a = (String, a)
-- tell :: String -> Writer (); tell x = (x, ())
-- execWriter :: Writer a -> String; execWriter = fst
-- render :: Licenses -> String
-- render (z_root) = execWriter $ do
-- forM_ (licenses $ z_root) $ \z_var0_license -> do
-- tell "licenseName "
-- tell (licenseCon $ z_var0_license)
-- tell " = "
-- tell (licenseName $ z_var0_license)
-- tell "\n"
-- @
--
-- which is not dependent on Zinza. You are free to use more efficient writer
-- as well.
--
-- === Expressions
--
-- @
-- {{ expression }}
-- @
--
-- Expression syntax has only few constructions:
--
-- * field access @foo.bar@
--
-- * function application @fun bar@ (though function can only be @not@)
--
-- /Note:/ you can provide your own /Prelude/ of functions. See @Bools.hs@
-- and @Bools.zinza@ in tests for an example.
-- You cannot define new functions in templates, but you can pass
-- them as template arguments.
--
-- === Control structures
--
-- The __for__ and __if__ statements are supported:
--
-- @
-- {% for value in values %}
-- ...
-- {% endfor %}
-- @
--
-- @
-- {% if boolExpression %}
-- ...
-- {% elif anotherBoolExpression %}
-- ...
-- {% else %}
-- ...
-- {% endif %}
-- @
--
-- If a control structure tag starts at the first column, the possible
-- trailing new line feed is stripped. This way full-line control tags
-- don't introduce new lines in the output.
--
-- === Blocks
--
-- It's possible to define blocks to be used (possibly multiple times) later:
--
-- @
-- {% defblock blockname %}
-- ...
-- {% endblock %}
-- @
--
-- And the block can be used later with:
--
-- @
-- {% useblock blockname %}
-- @
--
-- Blocks follow scopes of @if@ and @for@ control structures
--
-- === Comments
--
-- @
-- {\# Comments are omitted from the output #}
-- @
--
module Zinza (
parseAndCompileTemplate,
parseAndCompileTemplateIO,
-- * Compilation to Haskell module
parseAndCompileModule,
parseAndCompileModuleIO,
ModuleConfig (..),
simpleConfig,
-- * Input class
Zinza (..),
-- ** Generic deriving
genericToType,
genericToValue,
genericFromValue,
genericToTypeSFP,
genericToValueSFP,
genericFromValueSFP,
stripFieldPrefix,
GZinzaType, GZinzaValue, GZinzaFrom, GFieldNames,
-- * Templates
Node (..), Nodes, Expr (..), LExpr,
-- * Types
-- | Zinza's type-system is delibarately extremely simple.
Ty (..),
displayTy,
-- * Values
-- | 'Value's are passed at run-time, when the template is interpreted.
-- When compiled to the Haskell module, 'Value's aren't used.
Value (..),
-- * Errors
ParseError (..),
CompileError (..),
CompileOrParseError (..),
RuntimeError (..),
AsRuntimeError (..),
ThrowRuntime (..),
-- * Location
Loc (..), Located (..), zeroLoc, displayLoc, TraversableWithLoc (..),
-- * Variables
Var, Selector,
) where
import Control.Exception (throwIO)
import Data.Typeable (Typeable, typeRep)
import Zinza.Check
import Zinza.Errors
import Zinza.Expr
import Zinza.Generic
import Zinza.Module
import Zinza.Node
import Zinza.Parser
import Zinza.Pos
import Zinza.Type
import Zinza.Value
import Zinza.Var
-- | Parse and compile the template into Haskell function.
parseAndCompileTemplate
:: (Zinza a, ThrowRuntime m)
=> FilePath -- ^ name of the template
-> String -- ^ contents of the template
-> Either CompileOrParseError (a -> m String)
parseAndCompileTemplate name contents =
case parseTemplate name contents of
Left err -> Left (AParseError err)
Right nodes -> case check nodes of
Left err' -> Left (ACompileError err')
Right res -> Right res
-- | Like 'parseAndCompileTemplate' but reads file and (possibly)
-- throws 'CompileOrParseError'.
parseAndCompileTemplateIO :: (Zinza a, ThrowRuntime m) => FilePath -> IO (a -> m String)
parseAndCompileTemplateIO name = do
contents <- readFile name
either throwIO return $ parseAndCompileTemplate name contents
-- | Parse and compile the template into 'String' representing a Haskell module.
parseAndCompileModule
:: Zinza a
=> ModuleConfig a
-> FilePath
-> String
-> Either CompileOrParseError String
parseAndCompileModule mc name contents =
case parseTemplate name contents of
Left err -> Left (AParseError err)
Right nodes -> case checkModule mc nodes of
Left err -> Left (ACompileError err)
Right res -> Right res
-- | Like 'parseAndCompileModule' but reads file and (possibly)
-- throws 'CompileOrParseError'.
parseAndCompileModuleIO :: Zinza a => ModuleConfig a -> FilePath -> IO String
parseAndCompileModuleIO mc name = do
contents <- readFile name
either throwIO return $ parseAndCompileModule mc name contents
-- | Simple configuration to use with 'parseAndCompileModule' or
-- 'parseAndCompileModuleIO'.
simpleConfig
:: forall a. Typeable a
=> String -- ^ module name
-> [String] -- ^ imports
-> ModuleConfig a
simpleConfig moduleName imports = ModuleConfig
{ mcRender = "render"
, mcHeader =
[ "{-# OPTIONS_GHC -fno-warn-unused-imports #-}"
, "module " ++ moduleName ++ " (render) where"
, "import Prelude (String, fst, snd, ($), return)"
, "import Control.Monad (forM_)"
] ++
[ "import " ++ i
| i <- imports
] ++
[ "type Writer a = (String, a)"
, "tell :: String -> Writer (); tell x = (x, ())"
, "execWriter :: Writer a -> String; execWriter = fst"
, "render :: " ++ typeName ++ " -> String"
]
}
where
typeName = show (typeRep ([] :: [a]))