aeson-gadt-th-0.2.1.2: src/Data/Aeson/GADT/TH.hs
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE PatternGuards #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
module Data.Aeson.GADT.TH
( deriveJSONGADT
, deriveToJSONGADT
, deriveFromJSONGADT
, deriveJSONGADTWithOptions
, deriveToJSONGADTWithOptions
, deriveFromJSONGADTWithOptions
, JSONGADTOptions(JSONGADTOptions, gadtConstructorModifier)
, defaultJSONGADTOptions
) where
import Control.Monad
import Control.Monad.Trans.Class
import Control.Monad.Trans.Writer
import Data.Aeson
import Data.List
import Data.Maybe
import qualified Data.Set as Set
import Data.Some (Some (..))
import Language.Haskell.TH hiding (cxt)
newtype JSONGADTOptions = JSONGADTOptions
{ gadtConstructorModifier :: String -> String }
defaultJSONGADTOptions :: JSONGADTOptions
defaultJSONGADTOptions = JSONGADTOptions
{ gadtConstructorModifier = id }
-- | Derive 'ToJSON' and 'FromJSON' instances for the named GADT
deriveJSONGADT :: Name -> DecsQ
deriveJSONGADT = deriveJSONGADTWithOptions defaultJSONGADTOptions
deriveJSONGADTWithOptions :: JSONGADTOptions -> Name -> DecsQ
deriveJSONGADTWithOptions opts n = do
tj <- deriveToJSONGADTWithOptions opts n
fj <- deriveFromJSONGADTWithOptions opts n
return (tj ++ fj)
deriveToJSONGADT :: Name -> DecsQ
deriveToJSONGADT = deriveToJSONGADTWithOptions defaultJSONGADTOptions
deriveToJSONGADTWithOptions :: JSONGADTOptions -> Name -> DecsQ
deriveToJSONGADTWithOptions opts n = do
x <- reify n
let cons = case x of
TyConI d -> decCons d
_ -> error $ "deriveToJSONGADT: Name `" ++ show n ++ "' does not appear to be the name of a type constructor."
topVars <- makeTopVars n
let n' = foldl (\c v -> AppT c (VarT v)) (ConT n) topVars
(matches, constraints') <- runWriterT (mapM (fmap pure . conMatchesToJSON opts (init topVars)) cons)
let constraints = map head . group . sort $ constraints' -- This 'head' is safe because 'group' returns a list of non-empty lists
impl <- funD (mkName "toJSON")
[ clause [] (normalB $ lamCaseE matches) []
]
return [ InstanceD Nothing constraints (AppT (ConT ''ToJSON) n') [impl] ]
makeTopVars :: Name -> Q [Name]
makeTopVars tyConName = do
(tyVarBndrs, kArity) <- tyConArity' tyConName
extraVars <- replicateM kArity (newName "topvar")
return (map tyVarBndrName tyVarBndrs ++ extraVars)
deriveFromJSONGADT :: Name -> DecsQ
deriveFromJSONGADT = deriveFromJSONGADTWithOptions defaultJSONGADTOptions
deriveFromJSONGADTWithOptions :: JSONGADTOptions -> Name -> DecsQ
deriveFromJSONGADTWithOptions opts n = do
x <- reify n
let decl = case x of
TyConI d -> d
_ -> error $ "deriveFromJSONGADT: Name `" ++ show n ++ "' does not appear to be the name of a type constructor."
cons = decCons decl
allConNames =
intercalate ", " $
map (gadtConstructorModifier opts . nameBase . conName) cons
wildName <- newName "s"
let wild = match (varP wildName) (normalB [e|
fail $
"Expected tag to be one of [" ++ allConNames ++ "] but got: "
++ $(varE wildName)
|]) []
topVars <- init <$> makeTopVars n
let n' = foldl (\c v -> AppT c (VarT v)) (ConT n) topVars
(matches, constraints') <- runWriterT $ mapM (conMatchesParseJSON opts topVars [|_v'|]) cons
let constraints = map head . group . sort $ constraints' -- This 'head' is safe because 'group' returns a list of non-empty lists
v <- newName "v"
parser <- funD (mkName "parseJSON")
[ clause [varP v] (normalB [e|
do (tag', _v') <- parseJSON $(varE v)
$(caseE [|tag' :: String|] $ map pure matches ++ [wild])
|]) []
]
return [ InstanceD Nothing constraints (AppT (ConT ''FromJSON) (AppT (ConT ''Some) n')) [parser] ]
-- | Implementation of 'toJSON'
conMatchesToJSON :: JSONGADTOptions -> [Name] -> Con -> WriterT [Type] Q Match
conMatchesToJSON opts topVars c = do
let name = conName c
base = gadtConstructorModifier opts $ nameBase name
toJSONExp e = [| toJSON $(e) |]
vars <- lift $ replicateM (conArity c) (newName "x")
let body = toJSONExp $ tupE [ [| base :: String |] , tupE $ map (toJSONExp . varE) vars ]
_ <- conMatches (AppT (ConT ''ToJSON)) topVars c
lift $ match (conP name (map varP vars)) (normalB body) []
-- | Implementation of 'parseJSON'
conMatchesParseJSON :: JSONGADTOptions -> [Name] -> ExpQ -> Con -> WriterT [Type] Q Match
conMatchesParseJSON opts topVars e c = do
(pat, conApp) <- conMatches (AppT (ConT ''FromJSON)) topVars c
let match' = match (litP (StringL (gadtConstructorModifier opts $ nameBase (conName c))))
body = doE [ bindS (return pat) [| parseJSON $e |]
, noBindS [| return (This $(return conApp)) |]
]
lift $ match' (normalB body) []
conMatches
:: (Type -> Type) -- ^ Function to apply to form instance constraints
-> [Name] -- Names of variables used in the instance head in argument order
-> Con
-> WriterT [Type] Q (Pat, Exp)
conMatches mkConstraint topVars c = do
let name = conName c
forTypes types resultType = do
vars <- forM types $ \typ -> do
x <- lift $ newName "x"
case typ of
AppT (ConT tn) (VarT _) -> do
-- This may be a nested GADT, so check for special FromJSON instance
idec <- lift $ reifyInstances ''FromJSON [AppT (ConT ''Some) (ConT tn)]
case idec of
[] -> do
tell [mkConstraint (substVarsWith topVars resultType typ)]
return (VarP x, VarE x)
_ -> return $ (ConP 'This [VarP x], VarE x) -- If a FromJSON instance is found for Some f, then we use it.
_ -> do
tell [mkConstraint (substVarsWith topVars resultType typ)]
return (VarP x, VarE x)
let pat = TupP (map fst vars)
conApp = foldl AppE (ConE name) (map snd vars)
return (pat, conApp)
case c of
ForallC _ cxt (GadtC _ tys t) -> do
tell (map (substVarsWith topVars t) cxt)
forTypes (map snd tys) t
GadtC _ tys t -> forTypes (map snd tys) t
--NormalC _ tys -> forTypes (map snd tys) -- nb: If this comes up in a GADT-style declaration, please open an issue on the github repo with an example.
_ -> error "conMatches: Unmatched constructor type"
-----------------------------------------------------------------------------------------------------
-- | Assuming that we're building an instance of the form C (T v_1 ... v_(n-1)) for some GADT T, this function
-- takes a list of the variables v_1 ... v_(n-1) used in the instance head, as well as the result type of some data
-- constructor, say T x_1 ... x_(n-1) x_n, as well as the type t of some argument to it, and substitutes any of
-- x_i (1 <= i <= n-1) occurring in t for the corresponding v_i, taking care to avoid name capture by foralls in t.
substVarsWith
:: [Name] -- Names of variables used in the instance head in argument order
-> Type -- Result type of constructor
-> Type -- Type of argument to the constructor
-> Type -- Type of argument with variables substituted for instance head variables.
substVarsWith topVars resultType argType = subst Set.empty argType
where
topVars' = reverse topVars
AppT resultType' _indexType = resultType
subst bs = \case
ForallT bndrs cxt t ->
let bs' = Set.union bs (Set.fromList (map tyVarBndrName bndrs))
in ForallT bndrs (map (subst bs') cxt) (subst bs' t)
AppT f x -> AppT (subst bs f) (subst bs x)
SigT t k -> SigT (subst bs t) k
VarT v -> if Set.member v bs
then VarT v
else VarT (findVar v topVars' resultType')
InfixT t1 x t2 -> InfixT (subst bs t1) x (subst bs t2)
UInfixT t1 x t2 -> UInfixT (subst bs t1) x (subst bs t2)
ParensT t -> ParensT (subst bs t)
-- The following cases could all be covered by an "x -> x" case, but I'd rather know if new cases
-- need to be handled specially in future versions of Template Haskell.
PromotedT n -> PromotedT n
ConT n -> ConT n
TupleT k -> TupleT k
UnboxedTupleT k -> UnboxedTupleT k
#if MIN_VERSION_template_haskell(2,12,0)
UnboxedSumT k -> UnboxedSumT k
#endif
ArrowT -> ArrowT
EqualityT -> EqualityT
ListT -> ListT
PromotedTupleT k -> PromotedTupleT k
PromotedNilT -> PromotedNilT
PromotedConsT -> PromotedConsT
StarT -> StarT
ConstraintT -> ConstraintT
LitT l -> LitT l
WildCardT -> WildCardT
findVar v (tv:_) (AppT _ (VarT v')) | v == v' = tv
findVar v (_:tvs) (AppT t (VarT _)) = findVar v tvs t
findVar v _ _ = error $ "substVarsWith: couldn't look up variable substitution for " ++ show v
++ " with topVars: " ++ show topVars ++ " resultType: " ++ show resultType ++ " argType: " ++ show argType
-- | Determine the 'Name' being bound by a 'TyVarBndr'.
tyVarBndrName :: TyVarBndr -> Name
tyVarBndrName = \case
PlainTV n -> n
KindedTV n _ -> n
-- | Determine the arity of a kind.
kindArity :: Kind -> Int
kindArity = \case
ForallT _ _ t -> kindArity t
AppT (AppT ArrowT _) t -> 1 + kindArity t
SigT t _ -> kindArity t
ParensT t -> kindArity t
_ -> 0
-- | Given the name of a type constructor, determine its full arity
tyConArity :: Name -> Q Int
tyConArity n = do
(ts, ka) <- tyConArity' n
return (length ts + ka)
-- | Given the name of a type constructor, determine a list of type variables bound as parameters by
-- its declaration, and the arity of the kind of type being defined (i.e. how many more arguments would
-- need to be supplied in addition to the bound parameters in order to obtain an ordinary type of kind *).
-- If the supplied 'Name' is anything other than a data or newtype, produces an error.
tyConArity' :: Name -> Q ([TyVarBndr], Int)
tyConArity' n = reify n >>= return . \case
TyConI (DataD _ _ ts mk _ _) -> (ts, fromMaybe 0 (fmap kindArity mk))
TyConI (NewtypeD _ _ ts mk _ _) -> (ts, fromMaybe 0 (fmap kindArity mk))
_ -> error $ "tyConArity': Supplied name reified to something other than a data declaration: " ++ show n
-- | Determine the constructors bound by a data or newtype declaration. Errors out if supplied with another
-- sort of declaration.
decCons :: Dec -> [Con]
decCons = \case
DataD _ _ _ _ cs _ -> cs
NewtypeD _ _ _ _ c _ -> [c]
_ -> error "decCons: Declaration found was not a data or newtype declaration."
-- | Determines the name of a data constructor. It's an error if the 'Con' binds more than one name (which
-- happens in the case where you use GADT syntax, and give multiple data constructor names separated by commas
-- in a type signature in the where clause).
conName :: Con -> Name
conName c = case c of
NormalC n _ -> n
RecC n _ -> n
InfixC _ n _ -> n
ForallC _ _ c' -> conName c'
GadtC [n] _ _ -> n
RecGadtC [n] _ _ -> n
_ -> error "conName: GADT constructors with multiple names not yet supported"
-- | Determine the arity of a data constructor.
conArity :: Con -> Int
conArity c = case c of
NormalC _ ts -> length ts
RecC _ ts -> length ts
InfixC _ _ _ -> 2
ForallC _ _ c' -> conArity c'
GadtC _ ts _ -> length ts
RecGadtC _ ts _ -> length ts