packages feed

jsonnet-0.4.0.0: app/Main.hs

{-# LANGUAGE ApplicativeDo #-}

module Main where

import qualified Data.Aeson as JSON
import Data.Bifunctor (first, second)
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 Data.Void (Void)
import Language.Jsonnet
import Language.Jsonnet.Error
import Language.Jsonnet.Pretty (ppJson, prettyError)
import Options.Applicative hiding (str)
import Paths_jsonnet (version)
import System.Environment (lookupEnv)
import System.Exit (die)
import qualified Text.Megaparsec as MP
import qualified Text.Megaparsec.Char as MPC

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 outputMode format) outp

encodeToLazyByteString :: Text -> L.ByteString
encodeToLazyByteString = toLazyByteString . encodeUtf8Builder

printResult :: Output -> OutputMode -> Format -> JSON.Value -> IO ()
printResult outp outputMode Json val =
  writeOutput (encode val) outp
  where
    encode = case outputMode of
      Pretty -> encodeToLazyByteString . T.pack . show . ppJson 4
      Compact -> JSON.encode
printResult outp _ Plaintext (JSON.String s) =
  writeOutput (encodeToLazyByteString s) outp
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 . prettyError

readSource :: Input -> IO Text
readSource = \case
  Stdin -> TIO.getContents
  FileInput path -> TIO.readFile path
  ExecInput str -> pure (T.pack str)

mkExtVar :: (Text, Either ExtVarType ExtVar) -> IO (Text, ExtVar)
mkExtVar = \case
  (s, Right extVar) -> pure (s, extVar)
  (envVar, Left extVarType) ->
    lookupEnv (T.unpack envVar) >>= \case
      Just extVar -> pure $ (envVar, ExtVar extVarType (Inline (T.pack extVar)))
      Nothing -> die "No env variable"

mkConfig :: Options -> IO Config
mkConfig Options {..} = do
  let fname = case input of
        Stdin -> ""
        FileInput path -> path
        ExecInput _ -> ""
  extVars' <- constructExtVars =<< traverse mkExtVar extVars
  pure Config {fname = fname, extVars = extVars'}

fileOutput :: Parser Output
fileOutput =
  fromMaybe Stdout
    <$> ( optional $
            FileOutput
              <$> strOption
                ( long "output-file"
                    <> short 'o'
                    <> metavar "<filename>"
                    <> help "Write to the output file rather than stdout"
                )
        )

fileInput :: Parser (Maybe String)
fileInput =
  optional $
    strArgument
      ( metavar "<filename>"
          <> help "Jsonnet source file or stdin"
      )

parseOpts :: Parser Options
parseOpts = do
  input <- mkInput <$> exec <*> fileInput
  output <- fileOutput
  outputMode <-
    flag
      Pretty
      Compact
      ( long "compact"
          <> help "Produce a compact JSON output"
      )
  format <-
    flag
      Json
      Plaintext
      ( long "string"
          <> short 'S'
          <> help "Expect a string, manifest as plain text"
      )
  extStrs <- parseExtStr
  extStrFiles <- parseExtStrFile
  extCodes <- parseExtCode
  extCodeFiles <- parseExtCodeFile
  pure Options {extVars = extStrs <> extStrFiles <> extCodes <> extCodeFiles, ..}

parseExtStr :: Parser [(Text, Either ExtVarType ExtVar)]
parseExtStr =
  many $
    option
      (second (interpretAs ExtStr) <$> extVarParser)
      ( long "ext-str"
          <> short 'V'
          <> metavar "VAR=VALUE"
          <> help "External string variable"
      )

parseExtStrFile :: Parser [(Text, Either ExtVarType ExtVar)]
parseExtStrFile =
  many $
    option
      (second (Right . ExtVar ExtStr) <$> extVarFileParser)
      ( long "ext-str-file"
          <> help "External string variable as file"
          <> metavar "FILE"
          <> action "file"
      )

parseExtCode :: Parser [(Text, Either ExtVarType ExtVar)]
parseExtCode =
  many $
    option
      (second (interpretAs ExtCode) <$> extVarParser)
      ( long "ext-code"
          <> metavar "VAR=EXPR"
          <> help "External code variable"
      )

parseExtCodeFile :: Parser [(Text, Either ExtVarType ExtVar)]
parseExtCodeFile =
  many $
    option
      (second (Right . ExtVar ExtCode) <$> extVarFileParser)
      ( long "ext-code-file"
          <> help "External code variable as file"
          <> metavar "FILE"
          <> action "file"
      )

interpretAs :: ExtVarType -> Maybe ExtVarContent -> Either ExtVarType ExtVar
interpretAs t = \case
  Nothing -> Left t
  Just s -> Right $ ExtVar t s

extVarParser :: ReadM (Text, Maybe ExtVarContent)
extVarParser = eitherReader f
  where
    f = first MP.errorBundlePretty . MP.runParser (MP.try pair <|> envVar) ""

    pair :: MP.Parsec Void String (Text, Maybe ExtVarContent)
    pair = second (Just . Inline . T.pack) <$> parsePair

    envVar :: MP.Parsec Void String (Text, Maybe ExtVarContent)
    envVar = (,Nothing) . T.pack <$> MP.someTill MPC.asciiChar MP.eof

extVarFileParser :: ReadM (Text, ExtVarContent)
extVarFileParser = eitherReader f
  where
    f = first MP.errorBundlePretty . MP.runParser (second File <$> parsePair) ""

parsePair :: MP.Parsec Void String (Text, String)
parsePair =
  first T.pack
    <$> liftA2
      (,)
      (MP.someTill MPC.asciiChar (MPC.char '='))
      (MP.someTill MPC.asciiChar MP.eof)

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,
    outputMode :: OutputMode,
    format :: Format,
    input :: Input,
    -- | ExtVarType determines the interpretation of the environment variable
    extVars :: [(Text, Either ExtVarType ExtVar)]
  }

data Input
  = FileInput FilePath
  | ExecInput String
  | Stdin
  deriving (Eq, Show)

data Output
  = FileOutput FilePath
  | Stdout
  deriving (Eq, Show)

data OutputMode
  = Pretty
  | Compact
  deriving (Eq, Show)

data Format = Json | Plaintext
  deriving (Eq, Show)