morpheus-graphql-server-0.28.2: src/Data/Morpheus/Server/Deriving/Schema.hs
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE NoImplicitPrelude #-}
module Data.Morpheus.Server.Deriving.Schema
( compileTimeSchemaValidation,
deriveSchema,
SCHEMA,
)
where
import Control.Monad.Except (MonadError (..))
import Data.Map (alter, findWithDefault, insert)
import Data.Morpheus.Core (defaultConfig, validateSchema)
import Data.Morpheus.Generic (CBox, runCBox)
import Data.Morpheus.Internal.Ext (GQLResult)
import Data.Morpheus.Internal.Utils (IsMap (..), toAssoc)
import Data.Morpheus.Server.Deriving.Utils.GScan
( FreeCatType (..),
scanFree,
)
import Data.Morpheus.Server.Deriving.Utils.Kinded (outputType)
import Data.Morpheus.Server.Deriving.Utils.Types (CatType (OutputType), GQLTypeNode (..), GQLTypeNodeExtension (..), NodeTypeVariant (..), coerceObject, fromSchema, nodeToType)
import Data.Morpheus.Server.Deriving.Utils.Use
( UseGQLType (useDeriveNode, useExploreRef, useFingerprint),
)
import Data.Morpheus.Server.Types.GQLType
( GQLType (..),
IgnoredResolver,
ignoreUndefined,
withGQL,
)
import Data.Morpheus.Server.Types.TypeName (TypeFingerprint (..))
import Data.Morpheus.Types.Internal.AST
( ANY,
CONST,
DirectiveDefinition,
OBJECT,
OUT,
Schema,
TypeCategory (..),
TypeContent (..),
TypeDefinition (..),
TypeName,
defineDirective,
defineSchemaWith,
mkEnumContent,
mkType,
msg,
toAny,
unitTypeName,
)
import Language.Haskell.TH (Exp, Q)
import Relude hiding (empty)
type SCHEMA qu mu su =
( GQLType (qu IgnoredResolver),
GQLType (mu IgnoredResolver),
GQLType (su IgnoredResolver)
)
-- | normal morpheus server validates schema at runtime (after the schema derivation).
-- this method allows you to validate it at compile time.
compileTimeSchemaValidation :: (SCHEMA qu mu su) => proxy (root m event qu mu su) -> Q Exp
compileTimeSchemaValidation = fromSchema . (deriveSchema >=> validateSchema True defaultConfig)
explore :: forall f (a :: (Type -> Type) -> Type). (GQLType (a IgnoredResolver)) => f a -> [CBox FreeCatType GQLType]
explore _ = scanFree (useExploreRef withGQL) (OutputType :: CatType OUT (a IgnoredResolver))
toDerivation :: TypeFingerprint -> GQLTypeNode c -> (TypeFingerprint, GQLTypeNode ANY)
toDerivation fp (GQLTypeNode node xs) = (fp, GQLTypeNode (toAny node) xs)
toDerivation fp (GQLDirectiveNode node) = (fp, GQLDirectiveNode node)
resolveNode :: (GQLType a) => FreeCatType a -> GQLResult (TypeFingerprint, GQLTypeNode ANY)
resolveNode (FreeCatType proxy) = toDerivation (useFingerprint withGQL proxy) <$> useDeriveNode withGQL proxy
deriveRoot :: (GQLType a) => f a -> GQLResult (TypeDefinition OBJECT CONST)
deriveRoot prx = useDeriveNode withGQL (outputType prx) >>= nodeToType >>= coerceObject
data SchemaState where
SchemaState ::
{ typeDefinitions :: Map TypeFingerprint (TypeDefinition ANY CONST),
implements :: Map TypeName [TypeName],
directiveDefinitions :: Map TypeFingerprint (DirectiveDefinition CONST)
} ->
SchemaState
instance Semigroup SchemaState where
SchemaState t1 i1 d1 <> SchemaState t2 i2 d2 = SchemaState (t1 <> t2) (i1 <> i2) (d1 <> d2)
instance Monoid SchemaState where
mempty = SchemaState mempty mempty mempty
insertImplements :: Map TypeName [TypeName] -> TypeDefinition c CONST -> TypeDefinition c CONST
insertImplements implementsMap TypeDefinition {typeContent = DataObject {..}, ..} = TypeDefinition {..}
where
typeContent = DataObject {objectImplements = objectImplements <> implements, ..}
implements = findWithDefault [] typeName implementsMap
insertImplements _ t = t
checkTypeCollisions :: [(TypeFingerprint, TypeDefinition k a)] -> GQLResult [TypeDefinition k a]
checkTypeCollisions = fmap toList . foldlM collectTypes mempty
where
collectTypes :: Map (TypeName, TypeFingerprint) (TypeDefinition k a) -> (TypeFingerprint, TypeDefinition k a) -> GQLResult (Map (TypeName, TypeFingerprint) (TypeDefinition k a))
collectTypes accum (fp, typ) = maybe addType (handleCollision typ) (key `lookup` accum)
where
addType = pure $ insert key typ accum
key = (typeName typ, withSameCategory fp)
handleCollision t1@TypeDefinition {typeContent = DataEnum {}} t2 | t1 == t2 = pure accum
handleCollision TypeDefinition {typeContent = DataScalar {}} TypeDefinition {typeContent = DataScalar {}} = pure accum
handleCollision TypeDefinition {typeName = name1} _ = failureRequirePrefix name1
failureRequirePrefix :: TypeName -> GQLResult b
failureRequirePrefix typename =
throwError
$ "It appears that the Haskell type "
<> msg typename
<> " was used as both input and output type, which is not allowed by GraphQL specifications."
<> "\n\n "
<> "If you use \"InputTypeNamespace\" directive, "
<> "you can override the default type names for "
<> msg typename
<> " to solve this problem."
withSameCategory :: TypeFingerprint -> TypeFingerprint
withSameCategory (TypeableFingerprint _ xs) = TypeableFingerprint OUT xs
withSameCategory x = x
execNode :: (TypeFingerprint, GQLTypeNode ANY) -> SchemaState -> SchemaState
execNode (InternalFingerprint {}, _) s = s
execNode (fp, GQLTypeNode t xs) s = foldr execExtension (s {typeDefinitions = insert fp t (typeDefinitions s)}) xs
execNode (fp, GQLDirectiveNode d) s = s {directiveDefinitions = insert fp d (directiveDefinitions s)}
execExtension :: GQLTypeNodeExtension -> SchemaState -> SchemaState
execExtension (ImplementsExtension interface types) s = s {implements = foldr insertInterface (implements s) types}
where
insertInterface = alter (Just . (interface :) . fromMaybe [])
execExtension (UnionVariantsExtension nodes) s = foldr execNodeTypeVariant s nodes
execNodeTypeVariant :: NodeTypeVariant -> SchemaState -> SchemaState
execNodeTypeVariant (NodeTypeVariant consName fields) s = s {typeDefinitions = insert fp t (typeDefinitions s)}
where
fp = CustomFingerprint consName
t = mkType consName fields
execNodeTypeVariant NodeUnitType s = s {typeDefinitions = insert fp t (typeDefinitions s)}
where
fp = CustomFingerprint unitTypeName
t = mkType unitTypeName (mkEnumContent [unitTypeName])
deriveSchema :: forall root f m e qu mu su. (SCHEMA qu mu su) => f (root m e qu mu su) -> GQLResult (Schema CONST)
deriveSchema _ = do
query <- deriveRoot (Proxy @(qu IgnoredResolver))
mutation <- traverse deriveRoot (ignoreUndefined (Proxy @(mu IgnoredResolver)))
subscription <- traverse deriveRoot (ignoreUndefined (Proxy @(su IgnoredResolver)))
typeNodes <- traverse (runCBox resolveNode) (explore (Proxy @qu) <> explore (Proxy @mu) <> explore (Proxy @su))
let SchemaState {..} = foldr execNode mempty typeNodes
types <- map (insertImplements implements) <$> checkTypeCollisions (toAssoc typeDefinitions)
schema <- defineSchemaWith types (Just query, mutation, subscription)
foldlM defineDirective schema directiveDefinitions