packages feed

graphql 0.11.0.0 → 0.11.1.0

raw patch · 15 files changed

+1779/−785 lines, 15 filesdep ~QuickCheckdep ~aesondep ~conduitPVP ok

version bump matches the API change (PVP)

Dependency ranges changed: QuickCheck, aeson, conduit, hspec, hspec-megaparsec, megaparsec, text, unordered-containers

API changes (from Hackage documentation)

+ Language.GraphQL.AST.Document: escape :: Char -> String
+ Language.GraphQL.AST.Document: instance GHC.Base.Functor Language.GraphQL.AST.Document.ObjectField
+ Language.GraphQL.Type.In: instance GHC.Show.Show Language.GraphQL.Type.In.InputObjectType
+ Language.GraphQL.Type.In: instance GHC.Show.Show Language.GraphQL.Type.In.Type
+ Language.GraphQL.Type.Out: instance GHC.Show.Show (Language.GraphQL.Type.Out.InterfaceType a)
+ Language.GraphQL.Type.Out: instance GHC.Show.Show (Language.GraphQL.Type.Out.ObjectType a)
+ Language.GraphQL.Type.Out: instance GHC.Show.Show (Language.GraphQL.Type.Out.Type a)
+ Language.GraphQL.Type.Out: instance GHC.Show.Show (Language.GraphQL.Type.Out.UnionType a)
+ Language.GraphQL.Type.Schema: implementations :: forall m. Schema m -> HashMap Name [Type m]
+ Language.GraphQL.Validate: overlappingFieldsCanBeMergedRule :: Rule m
+ Language.GraphQL.Validate: possibleFragmentSpreadsRule :: forall m. Rule m
+ Language.GraphQL.Validate: valuesOfCorrectTypeRule :: forall m. Rule m
+ Language.GraphQL.Validate: variablesInAllowedPositionRule :: forall m. Rule m

Files

