ede-0.3.4.0: app/Main.hs
{-# LANGUAGE CPP #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
module Main (main) where
import qualified Control.Monad.Fail as Fail
import qualified Data.Aeson as Aeson
#ifdef VERSION_attoparsec_aeson
import qualified Data.Aeson.Parser as Aeson
#endif
import qualified Data.Attoparsec.ByteString as Parsec
import qualified Data.ByteString as ByteString
import qualified Data.ByteString.Lazy as ByteString.Lazy
import Data.HashMap.Strict (HashMap)
import Data.Text (Text)
import qualified Data.Text as Text
import qualified Data.Text.Encoding as Text.Encoding
import qualified Data.Text.Lazy.IO as Text.Lazy.IO
import qualified Options.Applicative as Options
import qualified Options.Applicative.Help.Pretty as Pretty
import qualified Prettyprinter as PP
import qualified Prettyprinter.Render.Terminal as PP
import qualified System.Exit as Exit
import qualified Text.EDE as EDE
import qualified Text.EDE.Internal.Compat as Compat
data Options = Options
{ templateFile :: FilePath,
jsonFile :: Maybe FilePath,
jsonObject :: Maybe (HashMap Text Aeson.Value)
}
optionsParser :: Options.Parser Options
optionsParser =
Options
<$> Options.strOption
( Options.long "template-file"
<> Options.metavar "PATH"
<> Options.help
"Path of a file containing an EDE template"
)
<*> Options.optional
( Options.strOption
( Options.long "context-file"
<> Options.metavar "PATH"
<> Options.help
("Path of a file containing a JSON object which is used " <>
"as the template context")
)
)
<*> Options.optional
( Options.option
readObject
( Options.long "context-json"
<> Options.metavar "OBJECT"
<> Options.help
"Template context as a JSON object"
)
)
parserInfo :: Options.ParserInfo Options
parserInfo =
Options.info
(Options.helper <*> optionsParser)
(Options.header "EDE template processor" <> Options.progDescDoc (Just usage))
where
usage =
Pretty.vcat
[ Pretty.emptyDoc,
"The --context-file and --context-json options are processed as follows:",
Pretty.indent 2 "1.)"
Pretty.<+> ( Pretty.align
( Pretty.vcat
[ "Both are provided",
"Both objects are merged into one with the keys of " <>
"--context-json taking precedence over those in the " <>
"file provided by --context-file."
]
)
),
Pretty.indent 2 "2.)"
Pretty.<+> ( Pretty.align
( Pretty.vcat
[ "None of them are provided",
"The JSON object is read from STDIN."
]
)
),
Pretty.indent 2 "3.)"
Pretty.<+> ( Pretty.align
( Pretty.vcat
[ "One of them is provided",
"The JSON object is read from the supplied option."
]
)
)
]
main :: IO ()
main = do
options <- Options.execParser parserInfo
ctx <-
case (jsonFile options, jsonObject options) of
(Just path, Just obj1) -> do
eobj2 <- Aeson.eitherDecode <$> ByteString.Lazy.readFile path
case eobj2 of
Left err -> fail err
Right obj2 -> pure (obj1 <> obj2)
--
(Just path, Nothing) -> do
eobj <- Aeson.eitherDecode <$> ByteString.Lazy.readFile path
case eobj of
Left err -> fail err
Right obj -> pure obj
--
(Nothing, Just obj) ->
pure obj
--
(Nothing, Nothing) ->
ByteString.getContents
>>= either fail pure . Parsec.parseOnly stdinParser
EDE.parseFile (templateFile options) >>= \case
EDE.Failure err -> PP.putDoc (err <> PP.hardline) >> Exit.exitFailure
EDE.Success tpl ->
case EDE.render tpl ctx of
EDE.Failure err -> PP.putDoc (err <> PP.hardline) >> Exit.exitFailure
EDE.Success output -> Text.Lazy.IO.putStr output
stdinParser :: Parsec.Parser (HashMap Text Aeson.Value)
stdinParser = Aeson.json >>= requireObject
readValue :: Options.ReadM Aeson.Value
readValue = Options.str >>= decodeJsonStr
decodeJsonStr :: Fail.MonadFail m => String -> m Aeson.Value
decodeJsonStr =
either fail pure . Aeson.eitherDecode . ByteString.Lazy.fromStrict
. Text.Encoding.encodeUtf8
. Text.pack
readObject :: Options.ReadM (HashMap Text Aeson.Value)
readObject = readValue >>= requireObject
requireObject :: Fail.MonadFail m => Aeson.Value -> m (HashMap Text Aeson.Value)
requireObject = \case
Aeson.Object obj -> pure (Compat.toHashMapText obj)
_ -> fail "JSON value must be an object"