aboutsummaryrefslogtreecommitdiff
path: root/src/Language/GraphQL
diff options
context:
space:
mode:
Diffstat (limited to 'src/Language/GraphQL')
-rw-r--r--src/Language/GraphQL/AST/Core.hs65
-rw-r--r--src/Language/GraphQL/AST/Document.hs41
-rw-r--r--src/Language/GraphQL/AST/Encoder.hs152
-rw-r--r--src/Language/GraphQL/AST/Parser.hs48
-rw-r--r--src/Language/GraphQL/Error.hs51
-rw-r--r--src/Language/GraphQL/Execute.hs87
-rw-r--r--src/Language/GraphQL/Execute/Coerce.hs230
-rw-r--r--src/Language/GraphQL/Execute/Execution.hs229
-rw-r--r--src/Language/GraphQL/Execute/Transform.hs465
-rw-r--r--src/Language/GraphQL/Schema.hs138
-rw-r--r--src/Language/GraphQL/Trans.hs11
-rw-r--r--src/Language/GraphQL/Type.hs65
-rw-r--r--src/Language/GraphQL/Type/Definition.hs115
-rw-r--r--src/Language/GraphQL/Type/Directive.hs9
-rw-r--r--src/Language/GraphQL/Type/In.hs101
-rw-r--r--src/Language/GraphQL/Type/Out.hs168
-rw-r--r--src/Language/GraphQL/Type/Schema.hs112
17 files changed, 1559 insertions, 528 deletions
diff --git a/src/Language/GraphQL/AST/Core.hs b/src/Language/GraphQL/AST/Core.hs
index 084ae21..0fe3e03 100644
--- a/src/Language/GraphQL/AST/Core.hs
+++ b/src/Language/GraphQL/AST/Core.hs
@@ -1,41 +1,11 @@
-- | This is the AST meant to be executed.
module Language.GraphQL.AST.Core
- ( Alias
- , Arguments(..)
- , Directive(..)
- , Document
- , Field(..)
- , Fragment(..)
- , Name
- , Operation(..)
- , Selection(..)
- , TypeCondition
- , Value(..)
+ ( Arguments(..)
) where
-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.
-data Operation
- = Query (Maybe Text) (Seq Selection)
- | Mutation (Maybe Text) (Seq Selection)
- deriving (Eq, Show)
-
--- | Single GraphQL field.
-data Field
- = Field (Maybe Alias) Name Arguments (Seq Selection)
- deriving (Eq, Show)
+import Language.GraphQL.AST (Name)
+import Language.GraphQL.Type.Definition
-- | Argument list.
newtype Arguments = Arguments (HashMap Name Value)
@@ -47,32 +17,3 @@ instance Semigroup Arguments where
instance Monoid Arguments where
mempty = Arguments mempty
--- | Directive.
-data Directive = Directive Name Arguments
- deriving (Eq, Show)
-
--- | Represents fragments and inline fragments.
-data Fragment
- = Fragment TypeCondition (Seq Selection)
- deriving (Eq, Show)
-
--- | Single selection element.
-data Selection
- = SelectionFragment Fragment
- | SelectionField Field
- deriving (Eq, Show)
-
--- | Represents accordingly typed GraphQL values.
-data Value
- = Int Int32
- | Float Double -- ^ GraphQL Float is double precision
- | String Text
- | Boolean Bool
- | Null
- | Enum Name
- | List [Value]
- | Object (HashMap Name Value)
- deriving (Eq, Show)
-
-instance IsString Value where
- fromString = String . fromString
diff --git a/src/Language/GraphQL/AST/Document.hs b/src/Language/GraphQL/AST/Document.hs
index 3b13691..430e92a 100644
--- a/src/Language/GraphQL/AST/Document.hs
+++ b/src/Language/GraphQL/AST/Document.hs
@@ -8,6 +8,7 @@ module Language.GraphQL.AST.Document
( Alias
, Argument(..)
, ArgumentsDefinition(..)
+ , ConstValue(..)
, Definition(..)
, Description(..)
, Directive(..)
@@ -197,7 +198,7 @@ type TypeCondition = Name
-- ** Input Values
--- | Input value.
+-- | Input value (literal or variable).
data Value
= Variable Name
| Int Int32
@@ -207,18 +208,46 @@ data Value
| Null
| Enum Name
| List [Value]
- | Object [ObjectField]
+ | Object [ObjectField Value]
+ deriving (Eq, Show)
+
+-- | Constant input value.
+data ConstValue
+ = ConstInt Int32
+ | ConstFloat Double
+ | ConstString Text
+ | ConstBoolean Bool
+ | ConstNull
+ | ConstEnum Name
+ | ConstList [ConstValue]
+ | ConstObject [ObjectField ConstValue]
deriving (Eq, Show)
-- | Key-value pair.
--
--- A list of 'ObjectField's represents a GraphQL object type.
-data ObjectField = ObjectField Name Value deriving (Eq, Show)
+-- A list of 'ObjectField's represents a GraphQL object type.
+data ObjectField a = ObjectField Name a
+ deriving (Eq, Show)
-- ** Variables
-- | Variable definition.
-data VariableDefinition = VariableDefinition Name Type (Maybe Value)
+--
+-- Each operation can include a list of variables:
+--
+-- @
+-- query (protagonist: String = "Zarathustra") {
+-- getAuthor(protagonist: $protagonist)
+-- }
+-- @
+--
+-- This query defines an optional variable @protagonist@ of type @String@,
+-- its default value is "Zarathustra". If no default value is defined and no
+-- value is provided, a variable can still be @null@ if its type is nullable.
+--
+-- Variables are usually passed along with the query, but not in the query
+-- itself. They make queries reusable.
+data VariableDefinition = VariableDefinition Name Type (Maybe ConstValue)
deriving (Eq, Show)
-- ** Type References
@@ -445,7 +474,7 @@ instance Monoid ArgumentsDefinition where
--
-- The input type "Point2D" contains two value definitions: "x" and "y".
data InputValueDefinition
- = InputValueDefinition Description Name Type (Maybe Value) [Directive]
+ = InputValueDefinition Description Name Type (Maybe ConstValue) [Directive]
deriving (Eq, Show)
-- ** Unions
diff --git a/src/Language/GraphQL/AST/Encoder.hs b/src/Language/GraphQL/AST/Encoder.hs
index 69f5599..7fb0677 100644
--- a/src/Language/GraphQL/AST/Encoder.hs
+++ b/src/Language/GraphQL/AST/Encoder.hs
@@ -24,7 +24,6 @@ import Data.Text.Lazy.Builder (Builder)
import qualified Data.Text.Lazy.Builder as Builder
import Data.Text.Lazy.Builder.Int (decimal, hexadecimal)
import Data.Text.Lazy.Builder.RealFloat (realFloat)
-import qualified Language.GraphQL.AST as Full
import Language.GraphQL.AST.Document
-- | Instructs the encoder whether the GraphQL document should be minified or
@@ -53,32 +52,32 @@ document formatter defs
executableDefinition (ExecutableDefinition x) acc = definition formatter x : acc
executableDefinition _ acc = acc
--- | Converts a t'Full.ExecutableDefinition' into a string.
+-- | Converts a t'ExecutableDefinition' into a string.
definition :: Formatter -> ExecutableDefinition -> Lazy.Text
definition formatter x
| Pretty _ <- formatter = Lazy.Text.snoc (encodeDefinition x) '\n'
| Minified <- formatter = encodeDefinition x
where
- encodeDefinition (Full.DefinitionOperation operation)
+ encodeDefinition (DefinitionOperation operation)
= operationDefinition formatter operation
- encodeDefinition (Full.DefinitionFragment fragment)
+ encodeDefinition (DefinitionFragment fragment)
= fragmentDefinition formatter fragment
--- | Converts a 'Full.OperationDefinition into a string.
-operationDefinition :: Formatter -> Full.OperationDefinition -> Lazy.Text
-operationDefinition formatter (Full.SelectionSet sels)
+-- | Converts a 'OperationDefinition into a string.
+operationDefinition :: Formatter -> OperationDefinition -> Lazy.Text
+operationDefinition formatter (SelectionSet sels)
= selectionSet formatter sels
-operationDefinition formatter (Full.OperationDefinition Full.Query name vars dirs sels)
+operationDefinition formatter (OperationDefinition Query name vars dirs sels)
= "query " <> node formatter name vars dirs sels
-operationDefinition formatter (Full.OperationDefinition Full.Mutation name vars dirs sels)
+operationDefinition formatter (OperationDefinition Mutation name vars dirs sels)
= "mutation " <> node formatter name vars dirs sels
--- | Converts a Full.Query or Full.Mutation into a string.
+-- | Converts a Query or Mutation into a string.
node :: Formatter ->
- Maybe Full.Name ->
- [Full.VariableDefinition] ->
- [Full.Directive] ->
- Full.SelectionSet ->
+ Maybe Name ->
+ [VariableDefinition] ->
+ [Directive] ->
+ SelectionSet ->
Lazy.Text
node formatter name vars dirs sels
= Lazy.Text.fromStrict (fold name)
@@ -87,31 +86,31 @@ node formatter name vars dirs sels
<> eitherFormat formatter " " mempty
<> selectionSet formatter sels
-variableDefinitions :: Formatter -> [Full.VariableDefinition] -> Lazy.Text
+variableDefinitions :: Formatter -> [VariableDefinition] -> Lazy.Text
variableDefinitions formatter
= parensCommas formatter $ variableDefinition formatter
-variableDefinition :: Formatter -> Full.VariableDefinition -> Lazy.Text
-variableDefinition formatter (Full.VariableDefinition var ty dv)
+variableDefinition :: Formatter -> VariableDefinition -> Lazy.Text
+variableDefinition formatter (VariableDefinition var ty defaultValue')
= variable var
<> eitherFormat formatter ": " ":"
<> type' ty
- <> maybe mempty (defaultValue formatter) dv
+ <> maybe mempty (defaultValue formatter) defaultValue'
-defaultValue :: Formatter -> Full.Value -> Lazy.Text
+defaultValue :: Formatter -> ConstValue -> Lazy.Text
defaultValue formatter val
= eitherFormat formatter " = " "="
- <> value formatter val
+ <> value formatter (fromConstValue val)
-variable :: Full.Name -> Lazy.Text
+variable :: Name -> Lazy.Text
variable var = "$" <> Lazy.Text.fromStrict var
-selectionSet :: Formatter -> Full.SelectionSet -> Lazy.Text
+selectionSet :: Formatter -> SelectionSet -> Lazy.Text
selectionSet formatter
= bracesList formatter (selection formatter)
. NonEmpty.toList
-selectionSetOpt :: Formatter -> Full.SelectionSetOpt -> Lazy.Text
+selectionSetOpt :: Formatter -> SelectionSetOpt -> Lazy.Text
selectionSetOpt formatter = bracesList formatter $ selection formatter
indentSymbol :: Lazy.Text
@@ -120,14 +119,14 @@ indentSymbol = " "
indent :: (Integral a) => a -> Lazy.Text
indent indentation = Lazy.Text.replicate (fromIntegral indentation) indentSymbol
-selection :: Formatter -> Full.Selection -> Lazy.Text
+selection :: Formatter -> Selection -> Lazy.Text
selection formatter = Lazy.Text.append indent' . encodeSelection
where
- encodeSelection (Full.Field alias name args directives' selections) =
+ encodeSelection (Field alias name args directives' selections) =
field incrementIndent alias name args directives' selections
- encodeSelection (Full.InlineFragment typeCondition directives' selections) =
+ encodeSelection (InlineFragment typeCondition directives' selections) =
inlineFragment incrementIndent typeCondition directives' selections
- encodeSelection (Full.FragmentSpread name directives') =
+ encodeSelection (FragmentSpread name directives') =
fragmentSpread incrementIndent name directives'
incrementIndent
| Pretty indentation <- formatter = Pretty $ indentation + 1
@@ -139,13 +138,13 @@ selection formatter = Lazy.Text.append indent' . encodeSelection
colon :: Formatter -> Lazy.Text
colon formatter = eitherFormat formatter ": " ":"
--- | Converts Full.Field into a string
+-- | Converts Field into a string
field :: Formatter ->
- Maybe Full.Name ->
- Full.Name ->
- [Full.Argument] ->
- [Full.Directive] ->
- [Full.Selection] ->
+ Maybe Name ->
+ Name ->
+ [Argument] ->
+ [Directive] ->
+ [Selection] ->
Lazy.Text
field formatter alias name args dirs set
= optempty prependAlias (fold alias)
@@ -158,27 +157,27 @@ field formatter alias name args dirs set
selectionSetOpt' = (eitherFormat formatter " " "" <>)
. selectionSetOpt formatter
-arguments :: Formatter -> [Full.Argument] -> Lazy.Text
+arguments :: Formatter -> [Argument] -> Lazy.Text
arguments formatter = parensCommas formatter $ argument formatter
-argument :: Formatter -> Full.Argument -> Lazy.Text
-argument formatter (Full.Argument name value')
+argument :: Formatter -> Argument -> Lazy.Text
+argument formatter (Argument name value')
= Lazy.Text.fromStrict name
<> colon formatter
<> value formatter value'
-- * Fragments
-fragmentSpread :: Formatter -> Full.Name -> [Full.Directive] -> Lazy.Text
+fragmentSpread :: Formatter -> Name -> [Directive] -> Lazy.Text
fragmentSpread formatter name directives'
= "..." <> Lazy.Text.fromStrict name
<> optempty (directives formatter) directives'
inlineFragment ::
Formatter ->
- Maybe Full.TypeCondition ->
- [Full.Directive] ->
- Full.SelectionSet ->
+ Maybe TypeCondition ->
+ [Directive] ->
+ SelectionSet ->
Lazy.Text
inlineFragment formatter tc dirs sels = "... on "
<> Lazy.Text.fromStrict (fold tc)
@@ -186,8 +185,8 @@ inlineFragment formatter tc dirs sels = "... on "
<> eitherFormat formatter " " mempty
<> selectionSet formatter sels
-fragmentDefinition :: Formatter -> Full.FragmentDefinition -> Lazy.Text
-fragmentDefinition formatter (Full.FragmentDefinition name tc dirs sels)
+fragmentDefinition :: Formatter -> FragmentDefinition -> Lazy.Text
+fragmentDefinition formatter (FragmentDefinition name tc dirs sels)
= "fragment " <> Lazy.Text.fromStrict name
<> " on " <> Lazy.Text.fromStrict tc
<> optempty (directives formatter) dirs
@@ -196,26 +195,39 @@ fragmentDefinition formatter (Full.FragmentDefinition name tc dirs sels)
-- * Miscellaneous
--- | Converts a 'Full.Directive' into a string.
-directive :: Formatter -> Full.Directive -> Lazy.Text
-directive formatter (Full.Directive name args)
+-- | Converts a 'Directive' into a string.
+directive :: Formatter -> Directive -> Lazy.Text
+directive formatter (Directive name args)
= "@" <> Lazy.Text.fromStrict name <> optempty (arguments formatter) args
-directives :: Formatter -> [Full.Directive] -> Lazy.Text
+directives :: Formatter -> [Directive] -> Lazy.Text
directives Minified = spaces (directive Minified)
directives formatter = Lazy.Text.cons ' ' . spaces (directive formatter)
--- | Converts a 'Full.Value' into a string.
-value :: Formatter -> Full.Value -> Lazy.Text
-value _ (Full.Variable x) = variable x
-value _ (Full.Int x) = Builder.toLazyText $ decimal x
-value _ (Full.Float x) = Builder.toLazyText $ realFloat x
-value _ (Full.Boolean x) = booleanValue x
-value _ Full.Null = "null"
-value formatter (Full.String string) = stringValue formatter string
-value _ (Full.Enum x) = Lazy.Text.fromStrict x
-value formatter (Full.List x) = listValue formatter x
-value formatter (Full.Object x) = objectValue formatter x
+-- | Converts a 'Value' into a string.
+value :: Formatter -> Value -> Lazy.Text
+value _ (Variable x) = variable x
+value _ (Int x) = Builder.toLazyText $ decimal x
+value _ (Float x) = Builder.toLazyText $ realFloat x
+value _ (Boolean x) = booleanValue x
+value _ Null = "null"
+value formatter (String string) = stringValue formatter string
+value _ (Enum x) = Lazy.Text.fromStrict x
+value formatter (List x) = listValue formatter x
+value formatter (Object x) = objectValue formatter x
+
+fromConstValue :: ConstValue -> Value
+fromConstValue (ConstInt x) = Int x
+fromConstValue (ConstFloat x) = Float x
+fromConstValue (ConstBoolean x) = Boolean x
+fromConstValue ConstNull = Null
+fromConstValue (ConstString string) = String string
+fromConstValue (ConstEnum x) = Enum x
+fromConstValue (ConstList x) = List $ fromConstValue <$> x
+fromConstValue (ConstObject x) = Object $ fromConstObjectField <$> x
+ where
+ fromConstObjectField (ObjectField key value') =
+ ObjectField key $ fromConstValue value'
booleanValue :: Bool -> Lazy.Text
booleanValue True = "true"
@@ -271,10 +283,10 @@ escape char'
where
unicode prefix = mappend (Builder.fromString prefix) . (hexadecimal . ord)
-listValue :: Formatter -> [Full.Value] -> Lazy.Text
+listValue :: Formatter -> [Value] -> Lazy.Text
listValue formatter = bracketsCommas formatter $ value formatter
-objectValue :: Formatter -> [Full.ObjectField] -> Lazy.Text
+objectValue :: Formatter -> [ObjectField Value] -> Lazy.Text
objectValue formatter = intercalate $ objectField formatter
where
intercalate f
@@ -282,22 +294,22 @@ objectValue formatter = intercalate $ objectField formatter
. Lazy.Text.intercalate (eitherFormat formatter ", " ",")
. fmap f
-objectField :: Formatter -> Full.ObjectField -> Lazy.Text
-objectField formatter (Full.ObjectField name value') =
+objectField :: Formatter -> ObjectField Value -> Lazy.Text
+objectField formatter (ObjectField name value') =
Lazy.Text.fromStrict name <> colon formatter <> value formatter value'
--- | Converts a 'Full.Type' a type into a string.
-type' :: Full.Type -> Lazy.Text
-type' (Full.TypeNamed x) = Lazy.Text.fromStrict x
-type' (Full.TypeList x) = listType x
-type' (Full.TypeNonNull x) = nonNullType x
+-- | Converts a 'Type' a type into a string.
+type' :: Type -> Lazy.Text
+type' (TypeNamed x) = Lazy.Text.fromStrict x
+type' (TypeList x) = listType x
+type' (TypeNonNull x) = nonNullType x
-listType :: Full.Type -> Lazy.Text
+listType :: Type -> Lazy.Text
listType x = brackets (type' x)
-nonNullType :: Full.NonNullType -> Lazy.Text
-nonNullType (Full.NonNullTypeNamed x) = Lazy.Text.fromStrict x <> "!"
-nonNullType (Full.NonNullTypeList x) = listType x <> "!"
+nonNullType :: NonNullType -> Lazy.Text
+nonNullType (NonNullTypeNamed x) = Lazy.Text.fromStrict x <> "!"
+nonNullType (NonNullTypeList x) = listType x <> "!"
-- * Internal
diff --git a/src/Language/GraphQL/AST/Parser.hs b/src/Language/GraphQL/AST/Parser.hs
index 3449903..c18c36a 100644
--- a/src/Language/GraphQL/AST/Parser.hs
+++ b/src/Language/GraphQL/AST/Parser.hs
@@ -403,32 +403,38 @@ typeCondition = symbol "on" *> name
value :: Parser Value
value = Variable <$> variable
- <|> Float <$> try float
- <|> Int <$> integer
- <|> Boolean <$> booleanValue
- <|> Null <$ symbol "null"
- <|> String <$> blockString
- <|> String <$> string
- <|> Enum <$> try enumValue
- <|> List <$> listValue
- <|> Object <$> objectValue
+ <|> Float <$> try float
+ <|> Int <$> integer
+ <|> Boolean <$> booleanValue
+ <|> Null <$ symbol "null"
+ <|> String <$> blockString
+ <|> String <$> string
+ <|> Enum <$> try enumValue
+ <|> List <$> brackets (some value)
+ <|> Object <$> braces (some $ objectField value)
<?> "value error!"
- where
- booleanValue :: Parser Bool
- booleanValue = True <$ symbol "true"
- <|> False <$ symbol "false"
- listValue :: Parser [Value]
- listValue = brackets $ some value
+constValue :: Parser ConstValue
+constValue = ConstFloat <$> try float
+ <|> ConstInt <$> integer
+ <|> ConstBoolean <$> booleanValue
+ <|> ConstNull <$ symbol "null"
+ <|> ConstString <$> blockString
+ <|> ConstString <$> string
+ <|> ConstEnum <$> try enumValue
+ <|> ConstList <$> brackets (some constValue)
+ <|> ConstObject <$> braces (some $ objectField constValue)
+ <?> "value error!"
- objectValue :: Parser [ObjectField]
- objectValue = braces $ some objectField
+booleanValue :: Parser Bool
+booleanValue = True <$ symbol "true"
+ <|> False <$ symbol "false"
enumValue :: Parser Name
enumValue = but (symbol "true") *> but (symbol "false") *> but (symbol "null") *> name
-objectField :: Parser ObjectField
-objectField = ObjectField <$> name <* colon <*> value
+objectField :: Parser a -> Parser (ObjectField a)
+objectField valueParser = ObjectField <$> name <* colon <*> valueParser
-- * Variables
@@ -446,8 +452,8 @@ variableDefinition = VariableDefinition
variable :: Parser Name
variable = dollar *> name
-defaultValue :: Parser (Maybe Value)
-defaultValue = optional (equals *> value) <?> "DefaultValue"
+defaultValue :: Parser (Maybe ConstValue)
+defaultValue = optional (equals *> constValue) <?> "DefaultValue"
-- * Input Types
diff --git a/src/Language/GraphQL/Error.hs b/src/Language/GraphQL/Error.hs
index 91911b7..59719b0 100644
--- a/src/Language/GraphQL/Error.hs
+++ b/src/Language/GraphQL/Error.hs
@@ -5,21 +5,20 @@
module Language.GraphQL.Error
( parseError
, CollectErrsT
+ , Resolution(..)
, addErr
, addErrMsg
, runCollectErrs
- , runAppendErrs
, singleError
) where
+import Control.Monad.Trans.State (StateT, modify, runStateT)
import qualified Data.Aeson as Aeson
+import Data.HashMap.Strict (HashMap)
import Data.Text (Text)
import Data.Void (Void)
-import Control.Monad.Trans.Class (lift)
-import Control.Monad.Trans.State ( StateT
- , modify
- , runStateT
- )
+import Language.GraphQL.AST.Document (Name)
+import Language.GraphQL.Type.Schema
import Text.Megaparsec
( ParseErrorBundle(..)
, PosState(..)
@@ -30,6 +29,12 @@ import Text.Megaparsec
, unPos
)
+-- | Executor context.
+data Resolution m = Resolution
+ { errors :: [Aeson.Value]
+ , types :: HashMap Name (Type m)
+ }
+
-- | Wraps a parse error into a list of errors.
parseError :: Applicative f => ParseErrorBundle Text Void -> f Aeson.Value
parseError ParseErrorBundle{..} =
@@ -46,11 +51,13 @@ parseError ParseErrorBundle{..} =
in (errorObject x sourcePosition : result, newState)
-- | A wrapper to pass error messages around.
-type CollectErrsT m = StateT [Aeson.Value] m
+type CollectErrsT m = StateT (Resolution m) m
-- | Adds an error to the list of errors.
addErr :: Monad m => Aeson.Value -> CollectErrsT m ()
-addErr v = modify (v :)
+addErr v = modify appender
+ where
+ appender resolution@Resolution{..} = resolution{ errors = v : errors }
makeErrorMessage :: Text -> Aeson.Value
makeErrorMessage s = Aeson.object [("message", Aeson.toJSON s)]
@@ -66,23 +73,17 @@ singleError message = Aeson.object
addErrMsg :: Monad m => Text -> CollectErrsT m ()
addErrMsg = addErr . makeErrorMessage
--- | Appends the given list of errors to the current list of errors.
-appendErrs :: Monad m => [Aeson.Value] -> CollectErrsT m ()
-appendErrs errs = modify (errs ++)
-
-- | Runs the given query computation, but collects the errors into an error
-- list, which is then sent back with the data.
-runCollectErrs :: Monad m => CollectErrsT m Aeson.Value -> m Aeson.Value
-runCollectErrs res = do
- (dat, errs) <- runStateT res []
- if null errs
+runCollectErrs :: Monad m
+ => HashMap Name (Type m)
+ -> CollectErrsT m Aeson.Value
+ -> m Aeson.Value
+runCollectErrs types' res = do
+ (dat, Resolution{..}) <- runStateT res $ Resolution{ errors = [], types = types' }
+ if null errors
then return $ Aeson.object [("data", dat)]
- else return $ Aeson.object [("data", dat), ("errors", Aeson.toJSON $ reverse errs)]
-
--- | Runs the given computation, collecting the errors and appending them
--- to the previous list of errors.
-runAppendErrs :: Monad m => CollectErrsT m a -> CollectErrsT m a
-runAppendErrs f = do
- (v, errs) <- lift $ runStateT f []
- appendErrs errs
- return v
+ else return $ Aeson.object
+ [ ("data", dat)
+ , ("errors", Aeson.toJSON $ reverse errors)
+ ]
diff --git a/src/Language/GraphQL/Execute.hs b/src/Language/GraphQL/Execute.hs
index 204d08c..45bace0 100644
--- a/src/Language/GraphQL/Execute.hs
+++ b/src/Language/GraphQL/Execute.hs
@@ -1,5 +1,3 @@
-{-# LANGUAGE OverloadedStrings #-}
-
-- | This module provides functions to execute a @GraphQL@ request.
module Language.GraphQL.Execute
( execute
@@ -7,33 +5,30 @@ module Language.GraphQL.Execute
) where
import qualified Data.Aeson as Aeson
-import Data.List.NonEmpty (NonEmpty(..))
-import qualified Data.List.NonEmpty as NonEmpty
import Data.HashMap.Strict (HashMap)
import qualified Data.HashMap.Strict as HashMap
+import Data.Sequence (Seq(..))
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.AST.Document (Document, Name)
+import Language.GraphQL.Execute.Coerce
+import Language.GraphQL.Execute.Execution
import qualified Language.GraphQL.Execute.Transform as Transform
import Language.GraphQL.Error
-import qualified Language.GraphQL.Schema as Schema
+import qualified Language.GraphQL.Type.Definition as Definition
+import qualified Language.GraphQL.Type.Out as Out
+import Language.GraphQL.Type.Schema
-- | 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
- => HashMap Text (NonEmpty (Schema.Resolver m)) -- ^ Resolvers.
- -> Schema.Subs -- ^ Variable substitution function.
+execute :: (Monad m, VariableValue a)
+ => Schema m -- ^ Resolvers.
+ -> 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 = executeRequest 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
@@ -41,46 +36,36 @@ 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
- => HashMap Text (NonEmpty (Schema.Resolver m)) -- ^ Resolvers
+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 name subs doc =
- maybe transformError (document schema $ Just name)
- $ Transform.document subs doc
- where
- transformError = return $ singleError "Schema transformation error."
+executeWithName schema operationName =
+ executeRequest schema (Just operationName)
-document :: Monad m
- => HashMap Text (NonEmpty (Schema.Resolver m))
+executeRequest :: (Monad m, VariableValue a)
+ => Schema m
-> Maybe Text
- -> AST.Core.Document
+ -> HashMap.HashMap Name a
+ -> Document
-> m Aeson.Value
-document schema Nothing (op :| []) = operation schema op
-document schema (Just name) operations = case NonEmpty.dropWhile matchingName operations of
- [] -> return $ singleError
- $ Text.unwords ["Operation", name, "couldn't be found in the document."]
- (op:_) -> operation schema op
- where
- matchingName (AST.Core.Query (Just name') _) = name == name'
- matchingName (AST.Core.Mutation (Just name') _) = name == name'
- matchingName _ = False
-document _ _ _ = return $ singleError "Missing operation name."
+executeRequest schema operationName subs document =
+ case Transform.document schema operationName subs document of
+ Left queryError -> pure $ singleError $ Transform.queryError queryError
+ Right (Transform.Document types' rootObjectType operation)
+ | (Transform.Query _ fields) <- operation ->
+ executeOperation types' rootObjectType fields
+ | (Transform.Mutation _ fields) <- operation ->
+ executeOperation types' rootObjectType fields
-operation :: Monad m
- => HashMap Text (NonEmpty (Schema.Resolver m))
- -> AST.Core.Operation
+-- This is actually executeMutation, but we don't distinguish between queries
+-- and mutations yet.
+executeOperation :: Monad m
+ => HashMap Name (Type m)
+ -> Out.ObjectType m
+ -> Seq (Transform.Selection m)
-> m Aeson.Value
-operation schema = schemaOperation
- where
- runResolver fields = runCollectErrs
- . flip Schema.resolve fields
- . Schema.resolversToMap
- resolve fields queryType = maybe lookupError (runResolver fields)
- $ HashMap.lookup queryType schema
- lookupError = pure
- $ singleError "Root operation type couldn't be found in the schema."
- schemaOperation (AST.Core.Query _ fields) = resolve fields "Query"
- schemaOperation (AST.Core.Mutation _ fields) = resolve fields "Mutation"
+executeOperation types' objectType fields =
+ runCollectErrs types' $ executeSelectionSet Definition.Null objectType fields
diff --git a/src/Language/GraphQL/Execute/Coerce.hs b/src/Language/GraphQL/Execute/Coerce.hs
new file mode 100644
index 0000000..60fb71d
--- /dev/null
+++ b/src/Language/GraphQL/Execute/Coerce.hs
@@ -0,0 +1,230 @@
+{-# LANGUAGE ExplicitForAll #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE ViewPatterns #-}
+
+-- | Types and functions used for input and result coercion.
+module Language.GraphQL.Execute.Coerce
+ ( Output(..)
+ , Serialize(..)
+ , VariableValue(..)
+ , coerceInputLiteral
+ , matchFieldValues
+ ) where
+
+import qualified Data.Aeson as Aeson
+import Data.Int (Int32)
+import Data.HashMap.Strict (HashMap)
+import qualified Data.HashMap.Strict as HashMap
+import Data.Map.Strict (Map)
+import Data.String (IsString(..))
+import Data.Text (Text)
+import qualified Data.Text.Lazy as Text.Lazy
+import qualified Data.Text.Lazy.Builder as Text.Builder
+import qualified Data.Text.Lazy.Builder.Int as Text.Builder
+import Data.Scientific (toBoundedInteger, toRealFloat)
+import Language.GraphQL.AST (Name)
+import qualified Language.GraphQL.Type as Type
+import qualified Language.GraphQL.Type.In as In
+import qualified Language.GraphQL.Type.Out as Out
+
+-- | 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
+ :: In.Type -- ^ Expected type (variable type given in the query).
+ -> a -- ^ Variable value being coerced.
+ -> Maybe Type.Value -- ^ Coerced value on success, 'Nothing' otherwise.
+
+instance VariableValue Aeson.Value where
+ coerceVariableValue _ Aeson.Null = Just Type.Null
+ coerceVariableValue (In.ScalarBaseType scalarType) value
+ | (Aeson.String stringValue) <- value = Just $ Type.String stringValue
+ | (Aeson.Bool booleanValue) <- value = Just $ Type.Boolean booleanValue
+ | (Aeson.Number numberValue) <- value
+ , (Type.ScalarType "Float" _) <- scalarType =
+ Just $ Type.Float $ toRealFloat numberValue
+ | (Aeson.Number numberValue) <- value = -- ID or Int
+ Type.Int <$> toBoundedInteger numberValue
+ coerceVariableValue (In.EnumBaseType _) (Aeson.String stringValue) =
+ Just $ Type.Enum stringValue
+ coerceVariableValue (In.InputObjectBaseType objectType) value
+ | (Aeson.Object objectValue) <- value = do
+ let (In.InputObjectType _ _ inputFields) = objectType
+ (newObjectValue, resultMap) <- foldWithKey objectValue inputFields
+ if HashMap.null newObjectValue
+ then Just $ Type.Object resultMap
+ else Nothing
+ where
+ foldWithKey objectValue = HashMap.foldrWithKey matchFieldValues'
+ $ Just (objectValue, HashMap.empty)
+ matchFieldValues' _ _ Nothing = Nothing
+ matchFieldValues' fieldName inputField (Just (objectValue, resultMap)) =
+ let (In.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 (In.ListBaseType listType) value
+ | (Aeson.Array arrayValue) <- value =
+ Type.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
+
+-- | Looks up a value by name in the given map, coerces it and inserts into the
+-- result map. If the coercion fails, returns 'Nothing'. If the value isn't
+-- given, but a default value is known, inserts the default value into the
+-- result map. Otherwise it fails with 'Nothing' if the Input Type is a
+-- Non-Nullable type, or returns the unchanged, original map.
+matchFieldValues :: forall a
+ . (In.Type -> a -> Maybe Type.Value)
+ -> HashMap Name a
+ -> Name
+ -> In.Type
+ -> Maybe Type.Value
+ -> Maybe (HashMap Name Type.Value)
+ -> Maybe (HashMap Name Type.Value)
+matchFieldValues coerce values' fieldName type' defaultValue resultMap =
+ case HashMap.lookup fieldName values' of
+ Just variableValue -> coerceRuntimeValue $ coerce type' variableValue
+ Nothing
+ | Just value <- defaultValue ->
+ HashMap.insert fieldName value <$> resultMap
+ | Nothing <- defaultValue
+ , In.isNonNullType type' -> Nothing
+ | otherwise -> resultMap
+ where
+ coerceRuntimeValue (Just Type.Null)
+ | In.isNonNullType type' = Nothing
+ coerceRuntimeValue coercedValue =
+ HashMap.insert fieldName <$> coercedValue <*> resultMap
+
+-- | Coerces operation arguments according to the input coercion rules for the
+-- corresponding types.
+coerceInputLiteral :: In.Type -> Type.Value -> Maybe Type.Value
+coerceInputLiteral (In.isNonNullType -> False) Type.Null = Just Type.Null
+coerceInputLiteral (In.ScalarBaseType type') value
+ | (Type.String stringValue) <- value
+ , (Type.ScalarType "String" _) <- type' = Just $ Type.String stringValue
+ | (Type.Boolean booleanValue) <- value
+ , (Type.ScalarType "Boolean" _) <- type' = Just $ Type.Boolean booleanValue
+ | (Type.Int intValue) <- value
+ , (Type.ScalarType "Int" _) <- type' = Just $ Type.Int intValue
+ | (Type.Float floatValue) <- value
+ , (Type.ScalarType "Float" _) <- type' = Just $ Type.Float floatValue
+ | (Type.Int intValue) <- value
+ , (Type.ScalarType "Float" _) <- type' =
+ Just $ Type.Float $ fromIntegral intValue
+ | (Type.String stringValue) <- value
+ , (Type.ScalarType "ID" _) <- type' = Just $ Type.String stringValue
+ | (Type.Int intValue) <- value
+ , (Type.ScalarType "ID" _) <- type' = Just $ decimal intValue
+ where
+ decimal = Type.String
+ . Text.Lazy.toStrict
+ . Text.Builder.toLazyText
+ . Text.Builder.decimal
+coerceInputLiteral (In.EnumBaseType type') (Type.Enum enumValue)
+ | member enumValue type' = Just $ Type.Enum enumValue
+ where
+ member value (Type.EnumType _ _ members) = HashMap.member value members
+coerceInputLiteral (In.InputObjectBaseType type') (Type.Object values) =
+ let (In.InputObjectType _ _ inputFields) = type'
+ in Type.Object
+ <$> HashMap.foldrWithKey (matchFieldValues' values) (Just HashMap.empty) inputFields
+ where
+ matchFieldValues' values' fieldName (In.InputField _ inputFieldType defaultValue) =
+ matchFieldValues coerceInputLiteral values' fieldName inputFieldType defaultValue
+coerceInputLiteral (In.ListBaseType listType) (Type.List list) =
+ Type.List <$> traverse (coerceInputLiteral listType) list
+coerceInputLiteral (In.ListBaseType listType) singleton =
+ wrapSingleton listType singleton
+ where
+ wrapSingleton (In.ListBaseType listType') singleton' =
+ Type.List <$> sequence [wrapSingleton listType' singleton']
+ wrapSingleton listType' singleton' =
+ Type.List <$> sequence [coerceInputLiteral listType' singleton']
+coerceInputLiteral _ _ = Nothing
+
+-- | 'Serialize' describes how a @GraphQL@ value should be serialized.
+class Serialize a where
+ -- | Serializes a @GraphQL@ value according to the given serialization
+ -- format.
+ --
+ -- Type infomration is given as a hint, e.g. if you need to know what type
+ -- is being serialized to serialize it properly. Don't do any validation for
+ -- @GraphQL@ built-in types here.
+ --
+ -- If the value cannot be serialized without losing information, return
+ -- 'Nothing' — it will cause a field error.
+ serialize :: forall m
+ . Out.Type m -- ^ Expected output type.
+ -> Output a -- ^ The value to be serialized.
+ -> Maybe a -- ^ Serialized value on success or 'Nothing'.
+ -- | __null__ representation in the given serialization format.
+ null :: a
+
+-- | Intermediate type used to serialize a @GraphQL@ value.
+--
+-- The serialization is done during the execution, and 'Output' contains
+-- already serialized data (in 'List' and 'Object') as well as the new layer
+-- that has to be serialized in the current step. So 'Output' is parameterized
+-- by the serialization format.
+data Output a
+ = Int Int32
+ | Float Double
+ | String Text
+ | Boolean Bool
+ | Enum Name
+ | List [a]
+ | Object (Map Name a)
+ deriving (Eq, Show)
+
+instance forall a. IsString (Output a) where
+ fromString = String . fromString
+
+instance Serialize Aeson.Value where
+ serialize (Out.ScalarBaseType scalarType) value
+ | Type.ScalarType "Int" _ <- scalarType
+ , Int int <- value = Just $ Aeson.toJSON int
+ | Type.ScalarType "Float" _ <- scalarType
+ , Float float <- value = Just $ Aeson.toJSON float
+ | Type.ScalarType "String" _ <- scalarType
+ , String string <- value = Just $ Aeson.String string
+ | Type.ScalarType "ID" _ <- scalarType
+ , String string <- value = Just $ Aeson.String string
+ | Type.ScalarType "Boolean" _ <- scalarType
+ , Boolean boolean <- value = Just $ Aeson.Bool boolean
+ serialize _ (Enum enum) = Just $ Aeson.String enum
+ serialize _ (List list) = Just $ Aeson.toJSON list
+ serialize _ (Object object) = Just $ Aeson.toJSON object
+ serialize _ _ = Nothing
+ null = Aeson.Null
diff --git a/src/Language/GraphQL/Execute/Execution.hs b/src/Language/GraphQL/Execute/Execution.hs
new file mode 100644
index 0000000..0c10419
--- /dev/null
+++ b/src/Language/GraphQL/Execute/Execution.hs
@@ -0,0 +1,229 @@
+{-# LANGUAGE ExplicitForAll #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE ViewPatterns #-}
+
+module Language.GraphQL.Execute.Execution
+ ( executeSelectionSet
+ ) where
+
+import Control.Monad.Trans.Class (lift)
+import Control.Monad.Trans.Except (runExceptT)
+import Control.Monad.Trans.Reader (runReaderT)
+import Control.Monad.Trans.State (gets)
+import Data.List.NonEmpty (NonEmpty(..))
+import Data.Map.Strict (Map)
+import Data.HashMap.Strict (HashMap)
+import qualified Data.HashMap.Strict as HashMap
+import qualified Data.Map.Strict as Map
+import Data.Maybe (fromMaybe)
+import Data.Sequence (Seq(..))
+import Data.Text (Text)
+import Language.GraphQL.AST (Name)
+import Language.GraphQL.AST.Core
+import Language.GraphQL.Error
+import Language.GraphQL.Execute.Coerce
+import qualified Language.GraphQL.Execute.Transform as Transform
+import Language.GraphQL.Trans
+import qualified Language.GraphQL.Type as Type
+import qualified Language.GraphQL.Type.In as In
+import qualified Language.GraphQL.Type.Out as Out
+import Language.GraphQL.Type.Schema
+import Prelude hiding (null)
+
+resolveFieldValue :: Monad m
+ => Type.Value
+ -> Type.Subs
+ -> ActionT m a
+ -> m (Either Text a)
+resolveFieldValue result args =
+ flip runReaderT (Context {arguments = Arguments args, values = result})
+ . runExceptT
+ . runActionT
+
+collectFields :: Monad m
+ => Out.ObjectType m
+ -> Seq (Transform.Selection m)
+ -> Map Name (NonEmpty (Transform.Field m))
+collectFields objectType = foldl forEach Map.empty
+ where
+ forEach groupedFields (Transform.SelectionField field) =
+ let responseKey = aliasOrName field
+ in Map.insertWith (<>) responseKey (field :| []) groupedFields
+ forEach groupedFields (Transform.SelectionFragment selectionFragment)
+ | Transform.Fragment fragmentType fragmentSelectionSet <- selectionFragment
+ , doesFragmentTypeApply fragmentType objectType =
+ let fragmentGroupedFieldSet = collectFields objectType fragmentSelectionSet
+ in Map.unionWith (<>) groupedFields fragmentGroupedFieldSet
+ | otherwise = groupedFields
+
+aliasOrName :: forall m. Transform.Field m -> Name
+aliasOrName (Transform.Field alias name _ _) = fromMaybe name alias
+
+resolveAbstractType :: Monad m
+ => AbstractType m
+ -> Type.Subs
+ -> CollectErrsT m (Maybe (Out.ObjectType m))
+resolveAbstractType abstractType values'
+ | Just (Type.String typeName) <- HashMap.lookup "__typename" values' = do
+ types' <- gets types
+ case HashMap.lookup typeName types' of
+ Just (ObjectType objectType) ->
+ if instanceOf objectType abstractType
+ then pure $ Just objectType
+ else pure Nothing
+ _ -> pure Nothing
+ | otherwise = pure Nothing
+
+doesFragmentTypeApply :: forall m
+ . CompositeType m
+ -> Out.ObjectType m
+ -> Bool
+doesFragmentTypeApply (CompositeObjectType fragmentType) objectType =
+ fragmentType == objectType
+doesFragmentTypeApply (CompositeInterfaceType fragmentType) objectType =
+ instanceOf objectType $ AbstractInterfaceType fragmentType
+doesFragmentTypeApply (CompositeUnionType fragmentType) objectType =
+ instanceOf objectType $ AbstractUnionType fragmentType
+
+instanceOf :: forall m. Out.ObjectType m -> AbstractType m -> Bool
+instanceOf objectType (AbstractInterfaceType interfaceType) =
+ let Out.ObjectType _ _ interfaces _ = objectType
+ in foldr go False interfaces
+ where
+ go objectInterfaceType@(Out.InterfaceType _ _ interfaces _) acc =
+ acc || foldr go (interfaceType == objectInterfaceType) interfaces
+instanceOf objectType (AbstractUnionType unionType) =
+ let Out.UnionType _ _ members = unionType
+ in foldr go False members
+ where
+ go unionMemberType acc = acc || objectType == unionMemberType
+
+executeField :: (Monad m, Serialize a)
+ => Out.Resolver m
+ -> Type.Value
+ -> NonEmpty (Transform.Field m)
+ -> CollectErrsT m a
+executeField (Out.Resolver fieldDefinition resolver) prev fields = do
+ let Out.Field _ fieldType argumentDefinitions = fieldDefinition
+ let (Transform.Field _ _ arguments' _ :| []) = fields
+ case coerceArgumentValues argumentDefinitions arguments' of
+ Nothing -> errmsg "Argument coercing failed."
+ Just argumentValues -> do
+ answer <- lift $ resolveFieldValue prev argumentValues resolver
+ case answer of
+ Right result -> completeValue fieldType fields result
+ Left errorMessage -> errmsg errorMessage
+
+completeValue :: (Monad m, Serialize a)
+ => Out.Type m
+ -> NonEmpty (Transform.Field m)
+ -> Type.Value
+ -> CollectErrsT m a
+completeValue (Out.isNonNullType -> False) _ Type.Null = pure null
+completeValue outputType@(Out.ListBaseType listType) fields (Type.List list)
+ = traverse (completeValue listType fields) list
+ >>= coerceResult outputType . List
+completeValue outputType@(Out.ScalarBaseType _) _ (Type.Int int) =
+ coerceResult outputType $ Int int
+completeValue outputType@(Out.ScalarBaseType _) _ (Type.Boolean boolean) =
+ coerceResult outputType $ Boolean boolean
+completeValue outputType@(Out.ScalarBaseType _) _ (Type.Float float) =
+ coerceResult outputType $ Float float
+completeValue outputType@(Out.ScalarBaseType _) _ (Type.String string) =
+ coerceResult outputType $ String string
+completeValue outputType@(Out.EnumBaseType enumType) _ (Type.Enum enum) =
+ let Type.EnumType _ _ enumMembers = enumType
+ in if HashMap.member enum enumMembers
+ then coerceResult outputType $ Enum enum
+ else errmsg "Value completion failed."
+completeValue (Out.ObjectBaseType objectType) fields result =
+ executeSelectionSet result objectType $ mergeSelectionSets fields
+completeValue (Out.InterfaceBaseType interfaceType) fields result
+ | Type.Object objectMap <- result = do
+ let abstractType = AbstractInterfaceType interfaceType
+ concreteType <- resolveAbstractType abstractType objectMap
+ case concreteType of
+ Just objectType -> executeSelectionSet result objectType
+ $ mergeSelectionSets fields
+ Nothing -> errmsg "Value completion failed."
+completeValue (Out.UnionBaseType unionType) fields result
+ | Type.Object objectMap <- result = do
+ let abstractType = AbstractUnionType unionType
+ concreteType <- resolveAbstractType abstractType objectMap
+ case concreteType of
+ Just objectType -> executeSelectionSet result objectType
+ $ mergeSelectionSets fields
+ Nothing -> errmsg "Value completion failed."
+completeValue _ _ _ = errmsg "Value completion failed."
+
+mergeSelectionSets :: Monad m => NonEmpty (Transform.Field m) -> Seq (Transform.Selection m)
+mergeSelectionSets = foldr forEach mempty
+ where
+ forEach (Transform.Field _ _ _ fieldSelectionSet) selectionSet =
+ selectionSet <> fieldSelectionSet
+
+errmsg :: (Monad m, Serialize a) => Text -> CollectErrsT m a
+errmsg errorMessage = addErrMsg errorMessage >> pure null
+
+coerceResult :: (Monad m, Serialize a)
+ => Out.Type m
+ -> Output a
+ -> CollectErrsT m a
+coerceResult outputType result
+ | Just serialized <- serialize outputType result = pure serialized
+ | otherwise = errmsg "Result coercion failed."
+
+-- | Takes an 'Out.ObjectType' and a list of 'Transform.Selection's and applies
+-- each field to each 'Transform.Selection'. Resolves into a value containing
+-- the resolved 'Transform.Selection', or a null value and error information.
+executeSelectionSet :: (Monad m, Serialize a)
+ => Type.Value
+ -> Out.ObjectType m
+ -> Seq (Transform.Selection m)
+ -> CollectErrsT m a
+executeSelectionSet result objectType@(Out.ObjectType _ _ _ resolvers) selectionSet = do
+ let fields = collectFields objectType selectionSet
+ resolvedValues <- Map.traverseMaybeWithKey forEach fields
+ coerceResult (Out.NonNullObjectType objectType) $ Object resolvedValues
+ where
+ forEach _ fields@(field :| _) =
+ let Transform.Field _ name _ _ = field
+ in traverse (tryResolver fields) $ lookupResolver name
+ lookupResolver = flip HashMap.lookup resolvers
+ tryResolver fields resolver =
+ executeField resolver result fields >>= lift . pure
+
+coerceArgumentValues
+ :: HashMap Name In.Argument
+ -> HashMap Name Transform.Input
+ -> Maybe Type.Subs
+coerceArgumentValues argumentDefinitions argumentValues =
+ HashMap.foldrWithKey forEach (pure mempty) argumentDefinitions
+ where
+ forEach variableName (In.Argument _ variableType defaultValue) =
+ matchFieldValues coerceArgumentValue argumentValues variableName variableType defaultValue
+ coerceArgumentValue inputType (Transform.Int integer) =
+ coerceInputLiteral inputType (Type.Int integer)
+ coerceArgumentValue inputType (Transform.Boolean boolean) =
+ coerceInputLiteral inputType (Type.Boolean boolean)
+ coerceArgumentValue inputType (Transform.String string) =
+ coerceInputLiteral inputType (Type.String string)
+ coerceArgumentValue inputType (Transform.Float float) =
+ coerceInputLiteral inputType (Type.Float float)
+ coerceArgumentValue inputType (Transform.Enum enum) =
+ coerceInputLiteral inputType (Type.Enum enum)
+ coerceArgumentValue inputType Transform.Null
+ | In.isNonNullType inputType = Nothing
+ | otherwise = coerceInputLiteral inputType Type.Null
+ coerceArgumentValue (In.ListBaseType inputType) (Transform.List list) =
+ let coerceItem = coerceInputLiteral inputType
+ in Type.List <$> traverse coerceItem list
+ coerceArgumentValue (In.InputObjectBaseType inputType) (Transform.Object object)
+ | In.InputObjectType _ _ inputFields <- inputType =
+ let go = forEachField object
+ resultMap = HashMap.foldrWithKey go (pure mempty) inputFields
+ in Type.Object <$> resultMap
+ coerceArgumentValue _ (Transform.Variable variable) = pure variable
+ coerceArgumentValue _ _ = Nothing
+ forEachField object variableName (In.InputField _ variableType defaultValue) =
+ matchFieldValues coerceArgumentValue object variableName variableType defaultValue
diff --git a/src/Language/GraphQL/Execute/Transform.hs b/src/Language/GraphQL/Execute/Transform.hs
index 5a9eef8..733ac8c 100644
--- a/src/Language/GraphQL/Execute/Transform.hs
+++ b/src/Language/GraphQL/Execute/Transform.hs
@@ -1,125 +1,356 @@
{-# LANGUAGE ExplicitForAll #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE RecordWildCards #-}
{-# 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.
+-- | After the document is parsed, before getting executed, the AST is
+-- transformed into a similar, simpler AST. Performed transformations include:
+--
+-- * Replacing variables with their values.
+-- * Inlining fragments. Some fragments can be completely eliminated and
+-- replaced by the selection set they represent. Invalid (recursive and
+-- non-existing) fragments are skipped. The most fragments are inlined, so the
+-- executor doesn't have to perform additional lookups later.
+-- * Evaluating directives (@\@include@ and @\@skip@).
+--
+-- This module is also responsible for smaller rewrites that touch only parts of
+-- the original AST.
module Language.GraphQL.Execute.Transform
- ( document
+ ( Document(..)
+ , Field(..)
+ , Fragment(..)
+ , Input(..)
+ , Operation(..)
+ , QueryError(..)
+ , Selection(..)
+ , document
+ , queryError
) 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 Control.Monad.Trans.State (State, evalStateT, gets, modify)
+import Data.Foldable (find)
+import Data.Functor.Identity (Identity(..))
import Data.HashMap.Strict (HashMap)
import qualified Data.HashMap.Strict as HashMap
+import Data.Int (Int32)
+import Data.Maybe (fromMaybe)
+import Data.List.NonEmpty (NonEmpty(..))
import qualified Data.List.NonEmpty as NonEmpty
import Data.Sequence (Seq, (<|), (><))
+import Data.Text (Text)
+import qualified Data.Text as Text
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 Language.GraphQL.AST (Name)
+import Language.GraphQL.AST.Core
+import qualified Language.GraphQL.Execute.Coerce as Coerce
+import Language.GraphQL.Type.Directive (Directive(..))
import qualified Language.GraphQL.Type.Directive as Directive
+import qualified Language.GraphQL.Type as Type
+import qualified Language.GraphQL.Type.In as In
+import qualified Language.GraphQL.Type.Out as Out
+import Language.GraphQL.Type.Schema
--- | Associates a fragment name with a list of 'Core.Field's.
-data Replacement = Replacement
- { fragments :: HashMap Core.Name Core.Fragment
- , fragmentDefinitions :: HashMap Full.Name Full.FragmentDefinition
+-- | Associates a fragment name with a list of 'Field's.
+data Replacement m = Replacement
+ { fragments :: HashMap Full.Name (Fragment m)
+ , fragmentDefinitions :: FragmentDefinitions
+ , variableValues :: Type.Subs
+ , types :: HashMap Full.Name (Type m)
}
-type TransformT a = StateT Replacement (ReaderT Schema.Subs Maybe) a
+type FragmentDefinitions = HashMap Full.Name Full.FragmentDefinition
-liftJust :: forall a. a -> TransformT a
-liftJust = lift . lift . Just
+-- | Represents fragments and inline fragments.
+data Fragment m
+ = Fragment (CompositeType m) (Seq (Selection m))
+
+-- | Single selection element.
+data Selection m
+ = SelectionFragment (Fragment m)
+ | SelectionField (Field m)
+
+-- | GraphQL has 3 operation types: queries, mutations and subscribtions.
+--
+-- Currently only queries and mutations are supported.
+data Operation m
+ = Query (Maybe Text) (Seq (Selection m))
+ | Mutation (Maybe Text) (Seq (Selection m))
+
+-- | Single GraphQL field.
+data Field m = Field
+ (Maybe Full.Name) Full.Name (HashMap Full.Name Input) (Seq (Selection m))
+
+-- | Contains the operation to be executed along with its root type.
+data Document m = Document
+ (HashMap Full.Name (Type m)) (Out.ObjectType m) (Operation m)
+
+data OperationDefinition = OperationDefinition
+ Full.OperationType
+ (Maybe Full.Name)
+ [Full.VariableDefinition]
+ [Full.Directive]
+ Full.SelectionSet
+
+-- | Query error types.
+data QueryError
+ = OperationNotFound Text
+ | OperationNameRequired
+ | CoercionError
+ | TransformationError
+ | EmptyDocument
+ | UnsupportedRootOperation
+
+data Input
+ = Int Int32
+ | Float Double
+ | String Text
+ | Boolean Bool
+ | Null
+ | Enum Name
+ | List [Type.Value]
+ | Object (HashMap Name Input)
+ | Variable Type.Value
+ deriving (Eq, Show)
+
+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."
+queryError TransformationError = "Schema transformation error."
+queryError EmptyDocument =
+ "The document doesn't contain any executable operations."
+queryError UnsupportedRootOperation =
+ "Root operation type couldn't be found in the schema."
+
+getOperation
+ :: Maybe Full.Name
+ -> NonEmpty OperationDefinition
+ -> Either QueryError OperationDefinition
+getOperation Nothing (operation' :| []) = pure operation'
+getOperation Nothing _ = Left OperationNameRequired
+getOperation (Just operationName) operations
+ | Just operation' <- find matchingName operations = pure operation'
+ | otherwise = Left $ OperationNotFound operationName
+ where
+ matchingName (OperationDefinition _ name _ _ _) =
+ name == Just operationName
+
+lookupInputType
+ :: Full.Type
+ -> HashMap.HashMap Full.Name (Type m)
+ -> Maybe In.Type
+lookupInputType (Full.TypeNamed name) types =
+ case HashMap.lookup name types of
+ Just (ScalarType scalarType) ->
+ Just $ In.NamedScalarType scalarType
+ Just (EnumType enumType) ->
+ Just $ In.NamedEnumType enumType
+ Just (InputObjectType objectType) ->
+ Just $ In.NamedInputObjectType objectType
+ _ -> Nothing
+lookupInputType (Full.TypeList list) types
+ = In.ListType
+ <$> lookupInputType list types
+lookupInputType (Full.TypeNonNull (Full.NonNullTypeNamed nonNull)) types =
+ case HashMap.lookup nonNull types of
+ Just (ScalarType scalarType) ->
+ Just $ In.NonNullScalarType scalarType
+ Just (EnumType enumType) ->
+ Just $ In.NonNullEnumType enumType
+ Just (InputObjectType objectType) ->
+ Just $ In.NonNullInputObjectType objectType
+ _ -> Nothing
+lookupInputType (Full.TypeNonNull (Full.NonNullTypeList nonNull)) types
+ = In.NonNullListType
+ <$> lookupInputType nonNull types
+
+coerceVariableValues :: Coerce.VariableValue a
+ => forall m
+ . HashMap Full.Name (Type m)
+ -> OperationDefinition
+ -> HashMap.HashMap Full.Name a
+ -> Either QueryError Type.Subs
+coerceVariableValues types operationDefinition variableValues =
+ let OperationDefinition _ _ variableDefinitions _ _ = operationDefinition
+ in maybe (Left CoercionError) Right
+ $ foldr forEach (Just HashMap.empty) variableDefinitions
+ where
+ forEach variableDefinition coercedValues = do
+ let Full.VariableDefinition variableName variableTypeName defaultValue =
+ variableDefinition
+ let defaultValue' = constValue <$> defaultValue
+ variableType <- lookupInputType variableTypeName types
+
+ Coerce.matchFieldValues
+ coerceVariableValue'
+ variableValues
+ variableName
+ variableType
+ defaultValue'
+ coercedValues
+ coerceVariableValue' variableType value'
+ = Coerce.coerceVariableValue variableType value'
+ >>= Coerce.coerceInputLiteral variableType
+
+constValue :: Full.ConstValue -> Type.Value
+constValue (Full.ConstInt i) = Type.Int i
+constValue (Full.ConstFloat f) = Type.Float f
+constValue (Full.ConstString x) = Type.String x
+constValue (Full.ConstBoolean b) = Type.Boolean b
+constValue Full.ConstNull = Type.Null
+constValue (Full.ConstEnum e) = Type.Enum e
+constValue (Full.ConstList l) = Type.List $ constValue <$> l
+constValue (Full.ConstObject o) =
+ Type.Object $ HashMap.fromList $ constObjectField <$> o
+ where
+ constObjectField (Full.ObjectField key value') = (key, constValue value')
-- | 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 :: Coerce.VariableValue a
+ => forall m
+ . Schema m
+ -> Maybe Full.Name
+ -> HashMap Full.Name a
+ -> Full.Document
+ -> Either QueryError (Document m)
+document schema operationName subs ast = do
+ let referencedTypes = collectReferencedTypes schema
+
+ (operations, fragmentTable) <- defragment ast
+ chosenOperation <- getOperation operationName operations
+ coercedValues <- coerceVariableValues referencedTypes chosenOperation subs
+
+ let replacement = Replacement
+ { fragments = HashMap.empty
+ , fragmentDefinitions = fragmentTable
+ , variableValues = coercedValues
+ , types = referencedTypes
+ }
+ case chosenOperation of
+ OperationDefinition Full.Query _ _ _ _ ->
+ pure $ Document referencedTypes (query schema)
+ $ operation chosenOperation replacement
+ OperationDefinition Full.Mutation _ _ _ _
+ | Just mutationType <- mutation schema ->
+ pure $ Document referencedTypes mutationType
+ $ operation chosenOperation replacement
+ _ -> Left UnsupportedRootOperation
+
+defragment
+ :: Full.Document
+ -> Either QueryError (NonEmpty OperationDefinition, FragmentDefinitions)
+defragment ast =
+ let (operations, fragmentTable) = foldr defragment' ([], HashMap.empty) ast
+ nonEmptyOperations = NonEmpty.nonEmpty operations
+ emptyDocument = Left EmptyDocument
+ in (, fragmentTable) <$> maybe emptyDocument Right nonEmptyOperations
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 _ acc = 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 :: OperationDefinition -> Replacement m -> Operation m
+operation operationDefinition replacement
+ = runIdentity
+ $ evalStateT (collectFragments >> transform operationDefinition) replacement
+ where
+ transform (OperationDefinition Full.Query name _ _ sels) =
+ Query name <$> appendSelection sels
+ transform (OperationDefinition Full.Mutation name _ _ sels) =
+ Mutation name <$> appendSelection sels
-- * Selection
-selection ::
- Full.Selection ->
- TransformT (Either (Seq Core.Selection) Core.Selection)
+selection
+ :: Full.Selection
+ -> State (Replacement m) (Either (Seq (Selection m)) (Selection m))
selection (Full.Field alias name arguments' directives' selections) =
- maybe (Left mempty) (Right . Core.SelectionField) <$> do
- fieldArguments <- arguments arguments'
+ maybe (Left mempty) (Right . SelectionField) <$> do
+ fieldArguments <- foldM go HashMap.empty arguments'
fieldSelections <- appendSelection selections
fieldDirectives <- Directive.selection <$> directives directives'
- let field' = Core.Field alias name fieldArguments fieldSelections
+ let field' = Field alias name fieldArguments fieldSelections
pure $ field' <$ fieldDirectives
+ where
+ go arguments (Full.Argument name' value') =
+ inputField arguments name' value'
+
selection (Full.FragmentSpread name directives') =
- maybe (Left mempty) (Right . Core.SelectionFragment) <$> do
+ maybe (Left mempty) (Right . SelectionFragment) <$> do
spreadDirectives <- Directive.selection <$> directives directives'
fragments' <- gets fragments
- fragment <- maybe lookupDefinition liftJust (HashMap.lookup name fragments')
- pure $ fragment <$ spreadDirectives
- where
- lookupDefinition = do
+
fragmentDefinitions' <- gets fragmentDefinitions
- found <- lift . lift $ HashMap.lookup name fragmentDefinitions'
- fragmentDefinition found
+ case HashMap.lookup name fragments' of
+ Just definition -> lift $ pure $ definition <$ spreadDirectives
+ Nothing
+ | Just definition <- HashMap.lookup name fragmentDefinitions' -> do
+ fragDef <- fragmentDefinition definition
+ case fragDef of
+ Just fragment -> lift $ pure $ fragment <$ spreadDirectives
+ _ -> lift $ pure Nothing
+ | otherwise -> lift $ pure Nothing
selection (Full.InlineFragment type' directives' selections) = do
fragmentDirectives <- Directive.selection <$> directives directives'
case fragmentDirectives of
Nothing -> pure $ Left mempty
_ -> do
fragmentSelectionSet <- appendSelection selections
- pure $ maybe Left selectionFragment type' fragmentSelectionSet
+
+ case type' of
+ Nothing -> pure $ Left fragmentSelectionSet
+ Just typeName -> do
+ typeCondition' <- lookupTypeCondition typeName
+ case typeCondition' of
+ Just typeCondition -> pure $
+ selectionFragment typeCondition fragmentSelectionSet
+ Nothing -> pure $ Left mempty
where
selectionFragment typeName = Right
- . Core.SelectionFragment
- . Core.Fragment typeName
+ . SelectionFragment
+ . Fragment typeName
-appendSelection ::
- Traversable t =>
- t Full.Selection ->
- TransformT (Seq Core.Selection)
+appendSelection :: Traversable t
+ => t Full.Selection
+ -> State (Replacement m) (Seq (Selection m))
appendSelection = foldM go mempty
where
go acc sel = append acc <$> selection sel
append acc (Left list) = list >< acc
append acc (Right one) = one <| acc
-directives :: [Full.Directive] -> TransformT [Core.Directive]
+directives :: [Full.Directive] -> State (Replacement m) [Directive]
directives = traverse directive
where
- directive (Full.Directive directiveName directiveArguments) =
- Core.Directive directiveName <$> arguments directiveArguments
+ directive (Full.Directive directiveName directiveArguments)
+ = Directive directiveName . Arguments
+ <$> foldM go HashMap.empty directiveArguments
+ go arguments (Full.Argument name value') = do
+ substitutedValue <- value value'
+ return $ HashMap.insert name substitutedValue arguments
-- * Fragment replacement
-- | Extract fragment definitions into a single 'HashMap'.
-collectFragments :: TransformT ()
+collectFragments :: State (Replacement m) ()
collectFragments = do
fragDefs <- gets fragmentDefinitions
let nextValue = head $ HashMap.elems fragDefs
@@ -127,41 +358,79 @@ collectFragments = do
_ <- fragmentDefinition nextValue
collectFragments
-fragmentDefinition ::
- Full.FragmentDefinition ->
- TransformT Core.Fragment
+lookupTypeCondition :: Full.Name -> State (Replacement m) (Maybe (CompositeType m))
+lookupTypeCondition type' = do
+ types' <- gets types
+ case HashMap.lookup type' types' of
+ Just (ObjectType objectType) ->
+ lift $ pure $ Just $ CompositeObjectType objectType
+ Just (UnionType unionType) ->
+ lift $ pure $ Just $ CompositeUnionType unionType
+ Just (InterfaceType interfaceType) ->
+ lift $ pure $ Just $ CompositeInterfaceType interfaceType
+ _ -> lift $ pure Nothing
+
+fragmentDefinition
+ :: Full.FragmentDefinition
+ -> State (Replacement m) (Maybe (Fragment m))
fragmentDefinition (Full.FragmentDefinition name type' _ selections) = do
modify deleteFragmentDefinition
fragmentSelection <- appendSelection selections
- let newValue = Core.Fragment type' fragmentSelection
- modify $ insertFragment newValue
- liftJust newValue
+ compositeType <- lookupTypeCondition type'
+
+ case compositeType of
+ Just compositeType' -> do
+ let newValue = Fragment compositeType' fragmentSelection
+ modify $ insertFragment newValue
+ lift $ pure $ Just newValue
+ _ -> lift $ pure Nothing
+ where
+ deleteFragmentDefinition replacement@Replacement{..} =
+ let newDefinitions = HashMap.delete name fragmentDefinitions
+ in replacement{ fragmentDefinitions = newDefinitions }
+ insertFragment newValue replacement@Replacement{..} =
+ let newFragments = HashMap.insert name newValue fragments
+ in replacement{ fragments = newFragments }
+
+value :: forall m. Full.Value -> State (Replacement m) Type.Value
+value (Full.Variable name) =
+ gets (fromMaybe Type.Null . HashMap.lookup name . variableValues)
+value (Full.Int int) = pure $ Type.Int int
+value (Full.Float float) = pure $ Type.Float float
+value (Full.String string) = pure $ Type.String string
+value (Full.Boolean boolean) = pure $ Type.Boolean boolean
+value Full.Null = pure Type.Null
+value (Full.Enum enum) = pure $ Type.Enum enum
+value (Full.List list) = Type.List <$> traverse value list
+value (Full.Object object) =
+ Type.Object . HashMap.fromList <$> traverse objectField object
where
- deleteFragmentDefinition (Replacement fragments' fragmentDefinitions') =
- Replacement fragments' $ HashMap.delete name fragmentDefinitions'
- insertFragment newValue (Replacement fragments' fragmentDefinitions') =
- let newFragments = HashMap.insert name newValue fragments'
- in Replacement newFragments fragmentDefinitions'
-
-arguments :: [Full.Argument] -> TransformT Core.Arguments
-arguments = fmap Core.Arguments . foldM go HashMap.empty
+ objectField (Full.ObjectField name value') = (name,) <$> value value'
+
+input :: forall m. Full.Value -> State (Replacement m) (Maybe Input)
+input (Full.Variable name) =
+ gets (fmap Variable . HashMap.lookup name . variableValues)
+input (Full.Int int) = pure $ pure $ Int int
+input (Full.Float float) = pure $ pure $ Float float
+input (Full.String string) = pure $ pure $ String string
+input (Full.Boolean boolean) = pure $ pure $ Boolean boolean
+input Full.Null = pure $ pure Null
+input (Full.Enum enum) = pure $ pure $ Enum enum
+input (Full.List list) = pure . List <$> traverse value list
+input (Full.Object object) = do
+ objectFields <- foldM objectField HashMap.empty object
+ pure $ pure $ Object objectFields
where
- go arguments' (Full.Argument name value') = do
- substitutedValue <- value value'
- return $ HashMap.insert name substitutedValue arguments'
-
-value :: Full.Value -> TransformT Core.Value
-value (Full.Variable name) = lift (asks $ HashMap.lookup name) >>= lift . lift
-value (Full.Int i) = pure $ Core.Int i
-value (Full.Float f) = pure $ Core.Float f
-value (Full.String x) = pure $ Core.String x
-value (Full.Boolean b) = pure $ Core.Boolean b
-value Full.Null = pure Core.Null
-value (Full.Enum e) = pure $ Core.Enum e
-value (Full.List l) =
- Core.List <$> traverse value l
-value (Full.Object o) =
- Core.Object . HashMap.fromList <$> traverse objectField o
-
-objectField :: Full.ObjectField -> TransformT (Core.Name, Core.Value)
-objectField (Full.ObjectField name value') = (name,) <$> value value'
+ objectField resultMap (Full.ObjectField name value') =
+ inputField resultMap name value'
+
+inputField :: forall m
+ . HashMap Full.Name Input
+ -> Full.Name
+ -> Full.Value
+ -> State (Replacement m) (HashMap Full.Name Input)
+inputField resultMap name value' = do
+ objectFieldValue <- input value'
+ case objectFieldValue of
+ Just fieldValue -> pure $ HashMap.insert name fieldValue resultMap
+ Nothing -> pure resultMap
diff --git a/src/Language/GraphQL/Schema.hs b/src/Language/GraphQL/Schema.hs
deleted file mode 100644
index c678e48..0000000
--- a/src/Language/GraphQL/Schema.hs
+++ /dev/null
@@ -1,138 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-
--- | This module provides a representation of a @GraphQL@ Schema in addition to
--- functions for defining and manipulating schemas.
-module Language.GraphQL.Schema
- ( Resolver(..)
- , Subs
- , object
- , resolve
- , resolversToMap
- , scalar
- , wrappedObject
- , wrappedScalar
- -- * AST Reexports
- , Field
- , Value(..)
- ) where
-
-import Control.Monad.Trans.Class (lift)
-import Control.Monad.Trans.Except (runExceptT)
-import Control.Monad.Trans.Reader (runReaderT)
-import Data.Foldable (fold, toList)
-import Data.Maybe (fromMaybe)
-import qualified Data.Aeson as Aeson
-import Data.HashMap.Strict (HashMap)
-import qualified Data.HashMap.Strict as HashMap
-import Data.Sequence (Seq)
-import Data.Text (Text)
-import qualified Data.Text as T
-import Language.GraphQL.AST.Core
-import Language.GraphQL.Error
-import Language.GraphQL.Trans
-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
- Text -- ^ Name
- (Field -> CollectErrsT m Aeson.Object) -- ^ Resolver
-
--- | Converts resolvers to a map.
-resolversToMap
- :: (Foldable f, Functor f)
- => f (Resolver m)
- -> HashMap Text (Field -> CollectErrsT m Aeson.Object)
-resolversToMap = HashMap.fromList . toList . fmap toKV
- where
- toKV (Resolver name f) = (name, f)
-
--- | Contains variables for the query. The key of the map is a variable name,
--- and the value is the variable value.
-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 f = Resolver name $ resolveFieldValue f resolveRight
- where
- resolveRight fld@(Field _ _ _ flds) resolver
- = withField (resolve (resolversToMap resolver) flds) fld
-
--- | Like 'object' but can be null or a list of objects.
-wrappedObject ::
- Monad m =>
- Name ->
- ActionT m (Type.Wrapping [Resolver m]) ->
- Resolver m
-wrappedObject name f = Resolver name $ resolveFieldValue f resolveRight
- where
- resolveRight fld@(Field _ _ _ sels) resolver
- = withField (traverse (resolveMap sels) resolver) fld
- resolveMap = flip (resolve . 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 f = Resolver name $ resolveFieldValue f resolveRight
- where
- resolveRight fld result = withField (return result) fld
-
--- | 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 f = Resolver name $ resolveFieldValue f resolveRight
- where
- resolveRight fld (Type.Named result) = withField (return result) fld
- resolveRight fld Type.Null
- = return $ HashMap.singleton (aliasOrName fld) Aeson.Null
- resolveRight fld (Type.List result) = withField (return result) fld
-
-resolveFieldValue ::
- Monad m =>
- ActionT m a ->
- (Field -> a -> CollectErrsT m Aeson.Object) ->
- Field ->
- CollectErrsT m (HashMap Text Aeson.Value)
-resolveFieldValue f resolveRight fld@(Field _ _ args _) = do
- result <- lift $ reader . runExceptT . runActionT $ f
- either resolveLeft (resolveRight fld) result
- where
- reader = flip runReaderT $ Context {arguments=args}
- resolveLeft err = do
- _ <- addErrMsg err
- return $ HashMap.singleton (aliasOrName fld) Aeson.Null
-
--- | Helper function to facilitate error handling and result emitting.
-withField :: (Monad m, Aeson.ToJSON a)
- => CollectErrsT m a -> Field -> CollectErrsT m (HashMap Text Aeson.Value)
-withField v fld
- = HashMap.singleton (aliasOrName fld) . Aeson.toJSON <$> runAppendErrs v
-
--- | Takes a list of 'Resolver's and a list of 'Field's and applies each
--- 'Resolver' to each 'Field'. Resolves into a value containing the
--- resolved 'Field', or a null value and error information.
-resolve :: Monad m
- => HashMap Text (Field -> CollectErrsT m Aeson.Object)
- -> Seq Selection
- -> CollectErrsT m Aeson.Value
-resolve resolvers = fmap (Aeson.toJSON . fold) . traverse tryResolvers
- where
- resolveTypeName f = do
- value <- f $ Field Nothing "__typename" mempty mempty
- return $ HashMap.lookupDefault "" "__typename" value
- tryResolvers (SelectionField fld@(Field _ name _ _))
- = fromMaybe (errmsg fld) $ HashMap.lookup name resolvers <*> Just fld
- tryResolvers (SelectionFragment (Fragment typeCondition selections')) = do
- that <- traverse resolveTypeName $ HashMap.lookup "__typename" resolvers
- if maybe True (Aeson.String typeCondition ==) that
- then fmap fold . traverse tryResolvers $ selections'
- else return mempty
- errmsg fld@(Field _ name _ _) = do
- addErrMsg $ T.unwords ["field", name, "not resolved."]
- return $ HashMap.singleton (aliasOrName fld) Aeson.Null
-
-aliasOrName :: Field -> Text
-aliasOrName (Field alias name _ _) = fromMaybe name alias
diff --git a/src/Language/GraphQL/Trans.hs b/src/Language/GraphQL/Trans.hs
index 09c012b..fa7718a 100644
--- a/src/Language/GraphQL/Trans.hs
+++ b/src/Language/GraphQL/Trans.hs
@@ -1,8 +1,8 @@
-- | Monad transformer stack used by the @GraphQL@ resolvers.
module Language.GraphQL.Trans
- ( ActionT(..)
+ ( argument
+ , ActionT(..)
, Context(..)
- , argument
) where
import Control.Applicative (Alternative(..))
@@ -14,12 +14,15 @@ import Control.Monad.Trans.Reader (ReaderT, asks)
import qualified Data.HashMap.Strict as HashMap
import Data.Maybe (fromMaybe)
import Data.Text (Text)
+import Language.GraphQL.AST (Name)
import Language.GraphQL.AST.Core
+import Language.GraphQL.Type.Definition
import Prelude hiding (lookup)
-- | Resolution context holds resolver arguments.
-newtype Context = Context
+data Context = Context
{ arguments :: Arguments
+ , values :: Value
}
-- | Monad transformer stack used by the resolvers to provide error handling
@@ -54,7 +57,7 @@ instance Monad m => MonadPlus (ActionT m) where
mplus = (<|>)
-- | Retrieves an argument by its name. If the argument with this name couldn't
--- be found, returns 'Value.Null' (i.e. the argument is assumed to
+-- be found, returns 'Null' (i.e. the argument is assumed to
-- be optional then).
argument :: Monad m => Name -> ActionT m Value
argument argumentName = do
diff --git a/src/Language/GraphQL/Type.hs b/src/Language/GraphQL/Type.hs
index c8a9997..5dfd622 100644
--- a/src/Language/GraphQL/Type.hs
+++ b/src/Language/GraphQL/Type.hs
@@ -1,55 +1,16 @@
--- | Definitions for @GraphQL@ input types.
+-- | Reexports non-conflicting type system and schema definitions.
module Language.GraphQL.Type
- ( Wrapping(..)
+ ( In.InputField(..)
+ , In.InputObjectType(..)
+ , Out.Field(..)
+ , Out.InterfaceType(..)
+ , Out.ObjectType(..)
+ , Out.UnionType(..)
+ , module Language.GraphQL.Type.Definition
+ , module Language.GraphQL.Type.Schema
) where
-import Data.Aeson as Aeson (ToJSON, toJSON)
-import qualified Data.Aeson as Aeson
-
--- | GraphQL distinguishes between "wrapping" and "named" types. Each wrapping
--- type can wrap other wrapping or named types. Wrapping types are lists and
--- Non-Null types (named types are nullable by default).
---
--- This 'Wrapping' type doesn\'t reflect this distinction exactly but it is
--- used in the resolvers to take into account that the returned value can be
--- nullable or an (arbitrary nested) list.
-data Wrapping a
- = List [Wrapping a] -- ^ Arbitrary nested list
- | Named a -- ^ Named type without further wrapping
- | Null -- ^ Null
- deriving (Eq, Show)
-
-instance Functor Wrapping where
- fmap f (List list) = List $ fmap (fmap f) list
- fmap f (Named named) = Named $ f named
- fmap _ Null = Null
-
-instance Foldable Wrapping where
- foldr f acc (List list) = foldr (flip $ foldr f) acc list
- foldr f acc (Named named) = f named acc
- foldr _ acc Null = acc
-
-instance Traversable Wrapping where
- traverse f (List list) = List <$> traverse (traverse f) list
- traverse f (Named named) = Named <$> f named
- traverse _ Null = pure Null
-
-instance Applicative Wrapping where
- pure = Named
- Null <*> _ = Null
- _ <*> Null = Null
- (Named f) <*> (Named x) = Named $ f x
- (List fs) <*> (List xs) = List $ (<*>) <$> fs <*> xs
- (Named f) <*> list = f <$> list
- (List fs) <*> named = List $ (<*> named) <$> fs
-
-instance Monad Wrapping where
- return = pure
- Null >>= _ = Null
- (Named x) >>= f = f x
- (List xs) >>= f = List $ fmap (>>= f) xs
-
-instance ToJSON a => ToJSON (Wrapping a) where
- toJSON (List list) = toJSON list
- toJSON (Named named) = toJSON named
- toJSON Null = Aeson.Null
+import Language.GraphQL.Type.Definition
+import Language.GraphQL.Type.Schema (Schema(..))
+import qualified Language.GraphQL.Type.In as In
+import qualified Language.GraphQL.Type.Out as Out
diff --git a/src/Language/GraphQL/Type/Definition.hs b/src/Language/GraphQL/Type/Definition.hs
new file mode 100644
index 0000000..1379018
--- /dev/null
+++ b/src/Language/GraphQL/Type/Definition.hs
@@ -0,0 +1,115 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+-- | Types that can be used as both input and output types.
+module Language.GraphQL.Type.Definition
+ ( EnumType(..)
+ , EnumValue(..)
+ , ScalarType(..)
+ , Subs
+ , Value(..)
+ , boolean
+ , float
+ , id
+ , int
+ , string
+ ) where
+
+import Data.Int (Int32)
+import Data.HashMap.Strict (HashMap)
+import Data.String (IsString(..))
+import Data.Text (Text)
+import Language.GraphQL.AST.Document (Name)
+import Prelude hiding (id)
+
+-- | Represents accordingly typed GraphQL values.
+data Value
+ = Int Int32
+ | Float Double -- ^ GraphQL Float is double precision.
+ | String Text
+ | Boolean Bool
+ | Null
+ | Enum Name
+ | List [Value] -- ^ Arbitrary nested list.
+ | Object (HashMap Name Value)
+ deriving (Eq, Show)
+
+instance IsString Value where
+ fromString = String . fromString
+
+-- | Contains variables for the query. The key of the map is a variable name,
+-- and the value is the variable value.
+type Subs = HashMap Name 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)
+
+instance Eq ScalarType where
+ (ScalarType this _) == (ScalarType that _) = this == that
+
+-- | 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) (HashMap Name EnumValue)
+
+instance Eq EnumType where
+ (EnumType this _ _) == (EnumType that _ _) = this == that
+
+-- | Enum value is a single member of an 'EnumType'.
+newtype EnumValue = EnumValue (Maybe Text)
+
+-- | 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."
diff --git a/src/Language/GraphQL/Type/Directive.hs b/src/Language/GraphQL/Type/Directive.hs
index afd97da..017132c 100644
--- a/src/Language/GraphQL/Type/Directive.hs
+++ b/src/Language/GraphQL/Type/Directive.hs
@@ -1,11 +1,18 @@
{-# LANGUAGE OverloadedStrings #-}
module Language.GraphQL.Type.Directive
- ( selection
+ ( Directive(..)
+ , selection
) where
import qualified Data.HashMap.Strict as HashMap
+import Language.GraphQL.AST (Name)
import Language.GraphQL.AST.Core
+import Language.GraphQL.Type.Definition
+
+-- | Directive.
+data Directive = Directive Name Arguments
+ deriving (Eq, Show)
-- | Directive processing status.
data Status
diff --git a/src/Language/GraphQL/Type/In.hs b/src/Language/GraphQL/Type/In.hs
new file mode 100644
index 0000000..36e0e2c
--- /dev/null
+++ b/src/Language/GraphQL/Type/In.hs
@@ -0,0 +1,101 @@
+{-# LANGUAGE PatternSynonyms #-}
+{-# LANGUAGE ViewPatterns #-}
+
+-- | Input types and values.
+--
+-- This module is intended to be imported qualified, to avoid name clashes
+-- with 'Language.GraphQL.Type.Out'.
+module Language.GraphQL.Type.In
+ ( Argument(..)
+ , InputField(..)
+ , InputObjectType(..)
+ , Type(..)
+ , isNonNullType
+ , pattern EnumBaseType
+ , pattern ListBaseType
+ , pattern InputObjectBaseType
+ , pattern ScalarBaseType
+ ) where
+
+import Data.HashMap.Strict (HashMap)
+import Data.Text (Text)
+import Language.GraphQL.AST.Document (Name)
+import Language.GraphQL.Type.Definition
+
+-- | Single field of an 'InputObjectType'.
+data InputField = InputField (Maybe Text) Type (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)
+
+instance Eq InputObjectType where
+ (InputObjectType this _ _) == (InputObjectType that _ _) = this == that
+
+-- | These types may be used as input types for arguments and directives.
+--
+-- GraphQL distinguishes between "wrapping" and "named" types. Each wrapping
+-- type can wrap other wrapping or named types. Wrapping types are lists and
+-- Non-Null types (named types are nullable by default).
+data Type
+ = NamedScalarType ScalarType
+ | NamedEnumType EnumType
+ | NamedInputObjectType InputObjectType
+ | ListType Type
+ | NonNullScalarType ScalarType
+ | NonNullEnumType EnumType
+ | NonNullInputObjectType InputObjectType
+ | NonNullListType Type
+ deriving Eq
+
+-- | Field argument definition.
+data Argument = Argument (Maybe Text) Type (Maybe Value)
+
+-- | Matches either 'NamedScalarType' or 'NonNullScalarType'.
+pattern ScalarBaseType :: ScalarType -> Type
+pattern ScalarBaseType scalarType <- (isScalarType -> Just scalarType)
+
+-- | Matches either 'NamedEnumType' or 'NonNullEnumType'.
+pattern EnumBaseType :: EnumType -> Type
+pattern EnumBaseType enumType <- (isEnumType -> Just enumType)
+
+-- | Matches either 'NamedInputObjectType' or 'NonNullInputObjectType'.
+pattern InputObjectBaseType :: InputObjectType -> Type
+pattern InputObjectBaseType objectType <- (isInputObjectType -> Just objectType)
+
+-- | Matches either 'ListType' or 'NonNullListType'.
+pattern ListBaseType :: Type -> Type
+pattern ListBaseType listType <- (isListType -> Just listType)
+
+{-# COMPLETE EnumBaseType, ListBaseType, InputObjectBaseType, ScalarBaseType #-}
+
+isScalarType :: Type -> Maybe ScalarType
+isScalarType (NamedScalarType inputType) = Just inputType
+isScalarType (NonNullScalarType inputType) = Just inputType
+isScalarType _ = Nothing
+
+isInputObjectType :: Type -> Maybe InputObjectType
+isInputObjectType (NamedInputObjectType inputType) = Just inputType
+isInputObjectType (NonNullInputObjectType inputType) = Just inputType
+isInputObjectType _ = Nothing
+
+isEnumType :: Type -> Maybe EnumType
+isEnumType (NamedEnumType inputType) = Just inputType
+isEnumType (NonNullEnumType inputType) = Just inputType
+isEnumType _ = Nothing
+
+isListType :: Type -> Maybe Type
+isListType (ListType inputType) = Just inputType
+isListType (NonNullListType inputType) = Just inputType
+isListType _ = Nothing
+
+-- | Checks whether the given input type is a non-null type.
+isNonNullType :: Type -> Bool
+isNonNullType (NonNullScalarType _) = True
+isNonNullType (NonNullEnumType _) = True
+isNonNullType (NonNullInputObjectType _) = True
+isNonNullType (NonNullListType _) = True
+isNonNullType _ = False
diff --git a/src/Language/GraphQL/Type/Out.hs b/src/Language/GraphQL/Type/Out.hs
new file mode 100644
index 0000000..856c4f8
--- /dev/null
+++ b/src/Language/GraphQL/Type/Out.hs
@@ -0,0 +1,168 @@
+{-# LANGUAGE ExplicitForAll #-}
+{-# LANGUAGE PatternSynonyms #-}
+{-# LANGUAGE ViewPatterns #-}
+
+-- | Output types and values.
+--
+-- This module is intended to be imported qualified, to avoid name clashes
+-- with 'Language.GraphQL.Type.In'.
+module Language.GraphQL.Type.Out
+ ( Field(..)
+ , InterfaceType(..)
+ , ObjectType(..)
+ , Resolver(..)
+ , Type(..)
+ , UnionType(..)
+ , isNonNullType
+ , pattern EnumBaseType
+ , pattern InterfaceBaseType
+ , pattern ListBaseType
+ , pattern ObjectBaseType
+ , pattern ScalarBaseType
+ , pattern UnionBaseType
+ ) where
+
+import Data.HashMap.Strict (HashMap)
+import Data.Text (Text)
+import Language.GraphQL.AST (Name)
+import Language.GraphQL.Trans
+import Language.GraphQL.Type.Definition
+import qualified Language.GraphQL.Type.In as In
+
+-- | 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'.
+--
+-- 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 Resolver m = Resolver (Field m) (ActionT m Value)
+
+-- | 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 (Maybe Text) [InterfaceType m] (HashMap Name (Resolver m))
+
+instance forall a. Eq (ObjectType a) where
+ (ObjectType this _ _ _) == (ObjectType that _ _ _) = this == that
+
+-- | Interface Type Definition.
+--
+-- When a field can return one of a heterogeneous set of types, a Interface type
+-- is used to describe what types are possible, and what fields are in common
+-- across all types.
+data InterfaceType m = InterfaceType
+ Name (Maybe Text) [InterfaceType m] (HashMap Name (Field m))
+
+instance forall a. Eq (InterfaceType a) where
+ (InterfaceType this _ _ _) == (InterfaceType that _ _ _) = this == that
+
+-- | Union Type Definition.
+--
+-- When a field can return one of a heterogeneous set of types, a Union type is
+-- used to describe what types are possible.
+data UnionType m = UnionType Name (Maybe Text) [ObjectType m]
+
+instance forall a. Eq (UnionType a) where
+ (UnionType this _ _) == (UnionType that _ _) = this == that
+
+-- | Output object field definition.
+data Field m = Field
+ (Maybe Text) -- ^ Description.
+ (Type m) -- ^ Field type.
+ (HashMap Name In.Argument) -- ^ Arguments.
+
+-- | These types may be used as output types as the result of fields.
+--
+-- GraphQL distinguishes between "wrapping" and "named" types. Each wrapping
+-- type can wrap other wrapping or named types. Wrapping types are lists and
+-- Non-Null types (named types are nullable by default).
+data Type m
+ = NamedScalarType ScalarType
+ | NamedEnumType EnumType
+ | NamedObjectType (ObjectType m)
+ | NamedInterfaceType (InterfaceType m)
+ | NamedUnionType (UnionType m)
+ | ListType (Type m)
+ | NonNullScalarType ScalarType
+ | NonNullEnumType EnumType
+ | NonNullObjectType (ObjectType m)
+ | NonNullInterfaceType (InterfaceType m)
+ | NonNullUnionType (UnionType m)
+ | NonNullListType (Type m)
+ deriving Eq
+
+-- | Matches either 'NamedScalarType' or 'NonNullScalarType'.
+pattern ScalarBaseType :: forall m. ScalarType -> Type m
+pattern ScalarBaseType scalarType <- (isScalarType -> Just scalarType)
+
+-- | Matches either 'NamedEnumType' or 'NonNullEnumType'.
+pattern EnumBaseType :: forall m. EnumType -> Type m
+pattern EnumBaseType enumType <- (isEnumType -> Just enumType)
+
+-- | Matches either 'NamedObjectType' or 'NonNullObjectType'.
+pattern ObjectBaseType :: forall m. ObjectType m -> Type m
+pattern ObjectBaseType objectType <- (isObjectType -> Just objectType)
+
+-- | Matches either 'NamedInterfaceType' or 'NonNullInterfaceType'.
+pattern InterfaceBaseType :: forall m. InterfaceType m -> Type m
+pattern InterfaceBaseType interfaceType <-
+ (isInterfaceType -> Just interfaceType)
+
+-- | Matches either 'NamedUnionType' or 'NonNullUnionType'.
+pattern UnionBaseType :: forall m. UnionType m -> Type m
+pattern UnionBaseType unionType <- (isUnionType -> Just unionType)
+
+-- | Matches either 'ListType' or 'NonNullListType'.
+pattern ListBaseType :: forall m. Type m -> Type m
+pattern ListBaseType listType <- (isListType -> Just listType)
+
+{-# COMPLETE ScalarBaseType
+ , EnumBaseType
+ , ObjectBaseType
+ , ListBaseType
+ , InterfaceBaseType
+ , UnionBaseType
+ #-}
+
+isScalarType :: forall m. Type m -> Maybe ScalarType
+isScalarType (NamedScalarType outputType) = Just outputType
+isScalarType (NonNullScalarType outputType) = Just outputType
+isScalarType _ = Nothing
+
+isObjectType :: forall m. Type m -> Maybe (ObjectType m)
+isObjectType (NamedObjectType outputType) = Just outputType
+isObjectType (NonNullObjectType outputType) = Just outputType
+isObjectType _ = Nothing
+
+isEnumType :: forall m. Type m -> Maybe EnumType
+isEnumType (NamedEnumType outputType) = Just outputType
+isEnumType (NonNullEnumType outputType) = Just outputType
+isEnumType _ = Nothing
+
+isInterfaceType :: forall m. Type m -> Maybe (InterfaceType m)
+isInterfaceType (NamedInterfaceType interfaceType) = Just interfaceType
+isInterfaceType (NonNullInterfaceType interfaceType) = Just interfaceType
+isInterfaceType _ = Nothing
+
+isUnionType :: forall m. Type m -> Maybe (UnionType m)
+isUnionType (NamedUnionType unionType) = Just unionType
+isUnionType (NonNullUnionType unionType) = Just unionType
+isUnionType _ = Nothing
+
+isListType :: forall m. Type m -> Maybe (Type m)
+isListType (ListType outputType) = Just outputType
+isListType (NonNullListType outputType) = Just outputType
+isListType _ = Nothing
+
+-- | Checks whether the given output type is a non-null type.
+isNonNullType :: forall m. Type m -> Bool
+isNonNullType (NonNullScalarType _) = True
+isNonNullType (NonNullEnumType _) = True
+isNonNullType (NonNullObjectType _) = True
+isNonNullType (NonNullInterfaceType _) = True
+isNonNullType (NonNullUnionType _) = True
+isNonNullType (NonNullListType _) = True
+isNonNullType _ = False
diff --git a/src/Language/GraphQL/Type/Schema.hs b/src/Language/GraphQL/Type/Schema.hs
new file mode 100644
index 0000000..4d7b9eb
--- /dev/null
+++ b/src/Language/GraphQL/Type/Schema.hs
@@ -0,0 +1,112 @@
+{-# LANGUAGE ExplicitForAll #-}
+
+-- | This module provides a representation of a @GraphQL@ Schema in addition to
+-- functions for defining and manipulating schemas.
+module Language.GraphQL.Type.Schema
+ ( AbstractType(..)
+ , CompositeType(..)
+ , Schema(..)
+ , Type(..)
+ , collectReferencedTypes
+ ) where
+
+import Data.HashMap.Strict (HashMap)
+import qualified Data.HashMap.Strict as HashMap
+import Language.GraphQL.AST.Document (Name)
+import qualified Language.GraphQL.Type.Definition as Definition
+import qualified Language.GraphQL.Type.In as In
+import qualified Language.GraphQL.Type.Out as Out
+
+-- | These are all of the possible kinds of types.
+data Type m
+ = ScalarType Definition.ScalarType
+ | EnumType Definition.EnumType
+ | ObjectType (Out.ObjectType m)
+ | InputObjectType In.InputObjectType
+ | InterfaceType (Out.InterfaceType m)
+ | UnionType (Out.UnionType m)
+ deriving Eq
+
+-- | These types may describe the parent context of a selection set.
+data CompositeType m
+ = CompositeUnionType (Out.UnionType m)
+ | CompositeObjectType (Out.ObjectType m)
+ | CompositeInterfaceType (Out.InterfaceType m)
+ deriving Eq
+
+-- | These types may describe the parent context of a selection set.
+data AbstractType m
+ = AbstractUnionType (Out.UnionType m)
+ | AbstractInterfaceType (Out.InterfaceType m)
+ deriving Eq
+
+-- | 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 :: Out.ObjectType m
+ , mutation :: Maybe (Out.ObjectType m)
+ }
+
+-- | Traverses the schema and finds all referenced types.
+collectReferencedTypes :: forall m. Schema m -> HashMap Name (Type m)
+collectReferencedTypes schema =
+ let queryTypes = traverseObjectType (query schema) HashMap.empty
+ in maybe queryTypes (`traverseObjectType` queryTypes) $ mutation schema
+ where
+ collect traverser typeName element foundTypes
+ | HashMap.member typeName foundTypes = foundTypes
+ | otherwise = traverser $ HashMap.insert typeName element foundTypes
+ visitFields (Out.Field _ outputType arguments) foundTypes
+ = traverseOutputType outputType
+ $ foldr visitArguments foundTypes arguments
+ visitArguments (In.Argument _ inputType _) = traverseInputType inputType
+ visitInputFields (In.InputField _ inputType _) = traverseInputType inputType
+ traverseInputType (In.InputObjectBaseType objectType) =
+ let (In.InputObjectType typeName _ inputFields) = objectType
+ element = InputObjectType objectType
+ traverser = flip (foldr visitInputFields) inputFields
+ in collect traverser typeName element
+ traverseInputType (In.ListBaseType listType) =
+ traverseInputType listType
+ traverseInputType (In.ScalarBaseType scalarType) =
+ let (Definition.ScalarType typeName _) = scalarType
+ in collect Prelude.id typeName (ScalarType scalarType)
+ traverseInputType (In.EnumBaseType enumType) =
+ let (Definition.EnumType typeName _ _) = enumType
+ in collect Prelude.id typeName (EnumType enumType)
+ traverseOutputType (Out.ObjectBaseType objectType) =
+ traverseObjectType objectType
+ traverseOutputType (Out.InterfaceBaseType interfaceType) =
+ traverseInterfaceType interfaceType
+ traverseOutputType (Out.UnionBaseType unionType) =
+ let (Out.UnionType typeName _ types) = unionType
+ traverser = flip (foldr traverseObjectType) types
+ in collect traverser typeName (UnionType unionType)
+ traverseOutputType (Out.ListBaseType listType) =
+ traverseOutputType listType
+ traverseOutputType (Out.ScalarBaseType scalarType) =
+ let (Definition.ScalarType typeName _) = scalarType
+ in collect Prelude.id typeName (ScalarType scalarType)
+ traverseOutputType (Out.EnumBaseType enumType) =
+ let (Definition.EnumType typeName _ _) = enumType
+ in collect Prelude.id typeName (EnumType enumType)
+ traverseObjectType objectType foundTypes =
+ let (Out.ObjectType typeName _ interfaces resolvers) = objectType
+ element = ObjectType objectType
+ fields = extractObjectField <$> resolvers
+ traverser = polymorphicTraverser interfaces fields
+ in collect traverser typeName element foundTypes
+ traverseInterfaceType interfaceType foundTypes =
+ let (Out.InterfaceType typeName _ interfaces fields) = interfaceType
+ element = InterfaceType interfaceType
+ traverser = polymorphicTraverser interfaces fields
+ in collect traverser typeName element foundTypes
+ polymorphicTraverser interfaces fields
+ = flip (foldr visitFields) fields
+ . flip (foldr traverseInterfaceType) interfaces
+ extractObjectField (Out.Resolver field _) = field