hq-json-0.1.1: src/HQ/CLI.hs
{-# LANGUAGE ApplicativeDo #-}
{-# LANGUAGE ImportQualifiedPost #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE StrictData #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE NoImplicitPrelude #-}
module HQ.CLI (CLIOptions (..), optParserInfo, runCLI, renderIOError, withInput) where
import Control.Exception (IOException, catch)
import Data.Aeson (Value)
import Data.FileEmbed (embedStringFile, makeRelativeToProject)
import Data.Text.IO (hPutStrLn)
import Data.Version (showVersion)
import HQ.JSON.Encoder (Join (..), Raw (..))
import HQ.JSON.Encoder qualified as Enc
import HQ.JSON.Parser (parseValue)
import HQ.Optic (Optic)
import HQ.Optic.Parser (parseOptic)
import HQ.Query
import HQ.Runner (executeRunnerAction, jsonRunner)
import HQ.Transformation (Transformation (..))
import HQ.Transformation.Parser (parseTransformation)
import Options.Applicative hiding (Const)
import Paths_hq_json (version)
import Relude hiding (Const)
import System.IO hiding (hPutStrLn, hSetBuffering)
import Text.Megaparsec (errorBundlePretty)
data CLIOptions = CLIOptions
{ optQuery :: Query,
optFile :: Maybe FilePath,
optRaw :: Raw,
optCompact :: Enc.EncodeStyle,
optJoin :: Join
}
opticReader :: ReadM Optic
opticReader = eitherReader $ first errorBundlePretty . parseOptic . toText
valueReader :: ReadM Value
valueReader = eitherReader $ parseValue . toText
transformationReader :: ReadM Transformation
transformationReader =
eitherReader $ first errorBundlePretty . parseTransformation . toText
queryParser :: Parser Query
queryParser =
hsubparser
$ command "fold" (info (Fold <$> opticArg) mempty)
<> command "preview" (info (Preview <$> opticArg) mempty)
<> command "set" (info (Over <$> opticArg <*> (Const <$> valueArg)) mempty)
<> command "over" (info (Over <$> opticArg <*> transformationArg) mempty)
<> command "delete" (info (Delete <$> opticArg) mempty)
where
opticArg = argument opticReader $ metavar "OPTIC"
valueArg = argument valueReader $ metavar "VALUE"
transformationArg = argument transformationReader $ metavar "TRANSFORMATION"
optParser :: Parser CLIOptions
optParser = do
file <- optional $ strOption fileMod
raw <- bool NoRaw Raw <$> switch rawMod
compact <- bool (Enc.Pretty 2) Enc.Compact <$> switch compactMod
join' <- bool NoJoin Join <$> switch joinMod
query <- queryParser
pure $ CLIOptions query file raw compact join'
where
fileHelp = "Input JSON file, or '-' for stdin"
fileMod = long "file" <> short 'f' <> metavar "FILE" <> help fileHelp
rawMod = long "raw" <> short 'r' <> help "Print strings without JSON quotes"
compactMod = long "compact" <> short 'c' <> help "Print compact JSON"
joinMod = long "join" <> short 'j' <> help "Print without separators"
langHelp :: String
langHelp = $(embedStringFile =<< makeRelativeToProject "./docs/LANG.md")
optParserInfo :: ParserInfo CLIOptions
optParserInfo = info (optParser <**> helper) infoMod
where
infoMod =
mconcat
[ fullDesc,
progDesc headerMsg,
header description,
footerDoc $ Just $ fromString langHelp
]
headerMsg = "Query JSON using optics"
description = "hq v" <> showVersion version
-- | Open the input and run the query over it: stdin for 'Nothing' and
-- @-@, a file otherwise. An unopenable file is a CLI-level failure,
-- reported on stderr.
withInput :: Maybe FilePath -> (Handle -> IO a) -> IO a
withInput Nothing useHandle = useHandle stdin
withInput (Just "-") useHandle = useHandle stdin
withInput (Just path) useHandle =
catch open $ \err -> do
hPutStrLn stderr (renderIOError err)
exitFailure
where
open = withFile path ReadMode $ \h -> do
hSetBuffering h $ BlockBuffering Nothing
hSetBinaryMode h True
useHandle h
-- | Render an I/O failure for the CLI boundary, prefixed like every
-- other hq error.
renderIOError :: IOException -> Text
renderIOError err = "hq: " <> toText (displayException err)
runCLI :: IO ()
runCLI = do
CLIOptions {..} <- execParser optParserInfo
query <- case typecheckQuery optQuery of
Left err -> hPutStrLn stderr (renderTypeError err) >> exitFailure
Right q -> pure q
let cfg = Enc.EncoderConfig optCompact $ Enc.ValueOptions optRaw optJoin
withInput optFile $ executeRunnerAction jsonRunner query cfg