morpheus-graphql-0.3.0: src/Data/Morpheus/Execution/Server/Introspect.hs
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DefaultSignatures #-}
{-# 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
( TypeUpdater
, Introspect(..)
, ObjectFields(..)
, IntroCon
, resolveTypes
, updateLib
, buildType
) 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 (Context (..), ENUM, GQL_KIND, INPUT_OBJECT,
INPUT_UNION, OBJECT, SCALAR, UNION)
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 (..),
DataLeaf (..), DataTyCon (..), DataTypeLib,
TypeAlias (..), defineType, isTypeDefined,
toListField, toNullableField)
import Data.Morpheus.Types.Internal.Validation (SchemaValidation)
type IntroCon a = (GQLType a, ObjectFields (CUSTOM a) a)
-- | Generates internal GraphQL Schema for query validation and introspection rendering
class Introspect a where
field :: proxy a -> Text -> DataField
introspect :: proxy a -> TypeUpdater
-----------------------------------------------
default field :: GQLType a =>
proxy a -> Text -> DataField
field _ = buildField (Proxy @a) []
instance {-# OVERLAPPABLE #-} (GQLType a, IntrospectKind (KIND a) a) => Introspect a where
introspect _ = introspectKind (Context :: Context (KIND a) a)
-- Maybe
instance Introspect a => Introspect (Maybe a) where
field _ = toNullableField . field (Proxy @a)
introspect _ = introspect (Proxy @a)
-- List
instance Introspect a => Introspect [a] where
field _ = toListField . field (Proxy @a)
introspect _ = introspect (Proxy @a)
-- Tuple
instance Introspect (Pair k v) => Introspect (k, v) where
field _ = field (Proxy @(Pair k v))
introspect _ = introspect (Proxy @(Pair k v))
-- Set
instance Introspect [a] => Introspect (Set a) where
field _ = field (Proxy @[a])
introspect _ = introspect (Proxy @[a])
-- Map
instance Introspect (MapKind k v Maybe) => Introspect (Map k v) where
field _ = field (Proxy @(MapKind k v Maybe))
introspect _ = introspect (Proxy @(MapKind k v Maybe))
-- Resolver : a -> Resolver b
instance (ObjectFields 'False a, Introspect b) => Introspect (a -> m b) where
field _ name = (field (Proxy @b) name) {fieldArgs = fst $ objectFields (Proxy :: Proxy 'False) (Proxy @a)}
introspect _ typeLib = resolveTypes typeLib (introspect (Proxy @b) : argTypes)
where
argTypes :: [TypeUpdater]
argTypes = snd $ objectFields (Proxy :: Proxy 'False) (Proxy @a)
-- | Introspect With specific Kind: 'kind': object, scalar, enum ...
class IntrospectKind (kind :: GQL_KIND) a where
introspectKind :: Context kind a -> TypeUpdater -- Generates internal GraphQL Schema
-- SCALAR
instance (GQLType a, GQLScalar a) => IntrospectKind SCALAR a where
introspectKind _ = updateLib scalarType [] (Proxy @a)
where
scalarType = Leaf . CustomScalar . buildType (scalarValidator (Proxy @a))
-- ENUM
instance (GQL_TYPE a, EnumRep (Rep a)) => IntrospectKind ENUM a where
introspectKind _ = updateLib enumType [] (Proxy @a)
where
enumType = Leaf . LeafEnum . buildType (enumTags (Proxy @(Rep a)))
-- INPUT_OBJECT
instance (GQL_TYPE a, ObjectFields (CUSTOM a) a) => IntrospectKind INPUT_OBJECT a where
introspectKind _ = updateLib (InputObject . buildType fields) types (Proxy @a)
where
(fields, types) = objectFields (Proxy @(CUSTOM a)) (Proxy @a)
-- OBJECTS
instance (GQL_TYPE a, ObjectFields (CUSTOM a) a) => IntrospectKind OBJECT a where
introspectKind _ = updateLib (OutputObject . buildType (__typename : fields)) types (Proxy @a)
where
__typename =
( "__typename"
, DataField
{ fieldName = "__typename"
, fieldArgs = []
, fieldArgsType = Nothing
, fieldType = buildAlias "String"
, fieldHidden = True
})
(fields, types) = objectFields (Proxy @(CUSTOM a)) (Proxy @a)
-- UNION
instance (GQL_TYPE a, GQLRep UNION (Rep a)) => IntrospectKind UNION a where
introspectKind _ = updateLib (Union . buildType fields) stack (Proxy @a)
where
(fields, stack) = unzip $ gqlRep (Context :: Context UNION (Rep a))
-- INPUT_UNION
instance (GQL_TYPE a, GQLRep UNION (Rep a)) => IntrospectKind INPUT_UNION a where
introspectKind _ = updateLib (InputUnion . buildType (fieldTag : fields)) (tagsEnumType : stack) (Proxy @a)
where
(inputUnions, stack) = unzip $ gqlRep (Context :: Context UNION (Rep a))
fields = map toNullableField inputUnions
-- for every input Union 'User' adds enum type of possible TypeNames 'UserTags'
tagsEnumType :: TypeUpdater
tagsEnumType x = pure $ defineType (typeName, Leaf $ LeafEnum tagsEnum) x
where
tagsEnum =
DataTyCon
{ typeName
-- has same fingerprint as object because it depends on it
, typeFingerprint = __typeFingerprint (Proxy @a)
, typeVisibility = __typeVisibility (Proxy @a)
, typeDescription = Nothing
, typeData = map fieldName inputUnions
}
typeName = __typeName (Proxy @a) <> "Tags"
fieldTag =
DataField
{ fieldName = "tag"
, fieldArgs = []
, fieldArgsType = Nothing
, fieldType = buildAlias typeName
, fieldHidden = False
}
-- Types
type TypeUpdater = DataTypeLib -> SchemaValidation DataTypeLib
type GQL_TYPE a = (Generic a, GQLType a)
-- Object Fields
class ObjectFields (custom :: Bool) a where
objectFields :: proxy1 custom -> proxy2 a -> ([(Text, DataField)], [TypeUpdater])
instance GQLRep OBJECT (Rep a) => ObjectFields 'False a where
objectFields _ _ = unzip $ gqlRep (Context :: Context OBJECT (Rep a))
type family GQLRepResult (a :: GQL_KIND) :: *
type instance GQLRepResult OBJECT = (Text, DataField)
type instance GQLRepResult UNION = DataField
-- GENERIC UNION
class GQLRep (kind :: GQL_KIND) f where
gqlRep :: Context kind f -> [(GQLRepResult kind, TypeUpdater)]
instance GQLRep kind f => GQLRep kind (M1 D d f) where
gqlRep _ = gqlRep (Context :: Context kind f)
instance GQLRep kind f => GQLRep kind (M1 C c f) where
gqlRep _ = gqlRep (Context :: Context kind f)
-- | recursion for Object types, both of them : 'UNION' and 'INPUT_UNION'
instance (GQLRep UNION a, GQLRep UNION b) => GQLRep UNION (a :+: b) where
gqlRep _ = gqlRep (Context :: Context UNION a) ++ gqlRep (Context :: Context UNION b)
instance (GQL_TYPE a, Introspect a) => GQLRep UNION (M1 S s (Rec0 a)) where
gqlRep _ = [(buildField (Proxy @a) [] (__typeName (Proxy @a)), introspect (Proxy @a))]
-- | recursion for Object types, both of them : 'INPUT_OBJECT' and 'OBJECT'
instance (GQLRep OBJECT a, GQLRep OBJECT b) => GQLRep OBJECT (a :*: b) where
gqlRep _ = gqlRep (Context :: Context OBJECT a) ++ gqlRep (Context :: Context OBJECT b)
instance (Selector s, Introspect a) => GQLRep OBJECT (M1 S s (Rec0 a)) where
gqlRep _ = [((name, field (Proxy @a) name), introspect (Proxy @a))]
where
name = pack $ selName (undefined :: M1 S s (Rec0 ()) ())
instance GQLRep OBJECT U1 where
gqlRep _ = []
buildAlias :: Text -> TypeAlias
buildAlias aliasTyCon = TypeAlias {aliasTyCon, aliasWrappers = [], aliasArgs = Nothing}
-- Helper Functions
resolveTypes :: DataTypeLib -> [TypeUpdater] -> SchemaValidation DataTypeLib
resolveTypes = foldM (&)
buildField :: GQLType a => Proxy a -> DataArguments -> Text -> DataField
buildField proxy fieldArgs fieldName =
DataField
{fieldName, fieldArgs, fieldArgsType = Nothing, fieldType = buildAlias $ __typeName proxy, fieldHidden = False}
buildType :: GQLType a => t -> Proxy a -> DataTyCon t
buildType typeData proxy =
DataTyCon
{ 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)