morpheus-graphql-code-gen-0.18.0: src/Data/Morpheus/CodeGen/Interpreting/Transform.hs
{-# LANGUAGE CPP #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE NoImplicitPrelude #-}
module Data.Morpheus.CodeGen.Interpreting.Transform
( parseServerTypeDefinitions,
)
where
import Data.ByteString.Lazy.Char8 (ByteString)
import Data.Morpheus.CodeGen.Internal.AST
( CodeGenConfig (..),
DerivingClass (..),
FIELD_TYPE_WRAPPER (..),
GQLTypeDefinition (..),
Kind (..),
ServerConstructorDefinition (..),
ServerFieldDefinition (..),
ServerTypeDefinition (..),
)
import Data.Morpheus.CodeGen.Internal.Name
( camelCaseFieldName,
toHaskellTypeName,
)
import Data.Morpheus.CodeGen.Internal.TH
( ToName (toName),
camelCaseTypeName,
)
import Data.Morpheus.Core
( parseTypeDefinitions,
)
import Data.Morpheus.Error (gqlWarnings, renderGQLErrors)
import Data.Morpheus.Internal.Ext (GQLResult, Result (..))
import Data.Morpheus.Types.Internal.AST
( ANY,
ArgumentDefinition (..),
CONST,
DataEnumValue (..),
Description,
Directives,
FieldContent (..),
FieldDefinition (..),
FieldName,
FieldsDefinition,
GQLError,
IN,
OUT,
OperationType (Subscription),
TRUE,
Token,
TypeContent (..),
TypeDefinition (..),
TypeKind (..),
TypeName,
TypeRef (..),
UnionMember (..),
Value,
isPossibleInterfaceType,
isResolverType,
kindOf,
lookupWith,
unpackName,
)
import Language.Haskell.TH
( Dec (..),
Info (..),
Q,
TyVarBndr,
reify,
)
import Relude hiding (ByteString, get)
type ServerQ m = ReaderT (TypeContext CONST) m
class (Monad m, MonadFail m) => CodeGenMonad m where
isParametrizedType :: TypeName -> m Bool
printWarnings :: [GQLError] -> m ()
isParametrizedHaskellType :: Info -> Bool
isParametrizedHaskellType (TyConI x) = not $ null $ getTypeVariables x
isParametrizedHaskellType _ = False
#if MIN_VERSION_template_haskell(2,17,0)
getTypeVariables :: Dec -> [TyVarBndr ()]
#else
getTypeVariables :: Dec -> [TyVarBndr]
#endif
getTypeVariables (DataD _ _ args _ _ _) = args
getTypeVariables (NewtypeD _ _ args _ _ _) = args
getTypeVariables (TySynD _ args _) = args
getTypeVariables _ = []
instance CodeGenMonad Q where
isParametrizedType name = isParametrizedHaskellType <$> reify (toName name)
printWarnings = gqlWarnings
instance CodeGenMonad GQLResult where
isParametrizedType _ = pure False
printWarnings _ = pure ()
data TypeContext s = TypeContext
{ toArgsTypeName :: FieldName -> TypeName,
schema :: [TypeDefinition ANY s],
currentTypeName :: TypeName,
hasNamespace :: Bool,
currentKind :: Maybe TypeKind
}
parseServerTypeDefinitions :: CodeGenMonad m => CodeGenConfig -> ByteString -> m [ServerTypeDefinition]
parseServerTypeDefinitions ctx txt =
case parseTypeDefinitions txt of
Failure errors -> fail (renderGQLErrors errors)
Success {result = schema, warnings} -> printWarnings warnings >> toTHDefinitions (namespace ctx) schema
toTHDefinitions ::
CodeGenMonad m =>
Bool ->
[TypeDefinition ANY CONST] ->
m [ServerTypeDefinition]
toTHDefinitions namespace schema = concat <$> traverse generateTypes schema
where
generateTypes :: CodeGenMonad m => TypeDefinition ANY CONST -> m [ServerTypeDefinition]
generateTypes typeDef =
runReaderT
(genTypeDefinition typeDef)
TypeContext
{ toArgsTypeName = mkArgsTypeName namespace (typeName typeDef),
schema,
currentTypeName = typeName typeDef,
currentKind = Just (kindOf typeDef),
hasNamespace = namespace
}
inType :: MonadReader (TypeContext s) m => TypeName -> m a -> m a
inType currentTypeName = local (\x -> x {currentTypeName, currentKind = Nothing})
mkInterfaceName :: TypeName -> TypeName
mkInterfaceName = ("Interface" <>)
mkPossibleTypesName :: TypeName -> TypeName
mkPossibleTypesName = ("PossibleTypes" <>)
genTypeDefinition ::
CodeGenMonad m =>
TypeDefinition ANY CONST ->
ServerQ m [ServerTypeDefinition]
genTypeDefinition
typeDef@TypeDefinition
{ typeName = originalTypeName,
typeContent,
typeDescription
} = withType <$> genTypeContent originalTypeName typeContent
where
typeName = case typeContent of
DataInterface {} -> mkInterfaceName originalTypeName
_ -> originalTypeName
tKind = kindOf typeDef
tName = toHaskellTypeName typeName
gql =
Just
GQLTypeDefinition
{ gqlTypeDescription = typeDescription,
gqlTypeDescriptions = getDesc typeDef,
gqlTypeDirectives = getDirs typeDef,
gqlKind = derivingKind tKind,
gqlTypeDefaultValues =
fromList
$ mapMaybe getDefaultValue
$ getInputFields typeDef
}
typeParameters
| isResolverType tKind = ["m"]
| otherwise = []
derives = derivesClasses (isResolverType tKind)
-------------------------
withType (ConsIN tCons) = [ServerTypeDefinition {..}]
withType (ConsOUT others tCons) = ServerTypeDefinition {..} : others
derivingKind :: TypeKind -> Kind
derivingKind KindScalar = Scalar
derivingKind _ = Type
derivesClasses :: Bool -> [DerivingClass]
derivesClasses isResolver = GENERIC : [SHOW | not isResolver]
mkObjectCons :: TypeName -> [ServerFieldDefinition] -> [ServerConstructorDefinition]
mkObjectCons name = pure . ServerConstructorDefinition name
mkArgsTypeName :: Bool -> TypeName -> FieldName -> TypeName
mkArgsTypeName namespace typeName fieldName
| namespace = typeName <> argTName
| otherwise = argTName
where
argTName = camelCaseTypeName [fieldName] "Args"
isParametrizedResolverType :: CodeGenMonad m => TypeName -> [TypeDefinition ANY s] -> m Bool
isParametrizedResolverType "__TypeKind" _ = pure False
isParametrizedResolverType "Boolean" _ = pure False
isParametrizedResolverType "String" _ = pure False
isParametrizedResolverType "Int" _ = pure False
isParametrizedResolverType "Float" _ = pure False
isParametrizedResolverType name lib = case lookupWith typeName name lib of
Just x -> pure (isResolverType x)
Nothing -> isParametrizedType name
isSubscription :: TypeKind -> Bool
isSubscription (KindObject (Just Subscription)) = True
isSubscription _ = False
mkObjectField ::
CodeGenMonad m =>
FieldDefinition OUT CONST ->
ServerQ m ServerFieldDefinition
mkObjectField
FieldDefinition
{ fieldName = fName,
fieldContent,
fieldType = TypeRef {typeConName, typeWrappers}
} = do
isParametrized <- lift . isParametrizedResolverType typeConName =<< asks schema
genName <- asks toArgsTypeName
kind <- asks currentKind
fieldName <- genFieldName fName
pure
ServerFieldDefinition
{ fieldType = toHaskellTypeName typeConName,
wrappers =
mkFieldArguments fName genName (toArgList fieldContent)
<> [SUBSCRIPTION | fmap isSubscription kind == Just True]
<> [MONAD]
<> [GQL_WRAPPER typeWrappers]
<> [PARAMETRIZED | isParametrized],
..
}
mkFieldArguments :: FieldName -> (FieldName -> TypeName) -> [ArgumentDefinition s] -> [FIELD_TYPE_WRAPPER]
mkFieldArguments _ _ [] = []
mkFieldArguments
_
_
[ ArgumentDefinition FieldDefinition {fieldName, fieldType}
] = [TAGGED_ARG fieldName fieldType]
mkFieldArguments fName genName _ = [ARG (genName fName)]
toArgList :: Maybe (FieldContent bool cat s) -> [ArgumentDefinition s]
toArgList (Just (FieldArgs args)) = toList args
toArgList _ = []
data BuildPlan
= ConsIN [ServerConstructorDefinition]
| ConsOUT [ServerTypeDefinition] [ServerConstructorDefinition]
genInterfaceUnion :: Monad m => TypeName -> ServerQ m [ServerTypeDefinition]
genInterfaceUnion interfaceName =
mkInterface . map typeName . mapMaybe (isPossibleInterfaceType interfaceName)
<$> asks schema
where
tKind = KindUnion
mkInterface [] = []
mkInterface [possibleTypeName] = [mkGuardWithPossibleType possibleTypeName]
mkInterface members =
[ mkGuardWithPossibleType tName,
ServerTypeDefinition
{ tName = toHaskellTypeName tName,
tCons = map (mkUnionFieldDefinition tName) members,
tKind,
typeParameters = ["m"],
derives = derivesClasses True,
gql = Nothing
}
]
mkGuardWithPossibleType = ServerInterfaceDefinition interfaceName (mkInterfaceName interfaceName)
tName = mkPossibleTypesName interfaceName
genFieldName :: Monad m => FieldName -> ServerQ m FieldName
genFieldName fieldName = do
TypeContext {hasNamespace, currentTypeName} <- ask
pure $
if hasNamespace
then camelCaseFieldName currentTypeName fieldName
else fieldName
mkConsEnum :: Monad m => TypeName -> DataEnumValue CONST -> ServerQ m ServerConstructorDefinition
mkConsEnum name DataEnumValue {enumName} = do
namespace <- asks hasNamespace
pure
ServerConstructorDefinition
{ constructorName =
if namespace
then camelCaseTypeName [name] enumName
else enumName,
constructorFields = []
}
toNonResolverServerField :: Monad m => FieldDefinition c CONST -> ServerQ m ServerFieldDefinition
toNonResolverServerField
FieldDefinition
{ fieldType = TypeRef {typeConName, typeWrappers},
fieldName = fName
} = do
fieldName <- genFieldName fName
pure $
ServerFieldDefinition
{ fieldType = toHaskellTypeName typeConName,
fieldName,
wrappers = [GQL_WRAPPER typeWrappers]
}
genTypeContent ::
CodeGenMonad m =>
TypeName ->
TypeContent TRUE ANY CONST ->
ServerQ m BuildPlan
genTypeContent _ DataScalar {} = pure (ConsIN [])
genTypeContent typeName (DataEnum tags) = ConsIN <$> traverse (mkConsEnum typeName) tags
genTypeContent typeName (DataInputObject fields) =
ConsIN . mkObjectCons typeName <$> traverse toNonResolverServerField (toList fields)
genTypeContent _ DataInputUnion {} = fail "Input Unions not Supported"
genTypeContent typeName DataInterface {interfaceFields} =
ConsOUT
<$> ((<>) <$> genArgumentTypes interfaceFields <*> genInterfaceUnion typeName)
<*> ( do
let interfaceName = mkInterfaceName typeName
inType
interfaceName
( mkObjectCons interfaceName
<$> traverse mkObjectField (toList interfaceFields)
)
)
genTypeContent typeName DataObject {objectFields} =
ConsOUT <$> genArgumentTypes objectFields
<*> ( mkObjectCons typeName
<$> traverse mkObjectField (toList objectFields)
)
genTypeContent typeName (DataUnion members) =
pure $ ConsOUT [] (unionCon <$> toList members)
where
unionCon UnionMember {memberName} = mkUnionFieldDefinition typeName memberName
mkUnionFieldDefinition :: TypeName -> TypeName -> ServerConstructorDefinition
mkUnionFieldDefinition typeName memberName =
ServerConstructorDefinition
{ constructorName,
constructorFields =
[ ServerFieldDefinition
{ fieldName = coerce ("un" <> constructorName),
fieldType = toHaskellTypeName memberName,
wrappers = [PARAMETRIZED]
}
]
}
where
constructorName = camelCaseTypeName [typeName] memberName
genArgumentTypes :: Monad m => FieldsDefinition OUT CONST -> ServerQ m [ServerTypeDefinition]
genArgumentTypes = fmap concat . traverse genArgumentType . toList
genArgumentType :: Monad m => FieldDefinition OUT CONST -> ServerQ m [ServerTypeDefinition]
genArgumentType
FieldDefinition
{ fieldName,
fieldContent = Just (FieldArgs arguments)
}
| length arguments > 1 = do
tName <- (fieldName &) <$> asks toArgsTypeName
inType tName $ do
let argumentFields = argument <$> toList arguments
fields <- traverse toNonResolverServerField argumentFields
let tKind = KindInputObject
pure
[ ServerTypeDefinition
{ tName = toHaskellTypeName tName,
tKind,
tCons = mkObjectCons tName fields,
derives = derivesClasses False,
typeParameters = [],
gql =
Just
( GQLTypeDefinition
{ gqlKind = Type,
gqlTypeDescription = Nothing,
gqlTypeDescriptions = fromList (mapMaybe mkFieldDescription argumentFields),
gqlTypeDirectives = fromList (mkFieldDirective <$> argumentFields),
gqlTypeDefaultValues = fromList (mapMaybe getDefaultValue argumentFields)
}
)
}
]
genArgumentType _ = pure []
mkFieldDescription :: FieldDefinition cat s -> Maybe (Text, Description)
mkFieldDescription FieldDefinition {..} = (unpackName fieldName,) <$> fieldDescription
mkFieldDirective :: FieldDefinition cat s -> (Text, Directives s)
mkFieldDirective FieldDefinition {..} = (unpackName fieldName, fieldDirectives)
---
getDesc :: TypeDefinition c s -> Map Token Description
getDesc = fromList . get
getDirs :: TypeDefinition c s -> Map Token (Directives s)
getDirs = fromList . get
class Meta a v where
get :: a -> [(Token, v)]
instance (Meta a v) => Meta (Maybe a) v where
get (Just x) = get x
get _ = []
instance
( Meta (FieldsDefinition IN s) v,
Meta (FieldsDefinition OUT s) v,
Meta (DataEnumValue s) v
) =>
Meta (TypeDefinition c s) v
where
get TypeDefinition {typeContent} = get typeContent
instance
( Meta (FieldsDefinition IN s) v,
Meta (FieldsDefinition OUT s) v,
Meta (DataEnumValue s) v
) =>
Meta (TypeContent a c s) v
where
get DataObject {objectFields} = get objectFields
get DataInputObject {inputObjectFields} = get inputObjectFields
get DataInterface {interfaceFields} = get interfaceFields
get DataEnum {enumMembers} = concatMap get enumMembers
get _ = []
instance Meta (DataEnumValue s) Description where
get DataEnumValue {enumName, enumDescription = Just x} = [(unpackName enumName, x)]
get _ = []
instance Meta (DataEnumValue s) (Directives s) where
get DataEnumValue {enumName, enumDirectives}
| null enumDirectives = []
| otherwise = [(unpackName enumName, enumDirectives)]
instance
Meta (FieldDefinition c s) v =>
Meta (FieldsDefinition c s) v
where
get = concatMap get . toList
instance Meta (FieldDefinition c s) Description where
get FieldDefinition {fieldName, fieldDescription = Just x} = [(unpackName fieldName, x)]
get _ = []
instance Meta (FieldDefinition c s) (Directives s) where
get FieldDefinition {fieldName, fieldDirectives}
| null fieldDirectives = []
| otherwise = [(unpackName fieldName, fieldDirectives)]
getInputFields :: TypeDefinition c s -> [FieldDefinition IN s]
getInputFields TypeDefinition {typeContent = DataInputObject {inputObjectFields}} = toList inputObjectFields
getInputFields _ = []
getDefaultValue :: FieldDefinition c s -> Maybe (Text, Value s)
getDefaultValue
FieldDefinition
{ fieldName,
fieldContent = Just DefaultInputValue {defaultInputValue}
} = Just (unpackName fieldName, defaultInputValue)
getDefaultValue _ = Nothing