stache-2.2.0: app/Main.hs
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TemplateHaskell #-}
module Main (main) where
import Data.Aeson (Value (..))
import qualified Data.Aeson as Aeson
import qualified Data.HashMap.Strict as HM
import Data.List (foldl')
import Data.List.NonEmpty (NonEmpty (..))
import qualified Data.List.NonEmpty as NE
import Data.Semigroup (sconcat)
import qualified Data.Text.Lazy.IO as T
import Data.Version (showVersion)
import qualified Data.Yaml as Yaml
import Development.GitRev
import Options.Applicative
import Paths_stache (version)
import System.Exit (exitFailure)
import System.FilePath (takeExtension)
import System.IO (hPutStrLn, stderr)
import Text.Mustache
main :: IO ()
main = do
Opts {..} <- execParser optsParserInfo
template <-
sconcat
<$> mapM (compileMustacheDir optTarget) optTemplateDirs
context <-
foldl' mergeContexts emptyContext
<$> mapM loadContext optContextFiles
let rendered = renderMustache template context
case optOutputFile of
Nothing -> T.putStrLn rendered
Just ofile -> T.writeFile ofile rendered
----------------------------------------------------------------------------
-- Command line options parsing
-- | Command line options.
data Opts = Opts
{ -- | Context files.
optContextFiles :: [FilePath],
-- | Where to save the result.
optOutputFile :: Maybe FilePath,
-- | Name of the template to render.
optTarget :: PName,
-- | Directories with templates.
optTemplateDirs :: NonEmpty FilePath
}
optsParserInfo :: ParserInfo Opts
optsParserInfo =
info (helper <*> ver <*> optsParser) . mconcat $
[ fullDesc,
progDesc "Command line interface to the Stache template processor",
header "stache—a simple implementation of Mustache templates"
]
where
ver :: Parser (a -> a)
ver =
infoOption verStr . mconcat $
[ long "version",
short 'v',
help "Print version of the program"
]
verStr =
unwords
[ "stache",
showVersion version,
$gitBranch,
$gitHash
]
optsParser :: Parser Opts
optsParser =
Opts
<$> many
( (strOption . mconcat)
[ long "context",
short 'c',
metavar "CONTEXT",
help "Context file in YAML or JSON format"
]
)
<*> (optional . strOption . mconcat)
[ long "ofile",
short 'o',
metavar "OFILE",
help "Save the rendered document to this file (otherwise write to stdout)"
]
<*> (argument str . mconcat)
[ metavar "TARGET",
help "Name of the template to render"
]
<*> (fmap NE.fromList . some)
( (argument str . mconcat)
[ metavar "DIR",
help "Template directories"
]
)
----------------------------------------------------------------------------
-- Helpers
-- | File context from a YAML or JSON file.
loadContext :: FilePath -> IO Value
loadContext file = do
let readYaml =
either
(Left . Yaml.prettyPrintParseException)
Right
<$> Yaml.decodeFileEither file
econtext <- case takeExtension file of
".yml" -> readYaml
".yaml" -> readYaml
_ -> Aeson.eitherDecodeFileStrict file
case econtext of
Left err -> spitErrorAndDie err
Right v@(Aeson.Object _) -> return v
Right _ -> spitErrorAndDie "context file should contain an object"
mergeContexts :: Value -> Value -> Value
mergeContexts (Aeson.Object m0) (Aeson.Object m1) =
Aeson.Object (HM.union m0 m1)
mergeContexts _ _ = error "context merge failed"
emptyContext :: Value
emptyContext = Aeson.object []
spitErrorAndDie :: String -> IO a
spitErrorAndDie err = do
hPutStrLn stderr err
exitFailure