{-# LANGUAGE ConstraintKinds #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PolyKinds #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeOperators #-} {-# LANGUAGE UndecidableInstances #-} module Data.Morpheus.Execution.Server.Introspect ( introspectOutputType , TypeUpdater , ObjectRep(..) , resolveTypes ) where import Control.Monad (foldM) import Data.Function ((&)) import Data.Map (Map) import Data.Proxy (Proxy (..)) import Data.Semigroup ((<>)) import Data.Set (Set) import Data.Text (Text, pack) import GHC.Generics -- MORPHEUS import Data.Morpheus.Error.Schema (nameCollisionError) import Data.Morpheus.Execution.Server.Generics.EnumRep (EnumRep (..)) import Data.Morpheus.Kind (ENUM, INPUT_OBJECT, INPUT_UNION, OBJECT, SCALAR, UNION, WRAPPER) import Data.Morpheus.Types.Custom (MapKind, Pair) import Data.Morpheus.Types.GQLScalar (GQLScalar (..)) import Data.Morpheus.Types.GQLType (GQLType (..)) import Data.Morpheus.Types.Internal.Data (DataArguments, DataField (..), DataFullType (..), DataInputField, DataLeaf (..), DataType (..), DataTypeLib, DataTypeWrapper (..), DataValidator, defineType, isTypeDefined) import Data.Morpheus.Types.Internal.Validation (SchemaValidation) import Data.Morpheus.Types.Resolver (Resolver, SubResolver) type SelOf s = M1 S s (Rec0 ()) () type RecSel s a = M1 S s (Rec0 a) type TypeUpdater = DataTypeLib -> SchemaValidation DataTypeLib -- -- GENERIC UNION -- class UnionRep f t where possibleTypes :: Proxy f -> Proxy t -> [(DataField (), TypeUpdater)] instance UnionRep f t => UnionRep (M1 D x f) t where possibleTypes _ = possibleTypes (Proxy @f) instance UnionRep f t => UnionRep (M1 C x f) t where possibleTypes _ = possibleTypes (Proxy @f) instance (UnionRep a t, UnionRep b t) => UnionRep (a :+: b) t where possibleTypes _ x = possibleTypes (Proxy @a) x ++ possibleTypes (Proxy @b) x -- -- GENERIC OBJECT: INPUT and OUTPUT plus ARGUMENTS -- resolveTypes :: DataTypeLib -> [TypeUpdater] -> SchemaValidation DataTypeLib resolveTypes = foldM (&) class ObjectRep rep t where objectFieldTypes :: Proxy rep -> [((Text, DataField t), TypeUpdater)] instance ObjectRep f t => ObjectRep (M1 D x f) t where objectFieldTypes _ = objectFieldTypes (Proxy @f) instance ObjectRep f t => ObjectRep (M1 C x f) t where objectFieldTypes _ = objectFieldTypes (Proxy @f) instance (ObjectRep a t, ObjectRep b t) => ObjectRep (a :*: b) t where objectFieldTypes _ = objectFieldTypes (Proxy @a) ++ objectFieldTypes (Proxy @b) instance ObjectRep U1 t where objectFieldTypes _ = [] -- class Types class type GQL_TYPE a = (Generic a, GQLType a) type EnumConstraint a = (GQL_TYPE a, EnumRep (Rep a)) type InputObjectConstraint a = (GQL_TYPE a, ObjectRep (Rep a) ()) type ObjectConstraint a = (GQL_TYPE a, ObjectRep (Rep a) DataArguments) scalarTypeOf :: GQLType a => DataValidator -> Proxy a -> DataFullType scalarTypeOf validator = Leaf . CustomScalar . buildType validator enumTypeOf :: GQLType a => [Text] -> Proxy a -> DataFullType enumTypeOf tags' = Leaf . LeafEnum . buildType tags' type InputType = () type OutputType = DataArguments type InputOf t = Context t (KIND t) InputType type OutputOf t = Context t (KIND t) OutputType introspectOutputType :: forall a. Introspect a (KIND a) OutputType => Proxy a -> TypeUpdater introspectOutputType _ = introspect (Context :: OutputOf a) -- | context , like Proxy with multiple parameters -- contains types of : -- * 'a': actual gql type -- * 'kind': object, scalar, enum ... -- * 'args': InputType | OutputType data Context a kind args = Context buildField :: GQLType a => Proxy a -> t -> Text -> DataField t buildField proxy fieldArgs fieldName = DataField { fieldName , fieldArgs , fieldTypeWrappers = [NonNullType] , fieldType = __typeName proxy , fieldHidden = False } buildType :: GQLType a => t -> Proxy a -> DataType t buildType typeData proxy = DataType { typeName = __typeName proxy , typeFingerprint = __typeFingerprint proxy , typeDescription = description proxy , typeVisibility = __typeVisibility proxy , typeData } updateLib :: GQLType a => (Proxy a -> DataFullType) -> [TypeUpdater] -> Proxy a -> TypeUpdater updateLib typeBuilder stack proxy lib' = case isTypeDefined (__typeName proxy) lib' of Nothing -> resolveTypes (defineType (__typeName proxy, typeBuilder proxy) lib') stack Just fingerprint' | fingerprint' == __typeFingerprint proxy -> return lib' -- throw error if 2 different types has same name Just _ -> Left $ nameCollisionError (__typeName proxy) -- | Generates internal GraphQL Schema for query validation and introspection rendering -- * 'kind': object, scalar, enum ... -- * 'args': type of field arguments -- * '()' for 'input values' , they are just JSON properties and does not have any argument -- * 'DataArguments' for field Resolvers Types, where 'DataArguments' is type of arguments class Introspect a kind args where __field :: Context a kind args -> Text -> DataField args -- generates data field representation of object field -- according to parameter 'args' it could be -- * input object field: if args is '()' -- * object: if args is 'DataArguments' introspect :: Context a kind args -> TypeUpdater -- Generates internal GraphQL Schema type OutputConstraint a = Introspect a (KIND a) DataArguments -- -- SCALAR -- instance (GQLScalar a, GQLType a) => Introspect a SCALAR InputType where __field _ = buildField (Proxy @a) () introspect _ = updateLib (scalarTypeOf (scalarValidator $ Proxy @a)) [] (Proxy @a) instance (GQLScalar a, GQLType a) => Introspect a SCALAR OutputType where __field _ = buildField (Proxy @a) [] introspect _ = updateLib (scalarTypeOf (scalarValidator $ Proxy @a)) [] (Proxy @a) -- -- ENUM -- instance EnumConstraint a => Introspect a ENUM InputType where __field _ = buildField (Proxy @a) () introspect _ = introspectEnum (Context :: InputOf a) instance EnumConstraint a => Introspect a ENUM OutputType where __field _ = buildField (Proxy @a) [] introspect _ = introspectEnum (Context :: OutputOf a) introspectEnum :: forall a f. (GQLType a, EnumRep (Rep a)) => Context a (KIND a) f -> TypeUpdater introspectEnum _ = updateLib (enumTypeOf $ enumTags (Proxy @(Rep a))) [] (Proxy @a) -- -- OBJECTS , INPUT_OBJECT -- instance InputObjectConstraint a => Introspect a INPUT_OBJECT InputType where __field _ = buildField (Proxy @a) () introspect _ = updateLib (InputObject . buildType fields') stack' (Proxy @a) where (fields', stack') = unzip $ objectFieldTypes (Proxy @(Rep a)) instance ObjectConstraint a => Introspect a OBJECT OutputType where __field _ = buildField (Proxy @a) [] introspect _ = updateLib (OutputObject . buildType (__typename : fields')) stack' (Proxy @a) where __typename = ( "__typename" , DataField { fieldName = "__typename" , fieldArgs = [] , fieldTypeWrappers = [] , fieldType = "String" , fieldHidden = True }) (fields', stack') = unzip $ objectFieldTypes (Proxy @(Rep a)) -- | recursion for Object types, both of them : 'INPUT_OBJECT' and 'OBJECT' -- iterates on field types and introspects them recursively instance (Selector s, Introspect a (KIND a) f) => ObjectRep (RecSel s a) f where objectFieldTypes _ = [ ( (name, __field (Context :: Context a (KIND a) f) name) , introspect (Context :: Context a (KIND a) f)) ] where name = pack $ selName (undefined :: SelOf s) -- -- UNION -- -- | recursion for union types -- iterates on possible types for UNION and introspects them recursively instance (OutputConstraint a, ObjectConstraint a) => UnionRep (RecSel s a) OutputType where possibleTypes _ _ = [(buildField (Proxy @a) () "", introspect (Context :: OutputOf a))] instance (GQL_TYPE a, UnionRep (Rep a) OutputType) => Introspect a UNION OutputType where __field _ = buildField (Proxy @a) [] introspect _ = updateLib (Union . buildType fields) stack (Proxy @a) where (fields, stack) = unzip $ possibleTypes (Proxy @(Rep a)) (Proxy @OutputType) -- -- INPUT_UNION -- instance (GQL_TYPE a, Introspect a INPUT_OBJECT InputType) => UnionRep (RecSel s a) InputType where possibleTypes _ _ = [ ( maybeField $ buildField (Proxy @a) () (__typeName $ Proxy @a) , introspect (Context :: Context a INPUT_OBJECT InputType)) ] instance (GQL_TYPE a, UnionRep (Rep a) InputType) => Introspect a INPUT_UNION InputType where __field _ = buildField (Proxy @a) () introspect _ = updateLib (InputUnion . buildType (fieldTag : fields)) (tagsEnumType : stack) (Proxy @a) where (fields, stack) = unzip $ possibleTypes (Proxy @(Rep a)) (Proxy @InputType) -- for every input Union 'User' adds enum type of possible TypeNames 'UserTags' tagsEnumType :: TypeUpdater tagsEnumType x = pure $ defineType (enumTypeName, Leaf $ LeafEnum tagsEnum) x where tagsEnum = DataType { typeName = enumTypeName -- has same fingerprint as object because it depends on it , typeFingerprint = __typeFingerprint (Proxy @a) , typeVisibility = __typeVisibility (Proxy @a) , typeDescription = "" , typeData = map fieldName fields } enumTypeName = __typeName (Proxy @a) <> "Tags" fieldTag = DataField { fieldName = "tag" , fieldArgs = () , fieldTypeWrappers = [NonNullType] , fieldType = enumTypeName , fieldHidden = False } -- -- WRAPPER : Maybe, LIST , Resolver -- maybeField :: DataField f -> DataField f maybeField field@DataField {fieldTypeWrappers = NonNullType:xs} = field {fieldTypeWrappers = xs} maybeField field = field instance Introspect a (KIND a) f => Introspect (Maybe a) WRAPPER f where __field _ name = maybeField $ __field (Context :: Context a (KIND a) f) name introspect _ = introspect (Context :: Context a (KIND a) f) instance Introspect a (KIND a) f => Introspect [a] WRAPPER f where __field _ name = listField (__field (Context :: Context a (KIND a) f) name) where listField :: DataField f -> DataField f listField x = x {fieldTypeWrappers = [NonNullType, ListType] ++ fieldTypeWrappers x} introspect _ = introspect (Context :: Context a (KIND a) f) -- -- CUSTOM Types: Tuple, Map, Set -- instance Introspect (Pair k v) OBJECT f => Introspect (k, v) WRAPPER f where __field _ = __field (Context :: Context (Pair k v) OBJECT f) introspect _ = introspect (Context :: Context (Pair k v) OBJECT f) instance Introspect [a] WRAPPER f => Introspect (Set a) WRAPPER f where __field _ = __field (Context :: Context [a] WRAPPER f) introspect _ = introspect (Context :: Context [a] WRAPPER f) -- | introspection Does not care about resolving monad, some fake monad just for mocking type MockRes = (Resolver Maybe) instance Introspect (MapKind k v MockRes) OBJECT f => Introspect (Map k v) WRAPPER f where __field _ = __field (Context :: Context (MapKind k v MockRes) OBJECT f) introspect _ = introspect (Context :: Context (MapKind k v MockRes) OBJECT f) -- |introspects Of Resolver 'a' as argument and 'b' as output type instance (ObjectRep (Rep a) (), OutputConstraint b) => Introspect (a -> Resolver m b) WRAPPER OutputType where __field _ name = (__field (Context :: OutputOf b) name) {fieldArgs = map fst $ objectFieldTypes (Proxy @(Rep a))} introspect _ typeLib = resolveTypes typeLib $ map snd args ++ [introspect (Context :: OutputOf b)] where args :: [((Text, DataInputField), TypeUpdater)] args = objectFieldTypes (Proxy @(Rep a)) instance (ObjectRep (Rep a) (), OutputConstraint b) => Introspect (a -> Either String b) WRAPPER OutputType where __field _ name = (__field (Context :: OutputOf b) name) {fieldArgs = map fst $ objectFieldTypes (Proxy @(Rep a))} introspect _ typeLib = resolveTypes typeLib $ map snd args ++ [introspect (Context :: OutputOf b)] where args :: [((Text, DataInputField), TypeUpdater)] args = objectFieldTypes (Proxy @(Rep a)) instance (ObjectRep (Rep a) (), OutputConstraint b) => Introspect (a -> SubResolver m c v b) WRAPPER OutputType where __field _ name = (__field (Context :: OutputOf b) name) {fieldArgs = map fst $ objectFieldTypes (Proxy @(Rep a))} introspect _ typeLib = resolveTypes typeLib $ map snd args ++ [introspect (Context :: OutputOf b)] where args :: [((Text, DataInputField), TypeUpdater)] args = objectFieldTypes (Proxy @(Rep a))