packages feed

http-tower-hs-0.1.0.0: src/Network/HTTP/Tower/Middleware/Validate.hs

{-# LANGUAGE OverloadedStrings #-}

-- |
-- Module      : Network.HTTP.Tower.Middleware.Validate
-- Description : Response validation middleware
-- License     : MIT
--
-- Reject responses that don't meet expectations:
--
-- @
-- client '|>' 'withValidateStatus' (\\c -> c >= 200 && c < 300)
-- client '|>' 'withValidateContentType' \"application\/json\"
-- client '|>' 'withValidateHeader' \"X-Request-ID\"
-- @
module Network.HTTP.Tower.Middleware.Validate
  ( withValidateStatus
  , withValidateHeader
  , withValidateContentType
  ) where

import Data.ByteString (ByteString, isInfixOf)
import Data.Text (pack)
import qualified Network.HTTP.Client as HTTP
import qualified Network.HTTP.Types.Header as HTTP
import qualified Network.HTTP.Types.Status as HTTP

import Network.HTTP.Tower.Client (HttpResponse)
import Network.HTTP.Tower.Core (Service(..), Middleware)
import Network.HTTP.Tower.Error (ServiceError(..))

-- | Validate the response status code. Returns a 'CustomError' if the
-- predicate returns 'False'.
--
-- @
-- -- Only accept 2xx
-- client '|>' 'withValidateStatus' (\\c -> c >= 200 && c < 300)
-- @
withValidateStatus :: (Int -> Bool) -> Middleware HTTP.Request HttpResponse
withValidateStatus isValid inner = Service $ \req -> do
  result <- runService inner req
  case result of
    Right resp ->
      let code = HTTP.statusCode (HTTP.responseStatus resp)
      in if isValid code
          then pure (Right resp)
          else pure (Left (CustomError ("Unexpected status code: " <> pack (show code))))
    Left err -> pure (Left err)

-- | Validate that a specific response header is present.
withValidateHeader :: HTTP.HeaderName -> Middleware HTTP.Request HttpResponse
withValidateHeader headerName inner = Service $ \req -> do
  result <- runService inner req
  case result of
    Right resp ->
      case lookup headerName (HTTP.responseHeaders resp) of
        Just _  -> pure (Right resp)
        Nothing -> pure (Left (CustomError ("Missing required header: " <> pack (show headerName))))
    Left err -> pure (Left err)

-- | Validate the @Content-Type@ header contains the expected value.
--
-- Uses substring matching, so @\"application\/json\"@ matches
-- @\"application\/json; charset=utf-8\"@.
withValidateContentType :: ByteString -> Middleware HTTP.Request HttpResponse
withValidateContentType expected inner = Service $ \req -> do
  result <- runService inner req
  case result of
    Right resp ->
      case lookup "Content-Type" (HTTP.responseHeaders resp) of
        Just ct | expected `isInfixOf` ct -> pure (Right resp)
        Just ct -> pure (Left (CustomError ("Unexpected Content-Type: " <> pack (show ct))))
        Nothing -> pure (Left (CustomError "Missing Content-Type header"))
    Left err -> pure (Left err)