packages feed

http-tower-hs 0.2.0.0 → 0.3.0.0

raw patch · 18 files changed

+100/−67 lines, 18 filesdep ~tower-hsPVP ok

version bump matches the API change (PVP)

Dependency ranges changed: tower-hs

API changes (from Hackage documentation)

+ Network.HTTP.Tower: contramapService :: (a -> b) -> Service b res -> Service a res
+ Network.HTTP.Tower: dimapService :: (a -> b) -> (c -> d) -> Service b c -> Service a d

Files

CHANGELOG.md view
@@ -1,5 +1,12 @@ # Changelog +## 0.3.0.0 — 2026-04-07++- Re-export `contramapService` and `dimapService` from `tower-hs`+- Adopt `(&)` from `Data.Function` as idiomatic style in docs, tests, and examples+- `(|>)` operator remains available for newcomers+- Requires `tower-hs >= 0.2.0.0`+ ## 0.2.0.0 — 2026-04-06  ### Breaking Changes
http-tower-hs.cabal view
@@ -5,7 +5,7 @@ -- see: https://github.com/sol/hpack  name:           http-tower-hs-version:        0.2.0.0+version:        0.3.0.0 synopsis:       Composable HTTP client middleware for Haskell, built on tower-hs description:    http-tower-hs provides HTTP-specific middleware for composable HTTP clients,                 built on top of tower-hs. Includes logging, header manipulation, redirect@@ -59,7 +59,7 @@     , text >=2.0 && <2.2     , time >=1.11 && <1.15     , tls >=2.0 && <2.2-    , tower-hs >=0.1.0.0 && <0.2+    , tower-hs >=0.2.0.0 && <0.3     , unordered-containers ==0.2.*     , uuid ==1.3.*   default-language: Haskell2010
src/Network/HTTP/Tower.hs view
@@ -4,22 +4,25 @@ -- License     : MIT -- -- Inspired by Rust's <https://docs.rs/tower/latest/tower/ Tower> crate.--- Build composable middleware stacks for HTTP clients with the @('|>')@ operator.+-- Build composable middleware stacks for HTTP clients with @'Data.Function.&'@+-- and @'applyMiddleware'@, or use the @('|>')@ operator for a pipeline style. -- -- @+-- import Data.Function (('&')) -- import Network.HTTP.Tower -- import qualified Network.HTTP.Client as HTTP -- -- main :: IO () -- main = do --   client <- 'newClient'---   let configured = client---         '|>' 'withBearerAuth' \"my-token\"---         '|>' 'withRequestId'---         '|>' 'withRetry' ('constantBackoff' 3 1.0)---         '|>' 'withTimeout' 5000---         '|>' 'withValidateStatus' (\\c -> c >= 200 && c < 300)---         '|>' 'withTracing'+--   let configured = client '&' 'applyMiddleware'+--         ( 'withBearerAuth' \"my-token\"+--         . 'withRequestId'+--         . 'withRetry' ('constantBackoff' 3 1.0)+--         . 'withTimeout' 5000+--         . 'withValidateStatus' (\\c -> c >= 200 && c < 300)+--         . 'withTracing'+--         ) -- --   req <- HTTP.parseRequest \"https://api.example.com/v1/users\" --   result <- 'runRequest' configured req@@ -35,6 +38,8 @@     Service(..)   , Middleware   , mapService+  , contramapService+  , dimapService   , composeMiddleware     -- * Client   , Client(..)
src/Network/HTTP/Tower/Client.hs view
@@ -3,13 +3,15 @@ -- Description : HTTP client with composable middleware -- License     : MIT ----- Create an HTTP client and compose middleware using the @('|>')@ operator:+-- Create an HTTP client and compose middleware using @'Data.Function.&'@: -- -- @+-- import Data.Function (('&')) -- client <- 'newClient'--- let configured = client---       '|>' withRetry (constantBackoff 3 1.0)---       '|>' withTimeout 5000+-- let configured = client '&' 'applyMiddleware'+--       ( withRetry (constantBackoff 3 1.0)+--       . withTimeout 5000+--       ) -- result <- 'runRequest' configured request -- @ --@@ -150,7 +152,10 @@ applyMiddleware :: Middleware HTTP.Request HttpResponse -> Client -> Client applyMiddleware mw client = client { clientService = mw (clientService client) } --- | Operator for fluent middleware application.+-- | Operator for fluent middleware application (Rust\/Elixir-style).+--+-- Equivalent to using @'Data.Function.&'@ with individual middleware.+-- Provided for newcomers who prefer a left-to-right pipeline syntax: -- -- @ -- client <- 'newClient'
src/Network/HTTP/Tower/Middleware/FollowRedirect.hs view
@@ -9,7 +9,8 @@ -- configurable maximum number of hops. -- -- @--- client '|>' 'withFollowRedirects' 5  -- max 5 hops+-- import Data.Function (('&'))+-- svc '&' 'withFollowRedirects' 5  -- max 5 hops -- @ -- -- Handles 301, 302, 303, 307, and 308. On 303, the method is changed
src/Network/HTTP/Tower/Middleware/Logging.hs view
@@ -8,7 +8,8 @@ -- Logs HTTP method, host, status code, and duration for each request. -- -- @--- client '|>' 'withLogging' (\\msg -> Data.Text.IO.putStrLn msg)+-- import Data.Function (('&'))+-- svc '&' 'withLogging' (\\msg -> Data.Text.IO.putStrLn msg) -- @ module Network.HTTP.Tower.Middleware.Logging   ( withLogging
src/Network/HTTP/Tower/Middleware/RequestId.hs view
@@ -8,8 +8,9 @@ -- Adds a UUID v4 correlation ID to every request. -- -- @--- client '|>' 'withRequestId'                          -- X-Request-ID header--- client '|>' 'withRequestIdHeader' \"X-Correlation-ID\"  -- custom header+-- import Data.Function (('&'))+-- svc '&' 'withRequestId'                          -- X-Request-ID header+-- svc '&' 'withRequestIdHeader' \"X-Correlation-ID\"  -- custom header -- @ module Network.HTTP.Tower.Middleware.RequestId   ( withRequestId
src/Network/HTTP/Tower/Middleware/SetHeader.hs view
@@ -6,9 +6,10 @@ -- License     : MIT -- -- @--- client '|>' 'withBearerAuth' \"my-token\"--- client '|>' 'withUserAgent' \"my-app\/1.0\"--- client '|>' 'withHeader' \"X-Custom\" \"value\"+-- import Data.Function (('&'))+-- svc '&' 'withBearerAuth' \"my-token\"+-- svc '&' 'withUserAgent' \"my-app\/1.0\"+-- svc '&' 'withHeader' \"X-Custom\" \"value\" -- @ module Network.HTTP.Tower.Middleware.SetHeader   ( withHeader
src/Network/HTTP/Tower/Middleware/TestDouble.hs view
@@ -4,15 +4,16 @@ -- License     : MIT -- -- @+-- import Data.Function (('&')) -- -- Replace the service entirely--- let testClient = client '|>' 'withMock' (\\req -> pure (Right fakeResponse))+-- let testSvc = svc '&' 'withMock' (\\req -> pure (Right fakeResponse)) -- -- -- Route-based mocks--- let testClient = client '|>' 'withMockMap' mocks+-- let testSvc = svc '&' 'withMockMap' mocks -- -- -- Record requests for assertions -- recorder <- newIORef []--- let testClient = client '|>' 'withRecorder' recorder+-- let testSvc = svc '&' 'withRecorder' recorder -- @ module Network.HTTP.Tower.Middleware.TestDouble   ( withMock@@ -41,11 +42,12 @@ -- Falls through to the inner service if no match is found. -- -- @+-- import Data.Function (('&')) -- let mocks = Map.fromList --       [ (\"api.example.com\/v1\/users\", Right usersResponse) --       , (\"api.example.com\/v1\/health\", Right healthResponse) --       ]--- let testClient = client '|>' 'withMockMap' mocks+-- let testSvc = svc '&' 'withMockMap' mocks -- @ withMockMap   :: Map ByteString (Either ServiceError HttpResponse)@@ -60,9 +62,10 @@ -- The recorder stores requests in reverse order (most recent first). -- -- @+-- import Data.Function (('&')) -- recorder <- newIORef []--- let testClient = client '|>' 'withRecorder' recorder--- _ <- runRequest testClient someRequest+-- let testSvc = svc '&' 'withRecorder' recorder+-- _ <- runService testSvc someRequest -- recorded <- readIORef recorder -- length recorded \`shouldBe\` 1 -- @
src/Network/HTTP/Tower/Middleware/Tracing.hs view
@@ -36,7 +36,8 @@ -- If no TracerProvider is configured, this is a no-op (zero overhead). -- -- @--- let client' = client |> withTracing+-- import Data.Function (('&'))+-- let svc' = svc '&' 'withTracing' -- @ withTracing :: Middleware HTTP.Request HttpResponse withTracing inner = Service $ \req -> do
src/Network/HTTP/Tower/Middleware/Validate.hs view
@@ -8,9 +8,10 @@ -- Reject responses that don't meet expectations: -- -- @--- client '|>' 'withValidateStatus' (\\c -> c >= 200 && c < 300)--- client '|>' 'withValidateContentType' \"application\/json\"--- client '|>' 'withValidateHeader' \"X-Request-ID\"+-- import Data.Function (('&'))+-- svc '&' 'withValidateStatus' (\\c -> c >= 200 && c < 300)+-- svc '&' 'withValidateContentType' \"application\/json\"+-- svc '&' 'withValidateHeader' \"X-Request-ID\" -- @ module Network.HTTP.Tower.Middleware.Validate   ( withValidateStatus
test/Network/HTTP/Tower/Middleware/FollowRedirectSpec.hs view
@@ -13,13 +13,14 @@ import Tower.Service import Tower.Error import Tower.Error.Testing ()+import Data.Function ((&)) import Network.HTTP.Tower.Middleware.FollowRedirect  spec :: Spec spec = describe "FollowRedirect middleware" $ do   it "passes non-redirect responses through" $ do     let svc = Service $ \_ -> pure (Right (fakeResponseWith HTTP.status200 []))-        followed = withFollowRedirects 5 svc+        followed = svc & withFollowRedirects 5     req <- HTTP.parseRequest "http://example.com"     result <- runService followed req     case result of@@ -36,7 +37,7 @@               (HTTP.mkStatus 302 "Found")               [("Location", "http://example.com/redirected")]             else pure $ Right $ fakeResponseWith HTTP.status200 []-        followed = withFollowRedirects 5 svc+        followed = svc & withFollowRedirects 5     req <- HTTP.parseRequest "http://example.com/original"     result <- runService followed req     case result of@@ -48,7 +49,7 @@     let svc = Service $ \_ -> pure $ Right $ fakeResponseWith           (HTTP.mkStatus 301 "Moved")           [("Location", "http://example.com/loop")]-        followed = withFollowRedirects 3 svc+        followed = svc & withFollowRedirects 3     req <- HTTP.parseRequest "http://example.com/loop"     result <- runService followed req     case result of@@ -65,7 +66,7 @@               (HTTP.mkStatus 303 "See Other")               [("Location", "http://example.com/result")]             else pure $ Right $ fakeResponseWith HTTP.status200 []-        followed = withFollowRedirects 5 svc+        followed = svc & withFollowRedirects 5     req <- HTTP.parseRequest "http://example.com/action"     let postReq = req { HTTP.method = "POST" }     _ <- runService followed postReq
test/Network/HTTP/Tower/Middleware/LoggingSpec.hs view
@@ -13,6 +13,7 @@ import Network.HTTP.Tower.Client (HttpResponse) import Tower.Service import Tower.Error+import Data.Function ((&)) import Network.HTTP.Tower.Middleware.Logging  spec :: Spec@@ -21,7 +22,7 @@     logRef <- newIORef ([] :: [Text])     let logger msg = modifyIORef' logRef (msg :)         svc = Service $ \_ -> pure (Right fakeResponse)-        logged = withLogging logger svc+        logged = svc & withLogging logger     req <- HTTP.parseRequest "http://example.com/test"     _ <- runService logged req     logs <- readIORef logRef@@ -34,7 +35,7 @@     let logger msg = modifyIORef' logRef (msg :)         svc :: Service HTTP.Request HttpResponse         svc = Service $ \_ -> pure (Left TimeoutError)-        logged = withLogging logger svc+        logged = svc & withLogging logger     req <- HTTP.parseRequest "http://example.com/fail"     _ <- runService logged req     logs <- readIORef logRef@@ -45,7 +46,7 @@   it "does not alter the result" $ do     let logger _ = pure ()         svc = Service $ \_ -> pure (Right fakeResponse)-        logged = withLogging logger svc+        logged = svc & withLogging logger     req <- HTTP.parseRequest "http://example.com/passthrough"     result <- runService logged req     case result of
test/Network/HTTP/Tower/Middleware/RequestIdSpec.hs view
@@ -8,6 +8,7 @@  import Network.HTTP.Tower.Client (HttpResponse) import Tower.Service+import Data.Function ((&)) import Network.HTTP.Tower.Middleware.RequestId import Network.HTTP.Tower.Middleware.TestDouble (withRecorder) @@ -15,7 +16,7 @@ spec = describe "RequestId middleware" $ do   it "adds X-Request-ID header" $ do     recorder <- newIORef []-    let svc = withRequestId (withRecorder recorder (Service $ \_ -> pure (Right fakeResponse)))+    let svc = Service (\_ -> pure (Right fakeResponse)) & withRecorder recorder & withRequestId     req <- HTTP.parseRequest "http://example.com"     _ <- runService svc req     recorded <- readIORef recorder@@ -24,7 +25,7 @@    it "generates unique IDs per request" $ do     recorder <- newIORef []-    let svc = withRequestId (withRecorder recorder (Service $ \_ -> pure (Right fakeResponse)))+    let svc = Service (\_ -> pure (Right fakeResponse)) & withRecorder recorder & withRequestId     req <- HTTP.parseRequest "http://example.com"     _ <- runService svc req     _ <- runService svc req@@ -35,7 +36,7 @@    it "uses custom header name" $ do     recorder <- newIORef []-    let svc = withRequestIdHeader "X-Correlation-ID" (withRecorder recorder (Service $ \_ -> pure (Right fakeResponse)))+    let svc = Service (\_ -> pure (Right fakeResponse)) & withRecorder recorder & withRequestIdHeader "X-Correlation-ID"     req <- HTTP.parseRequest "http://example.com"     _ <- runService svc req     recorded <- readIORef recorder
test/Network/HTTP/Tower/Middleware/SetHeaderSpec.hs view
@@ -8,6 +8,7 @@  import Network.HTTP.Tower.Client (HttpResponse) import Tower.Service+import Data.Function ((&)) import Network.HTTP.Tower.Middleware.SetHeader import Network.HTTP.Tower.Middleware.TestDouble (withRecorder) @@ -15,7 +16,7 @@ spec = describe "SetHeader middleware" $ do   it "adds a single header to requests" $ do     recorder <- newIORef []-    let svc = withHeader "X-Custom" "value" (withRecorder recorder (Service $ \_ -> pure (Right fakeResponse)))+    let svc = Service (\_ -> pure (Right fakeResponse)) & withRecorder recorder & withHeader "X-Custom" "value"     req <- HTTP.parseRequest "http://example.com"     _ <- runService svc req     recorded <- readIORef recorder@@ -24,7 +25,7 @@    it "adds multiple headers" $ do     recorder <- newIORef []-    let svc = withHeaders [("X-A", "1"), ("X-B", "2")] (withRecorder recorder (Service $ \_ -> pure (Right fakeResponse)))+    let svc = Service (\_ -> pure (Right fakeResponse)) & withRecorder recorder & withHeaders [("X-A", "1"), ("X-B", "2")]     req <- HTTP.parseRequest "http://example.com"     _ <- runService svc req     recorded <- readIORef recorder@@ -34,7 +35,7 @@    it "adds Bearer auth header" $ do     recorder <- newIORef []-    let svc = withBearerAuth "my-token" (withRecorder recorder (Service $ \_ -> pure (Right fakeResponse)))+    let svc = Service (\_ -> pure (Right fakeResponse)) & withRecorder recorder & withBearerAuth "my-token"     req <- HTTP.parseRequest "http://example.com"     _ <- runService svc req     recorded <- readIORef recorder@@ -43,7 +44,7 @@    it "sets User-Agent header" $ do     recorder <- newIORef []-    let svc = withUserAgent "http-tower/0.1" (withRecorder recorder (Service $ \_ -> pure (Right fakeResponse)))+    let svc = Service (\_ -> pure (Right fakeResponse)) & withRecorder recorder & withUserAgent "http-tower/0.1"     req <- HTTP.parseRequest "http://example.com"     _ <- runService svc req     recorded <- readIORef recorder
test/Network/HTTP/Tower/Middleware/TestDoubleSpec.hs view
@@ -13,6 +13,7 @@ import Network.HTTP.Tower.Client (HttpResponse) import Tower.Service import Tower.Error+import Data.Function ((&)) import Network.HTTP.Tower.Middleware.TestDouble  spec :: Spec@@ -23,7 +24,7 @@       let inner = Service $ \_ -> do             writeIORef innerCalled True             pure (Right fakeResponse)-          mocked = withMock (\_ -> pure (Right fakeResponse)) inner+          mocked = inner & withMock (\_ -> pure (Right fakeResponse))       req <- HTTP.parseRequest "http://example.com"       result <- runService mocked req       case result of@@ -32,7 +33,7 @@       readIORef innerCalled >>= (`shouldBe` False)      it "can return errors" $ do-      let mocked = withMock (\_ -> pure (Left (CustomError "mock error"))) (Service $ \_ -> pure (Right fakeResponse))+      let mocked = Service (\_ -> pure (Right fakeResponse)) & withMock (\_ -> pure (Left (CustomError "mock error")))       req <- HTTP.parseRequest "http://example.com"       result <- runService mocked req       case result of@@ -44,7 +45,7 @@       let mocks = Map.fromList             [ ("example.com/api/users", Right fakeResponse)             ]-          mocked = withMockMap mocks (Service $ \_ -> pure (Left (CustomError "not mocked")))+          mocked = Service (\_ -> pure (Left (CustomError "not mocked"))) & withMockMap mocks       req <- HTTP.parseRequest "http://example.com/api/users"       result <- runService mocked req       case result of@@ -55,7 +56,7 @@       let mocks = Map.fromList             [ ("example.com/api/users", Right fakeResponse)             ]-          mocked = withMockMap mocks (Service $ \_ -> pure (Right fakeResponse))+          mocked = Service (\_ -> pure (Right fakeResponse)) & withMockMap mocks       req <- HTTP.parseRequest "http://example.com/api/other"       result <- runService mocked req       case result of@@ -65,7 +66,7 @@   describe "withRecorder" $ do     it "records all requests" $ do       recorder <- newIORef []-      let svc = withRecorder recorder (Service $ \_ -> pure (Right fakeResponse))+      let svc = Service (\_ -> pure (Right fakeResponse)) & withRecorder recorder       req1 <- HTTP.parseRequest "http://example.com/a"       req2 <- HTTP.parseRequest "http://example.com/b"       _ <- runService svc req1@@ -79,7 +80,7 @@       let inner = Service $ \_ -> do             modifyIORef' innerCalled (+ 1)             pure (Right fakeResponse)-          svc = withRecorder recorder inner+          svc = inner & withRecorder recorder       req <- HTTP.parseRequest "http://example.com"       _ <- runService svc req       readIORef innerCalled >>= (`shouldBe` 1)
test/Network/HTTP/Tower/Middleware/TracingSpec.hs view
@@ -21,6 +21,7 @@ import Tower.Service import Tower.Error import Tower.Error.Testing ()+import Data.Function ((&)) import Network.HTTP.Tower.Middleware.Tracing  spec :: Spec@@ -30,7 +31,7 @@    it "passes successful responses through unchanged" $ do     let svc = Service $ \_ -> pure (Right fakeResponse)-        traced = withTracing svc+        traced = svc & withTracing     req <- HTTP.parseRequest "http://example.com/test"     result <- runService traced req     case result of@@ -40,7 +41,7 @@   it "passes errors through unchanged" $ do     let svc :: Service HTTP.Request HttpResponse         svc = Service $ \_ -> pure (Left TimeoutError)-        traced = withTracing svc+        traced = svc & withTracing     req <- HTTP.parseRequest "http://example.com/fail"     result <- runService traced req     case result of@@ -52,7 +53,7 @@     let svc = Service $ \_ -> do           modifyIORef' callCount (+ 1)           pure (Right fakeResponse)-        traced = withTracing svc+        traced = svc & withTracing     req <- HTTP.parseRequest "http://example.com/once"     _ <- runService traced req     readIORef callCount >>= (`shouldBe` 1)@@ -68,7 +69,7 @@             })           (TracerOptions Nothing)         svc = Service $ \_ -> pure (Right fakeResponse)-        traced = withTracingTracer tracer svc+        traced = svc & withTracingTracer tracer     req <- HTTP.parseRequest "https://api.example.com/v1/data"     result <- runService traced req     case result of@@ -77,17 +78,17 @@    it "handles 4xx responses without altering the result" $ do     let svc = Service $ \_ -> pure (Right fake404Response)-        traced = withTracing svc+        traced = svc & withTracing     req <- HTTP.parseRequest "http://example.com/notfound"     result <- runService traced req     case result of       Right resp -> HTTP.responseStatus resp `shouldBe` HTTP.notFound404       Left err   -> expectationFailure $ "Expected Right, got: " ++ show err -  it "composes with the |> operator" $ do-    -- withTracing is now a pure Middleware, so it works directly with |>+  it "composes with the (&) operator" $ do+    -- withTracing is now a pure Middleware, so it works directly with (&)     let svc = Service $ \_ -> pure (Right fakeResponse)-        traced = withTracing svc+        traced = svc & withTracing     req <- HTTP.parseRequest "http://example.com/pipe"     result <- runService traced req     case result of
test/Network/HTTP/Tower/Middleware/ValidateSpec.hs view
@@ -12,6 +12,7 @@ import Tower.Service import Tower.Error import Tower.Error.Testing ()+import Data.Function ((&)) import Network.HTTP.Tower.Middleware.Validate  spec :: Spec@@ -19,7 +20,7 @@   describe "withValidateStatus" $ do     it "passes valid status codes" $ do       let svc = Service $ \_ -> pure (Right (fakeResponseWith HTTP.status200 []))-          validated = withValidateStatus (\c -> c >= 200 && c < 300) svc+          validated = svc & withValidateStatus (\c -> c >= 200 && c < 300)       req <- HTTP.parseRequest "http://example.com"       result <- runService validated req       case result of@@ -28,7 +29,7 @@      it "rejects invalid status codes" $ do       let svc = Service $ \_ -> pure (Right (fakeResponseWith HTTP.internalServerError500 []))-          validated = withValidateStatus (\c -> c >= 200 && c < 300) svc+          validated = svc & withValidateStatus (\c -> c >= 200 && c < 300)       req <- HTTP.parseRequest "http://example.com"       result <- runService validated req       case result of@@ -39,7 +40,7 @@     it "passes matching content type" $ do       let svc = Service $ \_ -> pure (Right (fakeResponseWith HTTP.status200               [("Content-Type", "application/json; charset=utf-8")]))-          validated = withValidateContentType "application/json" svc+          validated = svc & withValidateContentType "application/json"       req <- HTTP.parseRequest "http://example.com"       result <- runService validated req       case result of@@ -49,7 +50,7 @@     it "rejects non-matching content type" $ do       let svc = Service $ \_ -> pure (Right (fakeResponseWith HTTP.status200               [("Content-Type", "text/html")]))-          validated = withValidateContentType "application/json" svc+          validated = svc & withValidateContentType "application/json"       req <- HTTP.parseRequest "http://example.com"       result <- runService validated req       case result of@@ -58,7 +59,7 @@      it "rejects missing content type" $ do       let svc = Service $ \_ -> pure (Right (fakeResponseWith HTTP.status200 []))-          validated = withValidateContentType "application/json" svc+          validated = svc & withValidateContentType "application/json"       req <- HTTP.parseRequest "http://example.com"       result <- runService validated req       case result of@@ -69,7 +70,7 @@     it "passes when header is present" $ do       let svc = Service $ \_ -> pure (Right (fakeResponseWith HTTP.status200               [("X-Request-ID", "abc")]))-          validated = withValidateHeader "X-Request-ID" svc+          validated = svc & withValidateHeader "X-Request-ID"       req <- HTTP.parseRequest "http://example.com"       result <- runService validated req       case result of@@ -78,7 +79,7 @@      it "rejects when header is missing" $ do       let svc = Service $ \_ -> pure (Right (fakeResponseWith HTTP.status200 []))-          validated = withValidateHeader "X-Request-ID" svc+          validated = svc & withValidateHeader "X-Request-ID"       req <- HTTP.parseRequest "http://example.com"       result <- runService validated req       case result of