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)