grapesy-1.2.0: src/Network/GRPC/Util/Exception/ToExceptionDoc.hs
{-# LANGUAGE CPP #-}
{-# LANGUAGE ImplicitParams #-}
{-# LANGUAGE OverloadedStrings #-}
module Network.GRPC.Util.Exception.ToExceptionDoc (
ToExceptionDoc(..)
, LinesToExceptionDoc(..)
-- * Top-level rendering functions
, renderKnown
, renderAnyException
#if MIN_VERSION_base(4,20,0)
, renderAnyExceptionAnnotation
#endif
-- * Interaction with the 'FormatCtx'
, insertFormatCtx_
, defaultFormatCtx
) where
import Control.Exception (Exception)
import Control.Exception qualified as Base
import Data.Function
import Data.Proxy
import Data.Typeable
import GHC.Generics
import GHC.Stack
import GHC.TypeLits
#if MIN_VERSION_base(4,20,0)
import Control.Exception.Annotation
import Control.Exception.Context
#endif
import Network.GRPC.Util.Exception.Doc
import Network.GRPC.Util.Exception.Exact
import Network.GRPC.Util.Exception.FormatCtx
import Network.GRPC.Util.Exception.Shims
{-------------------------------------------------------------------------------
Definition
-------------------------------------------------------------------------------}
-- | 'ToExceptionDoc' is a convenience class
class ToExceptionDoc a where
toExceptionDoc :: FormatCtx -> a -> Doc
default toExceptionDoc :: (Generic a, GToDoc (Rep a)) => FormatCtx -> a -> Doc
toExceptionDoc d = gToDoc d . from
-- | Deriving-via support for 'ToExceptionDoc'
newtype LinesToExceptionDoc a = LinesToExceptionDoc a
instance Show a => ToExceptionDoc (LinesToExceptionDoc a) where
toExceptionDoc _ (LinesToExceptionDoc x) = fromLines (show x)
{-------------------------------------------------------------------------------
Top-level rendering functions
-------------------------------------------------------------------------------}
-- | Render known exception or exception annotation
--
-- By default there are two ways to render an exception: 'show' gives us a
-- Haskell value (or is supposed to), perhaps useful to copy/paste into a
-- regression test, and 'displayException' gives us a user-friendly string.
-- Neither of these is particularly useful for developers: 'show' is often
-- unreadable, and 'displayException' may omit information (such as backtraces).
-- We therefore introduce a third way to render an exception, with the
-- additional benefit that we will see all nested exceptions (see also blog post
-- "Exception Annotations: Lay of the Land",
-- <https://well-typed.com/blog/2026/05/lay-annotation-land/>).
renderKnown ::
ToExceptionDoc e
=> FormatCtx -> e -> String
renderKnown ctx = renderDoc . toExceptionDoc ctx
-- | Render exception of arbitrary type (see also 'renderKnown')
--
-- Unlike 'renderKnown', this does /not/ depend on 'ToExceptionDoc'. Instead,
-- the 'FormatCtx' argument allows user to add ways to print the exceptions they
-- are interested about. 'Exception's being 'Typeable' allows us to not rely on
-- static / type-class mechanisms which in turn allows us to not depend on all
-- downstream packages "too early".
renderAnyException ::
Exception e
=> FormatCtx -> e -> String
renderAnyException ctx = renderDoc . formatException ctx
#if MIN_VERSION_base(4,20,0)
-- | Render exception annotation of arbitrary type (see also 'renderKnown')
--
-- See 'renderAnyException' for detailed discussion.
renderAnyExceptionAnnotation ::
ExceptionAnnotation ann
=> FormatCtx -> ann -> String
renderAnyExceptionAnnotation ctx = renderDoc . formatExceptionAnnotation ctx
#endif
{-------------------------------------------------------------------------------
"Container"-like instances
-------------------------------------------------------------------------------}
instance ToExceptionDoc a => ToExceptionDoc (Maybe a) where
toExceptionDoc ctx = foldMap (toExceptionDoc ctx)
instance ToExceptionDoc a => ToExceptionDoc [a] where
toExceptionDoc ctx = foldMap (toExceptionDoc ctx)
{-------------------------------------------------------------------------------
Generics for 'ToDoc'
-------------------------------------------------------------------------------}
class GToDoc p where
gToDoc :: FormatCtx -> p a -> Doc
instance ( GToDoc p
, KnownSymbol typ
, KnownSymbol modl
) => GToDoc (D1 ('MetaData typ modl pkg isNewtype) p) where
gToDoc ctx (M1 x) =
withHeader (symbolVal (Proxy @modl) ++ "." ++ symbolVal (Proxy @typ)) $
gToDoc ctx x
instance ( GToDoc p
, KnownSymbol constr
) => GToDoc (C1 ('MetaCons constr fixity hasFields) p) where
gToDoc ctx (M1 x) =
withHeader (symbolVal (Proxy @constr)) $
gToDoc ctx x
instance ( GToDoc p
, KnownSymbol fieldSel
) => GToDoc (S1 ('MetaSel (Just fieldSel) unpack strict lazy) p) where
gToDoc ctx (M1 x) =
withHeader (symbolVal (Proxy @fieldSel)) $
gToDoc ctx x
instance GToDoc p => GToDoc (S1 ('MetaSel Nothing unpack strict lazy) p) where
gToDoc ctx (M1 x) = gToDoc ctx x
instance (GToDoc f, GToDoc g) => GToDoc (f :*: g) where
gToDoc ctx (x :*: y) = gToDoc ctx x <> gToDoc ctx y
instance (GToDoc f, GToDoc g) => GToDoc (f :+: g) where
gToDoc ctx (L1 x) = gToDoc ctx x
gToDoc ctx (R1 x) = gToDoc ctx x
instance GToDoc U1 where
gToDoc _ U1 = mempty
instance ToExceptionDoc a => GToDoc (K1 r a) where
gToDoc ctx (K1 x) = toExceptionDoc ctx x
{-------------------------------------------------------------------------------
Instances for common exception /annotations/
-------------------------------------------------------------------------------}
instance ToExceptionDoc CallStack where
toExceptionDoc _ cs =
fromLines $ prettyCallStack cs
instance ToExceptionDoc Backtraces where
toExceptionDoc _ bt =
fromLines $ displayBacktraces bt
#if MIN_VERSION_base(4,20,0)
instance ToExceptionDoc ExceptionContext where
toExceptionDoc ctx (ExceptionContext anns) = toExceptionDoc ctx anns
#endif
#ifdef PATCHED_GHC_FOR_EXCEPTION_DEBUGGING
instance ToExceptionDoc Base.WhileHandling where
toExceptionDoc ctx (Base.WhileHandling cs e) =
withHeader "WhileHandling" $ mconcat [
fromLines $ prettyCallStack cs
, toExceptionDoc ctx e
]
#elif MIN_VERSION_base(4,21,0)
instance ToExceptionDoc Base.WhileHandling where
toExceptionDoc ctx (Base.WhileHandling e) =
withHeader "WhileHandling" $ toExceptionDoc ctx e
#endif
#if MIN_VERSION_base(4,20,0)
instance ToExceptionDoc SomeExceptionAnnotation where
toExceptionDoc ctx (SomeExceptionAnnotation ann) =
formatExceptionAnnotation ctx ann
#endif
#ifndef PATCHED_GHC_FOR_EXCEPTION_DEBUGGING
deriving anyclass instance ToExceptionDoc AtomicallyBacktrace
#endif
{-------------------------------------------------------------------------------
Instances for common /exceptions/
NOTE: It is important that for every instance we provide here we also provide
an entry in the 'defaultFormatCtx'.
-------------------------------------------------------------------------------}
instance ToExceptionDoc Base.SomeException where
toExceptionDoc ctx (Base.SomeException e) =
mconcat [
formatException ctx e
#if MIN_VERSION_base(4,20,0)
, toExceptionDoc ctx ?exceptionContext
#endif
]
instance ToExceptionDoc Base.SomeAsyncException where
toExceptionDoc ctx (Base.SomeAsyncException e) =
withHeader "SomeAsyncException" $ formatException ctx e
deriving newtype instance ToExceptionDoc ExactException
{-------------------------------------------------------------------------------
Interaction with the 'FormatCtx'
-------------------------------------------------------------------------------}
insertFormatCtx_ :: forall e.
(Typeable e, ToExceptionDoc e)
=> Proxy e -> FormatCtx -> FormatCtx
insertFormatCtx_ _ = insertFormatCtx (toExceptionDoc @e)
-- | Default 'FormatCtx'
--
-- Notes:
--
-- * If we have an exception or exception annotation of known type, and that
-- type has a 'ToExceptionDoc' instance, we can call 'renderKnown'.
--
-- * If we are dealing with exceptions or annotations of unknown type, perhaps
-- defined in other libraries, there /might/ be a 'ToExceptionDoc' instance;
-- but we're not aware of it! This is the purpose of 'renderAnyException' and
-- 'renderAnyExceptionAnnotation': instead of doing a static lookup, we accept
-- a 'FormatCtx' as argument which upstream code might have populated with
-- suitable renderers.
--
-- The true upstream solution to this would be to make something add a
-- 'ToExceptionDoc' (or similiar) constraint to 'SomeException'l this would
-- obsolete the need for 'FormatCtx'.
--
-- * Since 'renderAnyException' and 'renderAnyExceptionAnnotation' do not have
-- any information to work with other than an 'Exception' or
-- 'ExceptionAnnotation' instance, they cannot even take advantage of any
-- instances that we provide here. Therefore it is very important that any
-- 'ToExceptionDoc' instance we define gets an entry in this list; without it,
-- if we catch say 'SomeException' somewhere, and call 'renderAnyException' on
-- it, even if it happens to be one of our own types, and that type has a
-- 'ToExceptionDoc' instance, we'd still not be able to take advantage of it.
--
-- * The entries for 'SomeException', 'ExactException' and
-- 'SomeExceptionAnnotation' are mostly for convenience: /if/ a user calls
-- 'renderAnyException' on an argument of type 'SomeException', we'll still get
-- the correct result (the user could call 'renderKnown' instead of course).
defaultFormatCtx :: FormatCtx
defaultFormatCtx = emptyFormatCtx
--
-- Exception annotations
--
& insertFormatCtx_ (Proxy @CallStack)
& insertFormatCtx_ (Proxy @Backtraces)
#if MIN_VERSION_base(4,20,0)
& insertFormatCtx_ (Proxy @ExceptionContext)
& insertFormatCtx_ (Proxy @SomeExceptionAnnotation)
#endif
#if MIN_VERSION_base(4,21,0)
& insertFormatCtx_ (Proxy @Base.WhileHandling)
#endif
#ifndef PATCHED_GHC_FOR_EXCEPTION_DEBUGGING
& insertFormatCtx_ (Proxy @AtomicallyBacktrace)
#endif
--
-- Exceptions
--
& insertFormatCtx_ (Proxy @Base.SomeException)
& insertFormatCtx_ (Proxy @Base.SomeAsyncException)
& insertFormatCtx_ (Proxy @ExactException)