{-# 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 :: a) == :: Annotation -> Annotation -> Bool
== Annotation (a
b :: b) =
        case (Typeable a, Typeable a) => Maybe (a :~: a)
forall k (a :: k) (b :: k).
(Typeable a, Typeable b) =>
Maybe (a :~: b)
eqT @a @b of
            Just a :~: a
Refl ->
                a
a a -> a -> Bool
forall a. Eq a => a -> a -> Bool
== a
a
b
            Maybe (a :~: a)
Nothing ->
                Bool
False

-- |
--
-- @since 0.1.0.0
instance Show Annotation where
    show :: Annotation -> String
show (Annotation a
a) = a -> String
forall a. Show a => a -> String
show a
a

-- |
--
-- @since 0.1.0.0
instance IsString Annotation where
    fromString :: String -> Annotation
fromString = Text -> Annotation
forall a. AnnC a => a -> Annotation
Annotation (Text -> Annotation) -> (String -> Text) -> String -> Annotation
forall b c a. (b -> c) -> (a -> b) -> a -> c
. String -> Text
Text.pack

-- | Wrap a value in an 'Annotation'.
--
-- @since 0.1.0.0
toAnnotation :: (AnnC a) => a -> Annotation
toAnnotation :: a -> Annotation
toAnnotation = a -> Annotation
forall a. AnnC a => a -> Annotation
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 -> Maybe a
castAnnotation (Annotation a
ann) =
    a -> Maybe a
forall a b. (Typeable a, Typeable b) => a -> Maybe b
cast a
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 :: Annotation -> Either a Annotation
tryAnnotation a :: Annotation
a@(Annotation a
val) =
    case a -> Maybe a
forall a b. (Typeable a, Typeable b) => a -> Maybe b
cast a
val of
        Just a
x ->
            a -> Either a Annotation
forall a b. a -> Either a b
Left a
x
        Maybe a
Nothing ->
            Annotation -> Either a Annotation
forall a b. b -> Either a b
Right Annotation
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 :: [Annotation] -> ([a], [Annotation])
tryAnnotations = [Either a Annotation] -> ([a], [Annotation])
forall a b. [Either a b] -> ([a], [b])
partitionEithers ([Either a Annotation] -> ([a], [Annotation]))
-> ([Annotation] -> [Either a Annotation])
-> [Annotation]
-> ([a], [Annotation])
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (Annotation -> Either a Annotation)
-> [Annotation] -> [Either a Annotation]
forall a b. (a -> b) -> [a] -> [b]
map Annotation -> Either a Annotation
forall a. Typeable a => Annotation -> Either a Annotation
tryAnnotation

-- | Returns the 'Set' of types that are in the given annotations.
--
-- @since 0.1.0.0
annotationTypes
    :: [Annotation]
    -> Set TypeRep
annotationTypes :: [Annotation] -> Set TypeRep
annotationTypes = [TypeRep] -> Set TypeRep
forall a. Ord a => [a] -> Set a
Set.fromList ([TypeRep] -> Set TypeRep)
-> ([Annotation] -> [TypeRep]) -> [Annotation] -> Set TypeRep
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (Annotation -> TypeRep) -> [Annotation] -> [TypeRep]
forall a b. (a -> b) -> [a] -> [b]
map (\(Annotation a
a) -> a -> TypeRep
forall a. Typeable a => a -> TypeRep
typeOf a
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 :: (a -> b) -> Annotation -> Maybe Annotation
mapAnnotation a -> b
f (Annotation a
ann) =
    b -> Annotation
forall a. AnnC a => a -> Annotation
Annotation (b -> Annotation) -> (a -> b) -> a -> Annotation
forall b c a. (b -> c) -> (a -> b) -> a -> c
. a -> b
f (a -> Annotation) -> Maybe a -> Maybe Annotation
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> a -> Maybe a
forall a b. (Typeable a, Typeable b) => a -> Maybe b
cast a
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 :: (a -> b) -> Annotation -> Annotation
mapMaybeAnnotation a -> b
f Annotation
ann =
    Annotation -> Maybe Annotation -> Annotation
forall a. a -> Maybe a -> a
fromMaybe Annotation
ann ((a -> b) -> Annotation -> Maybe Annotation
forall a b.
(AnnC a, AnnC b) =>
(a -> b) -> Annotation -> Maybe Annotation
mapAnnotation a -> b
f Annotation
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
    { CallStackAnnotation -> [(String, SrcLoc)]
unCallStackAnnotation :: [(String, SrcLoc)]
    }
    deriving (CallStackAnnotation -> CallStackAnnotation -> Bool
(CallStackAnnotation -> CallStackAnnotation -> Bool)
-> (CallStackAnnotation -> CallStackAnnotation -> Bool)
-> Eq CallStackAnnotation
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
/= :: CallStackAnnotation -> CallStackAnnotation -> Bool
$c/= :: CallStackAnnotation -> CallStackAnnotation -> Bool
== :: CallStackAnnotation -> CallStackAnnotation -> Bool
$c== :: CallStackAnnotation -> CallStackAnnotation -> Bool
Eq, Int -> CallStackAnnotation -> ShowS
[CallStackAnnotation] -> ShowS
CallStackAnnotation -> String
(Int -> CallStackAnnotation -> ShowS)
-> (CallStackAnnotation -> String)
-> ([CallStackAnnotation] -> ShowS)
-> Show CallStackAnnotation
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
showList :: [CallStackAnnotation] -> ShowS
$cshowList :: [CallStackAnnotation] -> ShowS
show :: CallStackAnnotation -> String
$cshow :: CallStackAnnotation -> String
showsPrec :: Int -> CallStackAnnotation -> ShowS
$cshowsPrec :: Int -> CallStackAnnotation -> ShowS
Show)

-- | Grab an 'Annotation' corresponding to the 'CallStack' that is
-- currently in scope.
--
-- @since 0.1.0.0
callStackAnnotation :: HasCallStack => Annotation
callStackAnnotation :: Annotation
callStackAnnotation = CallStack -> Annotation
callStackToAnnotation CallStack
HasCallStack => CallStack
callStack

-- | Stuff a 'CallStack' into an 'Annotation' via the 'CallStackAnnotation'
-- newtype wrapper.
--
-- @since 0.1.0.0
callStackToAnnotation :: CallStack -> Annotation
callStackToAnnotation :: CallStack -> Annotation
callStackToAnnotation CallStack
cs = CallStackAnnotation -> Annotation
forall a. AnnC a => a -> Annotation
Annotation (CallStackAnnotation -> Annotation)
-> CallStackAnnotation -> Annotation
forall a b. (a -> b) -> a -> b
$ [(String, SrcLoc)] -> CallStackAnnotation
CallStackAnnotation ([(String, SrcLoc)] -> CallStackAnnotation)
-> [(String, SrcLoc)] -> CallStackAnnotation
forall a b. (a -> b) -> a -> b
$ CallStack -> [(String, SrcLoc)]
getCallStack CallStack
cs

-- | Attempt to convert an 'Annotation' back into a 'CallStack'.
--
-- @since 0.1.0.0
callStackFromAnnotation :: CallStackAnnotation -> CallStack
callStackFromAnnotation :: CallStackAnnotation -> CallStack
callStackFromAnnotation CallStackAnnotation
ann =
    [(String, SrcLoc)] -> CallStack
fromCallSiteList ([(String, SrcLoc)] -> CallStack)
-> [(String, SrcLoc)] -> CallStack
forall a b. (a -> b) -> a -> b
$ CallStackAnnotation -> [(String, SrcLoc)]
unCallStackAnnotation CallStackAnnotation
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 :: [Annotation] -> ([CallStack], [Annotation])
callStackInAnnotations [Annotation]
anns =
    let ([CallStackAnnotation]
callStacks, [Annotation]
rest) =
            [Annotation] -> ([CallStackAnnotation], [Annotation])
forall a. Typeable a => [Annotation] -> ([a], [Annotation])
tryAnnotations [Annotation]
anns
    in
        ((CallStackAnnotation -> CallStack)
-> [CallStackAnnotation] -> [CallStack]
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
fmap CallStackAnnotation -> CallStack
callStackFromAnnotation [CallStackAnnotation]
callStacks, [Annotation]
rest)