restman 0.7.4.0 → 0.7.5.0
raw patch · 5 files changed
+226/−7 lines, 5 filesdep +directorydep +process
Dependencies added: directory, process
Files
- CHANGELOG.md +9/−0
- app/Main.hs +48/−5
- restman.cabal +5/−1
- test/BinarySpec.hs +161/−0
- test/Spec.hs +3/−1
CHANGELOG.md view
@@ -1,3 +1,12 @@+# v7.5.0 (2026-05-04)++## ✨ New Features+- [`abdc3a0`](https://gitlab.com/krakrjak/restman/-/commit/abdc3a0) ✨ Add --oneshot Mode to RESTman ++## 🐛 Bug Fixes++## 🏗️ Architecture Changes+ # v7.4.0 (2026-05-03) ## ✨ New Features
app/Main.hs view
@@ -2,20 +2,23 @@ -- base import Control.Applicative (many, optional, (<|>))+import Control.Exception (SomeException, try) import Data.Bits (Bits, unsafeShiftL)-import System.IO (IOMode(ReadMode), withFile)+import System.Exit (exitFailure)+import System.IO (IOMode(ReadMode), hPutStrLn, stderr, stdout, 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 Data.ByteString.Lazy (fromChunks, fromStrict, hPut) import qualified Data.ByteString.Lazy as Lazy (ByteString) -- case-insensitive import qualified Data.CaseInsensitive as CI -- microlens+import Lens.Micro ((%~), (&), (.~)) import Lens.Micro.Extras (view) -- optparse-applicatve@@ -39,15 +42,22 @@ , showDefault , strArgument , strOption+ , switch ) import qualified Options.Applicative as OptParse -- text-import Data.Text (Text)+import Data.Text (Text, unpack) -- utf8-string import qualified Data.ByteString.UTF8 as UTF8 +-- http-client+import qualified Network.HTTP.Client as HC++-- wreq+import qualified Network.Wreq as W+ -- local imports import HTTP.Client import UI@@ -134,8 +144,16 @@ , customHeaders :: [Header] , payload :: Maybe Payload , uri :: Maybe Text+ , oneshot :: Bool } +-- | Handles @--oneshot@: make one request, print the body, and exit without starting the TUI.+oneshotParser :: Parser Bool+oneshotParser = switch+ ( long "oneshot"+ <> help "Make the request, print the response body to stdout, and exit without starting the TUI."+ )+ helpParser :: Parser (a -> a) helpParser = abortOption (ShowHelpText mempty) ( long "help"@@ -152,7 +170,7 @@ ) where optionsParser =- MkOptions <$> methodParser <*> useDefaultParser <*> customHeadersParser <*> payloadParser <*> uriPositionalParser+ MkOptions <$> methodParser <*> useDefaultParser <*> customHeadersParser <*> payloadParser <*> uriPositionalParser <*> oneshotParser -- | Lens for the method in the options. optMethod :: Functor f => (Method -> f Method) -> Options -> f Options@@ -177,9 +195,34 @@ chunks <- go return $ chunk:chunks +-- | Non-TUI entry point: perform one request, write the response body to stdout, and exit.+--+-- Exits with failure if no URI was supplied or the request throws an exception.+runOneshot :: Options -> Maybe Lazy.ByteString -> IO ()+runOneshot opts payloadLbs = case uri opts of+ Nothing -> do+ hPutStrLn stderr "restman: --oneshot requires a URI argument"+ exitFailure+ Just u -> do+ manager <- HC.newManager robustSettings+ let baseOpts = W.defaults & W.manager .~ Right manager+ wreqOpts = case useDefaultHeaders opts of+ AppendCustomToDefaultHeaders -> baseOpts & headers %~ (customHeaders opts ++)+ ReplaceDefaultHeaders -> baseOpts & headers .~ customHeaders opts+ url = unpack u+ doReq = case payloadLbs of+ Nothing -> customMethodWith (method opts) wreqOpts url+ Just lbs -> customPayloadMethodWith (method opts) wreqOpts url lbs+ result <- try doReq :: IO (Either SomeException (W.Response Lazy.ByteString))+ case result of+ Left ex -> hPutStrLn stderr ("restman: " <> show ex) >> exitFailure+ Right resp -> hPut stdout (view responseBody resp)+ -- | 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)+ if oneshot options+ then runOneshot options payloadLbs+ else startUI (view optMethod options) (useDefaultHeaders options) (customHeaders options) payloadLbs (uri options)
restman.cabal view
@@ -1,6 +1,6 @@ cabal-version: 1.12 name: restman-version: 0.7.4.0+version: 0.7.5.0 license: BSD2 license-file: LICENSE copyright: Zac Slade or Boyd Stephen Smith Jr, 2024@@ -73,6 +73,7 @@ brick >=2.9 && <2.10, bytestring >=0.12.2.0 && <0.13, case-insensitive >=1.2.1.0 && <1.3,+ http-client >=0.7.19 && <0.8, http-types >=0.12.4 && <0.13, microlens >=0.4.14.0 && <0.5, optparse-applicative >=0.18.1.0 && <0.19,@@ -86,6 +87,7 @@ main-is: Spec.hs hs-source-dirs: test other-modules:+ BinarySpec HTTP.ClientIntegrationSpec HTTP.ClientSpec HTTP.EchoServer@@ -107,9 +109,11 @@ bytestring >=0.12.2.0 && <0.13, case-insensitive >=1.2.1.0 && <1.3, containers >=0.7 && <0.8,+ directory >=1.3.8.5 && <1.4, hedgehog >=1.5 && <1.6, http-client >=0.7.19 && <0.8, http-types >=0.12.4 && <0.13,+ process >=1.6.26.1 && <1.7, restman, skylighting >=0.14.7 && <0.15, tasty >=1.5.4 && <1.6,
+ test/BinarySpec.hs view
@@ -0,0 +1,161 @@+{-# language OverloadedStrings #-}+-- | Acceptance tests that spawn the @restman@ binary and inspect its+-- behaviour via the local echo server.+--+-- These tests require the @restman@ executable to be present on @PATH@.+-- When running via @stack test@, Stack builds the executable first and adds+-- the package bin-dir to @PATH@ automatically.+module BinarySpec (tests) where++-- base+import Data.List (isInfixOf)+import System.Exit (ExitCode(..))+import System.Process (readProcessWithExitCode)++-- aeson+import Data.Aeson (FromJSON(..), eitherDecode, withObject, (.:))++-- bytestring+import qualified Data.ByteString.Lazy.Char8 as BL8++-- containers+import Data.Map.Strict (Map)+import qualified Data.Map.Strict as Map++-- directory+import System.Directory (findExecutable)++-- text+import qualified Data.Text as T++-- tasty+import Test.Tasty+import Test.Tasty.HUnit++-- warp+import Network.Wai.Handler.Warp (Port)++-- local+import HTTP.EchoServer (withEchoServer)++-- ---------------------------------------------------------------------------+-- EchoResponse — mirrors the JSON produced by HTTP.EchoServer+-- ---------------------------------------------------------------------------++data EchoResponse = EchoResponse+ { echoMethod :: T.Text+ , echoHeaders :: Map T.Text T.Text+ , echoBody :: T.Text+ } deriving (Eq, Show)++instance FromJSON EchoResponse where+ parseJSON = withObject "EchoResponse" $ \o ->+ EchoResponse+ <$> o .: "method"+ <*> o .: "headers"+ <*> o .: "body"++-- ---------------------------------------------------------------------------+-- Helpers+-- ---------------------------------------------------------------------------++-- | Locate the @restman@ binary on @PATH@, failing the test if it is absent.+findRestmanExe :: IO FilePath+findRestmanExe = do+ mPath <- findExecutable "restman"+ case mPath of+ Nothing -> assertFailure+ "restman binary not found in PATH; run acceptance tests via 'stack test'"+ Just p -> pure p++-- | Run @restman@ with the supplied arguments, returning+-- @(exitCode, stdout, stderr)@.+runRestman :: [String] -> IO (ExitCode, String, String)+runRestman args = do+ exe <- findRestmanExe+ readProcessWithExitCode exe args ""++echoURL :: Port -> String -> String+echoURL port path = "http://localhost:" <> show port <> path++-- | Run @restman --oneshot@ against the local echo server with optional+-- extra arguments, decode the JSON response body, and return the result.+oneshotEcho :: Port -> [String] -> IO EchoResponse+oneshotEcho port extraArgs = do+ let args = extraArgs ++ ["--oneshot", echoURL port "/"]+ (code, out, err) <- runRestman args+ case code of+ ExitFailure n ->+ assertFailure $ "restman exited with code " <> show n+ <> "; stderr: " <> err+ ExitSuccess ->+ case eitherDecode (BL8.pack out) of+ Left e -> assertFailure+ $ "Failed to decode echo response: " <> e+ <> "\nstdout was: " <> out+ Right r -> pure r++-- ---------------------------------------------------------------------------+-- Tests+-- ---------------------------------------------------------------------------++tests :: TestTree+tests = testGroup "Application (acceptance)"+ [ cliTests+ , fetchTests+ ]++cliTests :: TestTree+cliTests = testGroup "CLI"+ [ testCase "--help exits with success and mentions RESTman" $ do+ (code, out, _) <- runRestman ["--help"]+ code @?= ExitSuccess+ assertBool ("expected 'RESTman' in help output\ngot: " <> out)+ ("RESTman" `isInfixOf` out)++ , testCase "unrecognised flag exits with failure" $ do+ (code, _, _) <- runRestman ["--not-a-real-flag"]+ case code of+ ExitFailure _ -> pure ()+ ExitSuccess -> assertFailure+ "expected non-zero exit for unrecognised flag"++ , testCase "--oneshot without URI exits with failure" $ do+ (code, _, err) <- runRestman ["--oneshot"]+ case code of+ ExitFailure _ ->+ assertBool ("expected 'requires' in error output\ngot: " <> err)+ ("requires" `isInfixOf` err)+ ExitSuccess ->+ assertFailure+ "expected non-zero exit when --oneshot has no URI"+ ]++fetchTests :: TestTree+fetchTests = testGroup "fetch (--oneshot)"+ [ testCase "default method is GET" $ withEchoServer $ \port -> do+ er <- oneshotEcho port []+ echoMethod er @?= "GET"++ , testCase "-m DELETE sends DELETE" $ withEchoServer $ \port -> do+ er <- oneshotEcho port ["-m", "DELETE"]+ echoMethod er @?= "DELETE"++ , testCase "-m POST with --payload sends request body" $ withEchoServer $ \port -> do+ er <- oneshotEcho port ["-m", "POST", "--payload", "ping"]+ echoBody er @?= "ping"++ , testCase "-h sends a custom header" $ withEchoServer $ \port -> do+ er <- oneshotEcho port ["-h", "X-Smoke:hello"]+ Map.lookup "x-smoke" (echoHeaders er) @?= Just "hello"++ , testCase "default headers include user-agent" $ withEchoServer $ \port -> do+ er <- oneshotEcho port []+ assertBool "expected 'user-agent' header with default settings"+ (Map.member "user-agent" (echoHeaders er))++ , testCase "--no-default-headers omits user-agent" $ withEchoServer $ \port -> do+ er <- oneshotEcho port ["--no-default-headers"]+ assertBool "expected 'user-agent' to be absent with --no-default-headers"+ (Map.notMember "user-agent" (echoHeaders er))+ ]
test/Spec.hs view
@@ -24,6 +24,7 @@ import Skylighting.Format.Vty (attrColor, attrStyle, colorDistance, formatVty, vtyStyleAttr) +import qualified BinarySpec import qualified HTTP.ClientIntegrationSpec import qualified HTTP.ClientSpec import qualified LibSpec@@ -35,7 +36,7 @@ import qualified UISpec main :: IO ()-main = defaultMainWithIngredients [antXMLRunner] tests+main = defaultMainWithIngredients (antXMLRunner : defaultIngredients) tests -- | A minimal Style with no token styles and no colors. emptyStyle :: Style@@ -51,6 +52,7 @@ tests = testGroup "restman" [ HTTP.ClientSpec.tests , HTTP.ClientIntegrationSpec.tests+ , BinarySpec.tests , LibSpec.tests , TypesSpec.tests , UISpec.tests