restman-0.7.1.1: app/Main.hs
module Main (main) where
-- base
import Control.Applicative (many, optional, (<|>))
import Data.Bits (Bits, unsafeShiftL)
import System.IO (IOMode(ReadMode), withFile)
-- bytestring
import Data.ByteString (ByteString, hGetSome)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Char8 as CS
import Data.ByteString.Lazy (fromChunks, fromStrict)
import qualified Data.ByteString.Lazy as Lazy (ByteString)
-- case-insensitive
import qualified Data.CaseInsensitive as CI
-- microlens
import Lens.Micro.Extras (view)
-- optparse-applicatve
import Options.Applicative
( ParseError(ShowHelpText)
, Parser
, ParserInfo
, abortOption
, completeWith
, execParser
, flag'
, footer
, fullDesc
, header
, help
, info
, long
, metavar
, progDesc
, short
, showDefault
, strArgument
, strOption
)
import qualified Options.Applicative as OptParse
-- text
import Data.Text (Text)
-- utf8-string
import qualified Data.ByteString.UTF8 as UTF8
-- local imports
import HTTP.Client
import UI
-- | Handles @-m@ / @--method@
methodParser :: Parser Method
methodParser = strOption
( short 'm'
<> long "method"
<> help "HTTP(S) Method (or \"Verb\")"
<> OptParse.value "GET"
<> showDefault
<> metavar "METHOD"
<> completeWith knownMethods
)
-- | handles @--[no-]default-headers@
useDefaultParser :: Parser UseDefaultHeaders
useDefaultParser = noDefaultHeaders <|> defaultHeaders <|> pure AppendCustomToDefaultHeaders
where
noDefaultHeaders = flag' ReplaceDefaultHeaders
( long "no-default-headers"
<> help "Send ONLY the custom headers specified."
)
defaultHeaders = flag' AppendCustomToDefaultHeaders
( long "default-headers"
<> help "Send custom headers after default headers. This is the default behavior."
)
{-|
Handles the option-argument for @-h@ / @--custom-header@.
Unicode value is split on the first colon. Anything before the colon must truncate to ISO-8859-1,
and is treated as the header name. Anything after the colon is treated as the header value, and
full Unicode is accepted and encoded as UTF-8.
No validation is done on the header name. No allowance is made for specifying an (invalid) header
name that contains a colon.
-}
optArgToHeader :: String -> Header
optArgToHeader arg = (CI.mk $ CS.pack name, UTF8.fromString value)
where
(name, colonValue) = break (':' ==) arg
value = drop 1 colonValue
-- | handles @-h@ / @--custom-header@
customHeadersParser :: Parser [Header]
customHeadersParser = many . fmap optArgToHeader $ strOption
( short 'h'
<> long "custom-header"
<> help "Add a custom header, argument is name:value (colon separates name from value)."
<> metavar "NAME_VALUE"
)
-- | Specification of the payload from the user
data Payload
= Literal ByteString -- ^ argument converted to bytes
| File FilePath -- ^ argument converts to file path
-- | Handles @--payload@ and @--payload-file@
payloadParser :: Parser (Maybe Payload)
payloadParser = optional ((Literal <$> literalParser) <|> (File <$> fileParser))
where
literalParser = UTF8.fromString <$> strOption
( long "payload"
<> help "Specify (in UTF-8) payload directly on command-line"
<> metavar "PAYLOAD"
)
fileParser = strOption
( long "payload-file"
<> help "Load payload from file on startup"
<> metavar "PATHNAME"
)
-- | Handle optional positional parameter for URI
uriPositionalParser :: Parser (Maybe Text)
uriPositionalParser = optional $ strArgument (metavar "URI")
-- | Options from the command-line
data Options = MkOptions
{ method :: Method
, useDefaultHeaders :: UseDefaultHeaders
, customHeaders :: [Header]
, payload :: Maybe Payload
, uri :: Maybe Text
}
helpParser :: Parser (a -> a)
helpParser = abortOption (ShowHelpText mempty)
( long "help"
<> help "Show command-line help and abort normal operation."
)
-- | Handles full command-line, generating help, and completions
appParser :: ParserInfo Options
appParser = info (helpParser <*> optionsParser)
( fullDesc
<> header "RESTman, an HTTP(S) application"
<> footer "https://gitlab.com/krakrjak/restman"
<> progDesc "A TUI application for interactively using the full range of HTTP(S) directly"
)
where
optionsParser =
MkOptions <$> methodParser <*> useDefaultParser <*> customHeadersParser <*> payloadParser <*> uriPositionalParser
-- | Lens for the method in the options.
optMethod :: Functor f => (Method -> f Method) -> Options -> f Options
optMethod embed opts = fmap (\m -> opts{ method = m}) . embed $ method opts
-- | 'unsafeShilfL'
(.<<.) :: Bits b => b -> Int -> b
(.<<.) = unsafeShiftL
-- | Go from payload specification to a payload ready for library.
loadPayload :: Payload -> IO Lazy.ByteString
loadPayload (Literal bs) = pure $ fromStrict bs
loadPayload (File fp) = fromChunks <$> withFile fp ReadMode readChunks
where
readChunks hdl = go
where
go = do
chunk <- hGetSome hdl (1 .<<. 15 {- 32k -})
if BS.null chunk
then return []
else do
chunks <- go
return $ chunk:chunks
-- | Application entry point.
main :: IO ()
main = do
options <- execParser appParser
payloadLbs <- traverse loadPayload $ payload options
startUI (view optMethod options) (useDefaultHeaders options) (customHeaders options) payloadLbs (uri options)