packages feed

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