{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TypeApplications #-}
-- | An 'Annotation' is attached to a 'LocatedException'. They're
-- essentially a dynamically typed value with a convenient 'IsString'
-- instance. I'd recommend using something like @Data.Aeson.Value@ or
-- possibly something more strongly typed.
module Data.Annotation
( module Data.Annotation
, module Data.Proxy
) where
import GHC.Stack
import Data.Dynamic
import Data.Either
import Data.Maybe
import Data.Proxy
import Data.Set (Set)
import qualified Data.Set as Set
import Data.String
import qualified Data.Text as Text
import Data.Typeable
-- | The constraints that the value inside an 'Annotation' must have.
--
-- We want 'Typeable' so we can do 'cast' and potentially get more useful
-- information out of it.
--
-- @since 0.1.0.0
type AnnC a = (Typeable a, Eq a, Show a)
-- | An 'Annotation' is a wrapper around a value that includes a 'Typeable'
-- constraint so we can later unpack it. It is essentially a 'Dynamic, but
-- we also include 'Show' and 'Eq' so it's more useful.
--
-- @since 0.1.0.0
data Annotation where
Annotation
:: AnnC a
=> a
-> Annotation
-- |
--
-- @since 0.1.0.0
instance Eq Annotation where
Annotation (a :: a) == Annotation (b :: b) =
case eqT @a @b of
Just Refl ->
a == b
Nothing ->
False
-- |
--
-- @since 0.1.0.0
instance Show Annotation where
show (Annotation a) = show a
-- |
--
-- @since 0.1.0.0
instance IsString Annotation where
fromString = Annotation . Text.pack
-- | Wrap a value in an 'Annotation'.
--
-- @since 0.1.0.0
toAnnotation :: (AnnC a) => a -> Annotation
toAnnotation = Annotation
-- | Attempt to 'cast' the underlying value out of an 'Annotation'.
--
-- @since 0.1.0.0
castAnnotation
:: forall a. (Typeable a)
=> Annotation
-> Maybe a
castAnnotation (Annotation ann) =
cast ann
-- | Attempt to 'cast' the underlying value out of an 'Annotation'.
-- Returns the original 'Annotation' if the cast isn't right.
--
-- @since 0.1.0.0
tryAnnotation
:: forall a. (Typeable a)
=> Annotation
-> Either a Annotation
tryAnnotation a@(Annotation val) =
case cast val of
Just x ->
Left x
Nothing ->
Right a
-- | Attempt to 'cast' list of 'Annotation' into the given type. Any
-- 'Annotation' that is not in that form is left untouched.
--
-- @since 0.1.0.0
tryAnnotations
:: forall a. (Typeable a)
=> [Annotation]
-> ([a], [Annotation])
tryAnnotations = partitionEithers . map tryAnnotation
-- | Returns the 'Set' of types that are in the given annotations.
--
-- @since 0.1.0.0
annotationTypes
:: [Annotation]
-> Set TypeRep
annotationTypes = Set.fromList . map (\(Annotation a) -> typeOf a)
-- | Map a function over the given 'Annotation'. If the types don't match
-- up, then the whole thing returns 'Nothing'.
--
-- @since 0.1.0.0
mapAnnotation
:: ((AnnC a, AnnC b))
=> (a -> b)
-> Annotation
-> Maybe Annotation
mapAnnotation f (Annotation ann) =
Annotation . f <$> cast ann
-- | Map a function over the 'Annotation', leaving it unchanged if the
-- types don't match.
--
-- @since 0.1.0.0
mapMaybeAnnotation
:: (AnnC a, AnnC b)
=> (a -> b)
-> Annotation
-> Annotation
mapMaybeAnnotation f ann =
fromMaybe ann (mapAnnotation f ann)
-- | A wrapper type for putting a 'CallStack' into an 'Annotation'. We need
-- this because 'CallStack' does not have an 'Eq' instance.
--
-- @since 0.1.0.0
newtype CallStackAnnotation = CallStackAnnotation
{ unCallStackAnnotation :: [(String, SrcLoc)]
}
deriving (Eq, Show)
-- | Grab an 'Annotation' corresponding to the 'CallStack' that is
-- currently in scope.
--
-- @since 0.1.0.0
callStackAnnotation :: HasCallStack => Annotation
callStackAnnotation = callStackToAnnotation callStack
-- | Stuff a 'CallStack' into an 'Annotation' via the 'CallStackAnnotation'
-- newtype wrapper.
--
-- @since 0.1.0.0
callStackToAnnotation :: CallStack -> Annotation
callStackToAnnotation cs = Annotation $ CallStackAnnotation $ getCallStack cs
-- | Attempt to convert an 'Annotation' back into a 'CallStack'.
--
-- @since 0.1.0.0
callStackFromAnnotation :: CallStackAnnotation -> CallStack
callStackFromAnnotation ann =
fromCallSiteList $ unCallStackAnnotation ann
-- | Extract the 'CallStack's from the @['Annotation']@. Any 'Annotation'
-- not corresponding to a 'CallStack' will be in the second element of the
-- tuple.
--
-- @since 0.1.0.0
callStackInAnnotations :: [Annotation] -> ([CallStack], [Annotation])
callStackInAnnotations anns =
let (callStacks, rest) =
tryAnnotations anns
in
(fmap callStackFromAnnotation callStacks, rest)