packages feed

amazonka-mtl-0.1.1.0: library/Control/Monad/AWS/Matchers.hs

{-# LANGUAGE AllowAmbiguousTypes #-}

module Control.Monad.AWS.Matchers
  ( Matchers
  , HasMatchers (..)
  , Matcher (..)
  , withMatcher
  , withMatchers
  , matchSend
  , matchAwait
  , UnmatchedRequestError (..)
  ) where

import Prelude

import Amazonka (AWSRequest, AWSResponse, Error)
import qualified Amazonka.Waiter as Waiter
import Control.Exception (Exception (..), throwIO)
import Control.Lens (Lens', view, (<>~))
import Control.Monad (guard)
import Control.Monad.IO.Class (MonadIO (..))
import Control.Monad.Reader (MonadReader (..))
import Data.Maybe (listToMaybe, mapMaybe)
import Data.Typeable

-- | Define a response to provide for any matched requests
data Matcher where
  -- | Matches calls to 'send' where the given predicate holds
  --
  -- @since 0.1.0.0
  SendMatcher
    :: forall a
     . (AWSRequest a, Typeable a, Typeable (AWSResponse a))
    => (a -> Bool)
    -> Either Error (AWSResponse a)
    -> Matcher
  -- | Matches calls to 'await' where the given predicate holds
  --
  -- @since 0.1.0.0
  AwaitMatcher
    :: forall a
     . (AWSRequest a, Typeable a)
    => (Waiter.Wait a -> a -> Bool)
    -> Either Error Waiter.Accept
    -> Matcher

-- |
--
-- @since 0.1.0.0
newtype Matchers = Matchers
  { unMatchers :: [Matcher]
  }
  deriving newtype (Semigroup, Monoid)

-- |
--
-- @since 0.1.0.0
class HasMatchers env where
  matchersL :: Lens' env Matchers

-- |
--
-- @since 0.1.0.0
instance HasMatchers Matchers where
  matchersL = id

-- | Add a 'Matcher' for the duration of the block
--
-- @since 0.1.0.0
withMatcher :: (MonadReader env m, HasMatchers env) => Matcher -> m a -> m a
withMatcher = withMatchers . pure

-- | Add multiple 'Matcher's for the duration of the block
--
-- @since 0.1.0.0
withMatchers :: (MonadReader env m, HasMatchers env) => [Matcher] -> m a -> m a
withMatchers ms = local $ matchersL <>~ Matchers ms

-- |
--
-- @since 0.1.0.0
matchSend
  :: forall m env a
   . ( MonadIO m
     , MonadReader env m
     , HasMatchers env
     , Typeable a
     , Typeable (AWSResponse a)
     )
  => a
  -> m (Either Error (AWSResponse a))
matchSend req = throwUnmatched @a =<< firstMatcher go
 where
  go = \case
    SendMatcher matchReq resp -> do
      guard . matchReq =<< cast req
      cast resp
    AwaitMatcher {} -> Nothing

-- |
--
-- @since 0.1.0.0
matchAwait
  :: forall m env a
   . (MonadIO m, MonadReader env m, HasMatchers env, Typeable a)
  => Waiter.Wait a
  -> a
  -> m (Either Error Waiter.Accept)
matchAwait w req = throwUnmatched @a =<< firstMatcher go
 where
  go = \case
    SendMatcher {} -> Nothing
    AwaitMatcher matchReq acc -> do
      guard =<< matchReq <$> cast w <*> cast req
      cast acc

firstMatcher
  :: (MonadReader env m, HasMatchers env)
  => (Matcher -> Maybe a)
  -> m (Maybe a)
firstMatcher f = do
  matchers <- view matchersL
  pure $ listToMaybe $ mapMaybe f $ unMatchers matchers

-- |
--
-- @since 0.1.0.0
newtype UnmatchedRequestError = UnmatchedRequestError
  { unmatchedRequestType :: String
  }
  deriving anyclass (Exception)

-- Morally-speaking, Show should be reserved for a Haskell-like string
-- representation (derived is best), and displayException is where you make it
-- human-readable. Sadly, too many tools (*cough* hspec) use show instead of
-- displayException, and we want it to look nice there. Sigh.
--
-- https://github.com/hspec/hspec/issues/289
instance Show UnmatchedRequestError where
  show ex =
    "Unexpected AWS request made within MockT: "
      <> unmatchedRequestType ex
      <> "\nUse withMatcher to add a Matcher for this request"

throwUnmatched :: forall req m a. (MonadIO m, Typeable req) => Maybe a -> m a
throwUnmatched =
  maybe
    (liftIO $ throwIO $ UnmatchedRequestError $ show $ typeRep $ Proxy @req)
    pure