packages feed

servant-tower-hs-0.1.0.0: src/Servant/Tower/Adapter.hs

{-# LANGUAGE DeriveAnyClass #-}

-- |
-- Module      : Servant.Tower.Adapter
-- Description : Bridge tower-hs middleware to servant's ClientMiddleware
-- License     : MIT
--
-- Adapt tower-hs middleware for use with servant-client.
--
-- @
-- import Servant.Tower.Adapter
-- import Tower.Middleware.Retry
-- import Tower.Middleware.Timeout
--
-- let env = ('withTowerMiddleware'
--              ('withRetry' ('constantBackoff' 3 1.0) . 'withTimeout' 5000))
--              (mkClientEnv manager baseUrl)
-- runClientM myApiCall env
-- @
module Servant.Tower.Adapter
  ( -- * Converting tower-hs middleware to servant ClientMiddleware
    toClientMiddleware
    -- * Error mapping
  , toClientError
  , toServiceError
    -- * Convenience
  , withTowerMiddleware
  ) where

import Control.Exception (Exception, SomeException, toException)
import Control.Monad.Error.Class (throwError)
import Control.Monad.IO.Class (liftIO)
import Control.Monad.Reader (ask)

import Servant.Client
  ( ClientEnv(..)
  , ClientError(..)
  , ClientM
  , runClientM
  )
import Servant.Client.Core (Request, Response)
import Servant.Client.Internal.HttpClient (ClientMiddleware)

import Tower.Error (ServiceError(..))
import Tower.Service (Service(..), Middleware)

-- | Convert a tower-hs 'Middleware' (specialized to servant's Request/Response)
-- into a servant 'ClientMiddleware'.
--
-- The tower-hs middleware wraps around the servant request pipeline. Errors
-- from the tower-hs middleware are mapped to 'ClientError' via 'toClientError'.
--
-- @
-- toClientMiddleware (withRetry (constantBackoff 3 1.0))
-- @
toClientMiddleware :: Middleware Request Response -> ClientMiddleware
toClientMiddleware towerMw servantApp req = do
  env <- ask
  let wrappedService = Service $ \r -> do
        result <- runClientM (servantApp r) env
        case result of
          Left clientErr -> pure (Left (toServiceError clientErr))
          Right resp     -> pure (Right resp)
      Service towered = towerMw wrappedService
  result <- liftIO (towered req)
  case result of
    Left svcErr -> throwError (toClientError svcErr)
    Right resp  -> pure resp

-- | Map a tower-hs 'ServiceError' to a servant 'ClientError'.
toClientError :: ServiceError -> ClientError
toClientError (TransportError e)    = ConnectionError e
toClientError TimeoutError          = ConnectionError (toException TimeoutEx)
toClientError CircuitBreakerOpen    = ConnectionError (toException CircuitBreakerEx)
toClientError (RetryExhausted _ e)  = toClientError e
toClientError (CustomError t)       = ConnectionError (toException (userError (show t)))

-- | Map a servant 'ClientError' to a tower-hs 'ServiceError'.
toServiceError :: ClientError -> ServiceError
toServiceError (ConnectionError e) = TransportError e
toServiceError e                   = TransportError (toException e)

-- | Apply tower-hs middleware to a servant 'ClientEnv'.
--
-- @
-- let env' = withTowerMiddleware (withRetry (constantBackoff 3 1.0)) env
-- runClientM myApiCall env'
-- @
withTowerMiddleware :: Middleware Request Response -> ClientEnv -> ClientEnv
withTowerMiddleware towerMw env =
  env { middleware = toClientMiddleware towerMw . middleware env }

-- Internal exception types for servant error mapping

data TimeoutEx = TimeoutEx
  deriving (Show, Exception)

data CircuitBreakerEx = CircuitBreakerEx
  deriving (Show, Exception)