packages feed

aeson-gadt-th 0.1.1.0 → 0.1.2.0

raw patch · 4 files changed

+216/−74 lines, 4 filesdep +aeson-gadt-thdep +markdown-unlitdep +transformersdep ~basedep ~dependent-sumnew-component:exe:readmePVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependencies added: aeson-gadt-th, markdown-unlit, transformers

Dependency ranges changed: base, dependent-sum

API changes (from Hackage documentation)

- Data.Aeson.GADT.TH: conArity :: Con -> Int
- Data.Aeson.GADT.TH: conMatchesEqTagged :: Con -> [MatchQ]
- Data.Aeson.GADT.TH: conMatchesParseJSON :: ExpQ -> Con -> MatchQ
- Data.Aeson.GADT.TH: conMatchesToJSON :: Con -> MatchQ
- Data.Aeson.GADT.TH: conName :: Con -> Name
- Data.Aeson.GADT.TH: decCons :: Dec -> [Con]
- Data.Aeson.GADT.TH: deriveGADTInstances :: Name -> DecsQ

Files

+ README.lhs view
@@ -0,0 +1,48 @@+# aeson-gadt-th++Provides Template Haskell expressions for deriving `ToJSON` and `FromJSON` instances for GADTs.++## Example Usage:++```haskell+{-# LANGUAGE GADTs #-}+{-# LANGUAGE KindSignatures #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}++import Data.Aeson+import Data.Aeson.GADT.TH++data A :: * -> * where+  A_a :: A a+  A_b :: Int -> A ()++data B c :: * -> * where+  B_a :: c -> A a -> B c a+  B_x :: B c x++deriveJSONGADT ''A+deriveJSONGADT ''B++main :: IO ()+main = return ()+```++## Encoding:+```+encode A_a+> "[\"A_a\",[]]"+```++## Decoding:++When decoding a JSON-encoded GADT, the result will be wrapped using [Data.Some.This](http://hackage.haskell.org/package/dependent-sum-0.4/docs/Data-Some.html).+```+case (decode $ encode A_a) :: Maybe (Some A) of+  Nothing -> error "Couldn't decode+  Just (This A_a) -> putStrLn "it worked"+  Just (This A_b) -> putStrLn "wat"+> it worked+```
+ README.md view
@@ -0,0 +1,48 @@+# aeson-gadt-th++Provides Template Haskell expressions for deriving `ToJSON` and `FromJSON` instances for GADTs.++## Example Usage:++```haskell+{-# LANGUAGE GADTs #-}+{-# LANGUAGE KindSignatures #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}++import Data.Aeson+import Data.Aeson.GADT.TH++data A :: * -> * where+  A_a :: A a+  A_b :: Int -> A ()++data B c :: * -> * where+  B_a :: c -> A a -> B c a+  B_x :: B c x++deriveJSONGADT ''A+deriveJSONGADT ''B++main :: IO ()+main = return ()+```++## Encoding:+```+encode A_a+> "[\"A_a\",[]]"+```++## Decoding:++When decoding a JSON-encoded GADT, the result will be wrapped using [Data.Some.This](http://hackage.haskell.org/package/dependent-sum-0.4/docs/Data-Some.html).+```+case (decode $ encode A_a) :: Maybe (Some A) of+  Nothing -> error "Couldn't decode+  Just (This A_a) -> putStrLn "it worked"+  Just (This A_b) -> putStrLn "wat"+> it worked+```
aeson-gadt-th.cabal view
@@ -1,6 +1,6 @@ cabal-version: >=1.10 name: aeson-gadt-th-version: 0.1.1.0+version: 0.1.2.0 synopsis: Derivation of Aeson instances for GADTs category: JSON description: Template Haskell for generating ToJSON and FromJSON instances for GADTs. See <https://github.com/obsidiansystems/aeson-gadt-th/blob/master/README.md README.md> for examples.@@ -10,15 +10,27 @@ maintainer: maintainer@obsidian.systems copyright: 2019 Obsidian Systems LLC build-type: Simple+extra-source-files: README.md  library   exposed-modules: Data.Aeson.GADT.TH   build-depends: base >= 4.8 && < 4.13                , aeson                , dependent-sum+               , transformers                , template-haskell   hs-source-dirs: src   default-language: Haskell2010++executable readme+  build-depends: base+               , aeson+               , dependent-sum+               , aeson-gadt-th+               , markdown-unlit+  default-language: Haskell2010+  main-is: README.lhs+  ghc-options: -pgmL markdown-unlit -Wall  source-repository head   type:     git
src/Data/Aeson/GADT/TH.hs view
@@ -15,16 +15,18 @@ {-# LANGUAGE TypeOperators #-} {-# LANGUAGE UndecidableInstances #-} -module Data.Aeson.GADT.TH where+module Data.Aeson.GADT.TH (deriveJSONGADT, deriveToJSONGADT, deriveFromJSONGADT) where  import Control.Monad+import Control.Monad.Trans.Class+import Control.Monad.Trans.Writer import Data.Aeson-import Data.Dependent.Sum-import Data.Functor.Classes+import Data.List+import Data.Maybe import Data.Some (Some (..)) import Language.Haskell.TH --- | Derive 'ToJSON' and 'FromJSON' instances for the name GADT+-- | Derive 'ToJSON' and 'FromJSON' instances for the named GADT deriveJSONGADT :: Name -> DecsQ deriveJSONGADT n = do   tj <- deriveToJSONGADT n@@ -56,20 +58,33 @@   GadtC _ ts _ -> length ts   RecGadtC _ ts _ -> length ts -{-# DEPRECATED deriveGADTInstances "Use deriveJSONGADT instead" #-}-deriveGADTInstances :: Name -> DecsQ-deriveGADTInstances = deriveJSONGADT- deriveToJSONGADT :: Name -> DecsQ deriveToJSONGADT n = do   x <- reify n   let cons = case x of        TyConI d -> decCons d        _ -> error "undefined"-  [d|-    instance ToJSON ($(conT n) a) where-      toJSON r = $(caseE [|r|] $ map conMatchesToJSON cons)-    |]+  arity <- tyConArity n+  tyVars <- replicateM arity (newName "topvar")+  let n' = foldr (\v c -> AppT c (VarT v)) (ConT n) tyVars+  (matches, typs) <- runWriterT (mapM (fmap pure . conMatchesToJSON tyVars) cons)+  let nubbedTypes = map head . group . sort $ typs -- This 'head' is safe because 'group' returns a list of non-empty lists+      constraints = map (AppT (ConT ''ToJSON)) nubbedTypes+  impl <- funD (mkName "toJSON")+    [ clause [] (normalB $ lamCaseE matches) []+    ]+  return [ InstanceD Nothing constraints (AppT (ConT ''ToJSON) n') [impl] ]+  +-- | Implementation of 'toJSON'+conMatchesToJSON :: [Name] -> Con -> WriterT [Type] Q Match+conMatchesToJSON topVars c = do+  let name = conName c+      base = 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 topVars c+  lift $ match (conP name (map varP vars)) (normalB body) []  deriveFromJSONGADT :: Name -> DecsQ deriveFromJSONGADT n = do@@ -78,74 +93,93 @@        TyConI d -> decCons d        _ -> error "undefined"   let wild = match wildP (normalB [|fail "deriveFromJSONGADT: Supposedly-complete GADT pattern match fell through in generated code. This shouldn't happen."|]) []-  [d|-    instance FromJSON (Some $(conT n)) where-      parseJSON v = do-        (tag', v') <- parseJSON v-        $(caseE [|tag' :: String|] $ map (conMatchesParseJSON [|v'|]) cons ++ [wild])-    |]+  arity <- tyConArity n+  tyVars <- replicateM (arity - 1) (newName "topvar")+  let n' = foldr (\v c -> AppT c (VarT v)) (ConT n) tyVars+  (matches, typs) <- runWriterT $ mapM (conMatchesParseJSON tyVars [|_v'|]) cons+  let nubbedTypes = map head . group . sort $ typs -- This 'head' is safe because 'group' returns a list of non-empty lists+      constraints = map (AppT (ConT ''FromJSON)) nubbedTypes+  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] ] --- | Generate all required matches (and some redundant ones...) for `eqTagged`--- for some constructor-conMatchesEqTagged :: Con -> [MatchQ]-conMatchesEqTagged c = case c of-    ForallC _ _ c' -> conMatchesEqTagged c'-    GadtC _ tys _ -> forTypes (map snd tys)-    _ -> error "conMatchesEqTagged: Unmatched constructor type"+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 (AppT resType _) = subst   where-    name = conName c-    forTypes ts =-      [ do-          as <- mapM (\_ -> newName "a") ts-          bs <- mapM (\_ -> newName "b") ts-          x <- newName "x"-          y <- newName "y"-          let compareTagFields = foldr (\(a, b) e -> [| $(varE a) == $(varE b) && $(e) |]) [| True |] (zip as bs)-          match-            (tupP [conP name (map varP as), conP name (map varP bs)])-            (normalB (lamE [varP x, varP y] [| $(compareTagFields) && eq1 $(varE x) $(varE y) |] ))-            []-      , match-          (tupP [conP name (map (const wildP) ts), wildP])-          (normalB [| \ _ _ -> False |])-          []-      ]+    topVars' = reverse topVars+    subst = \case+      AppT f x -> AppT (subst f) (subst x)+      SigT t k -> SigT (subst t) k+      VarT v -> VarT (findVar v topVars' resType)+      InfixT t1 x t2 -> InfixT (subst t1) x (subst t2)+      UInfixT t1 x t2 -> UInfixT (subst t1) x (subst t2)+      ParensT t -> ParensT (subst t)+      ConT n -> ConT n+      x -> error $ "substVarsWith: Unhandled substitution case: " <> show x --- | Implementation of 'toJSON'-conMatchesToJSON :: Con -> MatchQ-conMatchesToJSON c = do-  let name = conName c-      base = nameBase name-      toJSONExp e = [| toJSON $(e) |]-  vars <- replicateM (conArity c) (newName "x")-  let body = toJSONExp $ tupE [ [| base :: String |] , tupE $ map (toJSONExp . varE) vars ]-  match (conP name (map varP vars)) (normalB body) []+    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 +substVarsWith _ typ = error $ "substVarsWith: Unhandled result type: " <> show typ --- | Implementation of 'parseJSON'-conMatchesParseJSON :: ExpQ -> Con -> MatchQ-conMatchesParseJSON e c = do+conMatches+  :: [Name] -- Names of variables used in the instance head in argument order+  -> Con+  -> WriterT [Type] Q (Pat, Exp)+conMatches topVars c = do   let name = conName c-      match' = match (litP (StringL (nameBase name)))-  let forTypes types = do+      forTypes types resultType = do         vars <- forM types $ \typ -> do-          x <- newName "x"+          x <- lift $ newName "x"           case typ of-            AppT (ConT tn) (VarT vn) -> do+            AppT (ConT tn) (VarT _) -> do               -- This may be a nested GADT, so check for special FromJSON instance-              idec <- reifyInstances ''FromJSON [AppT (ConT ''Some) (ConT tn)]-              return $ case idec of-                [] -> (VarP x, VarE x)-                _ -> (ConP 'This [VarP x], VarE x) -- If a FromJSON instance is found for Some f, then we use it.-            _ -> return (VarP x, VarE x)-        let pat = return $ TupP (map fst vars)-            conApp = return $ foldl AppE (ConE name) (map snd vars)-            body = doE [ bindS pat [| parseJSON $e |]-                       , noBindS [| return (This $conApp) |]-                       ]-        match' (normalB body) []+              idec <- lift $ reifyInstances ''FromJSON [AppT (ConT ''Some) (ConT tn)]+              case idec of+                [] -> do+                  tell [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 [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 _ _ c' -> conMatchesParseJSON e c'-    GadtC _ tys _ -> forTypes (map snd tys)-    NormalC _ tys -> forTypes (map snd tys)-    _ -> error "conMatchesParseJSON: Unmatched constructor type"+    ForallC _ _ c' -> conMatches topVars c'+    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"++conMatchesParseJSON :: [Name] -> ExpQ -> Con -> WriterT [Type] Q Match+conMatchesParseJSON topVars e c = do+  (pat, conApp) <- conMatches topVars c+  let match' = match (litP (StringL (nameBase (conName c))))+      body = doE [ bindS (return pat) [| parseJSON $e |]+                 , noBindS [| return (This $(return conApp)) |]+                 ]+  lift $ match' (normalB body) []++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++tyConArity :: Name -> Q Int+tyConArity n = reify n >>= return . \case+  TyConI (DataD _ _ ts mk _ _) -> fromMaybe 0 (fmap kindArity mk) + length ts+  _ -> error $ "tyConArity: Supplied name reified to something other than a data declaration: " <> show n