{-# LANGUAGE ApplicativeDo #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE TemplateHaskell #-} module Main where import Control.Monad.Except import qualified Data.Aeson as JSON import Data.ByteString.Builder (toLazyByteString) import qualified Data.ByteString.Lazy as L import Data.ByteString.Lazy.Char8 as LC (putStrLn) import Data.Maybe import Data.Text (Text) import qualified Data.Text as T import Data.Text.Encoding (encodeUtf8Builder) import qualified Data.Text.IO as TIO import Data.Version (showVersion) import Language.Jsonnet import Language.Jsonnet.Annotate import Language.Jsonnet.Desugar import Language.Jsonnet.Error import Language.Jsonnet.Eval import Language.Jsonnet.Eval (mergeWith) import Language.Jsonnet.Eval.Monad import qualified Language.Jsonnet.Std.Lib as Lib import Language.Jsonnet.Std.TH (mkStdlib) import Language.Jsonnet.Value import Options.Applicative import Paths_jsonnet (version) import Text.PrettyPrint.ANSI.Leijen (Pretty, pretty) main :: IO () main = do runProgram =<< execParser options return () runProgram :: Options -> IO () runProgram opts@Options {..} = do src <- readSource input conf <- mkConfig opts outp <- interpret conf src either printError (printResult output format) outp printResult :: Output -> Format -> JSON.Value -> IO () printResult outp Json val = writeOutput (JSON.encode val) outp printResult outp Plaintext (JSON.String s) = writeOutput (encodeToLazyByteString s) outp where encodeToLazyByteString = toLazyByteString . encodeUtf8Builder printResult _ Plaintext _ = print "Runtime error: expected string result" writeOutput :: L.ByteString -> Output -> IO () writeOutput bs = \case FileOutput path -> L.writeFile path bs Stdout -> LC.putStrLn bs printError :: Error -> IO () printError = print . pretty readSource :: Input -> IO Text readSource = \case Stdin -> TIO.getContents FileInput path -> TIO.readFile path ExecInput str -> pure (T.pack str) -- the jsonnet stdlib is written in both jsonnet and Haskell, here we merge -- the native (a small subset) with the interpreted (the splice mkStdlib) std :: Eval Value std = eval core >>= flip mergeObjects Lib.std where core = desugar (annMap (const ()) $mkStdlib) mergeObjects (VObj x) (VObj y) = pure $ VObj (x `mergeWith` y) mkConfig :: Options -> IO Config mkConfig Options {..} = do let fname = case input of Stdin -> "" FileInput path -> path ExecInput _ -> "" stdlib <- mkThunk std pure Config {..} fileOutput :: Parser Output fileOutput = fromMaybe Stdout <$> ( optional $ FileOutput <$> strOption ( long "output-file" <> short 'o' <> metavar "" <> help "Write to the output file rather than stdout" ) ) fileInput :: Parser (Maybe String) fileInput = optional $ strArgument ( metavar "" <> help "Jsonnet source file or stdin" ) parseOpts :: Parser Options parseOpts = do input <- mkInput <$> exec <*> fileInput output <- fileOutput format <- flag Json Plaintext ( long "string" <> short 'S' <> help "Expect a string, manifest as plain text" ) pure Options {..} mkInput :: Bool -> Maybe String -> Input mkInput exec = \case Nothing -> Stdin Just e | exec -> ExecInput e Just p | otherwise -> FileInput p exec :: Parser Bool exec = switch ( long "exec" <> short 'e' <> help "Treat filename as code" ) ver :: Parser (a -> a) ver = infoOption verStr ( long "version" <> short 'v' <> help "Print version of the program" ) where verStr = "Jsonnet command line " <> showVersion version options :: ParserInfo Options options = info (ver <*> helper <*> parseOpts) ( fullDesc <> noIntersperse ) data Options = Options { output :: Output, format :: Format, input :: Input } data Input = FileInput FilePath | ExecInput String | Stdin deriving (Eq, Show) data Output = FileOutput FilePath | Stdout deriving (Eq, Show) data Format = Json | Plaintext deriving (Eq, Show)