packages feed

cleveland-0.1.0: src/Test/Cleveland/Internal/Exceptions.hs

-- SPDX-FileCopyrightText: 2020 Tocqueville Group
--
-- SPDX-License-Identifier: LicenseRef-MIT-TQ

-- | This module defines common exception types and exception-related functions
-- used throughout the "Test.Cleveland" modules.
module Test.Cleveland.Internal.Exceptions
  ( WithCallStack(..)
  , addCallStack
  , throwWithCallStack
  , tryWithCallStack
  , catchWithCallStack
  ) where

import Data.Typeable (cast)
import Fmt (Buildable(..), pretty, unlinesF)

----------------------------------------------------------------------------
-- WithCallStack
----------------------------------------------------------------------------

-- | Wraps an exception and adds some callstack information.
data WithCallStack where
  WithCallStack :: CallStack -> SomeException -> WithCallStack

deriving stock instance Show WithCallStack

instance Buildable WithCallStack where
  build (WithCallStack cstack e) = unlinesF
    [ prettyCallStack cstack
    , displayException e
    ]

instance Exception WithCallStack where
  displayException = pretty

-- | Wrap any exceptions thrown by some monadic action with 'WithCallStack'.
--
-- If the action throws an exception already wrapped in 'WithCallStack',
-- that callstack will be overriden with the current one.
addCallStack :: forall m a. (HasCallStack, MonadCatch m) => m a -> m a
addCallStack ma =
  ma `catch` \se@(SomeException ex) ->
    case fromException @WithCallStack se of
      Just (WithCallStack _ innerEx) -> throwWithCallStack callStack innerEx
      Nothing -> throwWithCallStack callStack ex

throwWithCallStack :: forall e a m. (MonadThrow m, Exception e) => CallStack -> e -> m a
throwWithCallStack cstack ex = throwM (WithCallStack cstack (toException ex))


-- | Similar to 'catch', but also catches exceptions of type @e@ wrapped in 'WithCallStack'.
catchWithCallStack
  :: forall e a m. (Exception e, MonadCatch m)
  => m a -> (Maybe CallStack -> e -> m a) -> m a
catchWithCallStack ma f =
  ma `catch` \(se :: SomeException) ->
    case fromExceptionWithCallStack @e se of
      Just (cstackMb, e) -> f cstackMb e
      Nothing -> throwM se

-- | Attempts to run the given action.
--
-- If it:
--
-- * suceeds, the value will be returned in a 'Right'.
-- * throws an exception of the given type @e@ (or an exception of
--   type @e@ wrapped in 'WithCallStack'), it will be returned in a 'Left'.
-- * throws an exception of any other type, it'll be rethrown
--   (retaining the original callstack, if any).
tryWithCallStack :: forall e a m. (MonadCatch m, Exception e) => m a -> m (Either (Maybe CallStack, e) a)
tryWithCallStack ma =
  try @_ @SomeException ma >>= \case
    Right a -> pure (Right a)
    Left se ->
      case fromExceptionWithCallStack @e se of
        Just e -> pure (Left e)
        Nothing -> throwM se

fromExceptionWithCallStack :: forall e. Exception e => SomeException -> Maybe (Maybe CallStack, e)
fromExceptionWithCallStack se =
  (do
    WithCallStack cstack e <- fromException @WithCallStack se
    e' <- cast @_ @e e
    Just (Just cstack, e')
  )
  <|>
  (fromException @e se <&> (Nothing,))