serdoc-core-0.1.0.0: src/Data/SerDoc/TH.hs
{-# LANGUAGE CPP #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuantifiedConstraints #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE NoStarIsType #-}
module Data.SerDoc.TH
( deriveSerializable
, deriveHasInfo
, deriveSerDoc
)
where
import Data.SerDoc.Info
import Data.SerDoc.Class
import Data.List
import Data.Proxy
import Language.Haskell.TH
import Language.Haskell.TH.Datatype
import Data.Char
import Control.Monad
-- * Deriving 'HasInfo' and 'Serializable' with Template Haskell
strippedFieldName :: Name -> Name -> String
strippedFieldName tyName fieldName =
let tyStr = nameBase tyName
fieldStr = nameBase fieldName
lcfirst [] = []
lcfirst (x:xs) = toLower x : xs
tyStrLC = lcfirst tyStr
in
if tyStrLC `isPrefixOf` fieldStr then
drop (length tyStrLC) fieldStr
else
fieldStr
nameToTy :: Name -> Type
nameToTy name = case nameBase name of
"" -> error "Empty names are not allowed"
c:_ | isLower c -> VarT name
c:_ | isUpper c -> ConT name
_ -> error $ "Unsupported name: " ++ show name
#if MIN_VERSION_template_haskell(2,17,0)
tyVarBndrName :: TyVarBndr a -> Name
tyVarBndrName (PlainTV name _) = name
tyVarBndrName (KindedTV name _ _) = name
#else
tyVarBndrName :: TyVarBndr -> Name
tyVarBndrName (PlainTV name ) = name
tyVarBndrName (KindedTV name _) = name
#endif
-- | Derive a 'HasInfo' instance for the given codec and type.
-- Currently only supports record types.
-- A matching 'Serializable' instance must serialize record fields in the order
-- they are declared in the source code, without any additional separators,
-- padding, or envelope around or between them. If your serializer does not meet
-- these requirements, you must write a custom 'HasInfo' instance instead.
deriveHasInfo :: Name -> [Name] -> Name -> DecsQ
deriveHasInfo codecName codecArgs typeName = do
TyConI (DataD _ _ codecVars _ _ _) <- reify codecName
let remainingVars = drop (length codecArgs) codecVars
let codecTy = foldl AppT (ConT codecName) (map nameToTy (codecArgs ++ map tyVarBndrName remainingVars))
reify typeName >>= \case
TyConI (DataD [] tyName tyVars Nothing [RecC _ fields] []) -> do
let constraintFields = filter (\(_, _, fieldTy) -> not . null . freeVariables $ fieldTy) fields
constraints <- forM constraintFields $
\(_, _, fieldTy) ->
[t| HasInfo $(pure codecTy) $(pure fieldTy) |]
fmap (:[]) $
instanceD
(pure constraints)
[t| HasInfo
$(pure codecTy)
$(foldl appT (conT tyName) [ varT (tyVarName bndr) | bndr <- tyVars ])
|]
[ funD
(mkName "info")
[ clause
[ varP (mkName "codec")
, varP (mkName "_")
]
(normalB [|
compoundField
$(litE (stringL (nameBase tyName)))
$(listE
[ [| ( $(litE (stringL (strippedFieldName tyName fieldName)))
, info codec (Proxy :: Proxy $(return fieldTy))
)
|]
| (fieldName, _, fieldTy) <- fields
]
)
|]
)
[]
]
]
x ->
error $ "Unsupported data type " ++ show typeName ++ ": " ++ show x
-- | Derive a 'Serializable' instance for the given codec and type.
-- Currently only supports record types.
-- The generated instance will serialize record fields in the order
-- they are declared in the source code, without any additional separators,
-- padding, or envelope around or between them, making it compatible with
-- 'deriveHasInfo'. (See also 'deriveSerDoc'.)
deriveSerializable :: Name -> [Name] -> Name -> DecsQ
deriveSerializable codecName codecArgs typeName = do
TyConI (DataD _ _ codecVars _ _ _) <- reify codecName
let remainingVars = drop (length codecArgs) codecVars
let codecTy = foldl AppT (ConT codecName) (map nameToTy (codecArgs ++ map tyVarBndrName remainingVars))
reify typeName >>= \case
TyConI (DataD [] tyName tyVars Nothing [RecC conName fields] []) -> do
let constraintFields =
if null remainingVars then
filter (\(_, _, fieldTy) -> not . null . freeVariables $ fieldTy) fields
else
fields
constraints1 <-
if null remainingVars then
pure []
else
sequence
[ [t| Monad (MonadEncode $(pure codecTy)) |]
, [t| Monad (MonadDecode $(pure codecTy)) |]
]
constraints2 <- forM constraintFields $
\(_, _, fieldTy) ->
[t| Serializable $(pure codecTy) $(pure fieldTy) |]
let constraints = constraints1 ++ constraints2
fmap (:[]) $
instanceD
(pure constraints)
[t| Serializable
$(pure codecTy)
$(foldl appT (conT tyName) [ varT (tyVarName bndr) | bndr <- tyVars ])
|]
[ funD
(mkName "encode")
[ clause
[ varP (mkName "p")
, varP (mkName "item")
]
(normalB $
[| sequence_
$(listE
[ [| encode p ($(varE fieldName) item) |]
| (fieldName, _, _) <- fields
])
|]
)
[]
]
, funD
(mkName "decode")
[ clause
[ varP (mkName "p")
]
(normalB $
[| $(foldApplicative
(conE conName)
[ [| decode p |] | _ <- fields ]
)
|]
)
[]
]
]
x ->
error . show $ x
-- | Derive both a 'HasInfo' instance and a matching 'Serializable' instance,
-- combining 'deriveHasInfo' and 'deriveSerializable'.
deriveSerDoc :: Name -> [Name] -> Name -> DecsQ
deriveSerDoc codecName codecArgs typeName =
(++) <$> deriveHasInfo codecName codecArgs typeName
<*> deriveSerializable codecName codecArgs typeName
-- <$> :: (a -> b) -> f a -> f b
-- <*> :: f (a -> b) -> f a -> f b
foldApplicative :: ExpQ -> [ExpQ] -> ExpQ
foldApplicative initial [] = [| pure $initial |]
foldApplicative initial [x] = [| $initial <$> $x |]
foldApplicative initial (x:xs) =
foldl (\a b -> [| $a <*> $b |]) [| $initial <$> $x |] xs
#if MIN_VERSION_template_haskell(2,17,0)
tyVarName :: TyVarBndr a -> Name
tyVarName (PlainTV n _) = n
tyVarName (KindedTV n _ _) = n
#else
tyVarName :: TyVarBndr -> Name
tyVarName (PlainTV n) = n
tyVarName (KindedTV n _) = n
#endif