packages feed

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 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