CHANGELOG.md view
@@ -6,6 +6,19 @@ and this project adheres to [Haskell Package Versioning Policy](https://pvp.haskell.org/). +## [0.11.1.0] - 2021-02-07+### Added+- `Validate.Rules`:+  - `overlappingFieldsCanBeMergedRule`+  - `possibleFragmentSpreadsRule`+  - `variablesInAllowedPositionRule`+  - `valuesOfCorrectTypeRule`+- `Type.Schema.implementations` contains a map from interfaces and objects to+  interfaces they implement.+- Show instances for GraphQL type definitions in the `Type` modules.+- Custom Show instances for type and value representations in the AST.+- `AST.Document.escape` escapes a single character in a `StringValue`.+ ## [0.11.0.0] - 2020-11-07 ### Changed - `AST.Document.Selection` wraps additional new types: `Field`, `FragmentSpread`@@ -400,6 +413,7 @@ ### Added - Data types for the GraphQL language. +[0.11.1.0]: https://www.caraus.tech/projects/pub-graphql/repository/23/diff?rev=v0.11.1.0&rev_to=v0.11.0.0 [0.11.0.0]: https://www.caraus.tech/projects/pub-graphql/repository/23/diff?rev=v0.11.0.0&rev_to=v0.10.0.0 [0.10.0.0]: https://www.caraus.tech/projects/pub-graphql/repository/23/diff?rev=v0.10.0.0&rev_to=v0.9.0.0 [0.9.0.0]: https://www.caraus.tech/projects/pub-graphql/repository/23/diff?rev=v0.9.0.0&rev_to=v0.8.0.0
README.md view
@@ -3,29 +3,10 @@ [![Simple Haskell](https://www.simplehaskell.org/badges/badge.svg)](https://www.simplehaskell.org) [![CI/CD](https://img.shields.io/badge/CI-CD-brightgreen)](https://build.caraus.tech/go/pipelines) -This implementation is relatively low-level by design, it doesn't provide any-mappings between the GraphQL types and Haskell's type system and avoids-compile-time magic. It focuses on flexibility instead, so other solutions can-be built on top of it.--## State of the work--For now this library provides:--- Parser for the query and schema languages, as well as a printer for the query-  language (minimizer and pretty-printer).-- Data structures to define a type system.-- Executor (queries, mutations and subscriptions are supported).-- Validation is work in progress.-- Introspection isn't available yet.--But the idea is to be a Haskell port of-[`graphql-js`](https://github.com/graphql/graphql-js).--For a more precise list of currently missing features see-[issues](https://www.caraus.tech/projects/pub-graphql/issues).+See https://www.caraus.tech/projects/pub-graphql. -## Documentation+Report issues on the+[bug tracker](https://www.caraus.tech/projects/pub-graphql/issues).  API documentation is available through [Hackage](https://hackage.haskell.org/package/graphql).
graphql.cabal view
@@ -1,10 +1,10 @@ cabal-version:      2.2 name:               graphql-version:            0.11.0.0+version:            0.11.1.0 license:            MPL-2.0 AND BSD-3-Clause license-file:       LICENSE LICENSE.MPL copyright:-    (c) 2019-2020 Eugen Wissner,+    (c) 2019-2021 Eugen Wissner,     (c) 2015-2017 J. Daniel Navarro  maintainer:         belka@caraus.de@@ -60,24 +60,25 @@      default-language: Haskell2010     build-depends:-        aeson >=1.4.7.1,+        aeson >=1.5.5.1,         base >=4.7 && <5,-        conduit >=1.3.3,+        conduit >=1.3.4,         containers >=0.6.2.1,         exceptions >=0.10.4,         hspec-expectations >=0.8.2,-        megaparsec >=8.0.0,+        megaparsec >=9.0.1,         parser-combinators >=1.2.1,         scientific >=0.3.6.2,-        text >=1.2.4.0,+        text >=1.2.4.1,         transformers >=0.5.6.2,-        unordered-containers >=0.2.10.0+        unordered-containers >=0.2.13.0  test-suite graphql-test     type:             exitcode-stdio-1.0     main-is:          Spec.hs     hs-source-dirs:   tests     other-modules:+        Language.GraphQL.AST.DocumentSpec         Language.GraphQL.AST.EncoderSpec         Language.GraphQL.AST.LexerSpec         Language.GraphQL.AST.ParserSpec@@ -85,7 +86,7 @@         Language.GraphQL.Execute.CoerceSpec         Language.GraphQL.ExecuteSpec         Language.GraphQL.Type.OutSpec-        Language.GraphQL.ValidateSpec+        Language.GraphQL.Validate.RulesSpec         Test.DirectiveSpec         Test.FragmentSpec         Test.RootOperationSpec@@ -95,20 +96,20 @@     default-language: Haskell2010     ghc-options:      -threaded -rtsopts -with-rtsopts=-N     build-depends:-        QuickCheck >=2.13.2,-        aeson >=1.4.7.1,+        QuickCheck >=2.14.2,+        aeson >=1.5.5.1,         base >=4.7 && <5,-        conduit >=1.3.3,+        conduit >=1.3.4,         containers >=0.6.2.1,         exceptions >=0.10.4,         graphql -any,-        hspec >=2.7.4,+        hspec >=2.7.8,         hspec-expectations >=0.8.2,-        hspec-megaparsec >=2.1.0,-        megaparsec >=8.0.0,+        hspec-megaparsec >=2.2.0,+        megaparsec >=9.0.1,         parser-combinators >=1.2.1,         raw-strings-qq >=1.1,         scientific >=0.3.6.2,-        text >=1.2.4.0,+        text >=1.2.4.1,         transformers >=0.5.6.2,-        unordered-containers >=0.2.10.0+        unordered-containers >=0.2.13.0
src/Language/GraphQL/AST/Document.hs view
@@ -1,5 +1,6 @@ {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE ExplicitForAll #-}+{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE Safe #-}@@ -47,11 +48,15 @@     , UnionMemberTypes(..)     , Value(..)     , VariableDefinition(..)+    , escape     ) where +import Data.Char (ord) import Data.Foldable (toList) import Data.Int (Int32)+import Data.List (intercalate) import Data.List.NonEmpty (NonEmpty)+import Numeric (showFloat, showHex) import Data.Text (Text) import qualified Data.Text as Text import Language.GraphQL.AST.DirectiveLocation (DirectiveLocation)@@ -79,8 +84,11 @@ data Node a = Node     { node :: a     , location :: Location-    } deriving (Eq, Show)+    } deriving Eq +instance Show a => Show (Node a) where+    show Node{ node } = show node+ instance Functor Node where     fmap f Node{..} = Node (f node) location @@ -218,6 +226,28 @@  -- ** Input Values +escape :: Char -> String+escape char'+    | char' == '\\' = "\\\\"+    | char' == '\"' = "\\\""+    | char' == '\b' = "\\b"+    | char' == '\f' = "\\f"+    | char' == '\n' = "\\n"+    | char' == '\r' = "\\r"+    | char' == '\t' = "\\t"+    | char' < '\x0010' = unicode  "\\u000" char'+    | char' < '\x0020' = unicode "\\u00" char'+    | otherwise = [char']+  where+    unicode prefix uchar = prefix <> (showHex $ ord uchar) ""++showList' :: Show a => [a] -> String+showList' list = "[" ++ intercalate ", " (show <$> list) ++ "]"++showObject :: Show a => [ObjectField a] -> String+showObject fields =+    "{ " ++ intercalate ", " (show <$> fields) ++ " }"+ -- | Input value (literal or variable). data Value     = Variable Name@@ -229,8 +259,20 @@     | Enum Name     | List [Value]     | Object [ObjectField Value]-    deriving (Eq, Show)+    deriving Eq +instance Show Value where+    showList = mappend . showList'+    show (Variable variableName) = '$' : Text.unpack variableName+    show (Int integer) = show integer+    show (Float float) = show $ ConstFloat float+    show (String text) = show $  ConstString text+    show (Boolean boolean) = show boolean+    show Null = "null"+    show (Enum name) = Text.unpack name+    show (List list) = show list+    show (Object fields) = showObject fields+ -- | Constant input value. data ConstValue     = ConstInt Int32@@ -241,8 +283,19 @@     | ConstEnum Name     | ConstList [ConstValue]     | ConstObject [ObjectField ConstValue]-    deriving (Eq, Show)+    deriving Eq +instance Show ConstValue where+    showList = mappend . showList'+    show (ConstInt integer) = show integer+    show (ConstFloat float) = showFloat float mempty+    show (ConstString text) = "\"" <> Text.foldr (mappend . escape) "\"" text+    show (ConstBoolean boolean) = show boolean+    show ConstNull = "null"+    show (ConstEnum name) = Text.unpack name+    show (ConstList list) = show list+    show (ConstObject fields) = showObject fields+ -- | Key-value pair. -- -- A list of 'ObjectField's represents a GraphQL object type.@@ -250,8 +303,14 @@     { name :: Name     , value :: Node a     , location :: Location-    } deriving (Eq, Show)+    } deriving Eq +instance Show a => Show (ObjectField a) where+    show ObjectField{..} = Text.unpack name ++ ": " ++ show value++instance Functor ObjectField where+    fmap f ObjectField{..} = ObjectField name (f <$> value) location+ -- ** Variables  -- | Variable definition.@@ -281,8 +340,13 @@     = TypeNamed Name     | TypeList Type     | TypeNonNull NonNullType-    deriving (Eq, Show)+    deriving Eq +instance Show Type where+    show (TypeNamed typeName) = Text.unpack typeName+    show (TypeList listType) = concat ["[", show listType, "]"]+    show (TypeNonNull nonNullType) = show nonNullType+ -- | Represents type names. type NamedType = Name @@ -290,7 +354,11 @@ data NonNullType     = NonNullTypeNamed Name     | NonNullTypeList Type-    deriving (Eq, Show)+    deriving Eq++instance Show NonNullType where+    show (NonNullTypeNamed typeName) = '!' : Text.unpack typeName+    show (NonNullTypeList listType) =  concat ["![", show listType, "]"]  -- ** Directives 
src/Language/GraphQL/AST/Encoder.hs view
@@ -16,7 +16,6 @@     , value     ) where -import Data.Char (ord) import Data.Foldable (fold) import qualified Data.List.NonEmpty as NonEmpty import Data.Text (Text)@@ -25,7 +24,7 @@ import qualified Data.Text.Lazy as Lazy.Text 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.Int (decimal) import Data.Text.Lazy.Builder.RealFloat (realFloat) import qualified Language.GraphQL.AST.Document as Full @@ -234,11 +233,12 @@ quote = Builder.singleton '\"'  oneLine :: Text -> Builder-oneLine string = quote <> Text.foldr (mappend . escape) quote string+oneLine string = quote <> Text.foldr merge quote string+  where+    merge = mappend . Builder.fromString . Full.escape  stringValue :: Formatter -> Text -> Lazy.Text-stringValue Minified string = Builder.toLazyText-    $ quote <> Text.foldr (mappend . escape) quote string+stringValue Minified string = Builder.toLazyText $ oneLine string stringValue (Pretty indentation) string =   if hasEscaped string   then stringValue Minified string@@ -265,21 +265,6 @@       transformLine line' acc             = Builder.fromLazyText (indent (indentation + 1))             <> line' <> newline <> acc--escape :: Char -> Builder-escape char'-    | char' == '\\' = Builder.fromString "\\\\"-    | char' == '\"' = Builder.fromString "\\\""-    | char' == '\b' = Builder.fromString "\\b"-    | char' == '\f' = Builder.fromString "\\f"-    | char' == '\n' = Builder.fromString "\\n"-    | char' == '\r' = Builder.fromString "\\r"-    | char' == '\t' = Builder.fromString "\\t"-    | char' < '\x0010' = unicode  "\\u000" char'-    | char' < '\x0020' = unicode "\\u00" char'-    | otherwise = Builder.singleton char'-  where-    unicode prefix = mappend (Builder.fromString prefix) . (hexadecimal . ord)  listValue :: Formatter -> [Full.Value] -> Lazy.Text listValue formatter = bracketsCommas formatter $ value formatter
src/Language/GraphQL/Type/Definition.hs view
@@ -22,6 +22,7 @@ import qualified Data.HashMap.Strict as HashMap import Data.String (IsString(..)) import Data.Text (Text)+import qualified Data.Text as Text import Language.GraphQL.AST (Name) import Prelude hiding (id) @@ -63,6 +64,9 @@ instance Eq ScalarType where     (ScalarType this _) == (ScalarType that _) = this == that +instance Show ScalarType where+    show (ScalarType typeName _) = Text.unpack typeName+ -- | Enum type definition. -- -- Some leaf values of requests and input values are Enums. GraphQL serializes@@ -72,6 +76,9 @@  instance Eq EnumType where     (EnumType this _ _) == (EnumType that _ _) = this == that++instance Show EnumType where+    show (EnumType typeName _ _) = Text.unpack typeName  -- | Enum value is a single member of an 'EnumType'. newtype EnumValue = EnumValue (Maybe Text)
src/Language/GraphQL/Type/In.hs view
@@ -24,6 +24,7 @@  import Data.HashMap.Strict (HashMap) import Data.Text (Text)+import qualified Data.Text as Text import Language.GraphQL.AST.Document (Name) import qualified Language.GraphQL.Type.Definition as Definition @@ -40,6 +41,9 @@ instance Eq InputObjectType where     (InputObjectType this _ _) == (InputObjectType that _ _) = this == that +instance Show InputObjectType where+    show (InputObjectType typeName _ _) = Text.unpack typeName+ -- | These types may be used as input types for arguments and directives. -- -- GraphQL distinguishes between "wrapping" and "named" types. Each wrapping@@ -55,6 +59,16 @@     | NonNullInputObjectType InputObjectType     | NonNullListType Type     deriving Eq++instance Show Type where+    show (NamedScalarType scalarType) = show scalarType+    show (NamedEnumType enumType) = show enumType+    show (NamedInputObjectType inputObjectType) = show inputObjectType+    show (ListType baseType) = concat ["[", show baseType, "]"]+    show (NonNullScalarType scalarType) = '!' : show scalarType+    show (NonNullEnumType enumType) = '!' : show enumType+    show (NonNullInputObjectType inputObjectType) = '!' : show inputObjectType+    show (NonNullListType baseType) = concat ["![", show baseType, "]"]  -- | Field argument definition. data Argument = Argument (Maybe Text) Type (Maybe Definition.Value)
src/Language/GraphQL/Type/Internal.hs view
@@ -14,11 +14,14 @@     , Type(..)     , directives     , doesFragmentTypeApply+    , implementations     , instanceOf+    , lookupCompositeField     , lookupInputType     , lookupTypeCondition     , lookupTypeField     , mutation+    , outToComposite     , subscription     , query     , types@@ -62,27 +65,32 @@     (Maybe (Out.ObjectType m))     Directives     (HashMap Full.Name (Type m))+    (HashMap Full.Name [Type m])  -- | Schema query type. query :: forall m. Schema m -> Out.ObjectType m-query (Schema query' _ _ _ _) = query'+query (Schema query' _ _ _ _ _) = query'  -- | Schema mutation type. mutation :: forall m. Schema m -> Maybe (Out.ObjectType m)-mutation (Schema _ mutation' _ _ _) = mutation'+mutation (Schema _ mutation' _ _ _ _) = mutation'  -- | Schema subscription type. subscription :: forall m. Schema m -> Maybe (Out.ObjectType m)-subscription (Schema _ _ subscription' _ _) = subscription'+subscription (Schema _ _ subscription' _ _ _) = subscription'  -- | Schema directive definitions. directives :: forall m. Schema m -> Directives-directives (Schema _ _ _ directives' _) = directives'+directives (Schema _ _ _ directives' _ _) = directives'  -- | Types referenced by the schema. types :: forall m. Schema m -> HashMap Full.Name (Type m)-types (Schema _ _ _ _ types') = types'+types (Schema _ _ _ _ types' _) = types' +-- | Interface implementations.+implementations :: forall m. Schema m -> HashMap Full.Name [Type m]+implementations (Schema _ _ _ _ _ implementations') = implementations'+ -- | These types may describe the parent context of a selection set. data CompositeType m     = CompositeUnionType (Out.UnionType m)@@ -160,12 +168,16 @@     <$> lookupInputType nonNull types'  lookupTypeField :: forall a. Full.Name -> Out.Type a -> Maybe (Out.Field a)-lookupTypeField fieldName = \case-    Out.ObjectBaseType objectType ->-        objectChild objectType-    Out.InterfaceBaseType interfaceType ->-        interfaceChild interfaceType-    Out.ListBaseType listType -> lookupTypeField fieldName listType+lookupTypeField fieldName outputType =+    outToComposite outputType >>= lookupCompositeField fieldName++lookupCompositeField :: forall a+    . Full.Name+    -> CompositeType a+    -> Maybe (Out.Field a)+lookupCompositeField fieldName = \case+    CompositeObjectType objectType -> objectChild objectType+    CompositeInterfaceType interfaceType -> interfaceChild interfaceType     _ -> Nothing   where     objectChild (Out.ObjectType _ _ _ resolvers) =@@ -174,3 +186,12 @@         HashMap.lookup fieldName fields     resolverType (Out.ValueResolver objectField _) = objectField     resolverType (Out.EventStreamResolver objectField _ _) = objectField++outToComposite :: forall a. Out.Type a -> Maybe (CompositeType a)+outToComposite = \case+    Out.ObjectBaseType objectType -> Just $ CompositeObjectType objectType+    Out.InterfaceBaseType interfaceType ->+        Just $ CompositeInterfaceType interfaceType+    Out.UnionBaseType unionType -> Just $ CompositeUnionType unionType+    Out.ListBaseType listType -> outToComposite listType+    _ -> Nothing
src/Language/GraphQL/Type/Out.hs view
@@ -38,6 +38,7 @@ import qualified Data.HashMap.Strict as HashMap import Data.Maybe (fromMaybe) import Data.Text (Text)+import qualified Data.Text as Text import Language.GraphQL.AST (Name) import Language.GraphQL.Type.Definition import qualified Language.GraphQL.Type.In as In@@ -52,6 +53,9 @@ instance forall a. Eq (ObjectType a) where     (ObjectType this _ _ _) == (ObjectType that _ _ _) = this == that +instance forall a. Show (ObjectType a) where+    show (ObjectType typeName _ _ _) = Text.unpack typeName+ -- | Interface Type Definition. -- -- When a field can return one of a heterogeneous set of types, a Interface type@@ -63,6 +67,9 @@ instance forall a. Eq (InterfaceType a) where     (InterfaceType this _ _ _) == (InterfaceType that _ _ _) = this == that +instance forall a. Show (InterfaceType a) where+    show (InterfaceType typeName _ _ _) = Text.unpack typeName+ -- | Union Type Definition. -- -- When a field can return one of a heterogeneous set of types, a Union type is@@ -72,6 +79,9 @@ instance forall a. Eq (UnionType a) where     (UnionType this _ _) == (UnionType that _ _) = this == that +instance forall a. Show (UnionType a) where+    show (UnionType typeName _ _) = Text.unpack typeName+ -- | Output object field definition. data Field m = Field     (Maybe Text) -- ^ Description.@@ -97,6 +107,20 @@     | NonNullUnionType (UnionType m)     | NonNullListType (Type m)     deriving Eq++instance forall a. Show (Type a) where+    show (NamedScalarType scalarType) = show scalarType+    show (NamedEnumType enumType) = show enumType+    show (NamedObjectType inputObjectType) = show inputObjectType+    show (NamedInterfaceType interfaceType) = show interfaceType+    show (NamedUnionType unionType) = show unionType+    show (ListType baseType) = concat ["[", show baseType, "]"]+    show (NonNullScalarType scalarType) = '!' : show scalarType+    show (NonNullEnumType enumType) = '!' : show enumType+    show (NonNullObjectType inputObjectType) = '!' : show inputObjectType+    show (NonNullInterfaceType interfaceType) = '!' : show interfaceType+    show (NonNullUnionType unionType) = '!' : show unionType+    show (NonNullListType baseType) = concat ["![", show baseType, "]"]  -- | Matches either 'NamedScalarType' or 'NonNullScalarType'. pattern ScalarBaseType :: forall m. ScalarType -> Type m
src/Language/GraphQL/Type/Schema.hs view
@@ -23,6 +23,7 @@     , Schema     , Type(..)     , directives+    , implementations     , mutation     , subscription     , query@@ -41,9 +42,11 @@     -> Directives -- ^ Directive definitions.     -> Schema m -- ^ Schema. schema queryRoot mutationRoot subscriptionRoot directiveDefinitions =-    Internal.Schema queryRoot mutationRoot subscriptionRoot allDirectives collectedTypes+    Internal.Schema queryRoot mutationRoot subscriptionRoot+        allDirectives collectedTypes collectedImplementations   where     collectedTypes = collectReferencedTypes queryRoot mutationRoot subscriptionRoot+    collectedImplementations = collectImplementations collectedTypes     allDirectives = HashMap.union directiveDefinitions defaultDirectives     defaultDirectives = HashMap.fromList         [ ("skip", skipDirective)@@ -153,3 +156,20 @@     polymorphicTraverser interfaces fields         = flip (foldr visitFields) fields         . flip (foldr traverseInterfaceType) interfaces++-- | Looks for objects and interfaces under the schema types and collects the+-- interfaces they implement.+collectImplementations :: forall m+    . HashMap Full.Name (Type m)+    -> HashMap Full.Name [Type m]+collectImplementations = HashMap.foldr go HashMap.empty+  where+    go implementation@(InterfaceType interfaceType) accumulator =+        let Out.InterfaceType _ _ interfaces _ = interfaceType+         in foldr (add implementation) accumulator interfaces+    go implementation@(ObjectType objectType) accumulator =+        let Out.ObjectType _ _ interfaces _ = objectType+         in foldr (add implementation) accumulator interfaces+    go _ accumulator = accumulator+    add implementation (Out.InterfaceType typeName _ _ _) accumulator =+        HashMap.insertWith (++) typeName [implementation] accumulator
src/Language/GraphQL/Validate.hs view
@@ -210,7 +210,7 @@         directives context rule scalarLocation directives'     Full.ObjectTypeDefinition _ _ _ directives' fields         -> directives context rule objectLocation directives'-         >< foldMap (fieldDefinition context rule) fields+        >< foldMap (fieldDefinition context rule) fields     Full.InterfaceTypeDefinition _ _ directives' fields         -> directives context rule interfaceLocation directives'         >< foldMap (fieldDefinition context rule) fields
src/Language/GraphQL/Validate/Rules.hs view
@@ -3,6 +3,7 @@    obtain one at https://mozilla.org/MPL/2.0/. -}  {-# LANGUAGE LambdaCase #-}+{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE ScopedTypeVariables #-}@@ -24,6 +25,8 @@     , noUndefinedVariablesRule     , noUnusedFragmentsRule     , noUnusedVariablesRule+    , overlappingFieldsCanBeMergedRule+    , possibleFragmentSpreadsRule     , providedRequiredInputFieldsRule     , providedRequiredArgumentsRule     , scalarLeafsRule@@ -35,22 +38,24 @@     , uniqueInputFieldNamesRule     , uniqueOperationNamesRule     , uniqueVariableNamesRule+    , valuesOfCorrectTypeRule+    , variablesInAllowedPositionRule     , variablesAreInputTypesRule     ) where  import Control.Monad ((>=>), foldM) import Control.Monad.Trans.Class (MonadTrans(..))-import Control.Monad.Trans.Reader (ReaderT(..), asks, mapReaderT)+import Control.Monad.Trans.Reader (ReaderT(..), ask, asks, mapReaderT) import Control.Monad.Trans.State (StateT, evalStateT, gets, modify) import Data.Bifunctor (first)-import Data.Foldable (find, toList)+import Data.Foldable (find, fold, foldl', toList) import qualified Data.HashMap.Strict as HashMap import Data.HashMap.Strict (HashMap) import Data.HashSet (HashSet) import qualified Data.HashSet as HashSet import Data.List (groupBy, sortBy, sortOn)-import Data.Maybe (isNothing, mapMaybe)-import Data.List.NonEmpty (NonEmpty)+import Data.Maybe (catMaybes, fromMaybe, isJust, isNothing, mapMaybe)+import Data.List.NonEmpty (NonEmpty(..)) import Data.Ord (comparing) import Data.Sequence (Seq(..), (|>)) import qualified Data.Sequence as Seq@@ -80,6 +85,7 @@     -- Fields     , fieldsOnCorrectTypeRule     , scalarLeafsRule+    , overlappingFieldsCanBeMergedRule     -- Arguments.     , knownArgumentNamesRule     , uniqueArgumentNamesRule@@ -91,7 +97,9 @@     , noUnusedFragmentsRule     , fragmentSpreadTargetDefinedRule     , noFragmentCyclesRule+    , possibleFragmentSpreadsRule     -- Values+    , valuesOfCorrectTypeRule     , knownInputFieldNamesRule     , uniqueInputFieldNamesRule     , providedRequiredInputFieldsRule@@ -104,6 +112,7 @@     , variablesAreInputTypesRule     , noUndefinedVariablesRule     , noUnusedVariablesRule+    , variablesInAllowedPositionRule     ]  -- | Definition must be OperationDefinition or FragmentDefinition.@@ -320,10 +329,8 @@ fragmentSpreadTypeExistenceRule = SelectionRule $ const $ \case     Full.FragmentSpreadSelection fragmentSelection         | Full.FragmentSpread fragmentName _ location' <- fragmentSelection -> do-            ast' <- asks ast-            let target = find (isSpreadTarget fragmentName) ast'-            typeCondition <- lift $ maybeToSeq $ target >>= extractTypeCondition             types' <- asks $ Schema.types . schema+            typeCondition <- findSpreadTarget fragmentName             case HashMap.lookup typeCondition types' of                 Nothing -> pure $ Error                     { message = spreadError fragmentName typeCondition@@ -342,10 +349,6 @@                 Just _ -> lift mempty     _ -> lift mempty   where-    extractTypeCondition (viewFragment -> Just fragmentDefinition) =-        let Full.FragmentDefinition _ typeCondition _ _ _ = fragmentDefinition-         in Just typeCondition-    extractTypeCondition _ = Nothing     spreadError fragmentName typeCondition = concat         [ "Fragment \""         , Text.unpack fragmentName@@ -451,8 +454,7 @@ noFragmentCyclesRule :: forall m. Rule m noFragmentCyclesRule = FragmentDefinitionRule $ \case     Full.FragmentDefinition fragmentName _ _ selections location' -> do-        state <- evalStateT (collectFields selections)-            (0, fragmentName)+        state <- evalStateT (collectCycles selections) (0, fragmentName)         let spreadPath = fst <$> sortBy (comparing snd) (HashMap.toList state)         case reverse spreadPath of             x : _ | x == fragmentName -> pure $ Error@@ -467,10 +469,10 @@                 }             _ -> lift mempty   where-    collectFields :: Traversable t+    collectCycles :: Traversable t         => t Full.Selection         -> StateT (Int, Full.Name) (ReaderT (Validation m) Seq) (HashMap Full.Name Int)-    collectFields selectionSet = foldM forEach HashMap.empty selectionSet+    collectCycles selectionSet = foldM forEach HashMap.empty selectionSet     forEach accumulator = \case         Full.FieldSelection fieldSelection -> forField accumulator fieldSelection         Full.InlineFragmentSelection fragmentSelection ->@@ -487,15 +489,15 @@             then pure newAccumulator             else collectFromSpread fragmentName newAccumulator     forInline accumulator (Full.InlineFragment _ _ selections _) =-        (accumulator <>) <$> collectFields selections+        (accumulator <>) <$> collectCycles selections     forField accumulator (Full.Field _ _ _ _ selections _) =-        (accumulator <>) <$> collectFields selections+        (accumulator <>) <$> collectCycles selections     collectFromSpread fragmentName accumulator = do         ast' <- lift $ asks ast         case findFragmentDefinition fragmentName ast' of             Nothing -> pure accumulator             Just (Full.FragmentDefinition _ _ _ selections _) ->-                (accumulator <>) <$> collectFields selections+                (accumulator <>) <$> collectCycles selections  findFragmentDefinition :: Text     -> NonEmpty Full.Definition@@ -531,15 +533,22 @@     extract (Full.Directive directiveName _ location') =         (directiveName, location') -filterDuplicates :: (a -> (Text, Full.Location)) -> String -> [a] -> Seq Error+groupSorted :: forall a. (a -> Text) -> [a] -> [[a]]+groupSorted getName = groupBy equalByName . sortOn getName+  where+    equalByName lhs rhs = getName lhs == getName rhs++filterDuplicates :: forall a+    . (a -> (Text, Full.Location))+    -> String+    -> [a]+    -> Seq Error filterDuplicates extract nodeType = Seq.fromList     . fmap makeError     . filter ((> 1) . length)-    . groupBy equalByName-    . sortOn getName+    . groupSorted getName   where     getName = fst . extract-    equalByName lhs rhs = getName lhs == getName rhs     makeError directives' = Error         { message = makeMessage $ head directives'         , locations = snd . extract <$> directives'@@ -647,12 +656,9 @@             lift $ lift $ mapArguments arguments <> mapDirectives directives'     variableFilter (Full.FragmentSpreadSelection spread)         | Full.FragmentSpread fragmentName _ _ <- spread = do-            definitions <- lift $ asks ast-            visited <- gets (HashSet.member fragmentName)-            modify (HashSet.insert fragmentName)-            case find (isSpreadTarget fragmentName) definitions of-                Just (viewFragment -> Just fragmentDefinition)-                    | not visited -> diveIntoSpread fragmentDefinition+            nonVisitedFragmentDefinition <- visitFragmentDefinition fragmentName+            case nonVisitedFragmentDefinition of+                Just fragmentDefinition -> diveIntoSpread fragmentDefinition                 _ -> lift $ lift mempty     diveIntoSpread (Full.FragmentDefinition _ _ directives' selections _)         = filterSelections' selections@@ -710,7 +716,7 @@     fieldRule parentType (Full.Field _ fieldName _ _ _ location')         | Just objectType <- parentType         , Nothing <- Type.lookupTypeField fieldName objectType-        , Just typeName <- compositeTypeName objectType = pure $ Error+        , Just typeName <- typeNameIfComposite objectType = pure $ Error             { message = errorMessage fieldName typeName             , locations = [location']             }@@ -723,21 +729,18 @@         , "\"."         ] -compositeTypeName :: forall m. Out.Type m -> Maybe Full.Name-compositeTypeName (Out.ObjectBaseType (Out.ObjectType typeName _ _ _)) =-    Just typeName-compositeTypeName (Out.InterfaceBaseType interfaceType) =+compositeTypeName :: forall m. Type.CompositeType m -> Full.Name+compositeTypeName (Type.CompositeObjectType (Out.ObjectType typeName _ _ _)) =+    typeName+compositeTypeName (Type.CompositeInterfaceType interfaceType) =     let Out.InterfaceType typeName _ _ _ = interfaceType-        in Just typeName-compositeTypeName (Out.UnionBaseType (Out.UnionType typeName _ _)) =-    Just typeName-compositeTypeName (Out.ScalarBaseType _) =-    Nothing-compositeTypeName (Out.EnumBaseType _) =-    Nothing-compositeTypeName (Out.ListBaseType wrappedType) =-    compositeTypeName wrappedType+     in typeName+compositeTypeName (Type.CompositeUnionType (Out.UnionType typeName _ _)) =+    typeName +typeNameIfComposite :: forall m. Out.Type m -> Maybe Full.Name+typeNameIfComposite = fmap compositeTypeName . Type.outToComposite+ -- | Field selections on scalars or enums are never allowed, because they are -- the leaf nodes of any GraphQL query. scalarLeafsRule :: forall m. Rule m@@ -794,7 +797,7 @@   where     fieldRule (Just objectType) (Full.Field _ fieldName arguments  _ _ _)         | Just typeField <- Type.lookupTypeField fieldName objectType-        , Just typeName <- compositeTypeName objectType =+        , Just typeName <- typeNameIfComposite objectType =             lift $ foldr (go typeName fieldName typeField) Seq.empty arguments     fieldRule _ _ = lift mempty     go typeName fieldName fieldDefinition (Full.Argument argumentName _ location') errors@@ -1013,3 +1016,590 @@         , Text.unpack typeName         , "\" is required, but it was not provided."         ]++-- | If multiple field selections with the same response names are encountered+-- during execution, the field and arguments to execute and the resulting value+-- should be unambiguous. Therefore any two field selections which might both be+-- encountered for the same object are only valid if they are equivalent.+--+-- For simple hand‐written GraphQL, this rule is obviously a clear developer+-- error, however nested fragments can make this difficult to detect manually.+overlappingFieldsCanBeMergedRule :: Rule m+overlappingFieldsCanBeMergedRule = OperationDefinitionRule $ \case+    Full.SelectionSet selectionSet _ -> do+        schema' <- asks schema+        go (toList selectionSet)+            $ Type.CompositeObjectType+            $ Schema.query schema'+    Full.OperationDefinition operationType _ _ _ selectionSet _ -> do+        schema' <- asks schema+        let root = go (toList selectionSet) . Type.CompositeObjectType+        case operationType of+            Full.Query -> root $ Schema.query schema'+            Full.Mutation+                | Just objectType <- Schema.mutation schema' -> root objectType+            Full.Subscription+                | Just objectType <- Schema.mutation schema' -> root objectType+            _ -> lift mempty+  where+    go selectionSet selectionType = do+        fieldTuples <- evalStateT (collectFields selectionType selectionSet) HashSet.empty+        fieldsInSetCanMerge fieldTuples+    fieldsInSetCanMerge :: forall m+        . HashMap Full.Name (NonEmpty (Full.Field, Type.CompositeType m))+        -> ReaderT (Validation m) Seq Error+    fieldsInSetCanMerge fieldTuples = do+        validation <- ask+        let (lonely, paired) = flattenPairs fieldTuples+        let reader = flip runReaderT validation+        lift $ foldMap (reader . visitLonelyFields) lonely+            <> foldMap (reader . forEachFieldTuple) paired+    forEachFieldTuple :: forall m+        . (FieldInfo m, FieldInfo m)+        -> ReaderT (Validation m) Seq Error+    forEachFieldTuple (fieldA, fieldB) =+        case (parent fieldA, parent fieldB) of+            (parentA@Type.CompositeObjectType{}, parentB@Type.CompositeObjectType{})+                | parentA /= parentB -> sameResponseShape fieldA fieldB+            _ -> mapReaderT (checkEquality (node fieldA) (node fieldB))+                $ sameResponseShape fieldA fieldB+    checkEquality fieldA fieldB Seq.Empty+        | Full.Field _ fieldNameA _ _ _ _ <- fieldA+        , Full.Field _ fieldNameB _ _ _ _ <- fieldB+        , fieldNameA /= fieldNameB = pure $ makeError fieldA fieldB+        | Full.Field _ fieldNameA argumentsA _ _ locationA <- fieldA+        , Full.Field _ _ argumentsB _ _ locationB <- fieldB+        , argumentsA /= argumentsB =+            let message = concat+                    [ "Fields \""+                    , Text.unpack fieldNameA+                    , "\" conflict because they have different arguments. Use "+                    , "different aliases on the fields to fetch both if this "+                    , "was intentional."+                    ]+             in pure $ Error message [locationB, locationA]+    checkEquality _ _ previousErrors = previousErrors+    visitLonelyFields FieldInfo{..} =+        let Full.Field _ _ _ _ subSelections _ = node+            compositeFieldType = Type.outToComposite type'+         in maybe (lift Seq.empty) (go subSelections) compositeFieldType+    sameResponseShape :: forall m+        . FieldInfo m+        -> FieldInfo m+        -> ReaderT (Validation m) Seq Error+    sameResponseShape fieldA fieldB =+        let Full.Field _ _ _ _ selectionsA _ = node fieldA+            Full.Field _ _ _ _ selectionsB _ = node fieldB+         in case unwrapTypes (type' fieldA) (type' fieldB) of+            Left True -> lift mempty+            Right (compositeA, compositeB) -> do+                validation <- ask+                let collectFields' composite = flip runReaderT validation+                        . flip evalStateT HashSet.empty+                        . collectFields composite+                let collectA = collectFields' compositeA selectionsA+                let collectB = collectFields' compositeB selectionsB+                fieldsInSetCanMerge+                    $ foldl' (HashMap.unionWith (<>)) HashMap.empty+                    $ collectA <> collectB+            _ -> pure $ makeError (node fieldA) (node fieldB)+    makeError fieldA fieldB =+        let Full.Field aliasA fieldNameA _ _ _ locationA = fieldA+            Full.Field _ fieldNameB _ _ _ locationB = fieldB+            message = concat+                [ "Fields \""+                , Text.unpack (fromMaybe fieldNameA aliasA)+                , "\" conflict because \""+                , Text.unpack fieldNameB+                , "\" and \""+                , Text.unpack fieldNameA+                , "\" are different fields. Use different aliases on the fields "+                , "to fetch both if this was intentional."+                ]+             in Error message [locationB, locationA]+    unwrapTypes typeA@Out.ScalarBaseType{} typeB@Out.ScalarBaseType{} =+        Left $ typeA == typeB+    unwrapTypes typeA@Out.EnumBaseType{} typeB@Out.EnumBaseType{} =+        Left $ typeA == typeB+    unwrapTypes (Out.ListType listA) (Out.ListType listB) =+        unwrapTypes listA listB+    unwrapTypes (Out.NonNullListType listA) (Out.NonNullListType listB) =+        unwrapTypes listA listB+    unwrapTypes typeA typeB+        | Out.isNonNullType typeA == Out.isNonNullType typeB+        , Just compositeA <- Type.outToComposite typeA+        , Just compositeB <- Type.outToComposite typeB =+            Right (compositeA, compositeB)+        | otherwise = Left False+    flattenPairs :: forall m+        . HashMap Full.Name (NonEmpty (Full.Field, Type.CompositeType m))+        -> (Seq (FieldInfo m), Seq (FieldInfo m, FieldInfo m))+    flattenPairs xs = HashMap.foldr splitSingleFields (Seq.empty, Seq.empty)+        $ foldr lookupTypeField [] <$> xs+    splitSingleFields :: forall m+        . [FieldInfo m]+        -> (Seq (FieldInfo m), Seq (FieldInfo m, FieldInfo m))+        -> (Seq (FieldInfo m), Seq (FieldInfo m, FieldInfo m))+    splitSingleFields [head'] (fields, pairList) = (fields |> head', pairList)+    splitSingleFields xs (fields, pairList) = (fields, pairs pairList xs)+    lookupTypeField (field, parentType) accumulator =+        let Full.Field _ fieldName _ _ _ _ = field+         in case Type.lookupCompositeField fieldName parentType of+            Nothing -> accumulator+            Just (Out.Field _ typeField _) ->+                FieldInfo field typeField parentType : accumulator+    pairs :: forall m+        . Seq (FieldInfo m, FieldInfo m)+        -> [FieldInfo m]+        -> Seq (FieldInfo m, FieldInfo m)+    pairs accumulator [] = accumulator+    pairs accumulator (fieldA : fields) =+        pair fieldA (pairs accumulator fields) fields+    pair _ accumulator [] = accumulator+    pair field accumulator (fieldA : fields) =+        pair field accumulator fields |> (field, fieldA)+    collectFields objectType = accumulateFields objectType mempty+    accumulateFields = foldM . forEach+    forEach parentType accumulator = \case+        Full.FieldSelection fieldSelection ->+            forField parentType accumulator fieldSelection+        Full.FragmentSpreadSelection fragmentSelection ->+            forSpread accumulator fragmentSelection+        Full.InlineFragmentSelection fragmentSelection ->+            forInline parentType accumulator fragmentSelection+    forField parentType accumulator field@(Full.Field alias fieldName _ _ _ _) =+        let key = fromMaybe fieldName alias+            value = (field, parentType) :| []+         in pure $ HashMap.insertWith (<>) key value accumulator+    forSpread accumulator (Full.FragmentSpread fragmentName _ _) = do+        inVisitetFragments <- gets $ HashSet.member fragmentName+        if inVisitetFragments+            then pure accumulator+            else collectFromSpread fragmentName accumulator+    forInline parentType accumulator = \case+        Full.InlineFragment maybeType _ selections _+            | Just typeCondition <- maybeType ->+                collectFromFragment typeCondition selections accumulator+            | otherwise -> accumulateFields parentType accumulator $ toList selections+    collectFromFragment typeCondition selectionSet' accumulator = do+        types' <- lift $ asks $ Schema.types . schema+        case Type.lookupTypeCondition typeCondition types' of+            Nothing -> pure accumulator+            Just compositeType ->+                accumulateFields compositeType accumulator $ toList selectionSet'+    collectFromSpread fragmentName accumulator = do+        modify $ HashSet.insert fragmentName+        ast' <- lift $ asks ast+        case findFragmentDefinition fragmentName ast' of+            Nothing -> pure accumulator+            Just (Full.FragmentDefinition _ typeCondition _ selectionSet' _) ->+                collectFromFragment typeCondition selectionSet' accumulator++data FieldInfo m = FieldInfo+    { node :: Full.Field+    , type' :: Out.Type m+    , parent :: Type.CompositeType m+    }++-- | Fragments are declared on a type and will only apply when the runtime+-- object type matches the type condition. They also are spread within the+-- context of a parent type. A fragment spread is only valid if its type+-- condition could ever apply within the parent type.+possibleFragmentSpreadsRule :: forall m. Rule m+possibleFragmentSpreadsRule = SelectionRule go+  where+    go (Just parentType) (Full.InlineFragmentSelection fragmentSelection)+        | Full.InlineFragment maybeType _ _ location' <- fragmentSelection+        , Just typeCondition <- maybeType = do+            (fragmentTypeName, parentTypeName) <-+                compareTypes typeCondition parentType+            pure $ Error+                { message = concat+                    [ "Fragment cannot be spread here as objects of type \""+                    , Text.unpack parentTypeName+                    , "\" can never be of type \""+                    , Text.unpack fragmentTypeName+                    , "\"."+                    ]+                , locations = [location']+                }+    go (Just parentType) (Full.FragmentSpreadSelection fragmentSelection)+        | Full.FragmentSpread fragmentName _ location' <- fragmentSelection = do+            typeCondition <- findSpreadTarget fragmentName+            (fragmentTypeName, parentTypeName) <-+                compareTypes typeCondition parentType+            pure $ Error+                { message = concat+                    [ "Fragment \""+                    , Text.unpack fragmentName+                    , "\" cannot be spread here as objects of type \""+                    , Text.unpack parentTypeName+                    , "\" can never be of type \""+                    , Text.unpack fragmentTypeName+                    , "\"."+                    ]+                , locations = [location']+                }+    go _ _ = lift mempty+    compareTypes typeCondition parentType = do+        types' <- asks $ Schema.types . schema+        fragmentType <- lift+            $ maybeToSeq+            $ Type.lookupTypeCondition typeCondition types'+        parentComposite <- lift+            $ maybeToSeq+            $ Type.outToComposite parentType+        possibleFragments <- getPossibleTypes fragmentType+        possibleParents <- getPossibleTypes parentComposite+        let fragmentTypeName = compositeTypeName fragmentType+        let parentTypeName = compositeTypeName parentComposite+        if HashSet.null $ HashSet.intersection possibleFragments possibleParents+            then pure (fragmentTypeName, parentTypeName)+            else lift mempty+    getPossibleTypeList (Type.CompositeObjectType objectType) =+        pure [Schema.ObjectType objectType]+    getPossibleTypeList (Type.CompositeUnionType unionType) =+        let Out.UnionType _ _ members = unionType+         in pure $ Schema.ObjectType <$> members+    getPossibleTypeList (Type.CompositeInterfaceType interfaceType) =+        let Out.InterfaceType typeName _ _ _ = interfaceType+         in HashMap.lookupDefault [] typeName+        <$> asks (Schema.implementations . schema)+    getPossibleTypes compositeType+        = foldr (HashSet.insert . internalTypeName) HashSet.empty+        <$> getPossibleTypeList compositeType++internalTypeName :: forall m. Schema.Type m -> Full.Name+internalTypeName (Schema.ScalarType (Definition.ScalarType typeName _)) =+    typeName+internalTypeName (Schema.EnumType (Definition.EnumType typeName _ _)) = typeName+internalTypeName (Schema.ObjectType (Out.ObjectType typeName _ _ _)) = typeName+internalTypeName (Schema.InputObjectType (In.InputObjectType typeName _ _)) =+    typeName+internalTypeName (Schema.InterfaceType (Out.InterfaceType typeName _ _ _)) =+    typeName+internalTypeName (Schema.UnionType (Out.UnionType typeName _ _)) = typeName++findSpreadTarget :: Full.Name -> ReaderT (Validation m1) Seq Full.TypeCondition+findSpreadTarget fragmentName = do+    ast' <- asks ast+    let target = find (isSpreadTarget fragmentName) ast'+    lift $ maybeToSeq $ target >>= extractTypeCondition+  where+    extractTypeCondition (viewFragment -> Just fragmentDefinition) =+        let Full.FragmentDefinition _ typeCondition _ _ _ = fragmentDefinition+            in Just typeCondition+    extractTypeCondition _ = Nothing++visitFragmentDefinition :: forall m+    . Text+    -> ValidationState m (Maybe Full.FragmentDefinition)+visitFragmentDefinition fragmentName = do+    definitions <- lift $ asks ast+    visited <- gets (HashSet.member fragmentName)+    modify (HashSet.insert fragmentName)+    case find (isSpreadTarget fragmentName) definitions of+        Just (viewFragment -> Just fragmentDefinition)+            | not visited -> pure $ Just fragmentDefinition+        _ -> pure Nothing++-- | Variable usages must be compatible with the arguments they are passed to.+--+-- Validation failures occur when variables are used in the context of types+-- that are complete mismatches, or if a nullable type in a variable is passed+-- to a non‐null argument type.+variablesInAllowedPositionRule :: forall m. Rule m+variablesInAllowedPositionRule = OperationDefinitionRule $ \case+    Full.OperationDefinition operationType _ variables _ selectionSet _ -> do+        schema' <- asks schema+        let root = go variables (toList selectionSet) . Type.CompositeObjectType+        case operationType of+            Full.Query -> root $ Schema.query schema'+            Full.Mutation+                | Just objectType <- Schema.mutation schema' -> root objectType+            Full.Subscription+                | Just objectType <- Schema.mutation schema' -> root objectType+            _ -> lift mempty+    _ -> lift mempty+  where+    go variables selections selectionType = mapReaderT (foldr (<>) Seq.empty)+        $ flip evalStateT HashSet.empty+        $ visitSelectionSet variables selectionType+        $ toList selections+    visitSelectionSet :: Foldable t+        => [Full.VariableDefinition]+        -> Type.CompositeType m+        -> t Full.Selection+        -> ValidationState m (Seq Error)+    visitSelectionSet variables selectionType selections =+        foldM (evaluateSelection variables selectionType) mempty selections+    evaluateFieldSelection variables selections accumulator = \case+        Just newParentType -> do+            let folder = evaluateSelection variables newParentType+            selectionErrors <- foldM folder accumulator selections+            pure $ accumulator <> selectionErrors+        Nothing -> pure accumulator+    evaluateSelection :: [Full.VariableDefinition]+        -> Type.CompositeType m+        -> Seq Error+        -> Full.Selection+        -> ValidationState m (Seq Error)+    evaluateSelection variables selectionType accumulator selection+        | Full.FragmentSpreadSelection spread <- selection+        , Full.FragmentSpread fragmentName _ _ <- spread = do+            types' <- lift $ asks $ Schema.types . schema+            nonVisitedFragmentDefinition <- visitFragmentDefinition fragmentName+            case nonVisitedFragmentDefinition of+                Just fragmentDefinition+                    | Full.FragmentDefinition _ typeCondition _ _ _ <- fragmentDefinition+                    , Just spreadType <- Type.lookupTypeCondition typeCondition types' -> do+                        spreadErrors <- spreadVariables variables spread+                        selectionErrors <- diveIntoSpread variables spreadType fragmentDefinition+                        pure $ accumulator <> spreadErrors <> selectionErrors+                _ -> lift $ lift mempty+        | Full.FieldSelection fieldSelection <- selection+        , Full.Field _ fieldName _ _ subselections _ <- fieldSelection =+            case Type.lookupCompositeField fieldName selectionType of+                Just (Out.Field _ typeField argumentTypes) -> do+                    fieldErrors <- fieldVariables variables argumentTypes fieldSelection+                    selectionErrors <- evaluateFieldSelection variables subselections accumulator+                            $ Type.outToComposite typeField+                    pure $ selectionErrors <> fieldErrors+                Nothing -> pure accumulator+        | Full.InlineFragmentSelection inlineSelection <- selection+        , Full.InlineFragment typeCondition _ subselections _ <- inlineSelection = do+            types' <- lift $ asks $ Schema.types . schema+            let inlineType = fromMaybe selectionType+                    $ typeCondition >>= flip Type.lookupTypeCondition types'+            fragmentErrors <- inlineVariables variables inlineSelection+            let folder = evaluateSelection variables inlineType+            selectionErrors <- foldM folder accumulator subselections+            pure $ accumulator <> fragmentErrors <> selectionErrors+    inlineVariables variables inline+        | Full.InlineFragment _ directives' _ _ <- inline =+            mapDirectives variables directives'+    fieldVariables :: [Full.VariableDefinition]+        -> In.Arguments+        -> Full.Field+        -> ValidationState m (Seq Error)+    fieldVariables variables argumentTypes fieldSelection = do+        let Full.Field _ _ arguments directives' _ _ = fieldSelection+        argumentErrors <- mapArguments variables argumentTypes arguments+        directiveErrors <- mapDirectives variables directives'+        pure $ argumentErrors <> directiveErrors+    spreadVariables variables (Full.FragmentSpread _ directives' _) =+        mapDirectives variables directives'+    diveIntoSpread variables fieldType fragmentDefinition = do+        let Full.FragmentDefinition _ _ directives' selections _ =+                fragmentDefinition+        selectionErrors <- visitSelectionSet variables fieldType selections+        directiveErrors <- mapDirectives variables directives'+        pure $ selectionErrors <> directiveErrors+    findDirectiveVariables variables directive = do+        let Full.Directive directiveName arguments _ = directive+        directiveDefinitions <- lift $ asks $ Schema.directives . schema+        case HashMap.lookup directiveName directiveDefinitions of+            Just (Schema.Directive _ _ directiveArguments) ->+                mapArguments variables directiveArguments arguments+            Nothing -> pure mempty+    mapArguments variables argumentTypes = fmap fold+        . traverse (findArgumentVariables variables argumentTypes)+    mapDirectives variables = fmap fold+        <$> traverse (findDirectiveVariables variables)+    lookupInputObject variables objectFieldValue locationInfo+        | Full.Node{ node = Full.Object objectFields } <- objectFieldValue+        , Just (expectedType, _) <- locationInfo+        , In.InputObjectBaseType inputObjectType <- expectedType+        , In.InputObjectType _ _ fieldTypes' <- inputObjectType =+            fold <$> traverse (traverseObjectField variables fieldTypes') objectFields+        | otherwise = pure mempty+    maybeUsageAllowed variableName variables locationInfo+        | Just (locationType, locationValue) <- locationInfo+        , findVariableDefinition' <- findVariableDefinition variableName+        , Just variableDefinition <- find findVariableDefinition' variables+            = maybeToSeq+            <$> isVariableUsageAllowed locationType locationValue variableDefinition+        | otherwise = pure mempty+    findArgumentVariables :: [Full.VariableDefinition]+        -> HashMap Full.Name In.Argument+        -> Full.Argument+        -> ValidationState m (Seq Error)+    findArgumentVariables variables argumentTypes argument+        | Full.Argument argumentName argumentValue _ <- argument+        , Full.Node{ node = Full.Variable variableName } <- argumentValue+            = maybeUsageAllowed variableName variables+            $ locationPair extractArgument argumentTypes argumentName+        | Full.Argument argumentName argumentValue _ <- argument+            = lookupInputObject variables argumentValue+            $ locationPair extractArgument argumentTypes argumentName+    extractField (In.InputField _ locationType locationValue) =+        (locationType, locationValue)+    extractArgument (In.Argument _ locationType locationValue) =+        (locationType, locationValue)+    locationPair extract fieldTypes name =+        extract <$> HashMap.lookup name fieldTypes+    traverseObjectField variables fieldTypes Full.ObjectField{..}+        | Full.Node{ node = Full.Variable variableName } <- value+            = maybeUsageAllowed variableName variables+            $ locationPair extractField fieldTypes name+        | otherwise = lookupInputObject variables value+            $  locationPair extractField fieldTypes name+    findVariableDefinition variableName variableDefinition =+        let Full.VariableDefinition variableName' _ _ _ = variableDefinition+         in variableName == variableName'+    isVariableUsageAllowed locationType locationDefaultValue variableDefinition+        | Full.VariableDefinition _ variableType _ _ <- variableDefinition+        , Full.TypeNonNull _ <- variableType =+            typesCompatibleOrError variableDefinition locationType+        | Just nullableLocationType <- unwrapInType locationType+        , Full.VariableDefinition _ variableType variableDefaultValue _ <-+            variableDefinition+        , hasNonNullVariableDefaultValue' <-+            hasNonNullVariableDefaultValue variableDefaultValue+        , hasLocationDefaultValue <- isJust locationDefaultValue =+            if (hasNonNullVariableDefaultValue' || hasLocationDefaultValue)+                && areTypesCompatible variableType nullableLocationType+                then pure Nothing+                else pure $ makeError variableDefinition locationType+        | otherwise = typesCompatibleOrError variableDefinition locationType+    typesCompatibleOrError variableDefinition locationType+        | Full.VariableDefinition _ variableType _ _ <- variableDefinition+        , areTypesCompatible variableType locationType = pure Nothing+        | otherwise = pure $ makeError variableDefinition locationType+    areTypesCompatible nonNullType (unwrapInType -> Just nullableLocationType)+        | Full.TypeNonNull (Full.NonNullTypeNamed namedType) <- nonNullType =+            areTypesCompatible (Full.TypeNamed namedType) nullableLocationType+        | Full.TypeNonNull (Full.NonNullTypeList namedList) <- nonNullType =+            areTypesCompatible (Full.TypeList namedList) nullableLocationType+    areTypesCompatible _ (In.isNonNullType -> True) = False+    areTypesCompatible (Full.TypeNonNull nonNullType) locationType+        | Full.NonNullTypeNamed namedType <- nonNullType =+            areTypesCompatible (Full.TypeNamed namedType) locationType+        | Full.NonNullTypeList namedType <- nonNullType =+            areTypesCompatible (Full.TypeList namedType) locationType+    areTypesCompatible variableType locationType+        | Full.TypeList itemVariableType <- variableType+        , In.ListType itemLocationType <- locationType =+            areTypesCompatible itemVariableType itemLocationType+        | areIdentical variableType locationType = True+        | otherwise = False+    areIdentical (Full.TypeList typeList) (In.ListType itemLocationType) =+        areIdentical typeList itemLocationType+    areIdentical (Full.TypeNonNull nonNullType) locationType+        | Full.NonNullTypeList nonNullList <- nonNullType+        , In.NonNullListType  itemLocationType <- locationType =+            areIdentical nonNullList itemLocationType+        | Full.NonNullTypeNamed _ <- nonNullType+        , In.ListBaseType _ <- locationType = False+        | Full.NonNullTypeNamed nonNullList <- nonNullType+        , In.isNonNullType locationType =+            nonNullList == inputTypeName locationType+    areIdentical (Full.TypeNamed _) (In.ListBaseType _) = False+    areIdentical (Full.TypeNamed typeNamed) locationType+        | not $ In.isNonNullType locationType =+            typeNamed == inputTypeName locationType+    areIdentical _ _ = False+    hasNonNullVariableDefaultValue (Just (Full.Node Full.ConstNull _)) = False+    hasNonNullVariableDefaultValue Nothing = False+    hasNonNullVariableDefaultValue _ = True+    unwrapInType (In.NonNullScalarType nonNullType) =+        Just $ In.NamedScalarType nonNullType+    unwrapInType (In.NonNullEnumType nonNullType) =+        Just $ In.NamedEnumType nonNullType+    unwrapInType (In.NonNullInputObjectType nonNullType) =+        Just $ In.NamedInputObjectType nonNullType+    unwrapInType (In.NonNullListType nonNullType) =+        Just $ In.ListType nonNullType+    unwrapInType _ = Nothing+    makeError variableDefinition expectedType =+        let Full.VariableDefinition variableName variableType _ location' =+                variableDefinition+         in Just $ Error+            { message = concat+                [ "Variable \"$"+                , Text.unpack variableName+                , "\" of type \""+                , show variableType+                , "\" used in position expecting type \""+                , show expectedType+                , "\"."+                ]+            , locations = [location']+            }++-- | Literal values must be compatible with the type expected in the position+-- they are found as per the coercion rules.+--+-- The type expected in a position include the type defined by the argument a+-- value is provided for, the type defined by an input object field a value is+-- provided for, and the type of a variable definition a default value is+-- provided for.+valuesOfCorrectTypeRule :: forall m. Rule m+valuesOfCorrectTypeRule = ValueRule go constGo+  where+    go (Just inputType) value+        | Just constValue <- toConstNode value =+            lift $ check inputType constValue+    go _ _ = lift mempty+    toConstNode Full.Node{..} = flip Full.Node location <$> toConst node+    toConst (Full.Variable _) = Nothing+    toConst (Full.Int integer) = Just $ Full.ConstInt integer+    toConst (Full.Float double) = Just $ Full.ConstFloat double+    toConst (Full.String string) = Just $ Full.ConstString string+    toConst (Full.Boolean boolean) = Just $ Full.ConstBoolean boolean+    toConst Full.Null = Just Full.ConstNull+    toConst (Full.Enum enum) = Just $ Full.ConstEnum enum+    toConst (Full.List values) =+        Just $ Full.ConstList $ catMaybes $ toConst <$> values+    toConst (Full.Object fields) = Just $ Full.ConstObject+        $ catMaybes $ constObjectField <$> fields+    constObjectField Full.ObjectField{..}+        | Just constValue <- toConstNode value =+            Just $ Full.ObjectField name constValue location+        | otherwise = Nothing+    constGo Nothing = const $ lift mempty+    constGo (Just inputType) = lift . check inputType+    check :: In.Type -> Full.Node Full.ConstValue -> Seq Error+    check _ Full.Node{ node = Full.ConstNull } =+        mempty -- Ignore, required fields are checked elsewhere.+    check (In.ScalarBaseType scalarType) Full.Node{ node }+        | Definition.ScalarType "Int" _ <- scalarType+        , Full.ConstInt _ <- node = mempty+        | Definition.ScalarType "Boolean" _ <- scalarType+        , Full.ConstBoolean _ <- node = mempty+        | Definition.ScalarType "String" _ <- scalarType+        , Full.ConstString _ <- node = mempty+        | Definition.ScalarType "ID" _ <- scalarType+        , Full.ConstString _ <- node = mempty+        | Definition.ScalarType "ID" _ <- scalarType+        , Full.ConstInt _ <- node = mempty+        | Definition.ScalarType "Float" _ <- scalarType+        , Full.ConstFloat _ <- node = mempty+        | Definition.ScalarType "Float" _ <- scalarType+        , Full.ConstInt _ <- node = mempty+    check (In.EnumBaseType enumType) Full.Node{ node }+        | Definition.EnumType _ _ members <- enumType+        , Full.ConstEnum memberValue <- node+        , HashMap.member memberValue members = mempty+    check (In.InputObjectBaseType objectType) Full.Node{ node }+        | In.InputObjectType _ _ typeFields <- objectType+        , Full.ConstObject valueFields <- node =+            foldMap (checkObjectField typeFields) valueFields+    check (In.ListBaseType listType) constValue@Full.Node{ .. }+        | Full.ConstList listValues <- node =+            foldMap (check listType) $ flip Full.Node location <$> listValues+        | otherwise = check listType constValue+    check inputType Full.Node{ .. } = pure $ Error+        { message = concat+            [ "Value "+            , show node, " cannot be coerced to type \""+            , show inputType+            , "\"."+            ]+        , locations = [location]+        }+    checkObjectField typeFields Full.ObjectField{..}+        | Just typeField <- HashMap.lookup name typeFields+        , In.InputField _ fieldType _ <- typeField =+            check fieldType value+    checkObjectField _ _ = mempty
+ tests/Language/GraphQL/AST/DocumentSpec.hs view
@@ -0,0 +1,20 @@+{-# LANGUAGE DuplicateRecordFields #-}+{-# LANGUAGE OverloadedStrings #-}+module Language.GraphQL.AST.DocumentSpec+    ( spec+    ) where++import Language.GraphQL.AST.Document+import Test.Hspec (Spec, describe, it, shouldBe)++spec :: Spec+spec = do+    describe "Document" $ do+        it "shows objects" $+            let zero = Location 0 0+                object = ConstObject+                    [ ObjectField "field1" (Node (ConstFloat 1.2) zero) zero+                    , ObjectField "field2" (Node ConstNull zero) zero+                    ]+                expected = "{ field1: 1.2, field2: null }"+             in show object `shouldBe` expected
+ tests/Language/GraphQL/Validate/RulesSpec.hs view
@@ -0,0 +1,914 @@+{- This Source Code Form is subject to the terms of the Mozilla Public License,+   v. 2.0. If a copy of the MPL was not distributed with this file, You can+   obtain one at https://mozilla.org/MPL/2.0/. -}++{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}++module Language.GraphQL.Validate.RulesSpec+    ( spec+    ) where++import Data.Foldable (toList)+import qualified Data.HashMap.Strict as HashMap+import Data.Text (Text)+import qualified Language.GraphQL.AST as AST+import Language.GraphQL.Type+import qualified Language.GraphQL.Type.In as In+import qualified Language.GraphQL.Type.Out as Out+import Language.GraphQL.Validate+import Test.Hspec (Spec, context, describe, it, shouldBe, shouldContain)+import Text.Megaparsec (parse, errorBundlePretty)+import Text.RawString.QQ (r)++petSchema :: Schema IO+petSchema = schema queryType Nothing (Just subscriptionType) mempty++queryType :: ObjectType IO+queryType = ObjectType "Query" Nothing [] $ HashMap.fromList+    [ ("dog", dogResolver)+    , ("cat", catResolver)+    , ("findDog", findDogResolver)+    ]+  where+    dogField = Field Nothing (Out.NamedObjectType dogType) mempty+    dogResolver = ValueResolver dogField $ pure Null+    findDogArguments = HashMap.singleton "complex"+        $ In.Argument Nothing (In.NonNullInputObjectType dogDataType) Nothing+    findDogField = Field Nothing (Out.NamedObjectType dogType) findDogArguments+    findDogResolver = ValueResolver findDogField $ pure Null+    catField = Field Nothing (Out.NamedObjectType catType) mempty+    catResolver = ValueResolver catField $ pure Null++catCommandType :: EnumType+catCommandType = EnumType "CatCommand" Nothing $ HashMap.fromList+    [ ("JUMP", EnumValue Nothing)+    ]++catType :: ObjectType IO+catType = ObjectType "Cat" Nothing [petType] $ HashMap.fromList+    [ ("name", nameResolver)+    , ("nickname", nicknameResolver)+    , ("doesKnowCommand", doesKnowCommandResolver)+    , ("meowVolume", meowVolumeResolver)+    ]+  where+    meowVolumeField = Field Nothing (Out.NamedScalarType int) mempty+    meowVolumeResolver = ValueResolver meowVolumeField $ pure $ Int 3+    doesKnowCommandField = Field Nothing (Out.NonNullScalarType boolean)+        $ HashMap.singleton "catCommand"+        $ In.Argument Nothing (In.NonNullEnumType catCommandType) Nothing+    doesKnowCommandResolver = ValueResolver doesKnowCommandField+        $ pure $ Boolean True++nameResolver :: Resolver IO+nameResolver = ValueResolver nameField $ pure "Name"+  where+    nameField = Field Nothing (Out.NonNullScalarType string) mempty++nicknameResolver :: Resolver IO+nicknameResolver = ValueResolver nicknameField $ pure "Nickname"+  where+    nicknameField = Field Nothing (Out.NamedScalarType string) mempty++dogCommandType :: EnumType+dogCommandType = EnumType "DogCommand" Nothing $ HashMap.fromList+    [ ("SIT", EnumValue Nothing)+    , ("DOWN", EnumValue Nothing)+    , ("HEEL", EnumValue Nothing)+    ]++dogType :: ObjectType IO+dogType = ObjectType "Dog" Nothing [petType] $ HashMap.fromList+    [ ("name", nameResolver)+    , ("nickname", nicknameResolver)+    , ("barkVolume", barkVolumeResolver)+    , ("doesKnowCommand", doesKnowCommandResolver)+    , ("isHousetrained", isHousetrainedResolver)+    , ("owner", ownerResolver)+    ]+  where+    barkVolumeField = Field Nothing (Out.NamedScalarType int) mempty+    barkVolumeResolver = ValueResolver barkVolumeField $ pure $ Int 3+    doesKnowCommandField = Field Nothing (Out.NonNullScalarType boolean)+        $ HashMap.singleton "dogCommand"+        $ In.Argument Nothing (In.NonNullEnumType dogCommandType) Nothing+    doesKnowCommandResolver = ValueResolver doesKnowCommandField+        $ pure $ Boolean True+    isHousetrainedField = Field Nothing (Out.NonNullScalarType boolean)+        $ HashMap.singleton "atOtherHomes"+        $ In.Argument Nothing (In.NamedScalarType boolean) Nothing+    isHousetrainedResolver = ValueResolver isHousetrainedField+        $ pure $ Boolean True+    ownerField = Field Nothing (Out.NamedObjectType humanType) mempty+    ownerResolver = ValueResolver ownerField $ pure Null++dogDataType :: InputObjectType+dogDataType = InputObjectType "DogData" Nothing+    $ HashMap.singleton "name" nameInputField+  where+    nameInputField = InputField Nothing (In.NonNullScalarType string) Nothing++sentientType :: InterfaceType IO+sentientType = InterfaceType "Sentient" Nothing []+    $ HashMap.singleton "name"+    $ Field Nothing (Out.NonNullScalarType string) mempty++petType :: InterfaceType IO+petType = InterfaceType "Pet" Nothing []+    $ HashMap.singleton "name"+    $ Field Nothing (Out.NonNullScalarType string) mempty++subscriptionType :: ObjectType IO+subscriptionType = ObjectType "Subscription" Nothing [] $ HashMap.fromList+    [ ("newMessage", newMessageResolver)+    , ("disallowedSecondRootField", newMessageResolver)+    ]+  where+    newMessageField = Field Nothing (Out.NonNullObjectType messageType) mempty+    newMessageResolver = ValueResolver newMessageField+        $ pure $ Object HashMap.empty++messageType :: ObjectType IO+messageType = ObjectType "Message" Nothing [] $ HashMap.fromList+    [ ("sender", senderResolver)+    , ("body", bodyResolver)+    ]+  where+    senderField = Field Nothing (Out.NonNullScalarType string) mempty+    senderResolver = ValueResolver senderField $ pure "Sender"+    bodyField = Field Nothing (Out.NonNullScalarType string) mempty+    bodyResolver = ValueResolver bodyField $ pure "Message body."++humanType :: ObjectType IO+humanType = ObjectType "Human" Nothing [sentientType] $ HashMap.fromList+    [ ("name", nameResolver)+    , ("pets", petsResolver)+    ]+  where+    petsField =+        Field Nothing (Out.ListType $ Out.NonNullInterfaceType petType) mempty+    petsResolver = ValueResolver petsField $ pure $ List []++validate :: Text -> [Error]+validate queryString =+    case parse AST.document "" queryString of+        Left parseErrors -> error $ errorBundlePretty parseErrors+        Right ast -> toList $ document petSchema specifiedRules ast++spec :: Spec+spec =+    describe "document" $ do+        context "executableDefinitionsRule" $+            it "rejects type definitions" $+                let queryString = [r|+                  query getDogName {+                    dog {+                      name+                      color+                    }+                  }++                  extend type Dog {+                    color: String+                  }+                |]+                    expected = Error+                        { message =+                            "Definition must be OperationDefinition or \+                            \FragmentDefinition."+                        , locations = [AST.Location 9 19]+                        }+                 in validate queryString `shouldContain` [expected]++        context "singleFieldSubscriptionsRule" $ do+            it "rejects multiple subscription root fields" $+                let queryString = [r|+                  subscription sub {+                    newMessage {+                      body+                      sender+                    }+                    disallowedSecondRootField+                  }+                |]+                    expected = Error+                        { message =+                            "Subscription \"sub\" must select only one top \+                            \level field."+                        , locations = [AST.Location 2 19]+                        }+                 in validate queryString `shouldContain` [expected]++            it "rejects multiple subscription root fields coming from a fragment" $+                let queryString = [r|+                  subscription sub {+                    ...multipleSubscriptions+                  }++                  fragment multipleSubscriptions on Subscription {+                    newMessage {+                      body+                      sender+                    }+                    disallowedSecondRootField+                  }+                |]+                    expected = Error+                        { message =+                            "Subscription \"sub\" must select only one top \+                            \level field."+                        , locations = [AST.Location 2 19]+                        }+                 in validate queryString `shouldContain` [expected]++            it "finds corresponding subscription fragment" $+                let queryString = [r|+                  subscription sub {+                    ...anotherSubscription+                    ...multipleSubscriptions+                  }+                  fragment multipleSubscriptions on Subscription {+                    newMessage {+                      body+                    }+                    disallowedSecondRootField {+                      sender+                    }+                  }+                  fragment anotherSubscription on Subscription {+                    newMessage {+                      body+                      sender+                    }+                  }+                |]+                    expected = Error+                        { message =+                            "Subscription \"sub\" must select only one top \+                            \level field."+                        , locations = [AST.Location 2 19]+                        }+                 in validate queryString `shouldBe` [expected]++        context "loneAnonymousOperationRule" $+            it "rejects multiple anonymous operations" $+                let queryString = [r|+                  {+                    dog {+                      name+                    }+                  }++                  query getName {+                    dog {+                      owner {+                        name+                      }+                    }+                  }+                |]+                    expected = Error+                        { message =+                            "This anonymous operation must be the only defined \+                            \operation."+                        , locations = [AST.Location 2 19]+                        }+                 in validate queryString `shouldBe` [expected]++        context "uniqueOperationNamesRule" $+            it "rejects operations with the same name" $+                let queryString = [r|+                  query dogOperation {+                    dog {+                      name+                    }+                  }++                  mutation dogOperation {+                    mutateDog {+                      id+                    }+                }+                |]+                    expected = Error+                        { message =+                            "There can be only one operation named \+                            \\"dogOperation\"."+                        , locations = [AST.Location 2 19, AST.Location 8 19]+                        }+                 in validate queryString `shouldBe` [expected]++        context "uniqueFragmentNamesRule" $+            it "rejects fragments with the same name" $+                let queryString = [r|+                  {+                    dog {+                      ...fragmentOne+                    }+                  }++                  fragment fragmentOne on Dog {+                    name+                  }++                  fragment fragmentOne on Dog {+                    owner {+                      name+                    }+                  }+                |]+                    expected = Error+                        { message =+                            "There can be only one fragment named \+                            \\"fragmentOne\"."+                        , locations = [AST.Location 8 19, AST.Location 12 19]+                        }+                 in validate queryString `shouldBe` [expected]++        context "fragmentSpreadTargetDefinedRule" $+            it "rejects the fragment spread without a target" $+                let queryString = [r|+                  {+                    dog {+                      ...undefinedFragment+                    }+                  }+                |]+                    expected = Error+                        { message =+                            "Fragment target \"undefinedFragment\" is \+                            \undefined."+                        , locations = [AST.Location 4 23]+                        }+                 in validate queryString `shouldBe` [expected]++        context "fragmentSpreadTypeExistenceRule" $ do+            it "rejects fragment spreads without an unknown target type" $+                let queryString = [r|+                  {+                    dog {+                      ...notOnExistingType+                    }+                  }+                  fragment notOnExistingType on NotInSchema {+                    name+                  }+                |]+                    expected = Error+                        { message =+                            "Fragment \"notOnExistingType\" is specified on \+                            \type \"NotInSchema\" which doesn't exist in the \+                            \schema."+                        , locations = [AST.Location 4 23]+                        }+                 in validate queryString `shouldBe` [expected]++            it "rejects inline fragments without a target" $+                let queryString = [r|+                  {+                    ... on NotInSchema {+                      name+                    }+                  }+                |]+                    expected = Error+                        { message =+                            "Inline fragment is specified on type \+                            \\"NotInSchema\" which doesn't exist in the schema."+                        , locations = [AST.Location 3 21]+                        }+                 in validate queryString `shouldBe` [expected]++        context "fragmentsOnCompositeTypesRule" $ do+            it "rejects fragments on scalar types" $+                let queryString = [r|+                  {+                    dog {+                      ...fragOnScalar+                    }+                  }+                  fragment fragOnScalar on Int {+                    name+                  }+                |]+                    expected = Error+                        { message =+                            "Fragment cannot condition on non composite type \+                            \\"Int\"."+                        , locations = [AST.Location 7 19]+                        }+                 in validate queryString `shouldContain` [expected]++            it "rejects inline fragments on scalar types" $+                let queryString = [r|+                  {+                    ... on Boolean {+                      name+                    }+                  }+                |]+                    expected = Error+                        { message =+                            "Fragment cannot condition on non composite type \+                            \\"Boolean\"."+                        , locations = [AST.Location 3 21]+                        }+                in validate queryString `shouldContain` [expected]++        context "noUnusedFragmentsRule" $+            it "rejects unused fragments" $+                let queryString = [r|+                  fragment nameFragment on Dog { # unused+                    name+                  }++                  {+                    dog {+                      name+                    }+                  }+                |]+                    expected = Error+                        { message =+                            "Fragment \"nameFragment\" is never used."+                        , locations = [AST.Location 2 19]+                        }+                 in validate queryString `shouldBe` [expected]++        context "noFragmentCyclesRule" $+            it "rejects spreads that form cycles" $+                let queryString = [r|+                  {+                    dog {+                      ...nameFragment+                    }+                  }+                  fragment nameFragment on Dog {+                    name+                    ...barkVolumeFragment+                  }+                  fragment barkVolumeFragment on Dog {+                    barkVolume+                    ...nameFragment+                  }+                |]+                    error1 = Error+                        { message =+                            "Cannot spread fragment \"barkVolumeFragment\" \+                            \within itself (via barkVolumeFragment -> \+                            \nameFragment -> barkVolumeFragment)."+                        , locations = [AST.Location 11 19]+                        }+                    error2 = Error+                        { message =+                            "Cannot spread fragment \"nameFragment\" within \+                            \itself (via nameFragment -> barkVolumeFragment -> \+                            \nameFragment)."+                        , locations = [AST.Location 7 19]+                        }+                 in validate queryString `shouldBe` [error1, error2]++        context "uniqueArgumentNamesRule" $+            it "rejects duplicate field arguments" $+                let queryString = [r|+                  {+                    dog {+                      isHousetrained(atOtherHomes: true, atOtherHomes: true)+                    }+                  }+                |]+                    expected = Error+                        { message =+                            "There can be only one argument named \+                            \\"atOtherHomes\"."+                        , locations = [AST.Location 4 38, AST.Location 4 58]+                        }+                 in validate queryString `shouldBe` [expected]++        context "uniqueDirectiveNamesRule" $+            it "rejects more than one directive per location" $+                let queryString = [r|+                  query ($foo: Boolean = true, $bar: Boolean = false) {+                    dog @skip(if: $foo) @skip(if: $bar) {+                      name+                    }+                  }+                |]+                    expected = Error+                        { message =+                            "There can be only one directive named \"skip\"."+                        , locations = [AST.Location 3 25, AST.Location 3 41]+                        }+                 in validate queryString `shouldBe` [expected]++        context "uniqueVariableNamesRule" $+            it "rejects duplicate variables" $+                let queryString = [r|+                  query houseTrainedQuery($atOtherHomes: Boolean, $atOtherHomes: Boolean) {+                    dog {+                      isHousetrained(atOtherHomes: $atOtherHomes)+                    }+                  }+                |]+                    expected = Error+                        { message =+                            "There can be only one variable named \+                            \\"atOtherHomes\"."+                        , locations = [AST.Location 2 43, AST.Location 2 67]+                        }+                 in validate queryString `shouldBe` [expected]++        context "variablesAreInputTypesRule" $+            it "rejects non-input types as variables" $+                let queryString = [r|+                  query takesDogBang($dog: Dog!) {+                    dog {+                      isHousetrained(atOtherHomes: $dog)+                    }+                  }+                |]+                    expected = Error+                        { message =+                            "Variable \"$dog\" cannot be non-input type \+                            \\"Dog\"."+                        , locations = [AST.Location 2 38]+                        }+                 in validate queryString `shouldContain` [expected]++        context "noUndefinedVariablesRule" $+            it "rejects undefined variables" $+                let queryString = [r|+                  query variableIsNotDefinedUsedInSingleFragment {+                    dog {+                      ...isHousetrainedFragment+                    }+                  }++                  fragment isHousetrainedFragment on Dog {+                    isHousetrained(atOtherHomes: $atOtherHomes)+                  }+                |]+                    expected = Error+                        { message =+                            "Variable \"$atOtherHomes\" is not defined by \+                            \operation \+                            \\"variableIsNotDefinedUsedInSingleFragment\"."+                        , locations = [AST.Location 9 50]+                        }+                 in validate queryString `shouldBe` [expected]++        context "noUnusedVariablesRule" $+            it "rejects unused variables" $+                let queryString = [r|+                  query variableUnused($atOtherHomes: Boolean) {+                    dog {+                      isHousetrained+                    }+                  }+                |]+                    expected = Error+                        { message =+                            "Variable \"$atOtherHomes\" is never used in \+                            \operation \"variableUnused\"."+                        , locations = [AST.Location 2 40]+                        }+                 in validate queryString `shouldBe` [expected]++        context "uniqueInputFieldNamesRule" $+            it "rejects duplicate fields in input objects" $+                let queryString = [r|+                  {+                    findDog(complex: { name: "Fido", name: "Jack" }) {+                      name+                    }+                  }+                |]+                    expected = Error+                        { message =+                            "There can be only one input field named \"name\"."+                        , locations = [AST.Location 3 40, AST.Location 3 54]+                        }+                 in validate queryString `shouldBe` [expected]++        context "fieldsOnCorrectTypeRule" $+            it "rejects undefined fields" $+                let queryString = [r|+                  {+                    dog {+                      meowVolume+                    }+                  }+                |]+                    expected = Error+                        { message =+                            "Cannot query field \"meowVolume\" on type \"Dog\"."+                        , locations = [AST.Location 4 23]+                        }+                 in validate queryString `shouldBe` [expected]++        context "scalarLeafsRule" $+            it "rejects scalar fields with not empty selection set" $+                let queryString = [r|+                  {+                    dog {+                      barkVolume {+                        sinceWhen+                      }+                    }+                  }+                |]+                    expected = Error+                        { message =+                            "Field \"barkVolume\" must not have a selection \+                            \since type \"Int\" has no subfields."+                        , locations = [AST.Location 4 23]+                        }+                 in validate queryString `shouldBe` [expected]++        context "knownArgumentNamesRule" $ do+            it "rejects field arguments missing in the type" $+                let queryString = [r|+                  {+                    dog {+                      doesKnowCommand(command: CLEAN_UP_HOUSE, dogCommand: SIT)+                    }+                  }+                |]+                    expected = Error+                        { message =+                            "Unknown argument \"command\" on field \+                            \\"Dog.doesKnowCommand\"."+                        , locations = [AST.Location 4 39]+                        }+                 in validate queryString `shouldBe` [expected]++            it "rejects directive arguments missing in the definition" $+                let queryString = [r|+                  {+                    dog {+                      isHousetrained(atOtherHomes: true) @include(unless: false, if: true)+                    }+                  }+                |]+                    expected = Error+                        { message =+                            "Unknown argument \"unless\" on directive \+                            \\"@include\"."+                        , locations = [AST.Location 4 67]+                        }+                 in validate queryString `shouldBe` [expected]++        context "knownDirectiveNamesRule" $+            it "rejects undefined directives" $+                let queryString = [r|+                  {+                    dog {+                      isHousetrained(atOtherHomes: true) @ignore(if: true)+                    }+                  }+                |]+                    expected = Error+                        { message = "Unknown directive \"@ignore\"."+                        , locations = [AST.Location 4 58]+                        }+                 in validate queryString `shouldBe` [expected]++        context "knownInputFieldNamesRule" $+            it "rejects undefined input object fields" $+                let queryString = [r|+                  {+                    findDog(complex: { favoriteCookieFlavor: "Bacon", name: "Jack" }) {+                      name+                    }+                  }+                |]+                    expected = Error+                        { message =+                            "Field \"favoriteCookieFlavor\" is not defined \+                            \by type \"DogData\"."+                        , locations = [AST.Location 3 40]+                        }+                 in validate queryString `shouldBe` [expected]++        context "directivesInValidLocationsRule" $+            it "rejects directives in invalid locations" $+                let queryString = [r|+                  query @skip(if: $foo) {+                    dog {+                      name+                    }+                  }+                |]+                    expected = Error+                        { message =+                            "Directive \"@skip\" may not be used on QUERY."+                        , locations = [AST.Location 2 25]+                        }+                 in validate queryString `shouldBe` [expected]++        context "overlappingFieldsCanBeMergedRule" $ do+            it "fails to merge fields of mismatching types" $+                let queryString = [r|+                  {+                    dog {+                      name: nickname+                      name+                    }+                  }+                |]+                    expected = Error+                        { message =+                            "Fields \"name\" conflict because \"nickname\" and \+                            \\"name\" are different fields. Use different \+                            \aliases on the fields to fetch both if this was \+                            \intentional."+                        , locations = [AST.Location 4 23, AST.Location 5 23]+                        }+                 in validate queryString `shouldBe` [expected]++            it "fails if the arguments of the same field don't match" $+                let queryString = [r|+                  {+                    dog {+                      doesKnowCommand(dogCommand: SIT)+                      doesKnowCommand(dogCommand: HEEL)+                    }+                  }+                |]+                    expected = Error+                        { message =+                            "Fields \"doesKnowCommand\" conflict because they \+                            \have different arguments. Use different aliases \+                            \on the fields to fetch both if this was \+                            \intentional."+                        , locations = [AST.Location 4 23, AST.Location 5 23]+                        }+                 in validate queryString `shouldBe` [expected]++            it "fails to merge same-named field and alias" $+                let queryString = [r|+                  {+                    dog {+                      doesKnowCommand(dogCommand: SIT)+                      doesKnowCommand: isHousetrained(atOtherHomes: true)+                    }+                  }+                |]+                    expected = Error+                        { message =+                            "Fields \"doesKnowCommand\" conflict because \+                            \\"doesKnowCommand\" and \"isHousetrained\" are \+                            \different fields. Use different aliases on the \+                            \fields to fetch both if this was intentional."+                        , locations = [AST.Location 4 23, AST.Location 5 23]+                        }+                 in validate queryString `shouldBe` [expected]++            it "looks for fields after a successfully merged field pair" $+                let queryString = [r|+                  {+                    dog {+                      name+                      doesKnowCommand(dogCommand: SIT)+                    }+                    dog {+                      name+                      doesKnowCommand: isHousetrained(atOtherHomes: true)+                    }+                  }+                |]+                    expected = Error+                        { message =+                            "Fields \"doesKnowCommand\" conflict because \+                            \\"doesKnowCommand\" and \"isHousetrained\" are \+                            \different fields. Use different aliases on the \+                            \fields to fetch both if this was intentional."+                        , locations = [AST.Location 5 23, AST.Location 9 23]+                        }+                 in validate queryString `shouldBe` [expected]++        context "possibleFragmentSpreadsRule" $ do+            it "rejects object inline spreads outside object scope" $+                let queryString = [r|+                  {+                    dog {+                      ... on Cat {+                        meowVolume+                      }+                    }+                  }+                |]+                    expected = Error+                        { message =+                            "Fragment cannot be spread here as objects of type \+                            \\"Dog\" can never be of type \"Cat\"."+                        , locations = [AST.Location 4 23]+                        }+                 in validate queryString `shouldBe` [expected]++            it "rejects object named spreads outside object scope" $+                let queryString = [r|+                  {+                    dog {+                      ... catInDogFragmentInvalid+                    }+                  }++                  fragment catInDogFragmentInvalid on Cat {+                    meowVolume+                  }+                |]+                    expected = Error+                        { message =+                            "Fragment \"catInDogFragmentInvalid\" cannot be \+                            \spread here as objects of type \"Dog\" can never \+                            \be of type \"Cat\"."+                        , locations = [AST.Location 4 23]+                        }+                 in validate queryString `shouldBe` [expected]++        context "providedRequiredInputFieldsRule" $+            it "rejects missing required input fields" $+                let queryString = [r|+                  {+                    findDog(complex: { name: null }) {+                      name+                    }+                  }+                |]+                    expected = Error+                        { message =+                            "Input field \"name\" of type \"DogData\" is \+                            \required, but it was not provided."+                        , locations = [AST.Location 3 38]+                        }+                 in validate queryString `shouldBe` [expected]++        context "providedRequiredArgumentsRule" $+            it "checks for (non-)nullable arguments" $+                let queryString = [r|+                  {+                    dog {+                      doesKnowCommand(dogCommand: null)+                    }+                  }+                |]+                    expected = Error+                        { message =+                            "Field \"doesKnowCommand\" argument \"dogCommand\" \+                            \of type \"DogCommand\" is required, but it was \+                            \not provided."+                        , locations = [AST.Location 4 23]+                        }+                 in validate queryString `shouldBe` [expected]++        context "variablesInAllowedPositionRule" $ do+            it "rejects wrongly typed variable arguments" $+                let queryString = [r|+                  query catCommandArgQuery($catCommandArg: CatCommand) {+                    cat {+                      doesKnowCommand(catCommand: $catCommandArg)+                    }+                  }+                |]+                    expected = Error+                        { message =+                            "Variable \"$catCommandArg\" of type \+                            \\"CatCommand\" used in position expecting type \+                            \\"!CatCommand\"."+                        , locations = [AST.Location 2 44]+                        }+                 in validate queryString `shouldBe` [expected]++            it "rejects wrongly typed variable arguments" $+                let queryString = [r|+                  query intCannotGoIntoBoolean($intArg: Int) {+                    dog {+                      isHousetrained(atOtherHomes: $intArg)+                    }+                  }+                |]+                    expected = Error+                        { message =+                            "Variable \"$intArg\" of type \"Int\" used in \+                            \position expecting type \"Boolean\"."+                        , locations = [AST.Location 2 48]+                        }+                 in validate queryString `shouldBe` [expected]++        context "valuesOfCorrectTypeRule" $+            it "rejects values of incorrect types" $+                let queryString = [r|+                  {+                    dog {+                      isHousetrained(atOtherHomes: 3)+                    }+                  }+                |]+                    expected = Error+                        { message =+                            "Value 3 cannot be coerced to type \"Boolean\"."+                        , locations = [AST.Location 4 52]+                        }+                 in validate queryString `shouldBe` [expected]
− tests/Language/GraphQL/ValidateSpec.hs
@@ -1,665 +0,0 @@-{- This Source Code Form is subject to the terms of the Mozilla Public License,-   v. 2.0. If a copy of the MPL was not distributed with this file, You can-   obtain one at https://mozilla.org/MPL/2.0/. -}--{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE QuasiQuotes #-}--module Language.GraphQL.ValidateSpec-    ( spec-    ) where--import Data.Foldable (toList)-import qualified Data.HashMap.Strict as HashMap-import Data.Text (Text)-import qualified Language.GraphQL.AST as AST-import Language.GraphQL.Type-import qualified Language.GraphQL.Type.In as In-import qualified Language.GraphQL.Type.Out as Out-import Language.GraphQL.Validate-import Test.Hspec (Spec, describe, it, shouldBe, shouldContain)-import Text.Megaparsec (parse)-import Text.RawString.QQ (r)--petSchema :: Schema IO-petSchema = schema queryType Nothing (Just subscriptionType) mempty--queryType :: ObjectType IO-queryType = ObjectType "Query" Nothing [] $ HashMap.fromList-    [ ("dog", dogResolver)-    , ("findDog", findDogResolver)-    ]-  where-    dogField = Field Nothing (Out.NamedObjectType dogType) mempty-    dogResolver = ValueResolver dogField $ pure Null-    findDogArguments = HashMap.singleton "complex"-        $ In.Argument Nothing (In.NonNullInputObjectType dogDataType) Nothing-    findDogField = Field Nothing (Out.NamedObjectType dogType) findDogArguments-    findDogResolver = ValueResolver findDogField $ pure Null--dogCommandType :: EnumType-dogCommandType = EnumType "DogCommand" Nothing $ HashMap.fromList-    [ ("SIT", EnumValue Nothing)-    , ("DOWN", EnumValue Nothing)-    , ("HEEL", EnumValue Nothing)-    ]--dogType :: ObjectType IO-dogType = ObjectType "Dog" Nothing [petType] $ HashMap.fromList-    [ ("name", nameResolver)-    , ("nickname", nicknameResolver)-    , ("barkVolume", barkVolumeResolver)-    , ("doesKnowCommand", doesKnowCommandResolver)-    , ("isHousetrained", isHousetrainedResolver)-    , ("owner", ownerResolver)-    ]-  where-    nameField = Field Nothing (Out.NonNullScalarType string) mempty-    nameResolver = ValueResolver nameField $ pure "Name"-    nicknameField = Field Nothing (Out.NamedScalarType string) mempty-    nicknameResolver = ValueResolver nicknameField $ pure "Nickname"-    barkVolumeField = Field Nothing (Out.NamedScalarType int) mempty-    barkVolumeResolver = ValueResolver barkVolumeField $ pure $ Int 3-    doesKnowCommandField = Field Nothing (Out.NonNullScalarType boolean)-        $ HashMap.singleton "dogCommand"-        $ In.Argument Nothing (In.NonNullEnumType dogCommandType) Nothing-    doesKnowCommandResolver = ValueResolver doesKnowCommandField-        $ pure $ Boolean True-    isHousetrainedField = Field Nothing (Out.NonNullScalarType boolean)-        $ HashMap.singleton "atOtherHomes"-        $ In.Argument Nothing (In.NamedScalarType boolean) Nothing-    isHousetrainedResolver = ValueResolver isHousetrainedField-        $ pure $ Boolean True-    ownerField = Field Nothing (Out.NamedObjectType humanType) mempty-    ownerResolver = ValueResolver ownerField $ pure Null--dogDataType :: InputObjectType-dogDataType = InputObjectType "DogData" Nothing-    $ HashMap.singleton "name" nameInputField-  where-    nameInputField = InputField Nothing (In.NonNullScalarType string) Nothing--sentientType :: InterfaceType IO-sentientType = InterfaceType "Sentient" Nothing []-    $ HashMap.singleton "name"-    $ Field Nothing (Out.NonNullScalarType string) mempty--petType :: InterfaceType IO-petType = InterfaceType "Pet" Nothing []-    $ HashMap.singleton "name"-    $ Field Nothing (Out.NonNullScalarType string) mempty--subscriptionType :: ObjectType IO-subscriptionType = ObjectType "Subscription" Nothing [] $ HashMap.fromList-    [ ("newMessage", newMessageResolver)-    , ("disallowedSecondRootField", newMessageResolver)-    ]-  where-    newMessageField = Field Nothing (Out.NonNullObjectType messageType) mempty-    newMessageResolver = ValueResolver newMessageField-        $ pure $ Object HashMap.empty--messageType :: ObjectType IO-messageType = ObjectType "Message" Nothing [] $ HashMap.fromList-    [ ("sender", senderResolver)-    , ("body", bodyResolver)-    ]-  where-    senderField = Field Nothing (Out.NonNullScalarType string) mempty-    senderResolver = ValueResolver senderField $ pure "Sender"-    bodyField = Field Nothing (Out.NonNullScalarType string) mempty-    bodyResolver = ValueResolver bodyField $ pure "Message body."--humanType :: ObjectType IO-humanType = ObjectType "Human" Nothing [sentientType] $ HashMap.fromList-    [ ("name", nameResolver)-    , ("pets", petsResolver)-    ]-  where-    nameField = Field Nothing (Out.NonNullScalarType string) mempty-    nameResolver = ValueResolver nameField $ pure "Name"-    petsField =-        Field Nothing (Out.ListType $ Out.NonNullInterfaceType petType) mempty-    petsResolver = ValueResolver petsField $ pure $ List []-{--catOrDogType :: UnionType IO-catOrDogType = UnionType "CatOrDog" Nothing [catType, dogType]--}-validate :: Text -> [Error]-validate queryString =-    case parse AST.document "" queryString of-        Left _ -> []-        Right ast -> toList $ document petSchema specifiedRules ast--spec :: Spec-spec =-    describe "document" $ do-        it "rejects type definitions" $-            let queryString = [r|-              query getDogName {-                dog {-                  name-                  color-                }-              }--              extend type Dog {-                color: String-              }-            |]-                expected = Error-                    { message =-                        "Definition must be OperationDefinition or FragmentDefinition."-                    , locations = [AST.Location 9 15]-                    }-               in validate queryString `shouldContain` [expected]--        it "rejects multiple subscription root fields" $-            let queryString = [r|-              subscription sub {-                newMessage {-                  body-                  sender-                }-                disallowedSecondRootField-              }-            |]-                expected = Error-                    { message =-                        "Subscription \"sub\" must select only one top level \-                        \field."-                    , locations = [AST.Location 2 15]-                    }-             in validate queryString `shouldContain` [expected]--        it "rejects multiple subscription root fields coming from a fragment" $-            let queryString = [r|-              subscription sub {-                ...multipleSubscriptions-              }--              fragment multipleSubscriptions on Subscription {-                newMessage {-                  body-                  sender-                }-                disallowedSecondRootField-              }-            |]-                expected = Error-                    { message =-                        "Subscription \"sub\" must select only one top level \-                        \field."-                    , locations = [AST.Location 2 15]-                    }-             in validate queryString `shouldContain` [expected]--        it "rejects multiple anonymous operations" $-            let queryString = [r|-              {-                dog {-                  name-                }-              }--              query getName {-                dog {-                  owner {-                    name-                  }-                }-              }-            |]-                expected = Error-                    { message =-                        "This anonymous operation must be the only defined operation."-                    , locations = [AST.Location 2 15]-                    }-             in validate queryString `shouldBe` [expected]--        it "rejects operations with the same name" $-            let queryString = [r|-              query dogOperation {-                dog {-                  name-                }-              }--              mutation dogOperation {-                mutateDog {-                  id-                }-            }-            |]-                expected = Error-                    { message =-                        "There can be only one operation named \"dogOperation\"."-                    , locations = [AST.Location 2 15, AST.Location 8 15]-                    }-             in validate queryString `shouldBe` [expected]--        it "rejects fragments with the same name" $-            let queryString = [r|-              {-                dog {-                  ...fragmentOne-                }-              }--              fragment fragmentOne on Dog {-                name-              }--              fragment fragmentOne on Dog {-                owner {-                  name-                }-              }-            |]-                expected = Error-                    { message =-                        "There can be only one fragment named \"fragmentOne\"."-                    , locations = [AST.Location 8 15, AST.Location 12 15]-                    }-             in validate queryString `shouldBe` [expected]--        it "rejects the fragment spread without a target" $-            let queryString = [r|-              {-                dog {-                  ...undefinedFragment-                }-              }-            |]-                expected = Error-                    { message =-                        "Fragment target \"undefinedFragment\" is undefined."-                    , locations = [AST.Location 4 19]-                    }-             in validate queryString `shouldBe` [expected]--        it "rejects fragment spreads without an unknown target type" $-            let queryString = [r|-              {-                dog {-                  ...notOnExistingType-                }-              }-              fragment notOnExistingType on NotInSchema {-                name-              }-            |]-                expected = Error-                    { message =-                        "Fragment \"notOnExistingType\" is specified on type \-                        \\"NotInSchema\" which doesn't exist in the schema."-                    , locations = [AST.Location 4 19]-                    }-             in validate queryString `shouldBe` [expected]--        it "rejects inline fragments without a target" $-            let queryString = [r|-              {-                ... on NotInSchema {-                  name-                }-              }-            |]-                expected = Error-                    { message =-                        "Inline fragment is specified on type \"NotInSchema\" \-                        \which doesn't exist in the schema."-                    , locations = [AST.Location 3 17]-                    }-             in validate queryString `shouldBe` [expected]--        it "rejects fragments on scalar types" $-            let queryString = [r|-              {-                dog {-                  ...fragOnScalar-                }-              }-              fragment fragOnScalar on Int {-                name-              }-            |]-                expected = Error-                    { message =-                        "Fragment cannot condition on non composite type \-                        \\"Int\"."-                    , locations = [AST.Location 7 15]-                    }-             in validate queryString `shouldContain` [expected]--        it "rejects inline fragments on scalar types" $-            let queryString = [r|-              {-                ... on Boolean {-                  name-                }-              }-            |]-                expected = Error-                    { message =-                        "Fragment cannot condition on non composite type \-                        \\"Boolean\"."-                    , locations = [AST.Location 3 17]-                    }-             in validate queryString `shouldContain` [expected]--        it "rejects unused fragments" $-            let queryString = [r|-              fragment nameFragment on Dog { # unused-                name-              }--              {-                dog {-                  name-                }-              }-            |]-                expected = Error-                    { message =-                        "Fragment \"nameFragment\" is never used."-                    , locations = [AST.Location 2 15]-                    }-             in validate queryString `shouldBe` [expected]--        it "rejects spreads that form cycles" $-            let queryString = [r|-              {-                dog {-                  ...nameFragment-                }-              }-              fragment nameFragment on Dog {-                name-                ...barkVolumeFragment-              }-              fragment barkVolumeFragment on Dog {-                barkVolume-                ...nameFragment-              }-            |]-                error1 = Error-                    { message =-                        "Cannot spread fragment \"barkVolumeFragment\" within \-                        \itself (via barkVolumeFragment -> nameFragment -> \-                        \barkVolumeFragment)."-                    , locations = [AST.Location 11 15]-                    }-                error2 = Error-                    { message =-                        "Cannot spread fragment \"nameFragment\" within itself \-                        \(via nameFragment -> barkVolumeFragment -> \-                        \nameFragment)."-                    , locations = [AST.Location 7 15]-                    }-             in validate queryString `shouldBe` [error1, error2]--        it "rejects duplicate field arguments" $ do-            let queryString = [r|-              {-                dog {-                  isHousetrained(atOtherHomes: true, atOtherHomes: true)-                }-              }-            |]-                expected = Error-                    { message =-                        "There can be only one argument named \"atOtherHomes\"."-                    , locations = [AST.Location 4 34, AST.Location 4 54]-                    }-             in validate queryString `shouldBe` [expected]--        it "rejects more than one directive per location" $ do-            let queryString = [r|-              query ($foo: Boolean = true, $bar: Boolean = false) {-                dog @skip(if: $foo) @skip(if: $bar) {-                  name-                }-              }-            |]-                expected = Error-                    { message =-                        "There can be only one directive named \"skip\"."-                    , locations = [AST.Location 3 21, AST.Location 3 37]-                    }-             in validate queryString `shouldBe` [expected]--        it "rejects duplicate variables" $-            let queryString = [r|-              query houseTrainedQuery($atOtherHomes: Boolean, $atOtherHomes: Boolean) {-                dog {-                  isHousetrained(atOtherHomes: $atOtherHomes)-                }-              }-            |]-                expected = Error-                    { message =-                        "There can be only one variable named \"atOtherHomes\"."-                    , locations = [AST.Location 2 39, AST.Location 2 63]-                    }-             in validate queryString `shouldBe` [expected]--        it "rejects non-input types as variables" $-            let queryString = [r|-              query takesDogBang($dog: Dog!) {-                dog {-                  isHousetrained(atOtherHomes: $dog)-                }-              }-            |]-                expected = Error-                    { message =-                        "Variable \"$dog\" cannot be non-input type \"Dog\"."-                    , locations = [AST.Location 2 34]-                    }-             in validate queryString `shouldBe` [expected]--        it "rejects undefined variables" $-            let queryString = [r|-              query variableIsNotDefinedUsedInSingleFragment {-                dog {-                  ...isHousetrainedFragment-                }-              }--              fragment isHousetrainedFragment on Dog {-                isHousetrained(atOtherHomes: $atOtherHomes)-              }-            |]-                expected = Error-                    { message =-                        "Variable \"$atOtherHomes\" is not defined by \-                        \operation \-                        \\"variableIsNotDefinedUsedInSingleFragment\"."-                    , locations = [AST.Location 9 46]-                    }-             in validate queryString `shouldBe` [expected]--        it "rejects unused variables" $-            let queryString = [r|-              query variableUnused($atOtherHomes: Boolean) {-                dog {-                  isHousetrained-                }-              }-            |]-                expected = Error-                    { message =-                        "Variable \"$atOtherHomes\" is never used in operation \-                        \\"variableUnused\"."-                    , locations = [AST.Location 2 36]-                    }-             in validate queryString `shouldBe` [expected]--        it "rejects duplicate fields in input objects" $-            let queryString = [r|-              {-                findDog(complex: { name: "Fido", name: "Jack" }) {-                  name-                }-              }-            |]-                expected = Error-                    { message =-                        "There can be only one input field named \"name\"."-                    , locations = [AST.Location 3 36, AST.Location 3 50]-                    }-             in validate queryString `shouldBe` [expected]--        it "rejects undefined fields" $-            let queryString = [r|-              {-                dog {-                  meowVolume-                }-              }-            |]-                expected = Error-                    { message =-                        "Cannot query field \"meowVolume\" on type \"Dog\"."-                    , locations = [AST.Location 4 19]-                    }-             in validate queryString `shouldBe` [expected]--        it "rejects scalar fields with not empty selection set" $-            let queryString = [r|-              {-                dog {-                  barkVolume {-                    sinceWhen-                  }-                }-              }-            |]-                expected = Error-                    { message =-                        "Field \"barkVolume\" must not have a selection since \-                        \type \"Int\" has no subfields."-                    , locations = [AST.Location 4 19]-                    }-             in validate queryString `shouldBe` [expected]--        it "rejects field arguments missing in the type" $-            let queryString = [r|-              {-                dog {-                  doesKnowCommand(command: CLEAN_UP_HOUSE, dogCommand: SIT)-                }-              }-            |]-                expected = Error-                    { message =-                        "Unknown argument \"command\" on field \-                        \\"Dog.doesKnowCommand\"."-                    , locations = [AST.Location 4 35]-                    }-             in validate queryString `shouldBe` [expected]--        it "rejects directive arguments missing in the definition" $-            let queryString = [r|-              {-                dog {-                  isHousetrained(atOtherHomes: true) @include(unless: false, if: true)-                }-              }-            |]-                expected = Error-                    { message =-                        "Unknown argument \"unless\" on directive \"@include\"."-                    , locations = [AST.Location 4 63]-                    }-             in validate queryString `shouldBe` [expected]--        it "rejects undefined directives" $-            let queryString = [r|-              {-                dog {-                  isHousetrained(atOtherHomes: true) @ignore(if: true)-                }-              }-            |]-                expected = Error-                    { message = "Unknown directive \"@ignore\"."-                    , locations = [AST.Location 4 54]-                    }-             in validate queryString `shouldBe` [expected]--        it "rejects undefined input object fields" $-            let queryString = [r|-              {-                findDog(complex: { favoriteCookieFlavor: "Bacon", name: "Jack" }) {-                  name-                }-              }-            |]-                expected = Error-                    { message =-                        "Field \"favoriteCookieFlavor\" is not defined \-                        \by type \"DogData\"."-                    , locations = [AST.Location 3 36]-                    }-             in validate queryString `shouldBe` [expected]--        it "rejects directives in invalid locations" $-            let queryString = [r|-              query @skip(if: $foo) {-                dog {-                  name-                }-              }-            |]-                expected = Error-                    { message = "Directive \"@skip\" may not be used on QUERY."-                    , locations = [AST.Location 2 21]-                    }-             in validate queryString `shouldBe` [expected]--        it "rejects missing required input fields" $-            let queryString = [r|-              {-                findDog(complex: { name: null }) {-                  name-                }-              }-            |]-                expected = Error-                    { message =-                        "Input field \"name\" of type \"DogData\" is required, \-                        \but it was not provided."-                    , locations = [AST.Location 3 34]-                    }-             in validate queryString `shouldBe` [expected]--        it "finds corresponding subscription fragment" $-            let queryString = [r|-              subscription sub {-                ...anotherSubscription-                ...multipleSubscriptions-              }-              fragment multipleSubscriptions on Subscription {-                newMessage {-                  body-                }-                disallowedSecondRootField {-                  sender-                }-              }-              fragment anotherSubscription on Subscription {-                newMessage {-                  body-                  sender-                }-              }-            |]-                expected = Error-                    { message =-                        "Subscription \"sub\" must select only one top level \-                        \field."-                    , locations = [AST.Location 2 15]-                    }-             in validate queryString `shouldBe` [expected]