packages feed

error-context (empty) → 0.1.0.0

raw patch · 8 files changed

+589/−0 lines, 8 filesdep +basedep +eitherdep +error-contextsetup-changed

Dependencies added: base, either, error-context, exceptions, monad-logger, mtl, resourcet, safe-exceptions, tasty, tasty-hunit, text, unliftio-core

Files

+ ChangeLog.md view
@@ -0,0 +1,3 @@+# Changelog for error-context++## Unreleased changes
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright Moritz Clasmeier (c) 2018++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++    * Redistributions of source code must retain the above copyright+      notice, this list of conditions and the following disclaimer.++    * Redistributions in binary form must reproduce the above+      copyright notice, this list of conditions and the following+      disclaimer in the documentation and/or other materials provided+      with the distribution.++    * Neither the name of Moritz Schulte nor the names of other+      contributors may be used to endorse or promote products derived+      from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,50 @@+# !! This is experimental work-in-progress !!++Welcome to *error-context*! This is a library providing context-aware+error and exception handling for Haskell.++## What problem does *error-context* attempt to solve?++Good error handling is hard. Sometimes it happens that when+propagating errors some context is lost. Call traces sometimes help,+but the current solutions in Haskell-land for accessing call traces+are rather limited. Furthermore, sometimes call traces that at written+by and for humans are more convenient to read.++The *error-context* library allows you to easily attach call traces+('error contexts') to errors, in particular to exceptions. Special+catch-functions are provided for accessing these contexts.++## How to use it?++Add an `ErrorContextT` layer to your monad transformer stack by adding+`runErrorContextT` to the transformer unwrapping code.++The `ErrorContextT` transformer provides the context-enriching logic+via special implementations of `MonadThrow`, `MonadCatch` and+`MonadIO`.++## Examples++See https://github.com/mtesseract/error-context/blob/master/test/Control/Error/Context/Test.hs.++## What about "pure" exceptions?++The `ErrorContextT` transformer implements `MonadThrow` and `MonadIO`,+therefore exceptions thrown by `throwM` and via `liftIO` are+automatically context-enriched. But the story for exceptional values+created via++```haskell+throw :: Exception e => e -> a+```++are not context-enriched. But there is a workaround for this use-case:++```haskell+ensureExceptionContext :: (MonadCatch m, MonadErrorContext m) => m a -> m a+```++This function provides context-aware enriching for any exceptions+thrown within some monadic value, including those thrown by evaluating+values created by `throw`.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ error-context.cabal view
@@ -0,0 +1,72 @@+-- This file has been generated from package.yaml by hpack version 0.20.0.+--+-- see: https://github.com/sol/hpack+--+-- hash: 73e6d19a6c9cb2896dc6e9b9b7465b7a556008ed26855c87123649feea1d426b++name:           error-context+version:        0.1.0.0+synopsis:       Provides API for enriching errors with contexts+description:    Please see the README on Github at <https://github.com/mtesseract/error-context#readme>+category:       Control, Error Handling+homepage:       https://github.com/mtesseract/error-context#readme+bug-reports:    https://github.com/mtesseract/error-context/issues+author:         Moritz Clasmeier+maintainer:     mtesseract@silverratio.net+copyright:      (c) 2018 Moritz Clasmeier+license:        BSD3+license-file:   LICENSE+build-type:     Simple+cabal-version:  >= 1.10++extra-source-files:+    ChangeLog.md+    README.md++source-repository head+  type: git+  location: https://github.com/mtesseract/error-context++library+  hs-source-dirs:+      src+  ghc-options: -Wall+  build-depends:+      base >=4.7 && <5+    , either+    , exceptions+    , monad-logger+    , mtl+    , resourcet+    , safe-exceptions+    , text+    , unliftio-core+  exposed-modules:+      Control.Error.Context+  other-modules:+      Paths_error_context+  default-language: Haskell2010++test-suite error-context-tests+  type: exitcode-stdio-1.0+  main-is: Spec.hs+  hs-source-dirs:+      test+  ghc-options: -Wall -g -threaded -rtsopts -with-rtsopts=-N+  build-depends:+      base >=4.7 && <5+    , either+    , error-context+    , exceptions+    , monad-logger+    , mtl+    , resourcet+    , safe-exceptions+    , tasty+    , tasty-hunit+    , text+    , unliftio-core+  other-modules:+      Control.Error.Context.Test+      Paths_error_context+  default-language: Haskell2010
+ src/Control/Error/Context.hs view
@@ -0,0 +1,291 @@+{-|+Module      : Control.Error.Context+Description : API for enriching errors with contexts+Copyright   : (c) Moritz Clasmeier 2018+License     : BSD3+Maintainer  : mtesseract@silverratio.net+Stability   : experimental+Portability : POSIX++Provides an API for enriching errors with contexts.+-}++{-# LANGUAGE DeriveFunctor              #-}+{-# LANGUAGE FlexibleInstances          #-}+{-# LANGUAGE FunctionalDependencies     #-}+{-# LANGUAGE GADTs                      #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE MultiParamTypeClasses      #-}+{-# LANGUAGE OverloadedStrings          #-}+{-# LANGUAGE ScopedTypeVariables        #-}+{-# LANGUAGE UndecidableInstances       #-}++module Control.Error.Context+  ( ErrorContext(..)+  , ErrorContextT+  , MonadErrorContext(..)+  , ErrorWithContext(..)+  , runErrorContextT+  , errorContextualize+  , errorContextForget+  , errorWithContextDump+  , catchWithoutContext+  , catchWithContext+  , tryWithContext+  , tryAnyWithContext+  , catchJustWithContext+  , catchAnyWithContext+  , ensureExceptionContext+  )+  where++import           Control.Exception.Safe       (SomeException (..), catchAny,+                                               catchJust, try, tryAny)+import           Control.Monad.Catch          (Exception (..), MonadCatch (..),+                                               MonadThrow, throwM)+import           Control.Monad.IO.Unlift+import           Control.Monad.Reader+import           Control.Monad.State+import           Control.Monad.Trans.Resource+import           Control.Monad.Writer+import           Data.Either.Combinators      (mapLeft)+import           Data.Text                    (Text)+import qualified Data.Text                    as Text+import           Data.Typeable++-- | Monad type class providing contextualized errors.+class (Monad m, MonadThrow m) => MonadErrorContext m where+  errorContextCollect :: m ErrorContext     -- ^ Return the current error context.+  withErrorContext    :: Text -> m a -> m a -- ^ Execute a monadic action while having the+                                            -- provided 'Text' being pushed to the error context.++-- | Data type implementing 'MonadErrorContext'.+newtype ErrorContextT m a =+  ErrorContextT { _runErrorContextT :: ReaderT ErrorContext m a+                } deriving ( Functor+                           , Applicative+                           , Monad+                           , MonadTrans+                           , MonadState s+                           , MonadWriter w )++-- | Boundles an error with an 'ErrorContext'.+data ErrorWithContext e =+  ErrorWithContext ErrorContext e+  deriving (Show)++instance Functor ErrorWithContext where+  fmap f (ErrorWithContext ctx e) = ErrorWithContext ctx (f e)++-- | An @ErrorWithContext e@ can be used as an exception.+instance Exception e => Exception (ErrorWithContext e) where+  toException exn = SomeException exn+  fromException (SomeException someExn) =+    case cast someExn :: Maybe (ErrorWithContext e) of+      Just (exnWithCtx @ (ErrorWithContext _ctx _exn)) ->+        Just exnWithCtx+      Nothing ->+        Nothing+  displayException (ErrorWithContext ctx exn) = do+    "Exception: " <> displayException exn <> "\n" <> errorContextAsString ctx++-- | Unwrap an 'ErrorContextT'. Exceptions of type @e@ thrown in the+-- provided action via 'throwM' will cause an @'ErrorWithContext' e@+-- exception to be propagated upwards.+runErrorContextT+  :: ErrorContextT m a+  -> m a+runErrorContextT ctx =+  runReaderT (_runErrorContextT ctx) (ErrorContext [])++instance (MonadCatch m, MonadIO m) => MonadIO (ErrorContextT m) where+  liftIO m = do+    ctx <- errorContextCollect+    lift $ errorContextualizeIO ctx m++    where errorContextualizeIO ctx io = liftIO $+            catchAny io $ \ (SomeException exn) -> throwM (ErrorWithContext ctx exn)++-- | Implement 'MonadErrorContext' for 'ErrorContextT'.+instance MonadThrow m => MonadErrorContext (ErrorContextT m) where+  errorContextCollect = ErrorContextT ask+  withErrorContext layer (ErrorContextT m) =+    ErrorContextT (local (errorContextPush layer) m)++-- | Implement 'MonadThrow' for 'ErrorContextT'.+instance MonadThrow m => MonadThrow (ErrorContextT m) where+  throwM e = do+    case fromException (toException e) :: Maybe (ErrorWithContext SomeException) of+      Just exnWithCtx ->+        lift $ throwM exnWithCtx+      Nothing -> do+        ctx <- errorContextCollect+        lift $ throwM (ErrorWithContext ctx (SomeException e))++-- | Implement 'MonadCatch' for 'ErrorContextT'.+instance MonadCatch m => MonadCatch (ErrorContextT m) where+  catch (ErrorContextT (ReaderT m)) c =+    ErrorContextT . ReaderT $+    \ r -> m r `catchWithoutContext` \ exn -> runReaderT (_runErrorContextT (c exn)) r++-- | Like 'catch', but the handler is required to be context-aware. Is+-- also able to catch exceptions of type 'e' (without context).+catchWithContext+  :: (MonadCatch m, Exception e) -- , MonadErrorContext m)+  => m a+  -> (ErrorWithContext e -> m a)+  -> m a+catchWithContext m handler = catchJust pre m handler+  where pre :: Exception e => SomeException -> Maybe (ErrorWithContext e)+        pre someExn =+          -- First we check if the exception is of the type+          -- 'ErrorWithContext e'. If so, provide it to the handler+          -- directly.+          case fromException someExn of+            Just (ErrorWithContext ctx someExnWithoutCtx :: ErrorWithContext SomeException) ->+              case fromException someExnWithoutCtx of+                Just exn -> Just (ErrorWithContext ctx exn)+                Nothing  -> Nothing+            Nothing  ->+              -- Then we check if the exception is of the type 'e',+              -- (without context). In this case we convert it into an+              -- 'ErrorWithContext e' by adding an empty context and+              -- provide the wrapped exception with context to the+              -- handler.+              case fromException someExn of+                Just exn ->+                  Just (ErrorWithContext (ErrorContext []) exn)+                Nothing ->+                  Nothing++-- | Like 'catch', but the handler is required to be context-unaware.+-- Is also able to catch exceptions with context, in which case the+-- context will be forgotten before the exception will be provided to+-- the handler.+catchWithoutContext+  :: forall a e m+   . (MonadCatch m, Exception e) -- , MonadErrorContext m)+  => m a+  -> (e -> m a)+  -> m a+catchWithoutContext m handler = catchJust pre m handler+  where pre :: SomeException -> Maybe e+        pre someExn =+          -- First we check if the exception is of the type 'e'+          -- (without context). If so, provide it to the handler+          -- directly.+          case fromException someExn :: Maybe e of+            Just exn ->+              Just exn+            Nothing  ->+              -- Then we check if the exception is of the type+              -- 'ErrorWithContext e'. In this case we forget the+              -- context and provide the exception without context to+              -- the handler.+              case fromException someExn :: Maybe (ErrorWithContext SomeException) of+                Just (ErrorWithContext _ctx someExnWithoutContext) ->+                  case fromException someExnWithoutContext :: Maybe e of+                    Just exn ->+                      Just exn+                    Nothing ->+                      Nothing+                Nothing  ->+                  Nothing++tryAnyWithContext+  :: (MonadErrorContext m, MonadCatch m)+  => m a+  -> m (Either (ErrorWithContext SomeException) a)+tryAnyWithContext m =+  mapLeft (ErrorWithContext (ErrorContext [])) <$> tryAny m++tryWithContext+  :: (MonadErrorContext m, MonadCatch m, Exception e)+  => m a+  -> m (Either (ErrorWithContext e) a)+tryWithContext m =+  mapLeft (ErrorWithContext (ErrorContext [])) <$> try m++-- | Implement 'MonadResource' for 'ErrorContextT'.+instance (MonadCatch m, MonadResource m) => MonadResource (ErrorContextT m) where+  liftResourceT = lift . liftResourceT++-- | Implement 'MonadReader' for 'ErrorContextT'.+instance MonadReader r m => MonadReader r (ErrorContextT m) where+  ask = ErrorContextT (lift ask)+  local f (ErrorContextT (ReaderT m)) =+    ErrorContextT (ReaderT (\ errCtx -> local f (m errCtx)))++-- | Encapsulates the error context — essentially a stack of 'Text'+-- values.+data ErrorContext = ErrorContext [Text] deriving (Show, Eq)++-- | Push a context layer (a 'Text') onto the provided 'ErrorContext'.+errorContextPush+  :: Text+  -> ErrorContext+  -> ErrorContext+errorContextPush layer (ErrorContext layers) =+  ErrorContext (layer : layers)++errorContextAsString :: ErrorContext -> String+errorContextAsString (ErrorContext layers) =+  concat $ map (\ layer -> "  caused by: " <> Text.unpack layer <> "\n") layers++-- | Dump an error with context to stdout.+errorWithContextDump+  :: (Show e, MonadIO m)+  => ErrorWithContext e+  -> m ()+errorWithContextDump (ErrorWithContext ctx err) = do+  liftIO . putStrLn $ "Error: " <> show err+  liftIO . putStrLn . errorContextAsString $ ctx++-- | Enrich an error value with an error context.+errorContextualize+  :: MonadErrorContext m+  => e+  -> m (ErrorWithContext e)+errorContextualize e = do+  ctx <- errorContextCollect+  pure $ ErrorWithContext ctx e++-- | Forgets the context from an enriched error.+errorContextForget+  :: ErrorWithContext e+  -> e+errorContextForget (ErrorWithContext _ctx e) = e++catchJustWithContext+  :: (MonadCatch m, Exception e)+  => (e -> Maybe b)+  -> m a+  -> (b -> m a)+  -> m a+catchJustWithContext f a b =+  a `catch` \ exn -> maybe (throwM exn) b $ f exn++-- | Context aware version of 'catchAny'.+catchAnyWithContext+  :: MonadCatch m+  => m a+  -> (ErrorWithContext SomeException -> m a)+  -> m a+catchAnyWithContext m handler = catchJust pre m handler+  where pre :: SomeException -> Maybe (ErrorWithContext SomeException)+        pre someExn =+          case fromException someExn :: Maybe (ErrorWithContext SomeException) of+            Just exn ->+              Just exn+            Nothing ->+              Just (ErrorWithContext (ErrorContext []) someExn)++ensureExceptionContext :: (MonadCatch m, MonadErrorContext m) => m a -> m a+ensureExceptionContext m =+  catchAny m $ \ someExn ->+  case fromException someExn :: Maybe (ErrorWithContext SomeException) of+    Just exnWithCtx ->+      throwM exnWithCtx+    Nothing -> do+      ctx <- errorContextCollect+      throwM $ ErrorWithContext ctx someExn
+ test/Control/Error/Context/Test.hs view
@@ -0,0 +1,131 @@+{-# LANGUAGE FlexibleInstances   #-}+{-# LANGUAGE OverloadedStrings   #-}+{-# LANGUAGE QuasiQuotes         #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TemplateHaskell     #-}++module Control.Error.Context.Test (tests) where++import           Control.Error.Context+import           Control.Exception      (Exception (..), throw, throwIO)+import           Control.Monad+import           Control.Monad.Catch    (catch, throwM, try)+import           Control.Monad.IO.Class+import           Test.Tasty+import           Test.Tasty.HUnit++tests :: TestTree+tests =+  testGroup+    "Tests"+    [ testCase "Contextualize IO Exception"+        testContextualizeIOException+    , testCase "throwM"+        testThrow+    , testCase "catchAnyWithContext"+        testCatchAnyWithContext+    -- , testCase "catchJustWithContext"+    --     testCatchJustWithContext+    , testCase "Catch context-enriched exception without context"+        testCatchWithoutContext+    , testCase "Contextualize error value"+        testContextualizeErrorValue+    , testCase "Forgetting error context"+        testForgetErrorContext+    , testCase "Dumping error context"+        testDumpErrorContext+    , testCase "Throw and catch"+        testThrowAndCatch+    , testCase "Catch non-contextualized exception with context"+        testNonContextualizedCatchWithContext+    , testCase "ensureExceptionContext"+        testEnsureExceptionContext+    ]++data TestException = TestException deriving (Show, Eq)++instance Exception TestException++testContextualizeIOException :: Assertion+testContextualizeIOException = do+  Left (ErrorWithContext (ErrorContext ctx) TestException) <- try . runErrorContextT $+    withErrorContext "A" $+    withErrorContext "B" $+    liftIO failingIOException+  ["B", "A"] @=? ctx++  where failingIOException :: IO ()+        failingIOException =+          throwIO TestException++testCatchWithoutContext :: Assertion+testCatchWithoutContext = do+  TestException <- runErrorContextT $+    withErrorContext "A" $+    withErrorContext "B" $+    catchWithoutContext (throwM TestException) $ \ (exn :: TestException) -> do+      pure exn+  pure ()++testContextualizeErrorValue :: Assertion+testContextualizeErrorValue = do+  ErrorWithContext (ErrorContext ctx) TestException <- runErrorContextT $+    withErrorContext "A" $+    withErrorContext "B" $+    errorContextualize TestException+  ["B", "A"] @=? ctx++testForgetErrorContext :: Assertion+testForgetErrorContext = do+  errWithCtx @ (ErrorWithContext _ctx TestException) <- runErrorContextT $+    withErrorContext "A" $+    withErrorContext "B" $+    errorContextualize TestException+  TestException @=? errorContextForget errWithCtx++testDumpErrorContext :: Assertion+testDumpErrorContext = do+  errWithCtx @ (ErrorWithContext _ctx _exn) <- runErrorContextT $+    withErrorContext "A" $+    withErrorContext "B" $+    errorContextualize TestException+  errorWithContextDump errWithCtx++testThrowAndCatch :: Assertion+testThrowAndCatch = do+  void . runErrorContextT $+    catch (throwM TestException) $ \ TestException -> pure ()++testThrow :: Assertion+testThrow = do+  catch (runErrorContextT (throwM TestException)) $ \ someExn -> do+    let Just (ErrorWithContext _ctx someInnerExn) = fromException someExn+    liftIO $ Just TestException @=? fromException someInnerExn++testCatchAnyWithContext :: Assertion+testCatchAnyWithContext = do+  catchAnyWithContext (runErrorContextT (throwM TestException)) $+    \ (ErrorWithContext _ctx someExn) -> do+      Just TestException @=? fromException someExn++testNonContextualizedCatchWithContext :: Assertion+testNonContextualizedCatchWithContext = do+  ErrorWithContext (ErrorContext ctx) TestException <- runErrorContextT $+    withErrorContext "A" $+    withErrorContext "B" $ do+    catchWithContext throwPureException $ \ (exn :: ErrorWithContext TestException) -> do+      pure exn+  [] @=? ctx++  where throwPureException = throw TestException++testEnsureExceptionContext :: Assertion+testEnsureExceptionContext = do+  Left someExn <- try . runErrorContextT $+    withErrorContext "A" $+    withErrorContext "B" $ do+    ensureExceptionContext $ do+      throw TestException+  let Just (ErrorWithContext ctx someExnWithoutCtx) = fromException someExn+  Just TestException @=? fromException someExnWithoutCtx+  ErrorContext ["B", "A"] @=? ctx
+ test/Spec.hs view
@@ -0,0 +1,10 @@+import qualified Control.Error.Context.Test+import           Test.Tasty+++main :: IO ()+main = do+  defaultMain $+    testGroup "Test Suite"+    [ Control.Error.Context.Test.tests+    ]