annotated-exception-0.1.0.0: src/Control/Exception/Annotated.hs
{-# LANGUAGE DeriveFoldable #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE DeriveTraversable #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE InstanceSigs #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TypeApplications #-}
-- | This module defines an exception wrapper 'AnnotatedException' that
-- carries a list of 'Annotation's, along with some helper methods for
-- throwing and catching that can make the annotations transparent.
--
-- While this library can be used directly, it is recommended that you
-- define your own types and functions specific to your domain. As an
-- example, 'checkpoint' is useful *only* for providing exception
-- annotation information. However, you probably want to use 'checkpoint'
-- in concert with other context adding features, like logging.
--
-- Likewise, the 'Annotation' type defined in "Data.Annotation" is
-- essentially a wrapper for a dynamically typed value. So you probably
-- want to define your own 'checkpoint' that uses a custom type that you
-- want to enforce throughout your application.
module Control.Exception.Annotated
( -- * The Main Type
AnnotatedException(..)
, new
, throwWithCallStack
-- * Annotating Exceptions
, checkpoint
, checkpointMany
, checkpointCallStack
, checkpointCallStackWith
-- * Handling Exceptions
, catch
, tryAnnotated
-- * Manipulating Annotated Exceptions
, check
, hide
, annotatedExceptionCallStack
, addCallStackToException
-- * Re-exports from "Data.Annotation"
, Annotation(..)
, CallStackAnnotation(..)
-- * Re-exports from "Control.Exception.Safe"
, Exception(..)
, Safe.SomeException(..)
, Safe.throw
, Safe.try
) where
import Control.Exception.Safe
(Exception, MonadCatch, MonadThrow, SomeException(..))
import qualified Control.Exception.Safe as Safe
import Data.Annotation
import Data.Maybe
import Data.Typeable
import GHC.Stack
-- | The 'AnnotatedException' type wraps an @exception@ with
-- a @['Annotation']@. This can provide a sort of a manual stack trace with
-- programmer provided data.
--
-- @since 0.1.0.0
data AnnotatedException exception
= AnnotatedException
{ annotations :: [Annotation]
, exception :: exception
}
deriving (Eq, Show, Functor, Foldable, Traversable)
instance Applicative AnnotatedException where
pure =
AnnotatedException []
AnnotatedException anns0 f <*> AnnotatedException anns1 a =
AnnotatedException (anns0 <> anns1) (f a)
-- | This instance of 'Exception' is a bit interesting. It tries to do as
-- much hiding and packing and flattening as possible to ensure that even
-- exception handling machinery outside of this package can still
-- intelligently handle it.
--
-- Any 'Exception' can be caught as a 'AnnotatedException' with
-- an empty context, so catching a @'AnnotatedException' e@ will also catch
-- a regular @e@ and give it an empty set of annotations.
--
-- Likewise, if a @'AnnotatedException' ('AnnotatedException' e)@ is thrown
-- somehow, then the 'fromException' will flatten it and combine their
-- contexts.
--
-- For the most up to date details, see the test suite.
--
-- @since 0.1.0.0
instance (Exception exception) => Exception (AnnotatedException exception) where
toException loc = SomeException $ hide loc
fromException (SomeException exn)
| Just x <- cast exn
=
pure x
| Just (AnnotatedException ann (e :: SomeException)) <- cast exn
, Just a <- Safe.fromException e
=
pure $ AnnotatedException ann a
fromException exn
| Just (e :: exception) <- Safe.fromException exn
=
pure $ new e
| Just x <- flatten <$> Safe.fromException exn
=
pure x
| otherwise
=
Nothing
-- | Attach an empty @['Annotation']@ to an exception.
--
-- @since 0.1.0.0
new :: e -> AnnotatedException e
new = pure
-- | Append the @['Annotation']@ to the 'AnnotatedException'.
--
-- @since 0.1.0.0
annotate :: [Annotation] -> AnnotatedException e -> AnnotatedException e
annotate ann (AnnotatedException anns e) = AnnotatedException (ann ++ anns) e
-- | Call 'toException' on the underlying 'Exception'.
--
-- @since 0.1.0.0
hide :: Exception e => AnnotatedException e -> AnnotatedException SomeException
hide = fmap Safe.toException
-- | Call 'fromException' on the underlying 'Exception', attaching the
-- annotations to the result.
--
-- @since 0.1.0.0
check :: Exception e => AnnotatedException SomeException -> Maybe (AnnotatedException e)
check = traverse Safe.fromException
-- | Catch an exception. This works just like 'Safe.catch', but it also
-- will attempt to catch @'AnnotatedException' e@. The annotations will be
-- preserved in the handler, so rethrowing exceptions will retain the
-- context.
--
-- Let's consider a few examples, that share this import and exception
-- type.
--
-- > import qualified Control.Exception.Safe as Safe
-- > import Control.Exception.Annotated
-- >
-- > data TestException deriving (Show, Exception)
--
-- We can throw an exception and catch it as usual.
--
-- > throw TestException `catch` \TestException ->
-- > putStrLn "ok!"
--
-- We can throw an exception and catch it with location.
--
-- > throw TestException `catch` \(AnnotatedException anns TestException) ->
-- > putStrLn "ok!"
--
--
-- We can throw an exception and catch it as a @'AnnotatedException'
-- 'SomeException'@.
--
-- > throw TestException `catch` \(AnnotatedException anns (e :: SomeException) ->
-- > putStrLn "ok!"
--
-- @since 0.1.0.0
catch :: (Exception e, MonadCatch m) => m a -> (e -> m a) -> m a
catch action handler =
Safe.catches
action
[ Safe.Handler handler
, Safe.Handler $ \(AnnotatedException anns e) ->
checkpointMany anns $ handler e
]
-- | Like 'catch', but always returns a 'AnnotatedException'.
--
-- @since 0.1.0.0
tryAnnotated :: (Exception e, MonadCatch m) => m a -> m (Either (AnnotatedException e) a)
tryAnnotated action =
(Right <$> action) `catch` (pure . Left)
-- | Attaches the 'CallStack' to the 'AnnotatedException' that is thrown.
--
-- The 'CallStack' will *not* be present as a 'CallStack' - it will be
-- a 'CallStackAnnotation'.
--
-- @since 0.1.0.0
throwWithCallStack
:: (HasCallStack, MonadThrow m, Exception e)
=> e -> m a
throwWithCallStack e =
Safe.throw (AnnotatedException [callStackAnnotation] e)
-- | Concatenate two lists of annotations.
--
-- @since 0.1.0.0
flatten :: AnnotatedException (AnnotatedException e) -> AnnotatedException e
flatten (AnnotatedException a (AnnotatedException b c)) = AnnotatedException (a ++ b) c
-- | Add a single 'Annotation' to any exceptions thrown in the following
-- action.
--
-- Example:
--
-- > main = do
-- > checkpoint "Foo" $ do
-- > print =<< readFile "I don't exist.markdown"
--
-- The exception thrown due to a missing file will now have an 'Annotation'
-- @"Foo"@.
--
-- @since 0.1.0.0
checkpoint :: MonadCatch m => Annotation -> m a -> m a
checkpoint ann = checkpointMany [ann]
-- | Add the current 'CallStack' to the checkpoint. This function searches any
-- thrown exception for a pre-existing 'CallStack' and will not overwrite or
-- replace the 'CallStack' if one is already present.
--
-- Primarily useful when you're wrapping a third party library.
--
-- @since 0.1.0.0
checkpointCallStackWith
:: (MonadCatch m, HasCallStack)
=> [Annotation]
-> m a
-> m a
checkpointCallStackWith ann action =
action `Safe.catch` \(exn :: SomeException) ->
Safe.throw
. annotate ann
. addCallStackToException callStack
$ case Safe.fromException exn of
Just (e' :: AnnotatedException SomeException) ->
case annotatedExceptionCallStack e' of
Nothing ->
annotate ann e'
Just _preexistingCallstack ->
e'
Nothing -> do
annotate ann $ new exn
-- | Add the current 'CallStack' to the checkpoint. This function searches any
-- thrown exception for a pre-existing 'CallStack' and will not overwrite or
-- replace the 'CallStack' if one is already present.
--
-- Primarily useful when you're wrapping a third party library.
--
-- @since 0.1.0.0
checkpointCallStack
:: (MonadCatch m, HasCallStack)
=> m a
-> m a
checkpointCallStack =
checkpointCallStackWith []
-- | Add the list of 'Annotations' to any exception thrown in the following
-- action.
--
-- @since 0.1.0.0
checkpointMany :: (MonadCatch m) => [Annotation] -> m a -> m a
checkpointMany ann action =
action `Safe.catch` \(exn :: SomeException) ->
Safe.throw . annotate ann $ case Safe.fromException exn of
Just (e' :: AnnotatedException SomeException) ->
e'
Nothing -> do
new exn
-- | Retrieves the 'CallStack' from an 'AnnotatedException' if one is present.
--
-- @since 0.1.0.0
annotatedExceptionCallStack :: AnnotatedException exception -> Maybe CallStack
annotatedExceptionCallStack exn =
let (stacks, _rest) = callStackInAnnotations (annotations exn)
in listToMaybe stacks
-- | Adds a 'CallStack' to the given 'AnnotatedException'. This function will
-- search through the existing annotations, and it will not add a second
-- 'CallStack' to the list.
--
-- @since 0.1.0.0
addCallStackToException
:: CallStack
-> AnnotatedException exception
-> AnnotatedException exception
addCallStackToException cs exn =
case annotatedExceptionCallStack exn of
Nothing ->
annotate [callStackToAnnotation cs] exn
Just _ ->
exn