summaryrefslogtreecommitdiff
path: root/src/Language
diff options
context:
space:
mode:
Diffstat (limited to 'src/Language')
-rw-r--r--src/Language/GraphQL.hs12
-rw-r--r--src/Language/GraphQL/AST/Core.hs5
-rw-r--r--src/Language/GraphQL/Execute.hs126
-rw-r--r--src/Language/GraphQL/Execute/Coerce.hs84
-rw-r--r--src/Language/GraphQL/Execute/Transform.hs75
-rw-r--r--src/Language/GraphQL/Schema.hs33
-rw-r--r--src/Language/GraphQL/Type/Definition.hs250
-rw-r--r--src/Language/GraphQL/Type/Schema.hs59
8 files changed, 554 insertions, 90 deletions
diff --git a/src/Language/GraphQL.hs b/src/Language/GraphQL.hs
index fff378d..aef23f0 100644
--- a/src/Language/GraphQL.hs
+++ b/src/Language/GraphQL.hs
@@ -5,11 +5,13 @@ module Language.GraphQL
) where
import qualified Data.Aeson as Aeson
+import Data.HashMap.Strict (HashMap)
import Data.Text (Text)
+import Language.GraphQL.AST.Document
+import Language.GraphQL.AST.Parser
import Language.GraphQL.Error
import Language.GraphQL.Execute
-import Language.GraphQL.AST.Parser
-import qualified Language.GraphQL.Schema as Schema
+import Language.GraphQL.Execute.Coerce
import Language.GraphQL.Type.Schema
import Text.Megaparsec (parse)
@@ -19,14 +21,14 @@ graphql :: Monad m
=> Schema m -- ^ Resolvers.
-> Text -- ^ Text representing a @GraphQL@ request document.
-> m Aeson.Value -- ^ Response.
-graphql = flip graphqlSubs mempty
+graphql = flip graphqlSubs (mempty :: Aeson.Object)
-- | If the text parses correctly as a @GraphQL@ query the substitution is
-- applied to the query and the query is then executed using to the given
-- 'Schema.Resolver's.
-graphqlSubs :: Monad m
+graphqlSubs :: (Monad m, VariableValue a)
=> Schema m -- ^ Resolvers.
- -> Schema.Subs -- ^ Variable substitution function.
+ -> HashMap Name a -- ^ Variable substitution function.
-> Text -- ^ Text representing a @GraphQL@ request document.
-> m Aeson.Value -- ^ Response.
graphqlSubs schema f
diff --git a/src/Language/GraphQL/AST/Core.hs b/src/Language/GraphQL/AST/Core.hs
index 084ae21..d719912 100644
--- a/src/Language/GraphQL/AST/Core.hs
+++ b/src/Language/GraphQL/AST/Core.hs
@@ -3,7 +3,6 @@ module Language.GraphQL.AST.Core
( Alias
, Arguments(..)
, Directive(..)
- , Document
, Field(..)
, Fragment(..)
, Name
@@ -15,15 +14,11 @@ module Language.GraphQL.AST.Core
import Data.Int (Int32)
import Data.HashMap.Strict (HashMap)
-import Data.List.NonEmpty (NonEmpty)
import Data.Sequence (Seq)
import Data.String (IsString(..))
import Data.Text (Text)
import Language.GraphQL.AST (Alias, Name, TypeCondition)
--- | GraphQL document is a non-empty list of operations.
-type Document = NonEmpty Operation
-
-- | GraphQL has 3 operation types: queries, mutations and subscribtions.
--
-- Currently only queries and mutations are supported.
diff --git a/src/Language/GraphQL/Execute.hs b/src/Language/GraphQL/Execute.hs
index e1bacbc..e21d5de 100644
--- a/src/Language/GraphQL/Execute.hs
+++ b/src/Language/GraphQL/Execute.hs
@@ -9,42 +9,42 @@ module Language.GraphQL.Execute
import qualified Data.Aeson as Aeson
import Data.Foldable (find)
+import qualified Data.HashMap.Strict as HashMap
import Data.List.NonEmpty (NonEmpty(..))
import Data.Text (Text)
import qualified Data.Text as Text
import Language.GraphQL.AST.Document
import qualified Language.GraphQL.AST.Core as AST.Core
+import Language.GraphQL.Execute.Coerce
import qualified Language.GraphQL.Execute.Transform as Transform
import Language.GraphQL.Error
import qualified Language.GraphQL.Schema as Schema
-import Language.GraphQL.Type.Definition
+import qualified Language.GraphQL.Type.Definition as Definition
import Language.GraphQL.Type.Schema
-- | Query error types.
data QueryError
= OperationNotFound Text
| OperationNameRequired
+ | CoercionError
queryError :: QueryError -> Text
queryError (OperationNotFound operationName) = Text.unwords
["Operation", operationName, "couldn't be found in the document."]
queryError OperationNameRequired = "Missing operation name."
+queryError CoercionError = "Coercion error."
-- | The substitution is applied to the document, and the resolvers are applied
-- to the resulting fields.
--
-- Returns the result of the query against the schema wrapped in a /data/
-- field, or errors wrapped in an /errors/ field.
-execute :: Monad m
+execute :: (Monad m, VariableValue a)
=> Schema m -- ^ Resolvers.
- -> Schema.Subs -- ^ Variable substitution function.
+ -> HashMap.HashMap Name a -- ^ Variable substitution function.
-> Document -- @GraphQL@ document.
-> m Aeson.Value
-execute schema subs doc =
- maybe transformError (document schema Nothing)
- $ Transform.document subs doc
- where
- transformError = return $ singleError "Schema transformation error."
+execute schema = document schema Nothing
-- | The substitution is applied to the document, and the resolvers are applied
-- to the resulting fields. The operation name can be used if the document
@@ -52,41 +52,105 @@ execute schema subs doc =
--
-- Returns the result of the query against the schema wrapped in a /data/
-- field, or errors wrapped in an /errors/ field.
-executeWithName :: Monad m
+executeWithName :: (Monad m, VariableValue a)
=> Schema m -- ^ Resolvers
-> Text -- ^ Operation name.
- -> Schema.Subs -- ^ Variable substitution function.
+ -> HashMap.HashMap Name a -- ^ Variable substitution function.
-> Document -- ^ @GraphQL@ Document.
-> m Aeson.Value
-executeWithName schema operationName subs doc =
- maybe transformError (document schema $ Just operationName)
- $ Transform.document subs doc
- where
- transformError = return $ singleError "Schema transformation error."
+executeWithName schema operationName = document schema (Just operationName)
getOperation
:: Maybe Text
- -> AST.Core.Document
- -> Either QueryError AST.Core.Operation
-getOperation Nothing (operation' :| []) = pure operation'
+ -> Transform.Document
+ -> Either QueryError Transform.OperationDefinition
+getOperation Nothing (Transform.Document (operation' :| []) _) = pure operation'
getOperation Nothing _ = Left OperationNameRequired
-getOperation (Just operationName) document'
- | Just operation' <- find matchingName document' = pure operation'
+getOperation (Just operationName) (Transform.Document operations _)
+ | Just operation' <- find matchingName operations = pure operation'
| otherwise = Left $ OperationNotFound operationName
where
- matchingName (AST.Core.Query (Just name') _) = operationName == name'
- matchingName (AST.Core.Mutation (Just name') _) = operationName == name'
- matchingName _ = False
+ matchingName (Transform.OperationDefinition _ name _ _ _) =
+ name == Just operationName
+
+lookupInputType
+ :: Type
+ -> HashMap.HashMap Name (Definition.TypeDefinition m)
+ -> Maybe Definition.InputType
+lookupInputType (TypeNamed name) types =
+ case HashMap.lookup name types of
+ Just (Definition.ScalarTypeDefinition scalarType) ->
+ Just $ Definition.ScalarInputType scalarType
+ Just (Definition.EnumTypeDefinition enumType) ->
+ Just $ Definition.EnumInputType enumType
+ Just (Definition.InputObjectTypeDefinition objectType) ->
+ Just $ Definition.ObjectInputType objectType
+ _ -> Nothing
+lookupInputType (TypeList list) types
+ = Definition.ListInputType
+ <$> lookupInputType list types
+lookupInputType (TypeNonNull (NonNullTypeNamed nonNull)) types =
+ case HashMap.lookup nonNull types of
+ Just (Definition.ScalarTypeDefinition scalarType) ->
+ Just $ Definition.NonNullScalarInputType scalarType
+ Just (Definition.EnumTypeDefinition enumType) ->
+ Just $ Definition.NonNullEnumInputType enumType
+ Just (Definition.InputObjectTypeDefinition objectType) ->
+ Just $ Definition.NonNullObjectInputType objectType
+ _ -> Nothing
+lookupInputType (TypeNonNull (NonNullTypeList nonNull)) types
+ = Definition.NonNullListInputType
+ <$> lookupInputType nonNull types
+
+coerceVariableValues :: (Monad m, VariableValue a)
+ => Schema m
+ -> Transform.OperationDefinition
+ -> HashMap.HashMap Name a
+ -> Either QueryError Schema.Subs
+coerceVariableValues schema (Transform.OperationDefinition _ _ variables _ _) values =
+ let referencedTypes = collectReferencedTypes schema
+ in maybe (Left CoercionError) Right
+ $ foldr (coerceValue referencedTypes) (Just HashMap.empty) variables
+ where
+ coerceValue referencedTypes variableDefinition coercedValues = do
+ let VariableDefinition variableName variableTypeName _defaultValue =
+ variableDefinition
+ variableType <- lookupInputType variableTypeName referencedTypes
+ value <- HashMap.lookup variableName values
+ coercedValue <- coerceVariableValue variableType value
+ HashMap.insert variableName coercedValue <$> coercedValues
-document :: Monad m
+executeRequest :: (Monad m, VariableValue a)
=> Schema m
-> Maybe Text
- -> AST.Core.Document
+ -> HashMap.HashMap Name a
+ -> Transform.Document
+ -> Either QueryError (Transform.OperationDefinition, Schema.Subs)
+executeRequest schema operationName subs document' = do
+ operation' <- getOperation operationName document'
+ coercedValues <- coerceVariableValues schema operation' subs
+ pure (operation', coercedValues)
+
+document :: (Monad m, VariableValue a)
+ => Schema m
+ -> Maybe Text
+ -> HashMap.HashMap Name a
+ -> Document
-> m Aeson.Value
-document schema operationName document' =
- case getOperation operationName document' of
- Left error' -> pure $ singleError $ queryError error'
- Right operation' -> operation schema operation'
+document schema operationName subs document' =
+ case Transform.document document' of
+ Just transformed -> executeRequest' transformed
+ Nothing -> pure $ singleError
+ "The document doesn't contain any executable operations."
+ where
+ transformOperation fragmentTable operation' subs' =
+ case Transform.operation fragmentTable subs' operation' of
+ Just operationResult -> operation schema operationResult
+ Nothing -> pure $ singleError "Schema transformation error."
+ executeRequest' transformed@(Transform.Document _ fragmentTable) =
+ case executeRequest schema operationName subs transformed of
+ Right (operation', subs') -> transformOperation fragmentTable operation' subs'
+ Left error' -> pure $ singleError $ queryError error'
operation :: Monad m
=> Schema m
@@ -96,7 +160,8 @@ operation = schemaOperation
where
resolve queryFields = runCollectErrs
. flip Schema.resolve queryFields
- . fields
+ . fmap getResolver
+ . Definition.fields
lookupError = pure
$ singleError "Root operation type couldn't be found in the schema."
schemaOperation Schema {query} (AST.Core.Query _ fields') =
@@ -105,3 +170,4 @@ operation = schemaOperation
resolve fields' mutation
schemaOperation Schema {mutation = Nothing} (AST.Core.Mutation _ _) =
lookupError
+ getResolver (Definition.Field _ _ _ resolver) = resolver
diff --git a/src/Language/GraphQL/Execute/Coerce.hs b/src/Language/GraphQL/Execute/Coerce.hs
new file mode 100644
index 0000000..5b26faa
--- /dev/null
+++ b/src/Language/GraphQL/Execute/Coerce.hs
@@ -0,0 +1,84 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+-- | Types and functions used for input and result coercion.
+module Language.GraphQL.Execute.Coerce
+ ( VariableValue(..)
+ ) where
+
+import qualified Data.Aeson as Aeson
+import qualified Data.HashMap.Strict as HashMap
+import Data.Scientific (toBoundedInteger, toRealFloat)
+import Language.GraphQL.AST.Core
+import Language.GraphQL.Type.Definition
+
+-- | Since variables are passed separately from the query, in an independent
+-- format, they should be first coerced to the internal representation used by
+-- this implementation.
+class VariableValue a where
+ -- | Only a basic, format-specific, coercion must be done here. Type
+ -- correctness or nullability shouldn't be validated here, they will be
+ -- validated later. The type information is provided only as a hint.
+ --
+ -- For example @GraphQL@ prohibits the coercion from a 't:Float' to an
+ -- 't:Int', but @JSON@ doesn't have integers, so whole numbers should be
+ -- coerced to 't:Int` when receiving variables as a JSON object. The same
+ -- holds for 't:Enum'. There are formats that support enumerations, @JSON@
+ -- doesn't, so the type information is given and 'coerceVariableValue' can
+ -- check that an 't:Enum' is expected and treat the given value
+ -- appropriately. Even checking whether this value is a proper member of the
+ -- corresponding 't:Enum' type isn't required here, since this can be
+ -- checked independently.
+ --
+ -- Another example is an @ID@. @GraphQL@ explicitly allows to coerce
+ -- integers and strings to @ID@s, so if an @ID@ is received as an integer,
+ -- it can be left as is and will be coerced later.
+ --
+ -- If a value cannot be coerced without losing information, 'Nothing' should
+ -- be returned, the coercion will fail then and the query won't be executed.
+ coerceVariableValue
+ :: InputType -- ^ Expected type (variable type given in the query).
+ -> a -- ^ Variable value being coerced.
+ -> Maybe Value -- ^ Coerced value on success, 'Nothing' otherwise.
+
+instance VariableValue Aeson.Value where
+ coerceVariableValue _ Aeson.Null = Just Null
+ coerceVariableValue (ScalarInputTypeDefinition scalarType) value
+ | (Aeson.String stringValue) <- value = Just $ String stringValue
+ | (Aeson.Bool booleanValue) <- value = Just $ Boolean booleanValue
+ | (Aeson.Number numberValue) <- value
+ , (ScalarType "Float" _) <- scalarType =
+ Just $ Float $ toRealFloat numberValue
+ | (Aeson.Number numberValue) <- value = -- ID or Int
+ Int <$> toBoundedInteger numberValue
+ coerceVariableValue (EnumInputTypeDefinition _) (Aeson.String stringValue) =
+ Just $ Enum stringValue
+ coerceVariableValue (ObjectInputTypeDefinition objectType) value
+ | (Aeson.Object objectValue) <- value = do
+ let (InputObjectType _ _ inputFields) = objectType
+ (newObjectValue, resultMap) <- foldWithKey objectValue inputFields
+ if HashMap.null newObjectValue
+ then Just $ Object resultMap
+ else Nothing
+ where
+ foldWithKey objectValue = HashMap.foldrWithKey matchFieldValues
+ $ Just (objectValue, HashMap.empty)
+ matchFieldValues _ _ Nothing = Nothing
+ matchFieldValues fieldName inputField (Just (objectValue, resultMap)) =
+ let (InputField _ fieldType _) = inputField
+ insert = flip (HashMap.insert fieldName) resultMap
+ newObjectValue = HashMap.delete fieldName objectValue
+ in case HashMap.lookup fieldName objectValue of
+ Just variableValue -> do
+ coerced <- coerceVariableValue fieldType variableValue
+ pure (newObjectValue, insert coerced)
+ Nothing -> Just (objectValue, resultMap)
+ coerceVariableValue (ListInputTypeDefinition listType) value
+ | (Aeson.Array arrayValue) <- value = List
+ <$> foldr foldVector (Just []) arrayValue
+ | otherwise = coerceVariableValue listType value
+ where
+ foldVector _ Nothing = Nothing
+ foldVector variableValue (Just list) = do
+ coerced <- coerceVariableValue listType variableValue
+ pure $ coerced : list
+ coerceVariableValue _ _ = Nothing
diff --git a/src/Language/GraphQL/Execute/Transform.hs b/src/Language/GraphQL/Execute/Transform.hs
index 5a9eef8..56b2a22 100644
--- a/src/Language/GraphQL/Execute/Transform.hs
+++ b/src/Language/GraphQL/Execute/Transform.hs
@@ -1,25 +1,28 @@
{-# LANGUAGE ExplicitForAll #-}
+{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE TupleSections #-}
-- | After the document is parsed, before getting executed the AST is
-- transformed into a similar, simpler AST. This module is responsible for
-- this transformation.
module Language.GraphQL.Execute.Transform
- ( document
+ ( Document(..)
+ , OperationDefinition(..)
+ , document
+ , operation
) where
-import Control.Arrow (first)
import Control.Monad (foldM, unless)
import Control.Monad.Trans.Class (lift)
import Control.Monad.Trans.Reader (ReaderT, asks, runReaderT)
import Control.Monad.Trans.State (StateT, evalStateT, gets, modify)
import Data.HashMap.Strict (HashMap)
import qualified Data.HashMap.Strict as HashMap
+import Data.List.NonEmpty (NonEmpty)
import qualified Data.List.NonEmpty as NonEmpty
import Data.Sequence (Seq, (<|), (><))
import qualified Language.GraphQL.AST as Full
import qualified Language.GraphQL.AST.Core as Core
-import Language.GraphQL.AST.Document (Definition(..), Document)
import qualified Language.GraphQL.Schema as Schema
import qualified Language.GraphQL.Type.Directive as Directive
@@ -34,36 +37,56 @@ type TransformT a = StateT Replacement (ReaderT Schema.Subs Maybe) a
liftJust :: forall a. a -> TransformT a
liftJust = lift . lift . Just
+-- | GraphQL document is a non-empty list of operations.
+data Document = Document
+ (NonEmpty OperationDefinition)
+ (HashMap Full.Name Full.FragmentDefinition)
+
+data OperationDefinition = OperationDefinition
+ Full.OperationType
+ (Maybe Full.Name)
+ [Full.VariableDefinition]
+ [Full.Directive]
+ Full.SelectionSet
+
-- | Rewrites the original syntax tree into an intermediate representation used
-- for query execution.
-document :: Schema.Subs -> Document -> Maybe Core.Document
-document subs document' =
- flip runReaderT subs
- $ evalStateT (collectFragments >> operations operationDefinitions)
- $ Replacement HashMap.empty fragmentTable
+document :: Full.Document -> Maybe Document
+document ast =
+ let (operations, fragmentTable) = foldr defragment ([], HashMap.empty) ast
+ in Document <$> NonEmpty.nonEmpty operations <*> pure fragmentTable
where
- (fragmentTable, operationDefinitions) = foldr defragment mempty document'
- defragment (ExecutableDefinition (Full.DefinitionOperation definition)) acc =
- (definition :) <$> acc
- defragment (ExecutableDefinition (Full.DefinitionFragment definition)) acc =
- let (Full.FragmentDefinition name _ _ _) = definition
- in first (HashMap.insert name definition) acc
+ defragment definition (operations, fragments')
+ | (Full.ExecutableDefinition executable) <- definition
+ , (Full.DefinitionOperation operation') <- executable =
+ (transform operation' : operations, fragments')
+ | (Full.ExecutableDefinition executable) <- definition
+ , (Full.DefinitionFragment fragment) <- executable
+ , (Full.FragmentDefinition name _ _ _) <- fragment =
+ (operations, HashMap.insert name fragment fragments')
defragment _ acc = acc
+ transform = \case
+ Full.OperationDefinition type' name variables directives' selections ->
+ OperationDefinition type' name variables directives' selections
+ Full.SelectionSet selectionSet ->
+ OperationDefinition Full.Query Nothing mempty mempty selectionSet
-- * Operation
-operations :: [Full.OperationDefinition] -> TransformT Core.Document
-operations operations' = do
- coreOperations <- traverse operation operations'
- lift . lift $ NonEmpty.nonEmpty coreOperations
-
-operation :: Full.OperationDefinition -> TransformT Core.Operation
-operation (Full.SelectionSet sels)
- = operation $ Full.OperationDefinition Full.Query mempty mempty mempty sels
-operation (Full.OperationDefinition Full.Query name _vars _dirs sels)
- = Core.Query name <$> appendSelection sels
-operation (Full.OperationDefinition Full.Mutation name _vars _dirs sels)
- = Core.Mutation name <$> appendSelection sels
+operation
+ :: HashMap Full.Name Full.FragmentDefinition
+ -> Schema.Subs
+ -> OperationDefinition
+ -> Maybe Core.Operation
+operation fragmentTable subs operationDefinition = flip runReaderT subs
+ $ evalStateT (collectFragments >> transform operationDefinition)
+ $ Replacement HashMap.empty fragmentTable
+ where
+ transform :: OperationDefinition -> TransformT Core.Operation
+ transform (OperationDefinition Full.Query name _ _ sels) =
+ Core.Query name <$> appendSelection sels
+ transform (OperationDefinition Full.Mutation name _ _ sels) =
+ Core.Mutation name <$> appendSelection sels
-- * Selection
diff --git a/src/Language/GraphQL/Schema.hs b/src/Language/GraphQL/Schema.hs
index e76b42e..752ce29 100644
--- a/src/Language/GraphQL/Schema.hs
+++ b/src/Language/GraphQL/Schema.hs
@@ -3,8 +3,7 @@
-- | This module provides a representation of a @GraphQL@ Schema in addition to
-- functions for defining and manipulating schemas.
module Language.GraphQL.Schema
- ( FieldResolver(..)
- , Resolver(..)
+ ( Resolver(..)
, Subs
, object
, resolve
@@ -31,21 +30,18 @@ import qualified Data.Text as T
import Language.GraphQL.AST.Core
import Language.GraphQL.Error
import Language.GraphQL.Trans
+import qualified Language.GraphQL.Type.Definition as Definition
import qualified Language.GraphQL.Type as Type
-- | Resolves a 'Field' into an @Aeson.@'Data.Aeson.Types.Object' with error
-- information (if an error has occurred). @m@ is an arbitrary monad, usually
-- 'IO'.
-data Resolver m = Resolver Name (FieldResolver m)
-
-data FieldResolver m
- = ValueResolver (ActionT m Aeson.Value)
- | NestingResolver (ActionT m (Type.Wrapping (HashMap Name (FieldResolver m))))
+data Resolver m = Resolver Name (Definition.FieldResolver m)
-- | Converts resolvers to a map.
resolversToMap :: (Foldable f, Functor f)
=> f (Resolver m)
- -> HashMap Text (FieldResolver m)
+ -> HashMap Text (Definition.FieldResolver m)
resolversToMap = HashMap.fromList . toList . fmap toKV
where
toKV (Resolver name r) = (name, r)
@@ -57,7 +53,7 @@ type Subs = HashMap Name Value
-- | Create a new 'Resolver' with the given 'Name' from the given 'Resolver's.
object :: Monad m => Name -> ActionT m [Resolver m] -> Resolver m
object name = Resolver name
- . NestingResolver
+ . Definition.NestingResolver
. fmap (Type.Named . resolversToMap)
-- | Like 'object' but can be null or a list of objects.
@@ -66,19 +62,19 @@ wrappedObject :: Monad m
-> ActionT m (Type.Wrapping [Resolver m])
-> Resolver m
wrappedObject name = Resolver name
- . NestingResolver
+ . Definition.NestingResolver
. (fmap . fmap) resolversToMap
-- | A scalar represents a primitive value, like a string or an integer.
scalar :: (Monad m, Aeson.ToJSON a) => Name -> ActionT m a -> Resolver m
-scalar name = Resolver name . ValueResolver . fmap Aeson.toJSON
+scalar name = Resolver name . Definition.ValueResolver . fmap Aeson.toJSON
-- | Like 'scalar' but can be null or a list of scalars.
wrappedScalar :: (Monad m, Aeson.ToJSON a)
=> Name
-> ActionT m (Type.Wrapping a)
-> Resolver m
-wrappedScalar name = Resolver name . ValueResolver . fmap Aeson.toJSON
+wrappedScalar name = Resolver name . Definition.ValueResolver . fmap Aeson.toJSON
resolveFieldValue :: Monad m => Field -> ActionT m a -> m (Either Text a)
resolveFieldValue field@(Field _ _ args _) =
@@ -91,11 +87,14 @@ convert Type.Null = Aeson.Null
convert (Type.Named value) = value
convert (Type.List value) = Aeson.toJSON value
-withField :: Monad m => Field -> FieldResolver m -> CollectErrsT m Aeson.Object
-withField field (ValueResolver resolver) = do
+withField :: Monad m
+ => Field
+ -> Definition.FieldResolver m
+ -> CollectErrsT m Aeson.Object
+withField field (Definition.ValueResolver resolver) = do
answer <- lift $ resolveFieldValue field resolver
either (errmsg field) (pure . HashMap.singleton (aliasOrName field)) answer
-withField field@(Field _ _ _ seqSelection) (NestingResolver resolver) = do
+withField field@(Field _ _ _ seqSelection) (Definition.NestingResolver resolver) = do
answer <- lift $ resolveFieldValue field resolver
case answer of
Right result -> do
@@ -112,7 +111,7 @@ errmsg field errorMessage = do
-- 'Resolver' to each 'Field'. Resolves into a value containing the
-- resolved 'Field', or a null value and error information.
resolve :: Monad m
- => HashMap Text (FieldResolver m)
+ => HashMap Text (Definition.FieldResolver m)
-> Seq Selection
-> CollectErrsT m Aeson.Value
resolve resolvers = fmap (Aeson.toJSON . fold) . traverse tryResolvers
@@ -122,7 +121,7 @@ resolve resolvers = fmap (Aeson.toJSON . fold) . traverse tryResolvers
| (Just resolver) <- lookupResolver name = withField fld resolver
| otherwise = errmsg fld $ T.unwords ["field", name, "not resolved."]
tryResolvers (SelectionFragment (Fragment typeCondition selections'))
- | Just (ValueResolver resolver) <- lookupResolver "__typename" = do
+ | Just (Definition.ValueResolver resolver) <- lookupResolver "__typename" = do
let fakeField = Field Nothing "__typename" mempty mempty
that <- lift $ resolveFieldValue fakeField resolver
if Right (Aeson.String typeCondition) == that
diff --git a/src/Language/GraphQL/Type/Definition.hs b/src/Language/GraphQL/Type/Definition.hs
index 016eeb8..5891f71 100644
--- a/src/Language/GraphQL/Type/Definition.hs
+++ b/src/Language/GraphQL/Type/Definition.hs
@@ -1,18 +1,256 @@
+{-# LANGUAGE ExplicitForAll #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE PatternSynonyms #-}
+{-# LANGUAGE ViewPatterns #-}
+
+-- | Types representing GraphQL type system.
module Language.GraphQL.Type.Definition
- ( ObjectType(..)
+ ( Argument(..)
+ , EnumType(..)
+ , Field(..)
+ , FieldResolver(..)
+ , InputField(..)
+ , InputObjectType(..)
+ , InputType(..)
+ , ObjectType(..)
+ , OutputType(..)
+ , ScalarType(..)
+ , TypeDefinition(..)
+ , pattern EnumInputTypeDefinition
+ , pattern ListInputTypeDefinition
+ , pattern ObjectInputTypeDefinition
+ , pattern ScalarInputTypeDefinition
+ , pattern EnumOutputTypeDefinition
+ , pattern ListOutputTypeDefinition
+ , pattern ObjectOutputTypeDefinition
+ , pattern ScalarOutputTypeDefinition
+ , boolean
+ , float
+ , id
+ , int
+ , string
) where
+import qualified Data.Aeson as Aeson
import Data.HashMap.Strict (HashMap)
+import Data.Set (Set)
import Data.Text (Text)
-import Language.GraphQL.Schema
-
-type Fields m = HashMap Text (FieldResolver m)
+import Language.GraphQL.AST.Core (Name, Value)
+import Language.GraphQL.Trans
+import qualified Language.GraphQL.Type as Type
+import Prelude hiding (id)
--- | Object Type Definition.
+-- | Object type definition.
--
-- Almost all of the GraphQL types you define will be object types. Object
-- types have a name, but most importantly describe their fields.
data ObjectType m = ObjectType
{ name :: Text
- , fields :: Fields m
+ , fields :: HashMap Name (Field m)
}
+
+-- | Output object field definition.
+data Field m = Field
+ (Maybe Text) (OutputType m) (HashMap Name Argument) (FieldResolver m)
+
+-- | Resolving a field can result in a leaf value or an object, which is
+-- represented as a list of nested resolvers, used to resolve the fields of that
+-- object.
+data FieldResolver m
+ = ValueResolver (ActionT m Aeson.Value)
+ | NestingResolver (ActionT m (Type.Wrapping (HashMap Name (FieldResolver m))))
+
+-- | Field argument definition.
+data Argument = Argument (Maybe Text) InputType (Maybe Value)
+
+-- | Scalar type definition.
+--
+-- The leaf values of any request and input values to arguments are Scalars (or
+-- Enums) .
+data ScalarType = ScalarType Name (Maybe Text)
+
+-- | Enum type definition.
+--
+-- Some leaf values of requests and input values are Enums. GraphQL serializes
+-- Enum values as strings, however internally Enums can be represented by any
+-- kind of type, often integers.
+data EnumType = EnumType Name (Maybe Text) (Set Text)
+
+-- | Single field of an 'InputObjectType'.
+data InputField = InputField (Maybe Text) InputType (Maybe Value)
+
+-- | Input object type definition.
+--
+-- An input object defines a structured collection of fields which may be
+-- supplied to a field argument.
+data InputObjectType = InputObjectType
+ Name (Maybe Text) (HashMap Name InputField)
+
+-- | These types may be used as input types for arguments and directives.
+data InputType
+ = ScalarInputType ScalarType
+ | EnumInputType EnumType
+ | ObjectInputType InputObjectType
+ | ListInputType InputType
+ | NonNullScalarInputType ScalarType
+ | NonNullEnumInputType EnumType
+ | NonNullObjectInputType InputObjectType
+ | NonNullListInputType InputType
+
+-- | These types may be used as output types as the result of fields.
+data OutputType m
+ = ScalarOutputType ScalarType
+ | EnumOutputType EnumType
+ | ObjectOutputType (ObjectType m)
+ | ListOutputType (OutputType m)
+ | NonNullScalarOutputType ScalarType
+ | NonNullEnumOutputType EnumType
+ | NonNullObjectOutputType (ObjectType m)
+ | NonNullListOutputType (OutputType m)
+
+-- | These are all of the possible kinds of types.
+data TypeDefinition m
+ = ScalarTypeDefinition ScalarType
+ | EnumTypeDefinition EnumType
+ | ObjectTypeDefinition (ObjectType m)
+ | InputObjectTypeDefinition InputObjectType
+
+-- | The @String@ scalar type represents textual data, represented as UTF-8
+-- character sequences. The String type is most often used by GraphQL to
+-- represent free-form human-readable text.
+string :: ScalarType
+string = ScalarType "String" (Just description)
+ where
+ description =
+ "The `String` scalar type represents textual data, represented as \
+ \UTF-8 character sequences. The String type is most often used by \
+ \GraphQL to represent free-form human-readable text."
+
+-- | The @Boolean@ scalar type represents @true@ or @false@.
+boolean :: ScalarType
+boolean = ScalarType "Boolean" (Just description)
+ where
+ description = "The `Boolean` scalar type represents `true` or `false`."
+
+-- | The @Int@ scalar type represents non-fractional signed whole numeric
+-- values. Int can represent values between \(-2^{31}\) and \(2^{31 - 1}\).
+int :: ScalarType
+int = ScalarType "Int" (Just description)
+ where
+ description =
+ "The `Int` scalar type represents non-fractional signed whole numeric \
+ \values. Int can represent values between -(2^31) and 2^31 - 1."
+
+-- | The @Float@ scalar type represents signed double-precision fractional
+-- values as specified by
+-- [IEEE 754](https://en.wikipedia.org/wiki/IEEE_floating_point).
+float :: ScalarType
+float = ScalarType "Float" (Just description)
+ where
+ description =
+ "The `Float` scalar type represents signed double-precision fractional \
+ \values as specified by \
+ \[IEEE 754](https://en.wikipedia.org/wiki/IEEE_floating_point)."
+
+-- | The @ID@ scalar type represents a unique identifier, often used to refetch
+-- an object or as key for a cache. The ID type appears in a JSON response as a
+-- String; however, it is not intended to be human-readable. When expected as an
+-- input type, any string (such as @"4"@) or integer (such as @4@) input value
+-- will be accepted as an ID.
+id :: ScalarType
+id = ScalarType "ID" (Just description)
+ where
+ description =
+ "The `ID` scalar type represents a unique identifier, often used to \
+ \refetch an object or as key for a cache. The ID type appears in a \
+ \JSON response as a String; however, it is not intended to be \
+ \human-readable. When expected as an input type, any string (such as \
+ \`\"4\"`) or integer (such as `4`) input value will be accepted as an ID."
+
+-- | Matches either 'ScalarInputType' or 'NonNullScalarInputType'.
+pattern ScalarInputTypeDefinition :: ScalarType -> InputType
+pattern ScalarInputTypeDefinition scalarType <-
+ (isScalarInputType -> Just scalarType)
+
+-- | Matches either 'EnumInputType' or 'NonNullEnumInputType'.
+pattern EnumInputTypeDefinition :: EnumType -> InputType
+pattern EnumInputTypeDefinition enumType <-
+ (isEnumInputType -> Just enumType)
+
+-- | Matches either 'ObjectInputType' or 'NonNullObjectInputType'.
+pattern ObjectInputTypeDefinition :: InputObjectType -> InputType
+pattern ObjectInputTypeDefinition objectType <-
+ (isObjectInputType -> Just objectType)
+
+-- | Matches either 'ListInputType' or 'NonNullListInputType'.
+pattern ListInputTypeDefinition :: InputType -> InputType
+pattern ListInputTypeDefinition listType <-
+ (isListInputType -> Just listType)
+
+{-# COMPLETE EnumInputTypeDefinition
+ , ListInputTypeDefinition
+ , ObjectInputTypeDefinition
+ , ScalarInputTypeDefinition
+ #-}
+
+pattern ScalarOutputTypeDefinition :: forall m. ScalarType -> OutputType m
+pattern ScalarOutputTypeDefinition scalarType <-
+ (isScalarOutputType -> Just scalarType)
+
+pattern EnumOutputTypeDefinition :: forall m. EnumType -> OutputType m
+pattern EnumOutputTypeDefinition enumType <-
+ (isEnumOutputType -> Just enumType)
+
+pattern ObjectOutputTypeDefinition :: forall m. ObjectType m -> OutputType m
+pattern ObjectOutputTypeDefinition objectType <-
+ (isObjectOutputType -> Just objectType)
+
+pattern ListOutputTypeDefinition :: forall m. OutputType m -> OutputType m
+pattern ListOutputTypeDefinition listType <-
+ (isListOutputType -> Just listType)
+
+{-# COMPLETE ScalarOutputTypeDefinition
+ , EnumOutputTypeDefinition
+ , ObjectOutputTypeDefinition
+ , ListOutputTypeDefinition
+ #-}
+
+isScalarInputType :: InputType -> Maybe ScalarType
+isScalarInputType (ScalarInputType inputType) = Just inputType
+isScalarInputType (NonNullScalarInputType inputType) = Just inputType
+isScalarInputType _ = Nothing
+
+isObjectInputType :: InputType -> Maybe InputObjectType
+isObjectInputType (ObjectInputType inputType) = Just inputType
+isObjectInputType (NonNullObjectInputType inputType) = Just inputType
+isObjectInputType _ = Nothing
+
+isEnumInputType :: InputType -> Maybe EnumType
+isEnumInputType (EnumInputType inputType) = Just inputType
+isEnumInputType (NonNullEnumInputType inputType) = Just inputType
+isEnumInputType _ = Nothing
+
+isListInputType :: InputType -> Maybe InputType
+isListInputType (ListInputType inputType) = Just inputType
+isListInputType (NonNullListInputType inputType) = Just inputType
+isListInputType _ = Nothing
+
+isScalarOutputType :: forall m. OutputType m -> Maybe ScalarType
+isScalarOutputType (ScalarOutputType outputType) = Just outputType
+isScalarOutputType (NonNullScalarOutputType outputType) = Just outputType
+isScalarOutputType _ = Nothing
+
+isObjectOutputType :: forall m. OutputType m -> Maybe (ObjectType m)
+isObjectOutputType (ObjectOutputType outputType) = Just outputType
+isObjectOutputType (NonNullObjectOutputType outputType) = Just outputType
+isObjectOutputType _ = Nothing
+
+isEnumOutputType :: forall m. OutputType m -> Maybe EnumType
+isEnumOutputType (EnumOutputType outputType) = Just outputType
+isEnumOutputType (NonNullEnumOutputType outputType) = Just outputType
+isEnumOutputType _ = Nothing
+
+isListOutputType :: forall m. OutputType m -> Maybe (OutputType m)
+isListOutputType (ListOutputType outputType) = Just outputType
+isListOutputType (NonNullListOutputType outputType) = Just outputType
+isListOutputType _ = Nothing
diff --git a/src/Language/GraphQL/Type/Schema.hs b/src/Language/GraphQL/Type/Schema.hs
index f830c26..fa44694 100644
--- a/src/Language/GraphQL/Type/Schema.hs
+++ b/src/Language/GraphQL/Type/Schema.hs
@@ -1,11 +1,68 @@
+{-# LANGUAGE ExplicitForAll #-}
+
+-- | Schema Definition.
module Language.GraphQL.Type.Schema
( Schema(..)
+ , collectReferencedTypes
) where
+import Data.HashMap.Strict (HashMap)
+import qualified Data.HashMap.Strict as HashMap
+import Language.GraphQL.AST.Core (Name)
import Language.GraphQL.Type.Definition
--- | Schema Definition
+-- | A Schema is created by supplying the root types of each type of operation,
+-- query and mutation (optional). A schema definition is then supplied to the
+-- validator and executor.
+--
+-- __Note:__ When the schema is constructed, by default only the types that
+-- are reachable by traversing the root types are included, other types must
+-- be explicitly referenced.
data Schema m = Schema
{ query :: ObjectType m
, mutation :: Maybe (ObjectType m)
}
+
+-- | Traverses the schema and finds all referenced types.
+collectReferencedTypes :: forall m. Schema m -> HashMap Name (TypeDefinition m)
+collectReferencedTypes schema =
+ let queryTypes = traverseObjectType (query schema) HashMap.empty
+ in maybe queryTypes (`traverseObjectType` queryTypes) $ mutation schema
+ where
+ collect traverser typeName element foundTypes =
+ let newMap = HashMap.insert typeName element foundTypes
+ in maybe (traverser newMap) (const foundTypes)
+ $ HashMap.lookup typeName foundTypes
+ visitFields (Field _ outputType arguments _) foundTypes
+ = traverseOutputType outputType
+ $ foldr visitArguments foundTypes arguments
+ visitArguments (Argument _ inputType _) = traverseInputType inputType
+ visitInputFields (InputField _ inputType _) = traverseInputType inputType
+ traverseInputType (ObjectInputTypeDefinition objectType) =
+ let (InputObjectType typeName _ inputFields) = objectType
+ element = InputObjectTypeDefinition objectType
+ traverser = flip (foldr visitInputFields) inputFields
+ in collect traverser typeName element
+ traverseInputType (ListInputTypeDefinition listType) =
+ traverseInputType listType
+ traverseInputType (ScalarInputTypeDefinition scalarType) =
+ let (ScalarType typeName _) = scalarType
+ in collect Prelude.id typeName (ScalarTypeDefinition scalarType)
+ traverseInputType (EnumInputTypeDefinition enumType) =
+ let (EnumType typeName _ _) = enumType
+ in collect Prelude.id typeName (EnumTypeDefinition enumType)
+ traverseOutputType (ObjectOutputTypeDefinition objectType) =
+ traverseObjectType objectType
+ traverseOutputType (ListOutputTypeDefinition listType) =
+ traverseOutputType listType
+ traverseOutputType (ScalarOutputTypeDefinition scalarType) =
+ let (ScalarType typeName _) = scalarType
+ in collect Prelude.id typeName (ScalarTypeDefinition scalarType)
+ traverseOutputType (EnumOutputTypeDefinition enumType) =
+ let (EnumType typeName _ _) = enumType
+ in collect Prelude.id typeName (EnumTypeDefinition enumType)
+ traverseObjectType objectType foundTypes =
+ let (ObjectType typeName objectFields) = objectType
+ element = ObjectTypeDefinition objectType
+ traverser = flip (foldr visitFields) objectFields
+ in collect traverser typeName element foundTypes