{-# LANGUAGE TupleSections #-}
{-# 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 #-}
{-# LANGUAGE RecordWildCards #-}
module Data.Morpheus.Execution.Server.Introspect
( TypeUpdater
, Introspect(..)
, IntrospectRep(..)
, IntroCon
, updateLib
, buildType
, introspectObjectFields
, TypeScope(..)
)
where
import Data.Map ( Map )
import Data.Proxy ( Proxy(..) )
import Data.Set ( Set )
import Data.Text ( Text
, pack
)
import GHC.Generics
import Data.Semigroup ( (<>) )
import Data.List ( partition )
import Data.Morpheus.Error.Utils ( globalErrorMessage )
import Data.Morpheus.Error.Schema ( nameCollisionError )
import Data.Morpheus.Execution.Server.Generics.EnumRep
( EnumRep(..) )
import Data.Morpheus.Kind ( Context(..)
, ENUM
, GQL_KIND
, SCALAR
, OUTPUT
, INPUT
)
import Data.Morpheus.Types.Types ( MapKind
, Pair
)
import Data.Morpheus.Types.GQLScalar ( GQLScalar(..) )
import Data.Morpheus.Types.GQLType ( GQLType(..) )
import Data.Morpheus.Types.Internal.Resolving
( Failure(..)
, resolveUpdates
, Resolver
)
import Data.Morpheus.Types.Internal.AST
( Name
, ArgumentsDefinition(..)
, Meta(..)
, FieldDefinition(..)
, TypeContent(..)
, TypeDefinition(..)
, Key
, createAlias
, defineType
, isTypeDefined
, toListField
, toNullableField
, createEnumValue
, TypeUpdater
, DataFingerprint(..)
, DataUnion
, FieldsDefinition(..)
, InputFieldsDefinition(..)
, TypeRef(..)
, Message
, unsafeFromFields
)
import Data.Morpheus.Types.Internal.Operation
( Empty(..)
, Singleton(..)
)
type IntroCon a = (GQLType a, IntrospectRep (CUSTOM a) a)
class Introspect a where
isObject :: proxy a -> Bool
default isObject :: GQLType a => proxy a -> Bool
isObject _ = isObjectKind (Proxy @a)
field :: proxy a -> Text -> FieldDefinition
introspect :: proxy a -> TypeUpdater
default field :: GQLType a =>
proxy a -> Text -> FieldDefinition
field _ = buildField (Proxy @a) NoArguments
instance {-# OVERLAPPABLE #-} (GQLType a, IntrospectKind (KIND a) a) => Introspect a where
introspect _ = introspectKind (Context :: Context (KIND a) a)
instance Introspect a => Introspect (Maybe a) where
isObject _ = False
field _ = toNullableField . field (Proxy @a)
introspect _ = introspect (Proxy @a)
instance Introspect a => Introspect [a] where
isObject _ = False
field _ = toListField . field (Proxy @a)
introspect _ = introspect (Proxy @a)
instance Introspect (Pair k v) => Introspect (k, v) where
isObject _ = True
field _ = field (Proxy @(Pair k v))
introspect _ = introspect (Proxy @(Pair k v))
instance Introspect [a] => Introspect (Set a) where
isObject _ = False
field _ = field (Proxy @[a])
introspect _ = introspect (Proxy @[a])
instance Introspect (MapKind k v Maybe) => Introspect (Map k v) where
isObject _ = True
field _ = field (Proxy @(MapKind k v Maybe))
introspect _ = introspect (Proxy @(MapKind k v Maybe))
instance (GQLType b, IntrospectRep 'False a, Introspect b) => Introspect (a -> m b) where
isObject _ = False
field _ name = fieldObj { fieldArgs }
where
fieldObj = field (Proxy @b) name
fieldArgs = ArgumentsDefinition Nothing $ unFieldsDefinition $ fst $ introspectObjectFields
(Proxy :: Proxy 'False)
(__typeName (Proxy @b), OutputType, Proxy @a)
introspect _ typeLib = resolveUpdates typeLib
(introspect (Proxy @b) : inputs)
where
name = "Arguments for " <> __typeName (Proxy @b)
inputs :: [TypeUpdater]
inputs =
snd $ introspectObjectFields (Proxy :: Proxy 'False) (name, InputType, Proxy @a)
instance (GQLType b, Introspect b) => Introspect (Resolver fo e m b) where
isObject _ = False
field _ = field (Proxy @b)
introspect _ = introspect (Proxy @b)
class IntrospectKind (kind :: GQL_KIND) a where
introspectKind :: Context kind a -> TypeUpdater
instance (GQLType a, GQLScalar a) => IntrospectKind SCALAR a where
introspectKind _ = updateLib scalarType [] (Proxy @a)
where scalarType = buildType $ DataScalar $ scalarValidator (Proxy @a)
instance (GQL_TYPE a, EnumRep (Rep a)) => IntrospectKind ENUM a where
introspectKind _ = updateLib enumType [] (Proxy @a)
where
enumType =
buildType $ DataEnum $ map createEnumValue $ enumTags (Proxy @(Rep a))
instance (GQL_TYPE a, IntrospectRep (CUSTOM a) a) => IntrospectKind INPUT a where
introspectKind _ = derivingData (Proxy @a) InputType
instance (GQL_TYPE a, IntrospectRep (CUSTOM a) a) => IntrospectKind OUTPUT a where
introspectKind _ = derivingData (Proxy @a) OutputType
derivingData
:: forall a
. (GQLType a, IntrospectRep (CUSTOM a) a)
=> Proxy a
-> TypeScope
-> TypeUpdater
derivingData _ scope = updateLib (buildType datatypeContent) updates (Proxy @a)
where
(datatypeContent, updates) = introspectRep
(Proxy @(CUSTOM a))
(Proxy @a, scope, baseName, baseFingerprint)
baseName = __typeName (Proxy @a)
baseFingerprint = __typeFingerprint (Proxy @a)
type GQL_TYPE a = (Generic a, GQLType a)
fromInput :: InputFieldsDefinition -> FieldsDefinition
fromInput = FieldsDefinition . unInputFieldsDefinition
toInput :: FieldsDefinition -> InputFieldsDefinition
toInput = InputFieldsDefinition . unFieldsDefinition
introspectObjectFields
:: IntrospectRep custom a
=> proxy1 (custom :: Bool)
-> (Name, TypeScope, proxy2 a)
-> (FieldsDefinition, [TypeUpdater])
introspectObjectFields p1 (name, scope, proxy) = withObject
(introspectRep p1 (proxy, scope, "", DataFingerprint "" []))
where
withObject (DataObject {objectFields}, ts) = (objectFields, ts)
withObject (DataInputObject x, ts) = (fromInput x, ts)
withObject _ = (empty, [introspectFailure (name <> " should have only one nonempty constructor")])
introspectFailure :: Message -> TypeUpdater
introspectFailure = const . failure . globalErrorMessage . ("invalid schema: " <>)
class IntrospectRep (custom :: Bool) a where
introspectRep :: proxy1 custom -> ( proxy2 a,TypeScope,Name,DataFingerprint) -> (TypeContent, [TypeUpdater])
instance (TypeRep (Rep a) , Generic a) => IntrospectRep 'False a where
introspectRep _ (_, scope, name, fing) =
derivingDataContent (Proxy @a) (name, fing) scope
buildField :: GQLType a => Proxy a -> ArgumentsDefinition -> Text -> FieldDefinition
buildField proxy fieldArgs fieldName = FieldDefinition
{ fieldType = createAlias $ __typeName proxy
, fieldMeta = Nothing
, ..
}
buildType :: GQLType a => TypeContent -> Proxy a -> TypeDefinition
buildType typeContent proxy = TypeDefinition
{ typeName = __typeName proxy
, typeFingerprint = __typeFingerprint proxy
, typeMeta = Just Meta { metaDescription = description proxy
, metaDirectives = []
}
, typeContent
}
updateLib
:: GQLType a
=> (Proxy a -> TypeDefinition)
-> [TypeUpdater]
-> Proxy a
-> TypeUpdater
updateLib typeBuilder stack proxy lib =
case isTypeDefined (__typeName proxy) lib of
Nothing -> resolveUpdates
(defineType (typeBuilder proxy) lib)
stack
Just fingerprint' | fingerprint' == __typeFingerprint proxy -> return lib
Just _ -> failure $ nameCollisionError (__typeName proxy)
data ConsRep = ConsRep {
consName :: Key,
consIsRecord :: Bool,
consFields :: [FieldRep]
}
data FieldRep = FieldRep {
fieldTypeName :: Name,
fieldData :: FieldDefinition,
fieldTypeUpdater :: TypeUpdater,
fieldIsObject :: Bool
}
data ResRep = ResRep {
enumCons :: [Name],
unionRef :: [Name],
unionRecordRep :: [ConsRep ]
}
isEmpty :: ConsRep -> Bool
isEmpty ConsRep { consFields = [] } = True
isEmpty _ = False
isUnionRef :: Name -> ConsRep -> Bool
isUnionRef baseName ConsRep { consName, consFields = [FieldRep { fieldIsObject = True, fieldTypeName }] }
= consName == baseName <> fieldTypeName
isUnionRef _ _ = False
setFieldNames :: ConsRep -> ConsRep
setFieldNames cons@ConsRep { consFields } = cons
{ consFields = zipWith setFieldName ([0 ..] :: [Int]) consFields
}
where
setFieldName i fieldR@FieldRep { fieldData = fieldD } = fieldR { fieldData = fieldD { fieldName } }
where fieldName = "_" <> pack (show i)
analyseRep :: Name -> [ConsRep] -> ResRep
analyseRep baseName cons = ResRep
{ enumCons = map consName enumRep
, unionRef = map fieldTypeName $ concatMap consFields unionRefRep
, unionRecordRep = unionRecordRep <> map setFieldNames anyonimousUnionRep
}
where
(enumRep , left1 ) = partition isEmpty cons
(unionRefRep , left2 ) = partition (isUnionRef baseName) left1
(unionRecordRep, anyonimousUnionRep) = partition consIsRecord left2
derivingDataContent
:: forall a
. (Generic a, TypeRep (Rep a))
=> Proxy a
-> (Name, DataFingerprint)
-> TypeScope
-> (TypeContent, [TypeUpdater])
derivingDataContent _ (baseName, baseFingerprint) scope =
builder $ typeRep $ Proxy @(Rep a)
where
builder [ConsRep { consFields }] = buildObject scope consFields
builder cons = genericUnion scope cons
where
genericUnion InputType = buildInputUnion (baseName, baseFingerprint)
genericUnion OutputType =
buildUnionType (baseName, baseFingerprint) DataUnion (DataObject [])
buildInputUnion
:: (Name, DataFingerprint) -> [ConsRep ] -> (TypeContent, [TypeUpdater])
buildInputUnion (baseName, baseFingerprint) cons = datatype
(analyseRep baseName cons)
where
datatype ResRep { unionRef = [], unionRecordRep = [], enumCons } =
(DataEnum (map createEnumValue enumCons), types)
datatype ResRep { unionRef, unionRecordRep, enumCons } =
(DataInputUnion typeMembers, types <> unionTypes)
where
typeMembers =
map (, True) (unionRef <> unionMembers) <> map (, False) enumCons
(unionMembers, unionTypes) =
buildUnions (DataInputObject . toInput) baseFingerprint unionRecordRep
types = map fieldTypeUpdater $ concatMap consFields cons
buildUnionType
:: (Name, DataFingerprint)
-> (DataUnion -> TypeContent)
-> (FieldsDefinition -> TypeContent)
-> [ConsRep]
-> (TypeContent, [TypeUpdater])
buildUnionType (baseName, baseFingerprint) wrapUnion wrapObject cons = datatype
(analyseRep baseName cons)
where
datatype ResRep { unionRef = [], unionRecordRep = [], enumCons } =
(DataEnum (map createEnumValue enumCons), types)
datatype ResRep { unionRef, unionRecordRep, enumCons } =
(wrapUnion typeMembers, types <> enumTypes <> unionTypes)
where
typeMembers = unionRef <> enumMembers <> unionMembers
(enumMembers, enumTypes) =
buildUnionEnum wrapObject baseName baseFingerprint enumCons
(unionMembers, unionTypes) =
buildUnions wrapObject baseFingerprint unionRecordRep
types = map fieldTypeUpdater $ concatMap consFields cons
buildObject :: TypeScope -> [FieldRep] -> (TypeContent, [TypeUpdater])
buildObject isOutput consFields = (wrapWith fields, types)
where
(fields, types) = buildDataObject consFields
wrapWith | isOutput == OutputType = DataObject []
| otherwise = DataInputObject . toInput
buildDataObject :: [FieldRep] -> (FieldsDefinition , [TypeUpdater])
buildDataObject consFields = (fields, types)
where
fields = unsafeFromFields $ map fieldData consFields
types = map fieldTypeUpdater consFields
buildUnions
:: (FieldsDefinition -> TypeContent)
-> DataFingerprint
-> [ConsRep]
-> ([Name], [TypeUpdater])
buildUnions wrapObject baseFingerprint cons = (members, map buildURecType cons)
where
buildURecType consRep = pure . defineType
(buildUnionRecord wrapObject baseFingerprint consRep)
members = map consName cons
buildUnionRecord
:: (FieldsDefinition -> TypeContent) -> DataFingerprint -> ConsRep -> TypeDefinition
buildUnionRecord wrapObject typeFingerprint ConsRep { consName, consFields } = TypeDefinition
{ typeName = consName
, typeFingerprint
, typeMeta = Nothing
, typeContent = wrapObject $ unsafeFromFields $ map fieldData consFields
}
buildUnionEnum
:: (FieldsDefinition -> TypeContent)
-> Name
-> DataFingerprint
-> [Name]
-> ([Name], [TypeUpdater])
buildUnionEnum wrapObject baseName baseFingerprint enums = (members, updates)
where
members | null enums = []
| otherwise = [enumTypeWrapperName]
enumTypeName = baseName <> "Enum"
enumTypeWrapperName = enumTypeName <> "Object"
updates :: [TypeUpdater]
updates
| null enums
= []
| otherwise
= [ buildEnumObject wrapObject
enumTypeWrapperName
baseFingerprint
enumTypeName
, buildEnum enumTypeName baseFingerprint enums
]
buildEnum :: Name -> DataFingerprint -> [Name] -> TypeUpdater
buildEnum typeName typeFingerprint tags = pure . defineType
TypeDefinition
{ typeMeta = Nothing
, typeContent = DataEnum $ map createEnumValue tags
, ..
}
buildEnumObject
:: (FieldsDefinition -> TypeContent)
-> Name
-> DataFingerprint
-> Name
-> TypeUpdater
buildEnumObject wrapObject typeName typeFingerprint enumTypeName =
pure . defineType
TypeDefinition
{ typeName
, typeFingerprint
, typeMeta = Nothing
, typeContent = wrapObject $ singleton
FieldDefinition
{ fieldName = "enum"
, fieldArgs = NoArguments
, fieldType = createAlias enumTypeName
, fieldMeta = Nothing
}
}
data TypeScope = InputType | OutputType deriving (Show,Eq,Ord)
class TypeRep f where
typeRep :: Proxy f -> [ConsRep]
instance TypeRep f => TypeRep (M1 D d f) where
typeRep _ = typeRep (Proxy @f)
instance (TypeRep a, TypeRep b) => TypeRep (a :+: b) where
typeRep _ = typeRep (Proxy @a) <> typeRep (Proxy @b)
instance (ConRep f, Constructor c) => TypeRep (M1 C c f) where
typeRep _ =
[ ConsRep { consName = pack $ conName (undefined :: (M1 C c f a))
, consFields = conRep (Proxy @f)
, consIsRecord = conIsRecord (undefined :: (M1 C c f a))
}
]
class ConRep f where
conRep :: Proxy f -> [FieldRep]
instance (ConRep a, ConRep b) => ConRep (a :*: b) where
conRep _ = conRep (Proxy @a) <> conRep (Proxy @b)
instance (Selector s, Introspect a) => ConRep (M1 S s (Rec0 a)) where
conRep _ =
[ FieldRep { fieldTypeName = typeConName $ fieldType fieldData
, fieldData = fieldData
, fieldTypeUpdater = introspect (Proxy @a)
, fieldIsObject = isObject (Proxy @a)
}
]
where
name = pack $ selName (undefined :: M1 S s (Rec0 ()) ())
fieldData = field (Proxy @a) name
instance ConRep U1 where
conRep _ = []