{-# LANGUAGE ConstrainedClassMethods #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
module Data.Morpheus.Execution.Client.Aeson
( deriveFromJSON
, deriveToJSON
, takeValueType
) where
import Data.Aeson
import Data.Aeson.Types
import qualified Data.HashMap.Lazy as H (lookup)
import Data.Semigroup ((<>))
import Data.Text (unpack)
import Language.Haskell.TH
import Data.Morpheus.Execution.Internal.Utils (nameSpaceTypeString)
import Data.Morpheus.Types.Internal.Data (DataField (..), isFieldNullable)
import Data.Morpheus.Types.Internal.DataD (ConsD (..), TypeD (..))
import Data.Morpheus.Types.Internal.TH (destructRecord, instanceFunD, instanceHeadT)
deriveFromJSON :: TypeD -> Q Dec
deriveFromJSON TypeD {tCons = []} = fail "Type Should Have at least one Constructor"
deriveFromJSON TypeD {tName, tNamespace, tCons = [cons]} = defineFromJSON name (aesonObject tNamespace) cons
where
name = nameSpaceTypeString tNamespace tName
deriveFromJSON typeD@TypeD {tName, tCons, tNamespace}
| isEnum tCons = defineFromJSON name aesonEnum tCons
| otherwise = defineFromJSON name (aesonUnionObject tNamespace) typeD
where
name = nameSpaceTypeString tNamespace tName
aesonObject :: [String] -> ConsD -> ExpQ
aesonObject tNamespace con@ConsD {cName} =
appE [|withObject name|] (lamE [varP (mkName "o")] ((aesonObjectBody tNamespace) con))
where
name = nameSpaceTypeString tNamespace cName
aesonObjectBody :: [String] -> ConsD -> ExpQ
aesonObjectBody namespace ConsD {cName, cFields} = handleFields cFields
where
consName = mkName $ nameSpaceTypeString namespace cName
handleFields [] = fail $ "No Empty Object"
handleFields fields = startExp fields
where
defField field@DataField {fieldName}
| isFieldNullable field = [|o .:? fName|]
| otherwise = [|o .: fName|]
where
fName = unpack fieldName
startExp fNames = uInfixE (conE consName) (varE '(<$>)) (applyFields fNames)
where
applyFields [] = fail "No Empty fields"
applyFields [x] = defField x
applyFields (x:xs) = uInfixE (defField x) (varE '(<*>)) (applyFields xs)
aesonUnionObject :: [String] -> TypeD -> ExpQ
aesonUnionObject namespace TypeD {tCons} =
appE (varE $ 'takeValueType) (lamCaseE ((map buildMatch tCons) <> [elseCaseEXP]))
where
buildMatch cons@ConsD {cName} = match pattern body []
where
pattern = tupP [litP (stringL cName), varP $ mkName "o"]
body = normalB $ aesonObjectBody namespace cons
takeValueType :: ((String, Object) -> Parser a) -> Value -> Parser a
takeValueType f (Object hMap) =
case H.lookup "__typename" hMap of
Nothing -> fail "key \"__typename\" not found on object"
Just (String x) -> pure (unpack x, hMap) >>= f
Just val -> fail $ "key \"__typename\" should be string but found: " <> show val
takeValueType _ _ = fail $ "expected Object"
defineFromJSON :: String -> (t -> ExpQ) -> t -> DecQ
defineFromJSON tName parseJ cFields = instanceD (cxt []) iHead [method]
where
iHead = instanceHeadT ''FromJSON tName []
method = instanceFunD 'parseJSON [] (parseJ cFields)
isEnum :: [ConsD] -> Bool
isEnum = not . isEmpty . filter (isEmpty . cFields)
where
isEmpty = (0 ==) . length
aesonEnum :: [ConsD] -> ExpQ
aesonEnum cons = lamCaseE handlers
where
handlers = (map buildMatch cons) <> [elseCaseEXP]
where
buildMatch ConsD {cName} = match pattern body []
where
pattern = litP $ stringL cName
body = normalB $ appE (varE 'pure) (conE $ mkName cName)
elseCaseEXP :: MatchQ
elseCaseEXP = match (varP varName) body []
where
varName = mkName "invalidValue"
body =
normalB $
appE
(varE $ mkName "fail")
(uInfixE (appE (varE 'show) (varE varName)) (varE '(<>)) (stringE $ " is Not Valid Union Constructor"))
deriveToJSON :: TypeD -> Q [Dec]
deriveToJSON TypeD {tCons = []} = fail "Type Should Have at least one Constructor"
deriveToJSON TypeD {tName, tCons = [ConsD {cFields}]} = pure <$> instanceD (cxt []) appHead methods
where
appHead = instanceHeadT ''ToJSON tName []
methods = [funD 'toJSON [clause argsE (normalB body) []]]
where
argsE = [destructRecord tName varNames]
body = appE (varE 'object) (listE $ map decodeVar varNames)
decodeVar name = [|name .= $(varName)|]
where
varName = varE $ mkName name
varNames = map (unpack . fieldName) cFields
deriveToJSON TypeD {tName, tCons}
| isEnum tCons = pure <$> instanceD (cxt []) (instanceHeadT ''ToJSON tName []) []
| otherwise = fail "Input Unions are not yet supported"