graphql 0.11.1.0 → 1.0.0.0
raw patch · 23 files changed
+1092/−398 lines, 23 filesdep +vectordep ~QuickCheckdep ~aesondep ~conduitPVP ok
version bump matches the API change (PVP)
Dependencies added: vector
Dependency ranges changed: QuickCheck, aeson, conduit, containers, exceptions, hspec, hspec-expectations, hspec-megaparsec, megaparsec, parser-combinators, raw-strings-qq, scientific, text, transformers, unordered-containers
API changes (from Hackage documentation)
+ Language.GraphQL.Execute.OrderedMap: data OrderedMap v
+ Language.GraphQL.Execute.OrderedMap: elems :: forall v. OrderedMap v -> [v]
+ Language.GraphQL.Execute.OrderedMap: empty :: forall v. OrderedMap v
+ Language.GraphQL.Execute.OrderedMap: foldlWithKey' :: forall v a. (a -> Text -> v -> a) -> a -> OrderedMap v -> a
+ Language.GraphQL.Execute.OrderedMap: insert :: Semigroup v => Text -> v -> OrderedMap v -> OrderedMap v
+ Language.GraphQL.Execute.OrderedMap: instance Data.Foldable.Foldable Language.GraphQL.Execute.OrderedMap.OrderedMap
+ Language.GraphQL.Execute.OrderedMap: instance Data.Traversable.Traversable Language.GraphQL.Execute.OrderedMap.OrderedMap
+ Language.GraphQL.Execute.OrderedMap: instance GHC.Base.Functor Language.GraphQL.Execute.OrderedMap.OrderedMap
+ Language.GraphQL.Execute.OrderedMap: instance GHC.Base.Semigroup v => GHC.Base.Monoid (Language.GraphQL.Execute.OrderedMap.OrderedMap v)
+ Language.GraphQL.Execute.OrderedMap: instance GHC.Base.Semigroup v => GHC.Base.Semigroup (Language.GraphQL.Execute.OrderedMap.OrderedMap v)
+ Language.GraphQL.Execute.OrderedMap: instance GHC.Classes.Eq v => GHC.Classes.Eq (Language.GraphQL.Execute.OrderedMap.OrderedMap v)
+ Language.GraphQL.Execute.OrderedMap: instance GHC.Show.Show v => GHC.Show.Show (Language.GraphQL.Execute.OrderedMap.OrderedMap v)
+ Language.GraphQL.Execute.OrderedMap: keys :: forall v. OrderedMap v -> [Text]
+ Language.GraphQL.Execute.OrderedMap: lookup :: forall v. Text -> OrderedMap v -> Maybe v
+ Language.GraphQL.Execute.OrderedMap: replace :: Text -> v -> OrderedMap v -> OrderedMap v
+ Language.GraphQL.Execute.OrderedMap: singleton :: forall v. Text -> v -> OrderedMap v
+ Language.GraphQL.Execute.OrderedMap: size :: forall v. OrderedMap v -> Int
+ Language.GraphQL.Execute.OrderedMap: toList :: forall v. OrderedMap v -> [(Text, v)]
+ Language.GraphQL.Execute.OrderedMap: traverseMaybe :: Applicative f => forall a. (a -> f (Maybe b)) -> OrderedMap a -> f (OrderedMap b)
+ Language.GraphQL.Type: schemaWithTypes :: forall m. Maybe Text -> ObjectType m -> Maybe (ObjectType m) -> Maybe (ObjectType m) -> [Type m] -> Directives -> Schema m
+ Language.GraphQL.Type.Schema: description :: forall m. Schema m -> Maybe Text
+ Language.GraphQL.Type.Schema: schemaWithTypes :: forall m. Maybe Text -> ObjectType m -> Maybe (ObjectType m) -> Maybe (ObjectType m) -> [Type m] -> Directives -> Schema m
- Language.GraphQL.AST.Document: ConstList :: [ConstValue] -> ConstValue
+ Language.GraphQL.AST.Document: ConstList :: [Node ConstValue] -> ConstValue
- Language.GraphQL.AST.Document: List :: [Value] -> Value
+ Language.GraphQL.AST.Document: List :: [Node Value] -> Value
- Language.GraphQL.Execute.Coerce: Object :: Map Name a -> Output a
+ Language.GraphQL.Execute.Coerce: Object :: OrderedMap a -> Output a
Files
- CHANGELOG.md +32/−1
- graphql.cabal +100/−105
- src/Language/GraphQL/AST/Document.hs +10/−4
- src/Language/GraphQL/AST/Encoder.hs +3/−3
- src/Language/GraphQL/AST/Parser.hs +25/−35
- src/Language/GraphQL/Error.hs +5/−2
- src/Language/GraphQL/Execute.hs +29/−21
- src/Language/GraphQL/Execute/Coerce.hs +7/−3
- src/Language/GraphQL/Execute/Execution.hs +86/−49
- src/Language/GraphQL/Execute/Internal.hs +31/−0
- src/Language/GraphQL/Execute/OrderedMap.hs +148/−0
- src/Language/GraphQL/Execute/Subscribe.hs +47/−31
- src/Language/GraphQL/Execute/Transform.hs +50/−41
- src/Language/GraphQL/Type.hs +1/−1
- src/Language/GraphQL/Type/Internal.hs +18/−15
- src/Language/GraphQL/Type/Schema.hs +40/−6
- src/Language/GraphQL/Validate.hs +0/−21
- src/Language/GraphQL/Validate/Rules.hs +37/−22
- tests/Language/GraphQL/AST/ParserSpec.hs +51/−0
- tests/Language/GraphQL/ErrorSpec.hs +23/−11
- tests/Language/GraphQL/Execute/OrderedMapSpec.hs +72/−0
- tests/Language/GraphQL/ExecuteSpec.hs +216/−14
- tests/Language/GraphQL/Validate/RulesSpec.hs +61/−13
CHANGELOG.md view
@@ -6,6 +6,36 @@ and this project adheres to [Haskell Package Versioning Policy](https://pvp.haskell.org/). +## [1.0.0.0]+### Added+- `Language.GraphQL.Execute.OrderedMap` is a map data structure, that preserves+ insertion order.+- `Language.GraphQL.Schema.schemaWithTypes` constructs a complete schema,+ including an optional schema description and user-defined types not referenced+ in the schema directly (for example interface implementations).+- `Language.GraphQL.Schema.description` returns the optional schema description.+- All errors that can be associated with a location in the query contain+ location information.++### Fixed+- Parser now accepts empty lists and objects.+- Parser now accepts all directive locations.+- `valuesOfCorrectTypeRule` doesn't check lists recursively since the+ validation traverser calls it on all list items.+- `valuesOfCorrectTypeRule` doesn't check objects recursively since the+ validation traverser calls it on all object properties.+- Validation of non-nullable values inside lists.+- `executeField` shouldn't assume that a selection has only one field with a+ given name, but it should take the first field. The underlying cause is a+ wrong pattern, which (because of the laziness) is executed only if the field+ has arguments.++### Changed+- `AST.Document.Value.List` and `AST.Document.ConstValue.ConstList` contain+ location information for each list item.+- `Error`: `singleError`, `addErr` and `addErrMsg` are deprecated. They are+ internal functions used by the executor for error handling.+ ## [0.11.1.0] - 2021-02-07 ### Added - `Validate.Rules`:@@ -104,7 +134,7 @@ `locations`. - Parsing comments in the front of definitions. - Some missing labels were added to the parsers, some labels were fixed to- refer to the AST nodes being parsed. + refer to the AST nodes being parsed. ### Added - `AST` reexports `AST.Parser`.@@ -413,6 +443,7 @@ ### Added - Data types for the GraphQL language. +[1.0.0.0]: https://www.caraus.tech/projects/pub-graphql/repository/23/diff?rev=v1.0.0.0&rev_to=v0.11.1.0 [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
graphql.cabal view
@@ -1,115 +1,110 @@-cabal-version: 2.2-name: graphql-version: 0.11.1.0-license: MPL-2.0 AND BSD-3-Clause-license-file: LICENSE LICENSE.MPL-copyright:- (c) 2019-2021 Eugen Wissner,- (c) 2015-2017 J. Daniel Navarro--maintainer: belka@caraus.de-author:- Danny Navarro <j@dannynavarro.net>,- Matthías Páll Gissurarson <mpg@mpg.is>,- Sólrún Halla Einarsdóttir <she@mpg.is>--homepage: https://www.caraus.tech/projects/pub-graphql-bug-reports: https://www.caraus.tech/projects/pub-graphql/issues-synopsis: Haskell GraphQL implementation-description:- Haskell <https://spec.graphql.org/June2018/ GraphQL> implementation.+cabal-version: 2.2 -category: Language-build-type: Simple+name: graphql+version: 1.0.0.0+synopsis: Haskell GraphQL implementation+description: Haskell <https://spec.graphql.org/June2018/ GraphQL> implementation.+category: Language+homepage: https://www.caraus.tech/projects/pub-graphql+bug-reports: https://www.caraus.tech/projects/pub-graphql/issues+author: Danny Navarro <j@dannynavarro.net>,+ Matthías Páll Gissurarson <mpg@mpg.is>,+ Sólrún Halla Einarsdóttir <she@mpg.is>+maintainer: belka@caraus.de+copyright: (c) 2019-2021 Eugen Wissner,+ (c) 2015-2017 J. Daniel Navarro+license: MPL-2.0 AND BSD-3-Clause+license-files: LICENSE,+ LICENSE.MPL+build-type: Simple extra-source-files: CHANGELOG.md README.md+tested-with: GHC == 8.10.4 source-repository head- type: git- location: git://caraus.tech/pub/graphql.git+ type: git+ location: git://caraus.tech/pub/graphql.git library- exposed-modules:- Language.GraphQL- Language.GraphQL.AST- Language.GraphQL.AST.DirectiveLocation- Language.GraphQL.AST.Document- Language.GraphQL.AST.Encoder- Language.GraphQL.AST.Lexer- Language.GraphQL.AST.Parser- Language.GraphQL.Error- Language.GraphQL.Execute- Language.GraphQL.Execute.Coerce- Language.GraphQL.Type- Language.GraphQL.Type.In- Language.GraphQL.Type.Out- Language.GraphQL.Type.Schema- Language.GraphQL.Validate- Language.GraphQL.Validate.Validation- Test.Hspec.GraphQL-- hs-source-dirs: src- other-modules:- Language.GraphQL.Execute.Execution- Language.GraphQL.Execute.Subscribe- Language.GraphQL.Execute.Transform- Language.GraphQL.Type.Definition- Language.GraphQL.Type.Internal- Language.GraphQL.Validate.Rules-- default-language: Haskell2010- build-depends:- aeson >=1.5.5.1,- base >=4.7 && <5,- conduit >=1.3.4,- containers >=0.6.2.1,- exceptions >=0.10.4,- hspec-expectations >=0.8.2,- megaparsec >=9.0.1,- parser-combinators >=1.2.1,- scientific >=0.3.6.2,- text >=1.2.4.1,- transformers >=0.5.6.2,- unordered-containers >=0.2.13.0+ exposed-modules:+ Language.GraphQL+ Language.GraphQL.AST+ Language.GraphQL.AST.DirectiveLocation+ Language.GraphQL.AST.Document+ Language.GraphQL.AST.Encoder+ Language.GraphQL.AST.Lexer+ Language.GraphQL.AST.Parser+ Language.GraphQL.Error+ Language.GraphQL.Execute+ Language.GraphQL.Execute.Coerce+ Language.GraphQL.Execute.OrderedMap+ Language.GraphQL.Type+ Language.GraphQL.Type.In+ Language.GraphQL.Type.Out+ Language.GraphQL.Type.Schema+ Language.GraphQL.Validate+ Language.GraphQL.Validate.Validation+ Test.Hspec.GraphQL+ other-modules:+ Language.GraphQL.Execute.Execution+ Language.GraphQL.Execute.Internal+ Language.GraphQL.Execute.Subscribe+ Language.GraphQL.Execute.Transform+ Language.GraphQL.Type.Definition+ Language.GraphQL.Type.Internal+ Language.GraphQL.Validate.Rules+ hs-source-dirs:+ src+ ghc-options: -Wall+ build-depends:+ aeson >= 1.5.6 && < 1.6+ , base >= 4.7 && < 5+ , conduit >= 1.3.4 && < 1.4+ , containers >= 0.6.2 && < 0.7+ , exceptions >= 0.10.4 && < 0.11+ , hspec-expectations >= 0.8.2 && < 0.9+ , megaparsec >= 9.0.1 && < 9.1+ , parser-combinators >= 1.3.0 && < 1.4+ , scientific >= 0.3.7 && < 0.4+ , text >= 1.2.4 && < 1.3+ , transformers >= 0.5.6 && < 0.6+ , unordered-containers >= 0.2.14 && < 0.3+ , vector >= 0.12.3 && < 0.13+ default-language: Haskell2010 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- Language.GraphQL.ErrorSpec- Language.GraphQL.Execute.CoerceSpec- Language.GraphQL.ExecuteSpec- Language.GraphQL.Type.OutSpec- Language.GraphQL.Validate.RulesSpec- Test.DirectiveSpec- Test.FragmentSpec- Test.RootOperationSpec- Paths_graphql-- autogen-modules: Paths_graphql- default-language: Haskell2010- ghc-options: -threaded -rtsopts -with-rtsopts=-N- build-depends:- QuickCheck >=2.14.2,- aeson >=1.5.5.1,- base >=4.7 && <5,- conduit >=1.3.4,- containers >=0.6.2.1,- exceptions >=0.10.4,- graphql -any,- hspec >=2.7.8,- hspec-expectations >=0.8.2,- 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.1,- transformers >=0.5.6.2,- unordered-containers >=0.2.13.0+ type: exitcode-stdio-1.0+ main-is: Spec.hs+ other-modules:+ Language.GraphQL.AST.DocumentSpec+ Language.GraphQL.AST.EncoderSpec+ Language.GraphQL.AST.LexerSpec+ Language.GraphQL.AST.ParserSpec+ Language.GraphQL.ErrorSpec+ Language.GraphQL.Execute.CoerceSpec+ Language.GraphQL.Execute.OrderedMapSpec+ Language.GraphQL.ExecuteSpec+ Language.GraphQL.Type.OutSpec+ Language.GraphQL.Validate.RulesSpec+ Test.DirectiveSpec+ Test.FragmentSpec+ Test.RootOperationSpec+ hs-source-dirs:+ tests+ ghc-options: -threaded -rtsopts -with-rtsopts=-N -Wall+ build-depends:+ QuickCheck >= 2.14.1 && < 2.15+ , aeson+ , base >= 4.7 && < 5+ , conduit+ , exceptions+ , graphql+ , hspec >= 2.8.2 && < 2.9+ , hspec-megaparsec >= 2.2.0 && < 2.3+ , megaparsec+ , raw-strings-qq >= 1.1 && < 1.2+ , scientific+ , text+ , unordered-containers+ default-language: Haskell2010
src/Language/GraphQL/AST/Document.hs view
@@ -226,6 +226,12 @@ -- ** Input Values +-- | Escapes a single character according to the GraphQL escaping rules for+-- double-quoted string values.+--+-- Characters, that should be escaped, are written as escaped characters with a+-- backslash or Unicode with an \"\\u\". Other characters are returned as+-- strings. escape :: Char -> String escape char' | char' == '\\' = "\\\\"@@ -257,7 +263,7 @@ | Boolean Bool | Null | Enum Name- | List [Value]+ | List [Node Value] | Object [ObjectField Value] deriving Eq @@ -281,7 +287,7 @@ | ConstBoolean Bool | ConstNull | ConstEnum Name- | ConstList [ConstValue]+ | ConstList [Node ConstValue] | ConstObject [ObjectField ConstValue] deriving Eq @@ -318,13 +324,13 @@ -- Each operation can include a list of variables: -- -- @--- query (protagonist: String = "Zarathustra") {+-- query (protagonist: String = \"Zarathustra\") { -- getAuthor(protagonist: $protagonist) -- } -- @ -- -- This query defines an optional variable @protagonist@ of type @String@,--- its default value is "Zarathustra". If no default value is defined and no+-- its default value is \"Zarathustra\". If no default value is defined and no -- value is provided, a variable can still be @null@ if its type is nullable. -- -- Variables are usually passed along with the query, but not in the query
src/Language/GraphQL/AST/Encoder.hs view
@@ -219,7 +219,7 @@ fromConstValue Full.ConstNull = Full.Null fromConstValue (Full.ConstString string) = Full.String string fromConstValue (Full.ConstEnum x) = Full.Enum x-fromConstValue (Full.ConstList x) = Full.List $ fromConstValue <$> x+fromConstValue (Full.ConstList x) = Full.List $ fmap fromConstValue <$> x fromConstValue (Full.ConstObject x) = Full.Object $ fromConstObjectField <$> x where fromConstObjectField Full.ObjectField{value = value', ..} =@@ -266,8 +266,8 @@ = Builder.fromLazyText (indent (indentation + 1)) <> line' <> newline <> acc -listValue :: Formatter -> [Full.Value] -> Lazy.Text-listValue formatter = bracketsCommas formatter $ value formatter+listValue :: Formatter -> [Full.Node Full.Value] -> Lazy.Text+listValue formatter = bracketsCommas formatter $ value formatter . Full.node objectValue :: Formatter -> [Full.ObjectField Full.Value] -> Lazy.Text objectValue formatter = intercalate $ objectField formatter
src/Language/GraphQL/AST/Parser.hs view
@@ -14,11 +14,7 @@ import Data.List.NonEmpty (NonEmpty(..)) import Data.Text (Text) import qualified Language.GraphQL.AST.DirectiveLocation as Directive-import Language.GraphQL.AST.DirectiveLocation- ( DirectiveLocation- , ExecutableDirectiveLocation- , TypeSystemDirectiveLocation- )+import Language.GraphQL.AST.DirectiveLocation (DirectiveLocation) import qualified Language.GraphQL.AST.Document as Full import Language.GraphQL.AST.Lexer import Text.Megaparsec@@ -96,34 +92,28 @@ <?> "DirectiveLocations" directiveLocation :: Parser DirectiveLocation-directiveLocation- = Directive.ExecutableDirectiveLocation <$> executableDirectiveLocation- <|> Directive.TypeSystemDirectiveLocation <$> typeSystemDirectiveLocation+directiveLocation = e (Directive.Query <$ symbol "QUERY")+ <|> e (Directive.Mutation <$ symbol "MUTATION")+ <|> e (Directive.Subscription <$ symbol "SUBSCRIPTION")+ <|> t (Directive.FieldDefinition <$ symbol "FIELD_DEFINITION")+ <|> e (Directive.Field <$ symbol "FIELD")+ <|> e (Directive.FragmentDefinition <$ "FRAGMENT_DEFINITION")+ <|> e (Directive.FragmentSpread <$ "FRAGMENT_SPREAD")+ <|> e (Directive.InlineFragment <$ "INLINE_FRAGMENT")+ <|> t (Directive.Schema <$ symbol "SCHEMA")+ <|> t (Directive.Scalar <$ symbol "SCALAR")+ <|> t (Directive.Object <$ symbol "OBJECT")+ <|> t (Directive.ArgumentDefinition <$ symbol "ARGUMENT_DEFINITION")+ <|> t (Directive.Interface <$ symbol "INTERFACE")+ <|> t (Directive.Union <$ symbol "UNION")+ <|> t (Directive.EnumValue <$ symbol "ENUM_VALUE")+ <|> t (Directive.Enum <$ symbol "ENUM")+ <|> t (Directive.InputObject <$ symbol "INPUT_OBJECT")+ <|> t (Directive.InputFieldDefinition <$ symbol "INPUT_FIELD_DEFINITION") <?> "DirectiveLocation"--executableDirectiveLocation :: Parser ExecutableDirectiveLocation-executableDirectiveLocation = Directive.Query <$ symbol "QUERY"- <|> Directive.Mutation <$ symbol "MUTATION"- <|> Directive.Subscription <$ symbol "SUBSCRIPTION"- <|> Directive.Field <$ symbol "FIELD"- <|> Directive.FragmentDefinition <$ "FRAGMENT_DEFINITION"- <|> Directive.FragmentSpread <$ "FRAGMENT_SPREAD"- <|> Directive.InlineFragment <$ "INLINE_FRAGMENT"- <?> "ExecutableDirectiveLocation"--typeSystemDirectiveLocation :: Parser TypeSystemDirectiveLocation-typeSystemDirectiveLocation = Directive.Schema <$ symbol "SCHEMA"- <|> Directive.Scalar <$ symbol "SCALAR"- <|> Directive.Object <$ symbol "OBJECT"- <|> Directive.FieldDefinition <$ symbol "FIELD_DEFINITION"- <|> Directive.ArgumentDefinition <$ symbol "ARGUMENT_DEFINITION"- <|> Directive.Interface <$ symbol "INTERFACE"- <|> Directive.Union <$ symbol "UNION"- <|> Directive.Enum <$ symbol "ENUM"- <|> Directive.EnumValue <$ symbol "ENUM_VALUE"- <|> Directive.InputObject <$ symbol "INPUT_OBJECT"- <|> Directive.InputFieldDefinition <$ symbol "INPUT_FIELD_DEFINITION"- <?> "TypeSystemDirectiveLocation"+ where+ e = fmap Directive.ExecutableDirectiveLocation+ t = fmap Directive.TypeSystemDirectiveLocation typeDefinition :: Full.Description -> Parser Full.TypeDefinition typeDefinition description' = scalarTypeDefinition description'@@ -460,7 +450,7 @@ <|> Full.Null <$ nullValue <|> Full.String <$> stringValue <|> Full.Enum <$> try enumValue- <|> Full.List <$> brackets (some value)+ <|> Full.List <$> brackets (some $ valueNode value) <|> Full.Object <$> braces (some $ objectField $ valueNode value) <?> "Value" @@ -471,8 +461,8 @@ <|> Full.ConstNull <$ nullValue <|> Full.ConstString <$> stringValue <|> Full.ConstEnum <$> try enumValue- <|> Full.ConstList <$> brackets (some constValue)- <|> Full.ConstObject <$> braces (some $ objectField $ valueNode constValue)+ <|> Full.ConstList <$> brackets (many $ valueNode constValue)+ <|> Full.ConstObject <$> braces (many $ objectField $ valueNode constValue) <?> "Value" booleanValue :: Parser Bool
src/Language/GraphQL/Error.hs view
@@ -1,6 +1,5 @@ {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE ExistentialQuantification #-}-{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} -- | Error handling.@@ -70,21 +69,25 @@ type CollectErrsT m = StateT (Resolution m) m -- | Adds an error to the list of errors.+{-# DEPRECATED #-} addErr :: Monad m => Error -> CollectErrsT m () addErr v = modify appender where appender :: Monad m => Resolution m -> Resolution m appender resolution@Resolution{..} = resolution{ errors = errors |> v } +{-# DEPRECATED #-} makeErrorMessage :: Text -> Error makeErrorMessage s = Error s [] [] -- | Constructs a response object containing only the error with the given -- message.+{-# DEPRECATED #-} singleError :: Serialize a => Text -> Response a-singleError message = Response null $ Seq.singleton $ makeErrorMessage message+singleError message = Response null $ Seq.singleton $ Error message [] [] -- | Convenience function for just wrapping an error message.+{-# DEPRECATED #-} addErrMsg :: (Monad m, Serialize a) => Text -> CollectErrsT m a addErrMsg errorMessage = (addErr . makeErrorMessage) errorMessage >> pure null
src/Language/GraphQL/Execute.hs view
@@ -1,4 +1,4 @@-{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ExplicitForAll #-} -- | This module provides functions to execute a @GraphQL@ request. module Language.GraphQL.Execute@@ -10,15 +10,22 @@ import Data.HashMap.Strict (HashMap) import Data.Sequence (Seq(..)) import Data.Text (Text)-import Language.GraphQL.AST.Document (Document, Name)+import qualified Language.GraphQL.AST.Document as Full import Language.GraphQL.Execute.Coerce import Language.GraphQL.Execute.Execution+import Language.GraphQL.Execute.Internal import qualified Language.GraphQL.Execute.Transform as Transform import qualified Language.GraphQL.Execute.Subscribe as Subscribe import Language.GraphQL.Error+ ( Error+ , ResponseEventStream+ , Response(..)+ , runCollectErrs+ ) import qualified Language.GraphQL.Type.Definition as Definition import qualified Language.GraphQL.Type.Out as Out import Language.GraphQL.Type.Schema+import Prelude hiding (null) -- | The substitution is applied to the document, and the resolvers are applied -- to the resulting fields. The operation name can be used if the document@@ -29,35 +36,36 @@ execute :: (MonadCatch m, VariableValue a, Serialize b) => Schema m -- ^ Resolvers. -> Maybe Text -- ^ Operation name.- -> HashMap Name a -- ^ Variable substitution function.- -> Document -- @GraphQL@ document.+ -> HashMap Full.Name a -- ^ Variable substitution function.+ -> Full.Document -- @GraphQL@ document. -> m (Either (ResponseEventStream m b) (Response b))-execute schema' operationName subs document =- case Transform.document schema' operationName subs document of- Left queryError -> pure- $ Right- $ singleError- $ Transform.queryError queryError- Right transformed -> executeRequest transformed+execute schema' operationName subs document+ = either (pure . rightErrorResponse . singleError [] . show) executeRequest+ $ Transform.document schema' operationName subs document executeRequest :: (MonadCatch m, Serialize a) => Transform.Document m -> m (Either (ResponseEventStream m a) (Response a)) executeRequest (Transform.Document types' rootObjectType operation)- | (Transform.Query _ fields) <- operation =- Right <$> executeOperation types' rootObjectType fields- | (Transform.Mutation _ fields) <- operation =- Right <$> executeOperation types' rootObjectType fields- | (Transform.Subscription _ fields) <- operation- = either (Right . singleError) Left- <$> Subscribe.subscribe types' rootObjectType fields+ | (Transform.Query _ fields objectLocation) <- operation =+ Right <$> executeOperation types' rootObjectType objectLocation fields+ | (Transform.Mutation _ fields objectLocation) <- operation =+ Right <$> executeOperation types' rootObjectType objectLocation fields+ | (Transform.Subscription _ fields objectLocation) <- operation+ = either rightErrorResponse Left+ <$> Subscribe.subscribe types' rootObjectType objectLocation fields -- This is actually executeMutation, but we don't distinguish between queries -- and mutations yet. executeOperation :: (MonadCatch m, Serialize a)- => HashMap Name (Type m)+ => HashMap Full.Name (Type m) -> Out.ObjectType m+ -> Full.Location -> Seq (Transform.Selection m) -> m (Response a)-executeOperation types' objectType fields =- runCollectErrs types' $ executeSelectionSet Definition.Null objectType fields+executeOperation types' objectType objectLocation fields+ = runCollectErrs types'+ $ executeSelectionSet Definition.Null objectType objectLocation fields++rightErrorResponse :: Serialize b => forall a. Error -> Either a (Response b)+rightErrorResponse = Right . Response null . pure
src/Language/GraphQL/Execute/Coerce.hs view
@@ -19,7 +19,6 @@ import Data.Int (Int32) import Data.HashMap.Strict (HashMap) import qualified Data.HashMap.Strict as HashMap-import Data.Map.Strict (Map) import Data.String (IsString(..)) import Data.Text (Text) import qualified Data.Text.Lazy as Text.Lazy@@ -27,6 +26,8 @@ import qualified Data.Text.Lazy.Builder.Int as Text.Builder import Data.Scientific (toBoundedInteger, toRealFloat) import Language.GraphQL.AST (Name)+import Language.GraphQL.Execute.OrderedMap (OrderedMap)+import qualified Language.GraphQL.Execute.OrderedMap as OrderedMap import qualified Language.GraphQL.Type as Type import qualified Language.GraphQL.Type.In as In import qualified Language.GraphQL.Type.Out as Out@@ -209,7 +210,7 @@ | Boolean Bool | Enum Name | List [a]- | Object (Map Name a)+ | Object (OrderedMap a) deriving (Eq, Show) instance forall a. IsString (Output a) where@@ -229,6 +230,9 @@ , Boolean boolean <- value = Just $ Aeson.Bool boolean serialize _ (Enum enum) = Just $ Aeson.String enum serialize _ (List list) = Just $ Aeson.toJSON list- serialize _ (Object object) = Just $ Aeson.toJSON object+ serialize _ (Object object) = Just+ $ Aeson.object+ $ OrderedMap.toList+ $ Aeson.toJSON <$> object serialize _ _ = Nothing null = Aeson.Null
src/Language/GraphQL/Execute/Execution.hs view
@@ -1,4 +1,5 @@ {-# LANGUAGE ExplicitForAll #-}+{-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ViewPatterns #-} @@ -13,16 +14,18 @@ import Control.Monad.Trans.Reader (runReaderT) import Control.Monad.Trans.State (gets) import Data.List.NonEmpty (NonEmpty(..))-import Data.Map.Strict (Map)+import qualified Data.List.NonEmpty as NonEmpty import Data.HashMap.Strict (HashMap) import qualified Data.HashMap.Strict as HashMap-import qualified Data.Map.Strict as Map import Data.Maybe (fromMaybe) import Data.Sequence (Seq(..)) import qualified Data.Text as Text-import Language.GraphQL.AST (Name)+import qualified Language.GraphQL.AST as Full import Language.GraphQL.Error import Language.GraphQL.Execute.Coerce+import Language.GraphQL.Execute.Internal+import Language.GraphQL.Execute.OrderedMap (OrderedMap)+import qualified Language.GraphQL.Execute.OrderedMap as OrderedMap import qualified Language.GraphQL.Execute.Transform as Transform import qualified Language.GraphQL.Type as Type import qualified Language.GraphQL.Type.In as In@@ -34,15 +37,17 @@ => Type.Value -> Type.Subs -> Type.Resolve m+ -> Full.Location -> CollectErrsT m Type.Value-resolveFieldValue result args resolver =+resolveFieldValue result args resolver location' = catch (lift $ runReaderT resolver context) handleFieldError where handleFieldError :: MonadCatch m => ResolverException -> CollectErrsT m Type.Value- handleFieldError e =- addErr (Error (Text.pack $ displayException e) [] []) >> pure Type.Null+ handleFieldError e+ = addError Type.Null+ $ Error (Text.pack $ displayException e) [location'] [] context = Type.Context { Type.arguments = Type.Arguments args , Type.values = result@@ -51,21 +56,21 @@ collectFields :: Monad m => Out.ObjectType m -> Seq (Transform.Selection m)- -> Map Name (NonEmpty (Transform.Field m))-collectFields objectType = foldl forEach Map.empty+ -> OrderedMap (NonEmpty (Transform.Field m))+collectFields objectType = foldl forEach OrderedMap.empty where forEach groupedFields (Transform.SelectionField field) = let responseKey = aliasOrName field- in Map.insertWith (<>) responseKey (field :| []) groupedFields+ in OrderedMap.insert responseKey (field :| []) groupedFields forEach groupedFields (Transform.SelectionFragment selectionFragment) | Transform.Fragment fragmentType fragmentSelectionSet <- selectionFragment , Internal.doesFragmentTypeApply fragmentType objectType = let fragmentGroupedFieldSet = collectFields objectType fragmentSelectionSet- in Map.unionWith (<>) groupedFields fragmentGroupedFieldSet+ in groupedFields <> fragmentGroupedFieldSet | otherwise = groupedFields -aliasOrName :: forall m. Transform.Field m -> Name-aliasOrName (Transform.Field alias name _ _) = fromMaybe name alias+aliasOrName :: forall m. Transform.Field m -> Full.Name+aliasOrName (Transform.Field alias name _ _ _) = fromMaybe name alias resolveAbstractType :: Monad m => Internal.AbstractType m@@ -95,11 +100,15 @@ where executeField' fieldDefinition resolver = do let Out.Field _ fieldType argumentDefinitions = fieldDefinition- let (Transform.Field _ _ arguments' _ :| []) = fields+ let Transform.Field _ _ arguments' _ location' = NonEmpty.head fields case coerceArgumentValues argumentDefinitions arguments' of- Nothing -> addErrMsg "Argument coercing failed."- Just argumentValues -> do- answer <- resolveFieldValue prev argumentValues resolver+ Left [] ->+ let errorMessage = "Not all required arguments are specified."+ in addError null $ Error errorMessage [location'] []+ Left errorLocations -> addError null+ $ Error "Argument coercing failed." errorLocations []+ Right argumentValues -> do+ answer <- resolveFieldValue prev argumentValues resolver location' completeValue fieldType fields answer completeValue :: (MonadCatch m, Serialize a)@@ -110,55 +119,67 @@ completeValue (Out.isNonNullType -> False) _ Type.Null = pure null completeValue outputType@(Out.ListBaseType listType) fields (Type.List list) = traverse (completeValue listType fields) list- >>= coerceResult outputType . List-completeValue outputType@(Out.ScalarBaseType _) _ (Type.Int int) =- coerceResult outputType $ Int int-completeValue outputType@(Out.ScalarBaseType _) _ (Type.Boolean boolean) =- coerceResult outputType $ Boolean boolean-completeValue outputType@(Out.ScalarBaseType _) _ (Type.Float float) =- coerceResult outputType $ Float float-completeValue outputType@(Out.ScalarBaseType _) _ (Type.String string) =- coerceResult outputType $ String string-completeValue outputType@(Out.EnumBaseType enumType) _ (Type.Enum enum) =+ >>= coerceResult outputType (firstFieldLocation fields) . List+completeValue outputType@(Out.ScalarBaseType _) fields (Type.Int int) =+ coerceResult outputType (firstFieldLocation fields) $ Int int+completeValue outputType@(Out.ScalarBaseType _) fields (Type.Boolean boolean) =+ coerceResult outputType (firstFieldLocation fields) $ Boolean boolean+completeValue outputType@(Out.ScalarBaseType _) fields (Type.Float float) =+ coerceResult outputType (firstFieldLocation fields) $ Float float+completeValue outputType@(Out.ScalarBaseType _) fields (Type.String string) =+ coerceResult outputType (firstFieldLocation fields) $ String string+completeValue outputType@(Out.EnumBaseType enumType) fields (Type.Enum enum) = let Type.EnumType _ _ enumMembers = enumType+ location = firstFieldLocation fields in if HashMap.member enum enumMembers- then coerceResult outputType $ Enum enum- else addErrMsg "Enum value completion failed."-completeValue (Out.ObjectBaseType objectType) fields result =- executeSelectionSet result objectType $ mergeSelectionSets fields+ then coerceResult outputType location $ Enum enum+ else addError null $ Error "Enum value completion failed." [location] []+completeValue (Out.ObjectBaseType objectType) fields result+ = executeSelectionSet result objectType (firstFieldLocation fields)+ $ mergeSelectionSets fields completeValue (Out.InterfaceBaseType interfaceType) fields result | Type.Object objectMap <- result = do let abstractType = Internal.AbstractInterfaceType interfaceType+ let location = firstFieldLocation fields concreteType <- resolveAbstractType abstractType objectMap case concreteType of- Just objectType -> executeSelectionSet result objectType+ Just objectType -> executeSelectionSet result objectType location $ mergeSelectionSets fields- Nothing -> addErrMsg "Interface value completion failed."+ Nothing -> addError null+ $ Error "Interface value completion failed." [location] [] completeValue (Out.UnionBaseType unionType) fields result | Type.Object objectMap <- result = do let abstractType = Internal.AbstractUnionType unionType+ let location = firstFieldLocation fields concreteType <- resolveAbstractType abstractType objectMap case concreteType of Just objectType -> executeSelectionSet result objectType- $ mergeSelectionSets fields- Nothing -> addErrMsg "Union value completion failed."-completeValue _ _ _ = addErrMsg "Value completion failed."+ location $ mergeSelectionSets fields+ Nothing -> addError null+ $ Error "Union value completion failed." [location] []+completeValue _ (Transform.Field _ _ _ _ location :| _) _ =+ addError null $ Error "Value completion failed." [location] [] mergeSelectionSets :: MonadCatch m => NonEmpty (Transform.Field m) -> Seq (Transform.Selection m) mergeSelectionSets = foldr forEach mempty where- forEach (Transform.Field _ _ _ fieldSelectionSet) selectionSet =+ forEach (Transform.Field _ _ _ fieldSelectionSet _) selectionSet = selectionSet <> fieldSelectionSet +firstFieldLocation :: MonadCatch m => NonEmpty (Transform.Field m) -> Full.Location+firstFieldLocation (Transform.Field _ _ _ _ fieldLocation :| _) = fieldLocation+ coerceResult :: (MonadCatch m, Serialize a) => Out.Type m+ -> Full.Location -> Output a -> CollectErrsT m a-coerceResult outputType result+coerceResult outputType parentLocation result | Just serialized <- serialize outputType result = pure serialized- | otherwise = addErrMsg "Result coercion failed."+ | otherwise = addError null+ $ Error "Result coercion failed." [parentLocation] [] -- | Takes an 'Out.ObjectType' and a list of 'Transform.Selection's and applies -- each field to each 'Transform.Selection'. Resolves into a value containing@@ -166,29 +187,45 @@ executeSelectionSet :: (MonadCatch m, Serialize a) => Type.Value -> Out.ObjectType m+ -> Full.Location -> Seq (Transform.Selection m) -> CollectErrsT m a-executeSelectionSet result objectType@(Out.ObjectType _ _ _ resolvers) selectionSet = do+executeSelectionSet result objectType@(Out.ObjectType _ _ _ resolvers) objectLocation selectionSet = do let fields = collectFields objectType selectionSet- resolvedValues <- Map.traverseMaybeWithKey forEach fields- coerceResult (Out.NonNullObjectType objectType) $ Object resolvedValues+ resolvedValues <- OrderedMap.traverseMaybe forEach fields+ coerceResult (Out.NonNullObjectType objectType) objectLocation+ $ Object resolvedValues where- forEach _ fields@(field :| _) =- let Transform.Field _ name _ _ = field+ forEach fields@(field :| _) =+ let Transform.Field _ name _ _ _ = field in traverse (tryResolver fields) $ lookupResolver name lookupResolver = flip HashMap.lookup resolvers tryResolver fields resolver = executeField resolver result fields >>= lift . pure coerceArgumentValues- :: HashMap Name In.Argument- -> HashMap Name Transform.Input- -> Maybe Type.Subs-coerceArgumentValues argumentDefinitions argumentValues =+ :: HashMap Full.Name In.Argument+ -> HashMap Full.Name (Full.Node Transform.Input)+ -> Either [Full.Location] Type.Subs+coerceArgumentValues argumentDefinitions argumentNodes = HashMap.foldrWithKey forEach (pure mempty) argumentDefinitions where- forEach variableName (In.Argument _ variableType defaultValue) =- matchFieldValues coerceArgumentValue argumentValues variableName variableType defaultValue+ forEach argumentName (In.Argument _ variableType defaultValue) = \case+ Right resultMap+ | Just matchedValues+ <- matchFieldValues' argumentName variableType defaultValue $ Just resultMap+ -> Right matchedValues+ | otherwise -> Left $ generateError argumentName []+ Left errorLocations+ | Just _+ <- matchFieldValues' argumentName variableType defaultValue $ pure mempty+ -> Left errorLocations+ | otherwise -> Left $ generateError argumentName errorLocations+ generateError argumentName errorLocations =+ case HashMap.lookup argumentName argumentNodes of+ Just (Full.Node _ errorLocation) -> [errorLocation]+ Nothing -> errorLocations+ matchFieldValues' = matchFieldValues coerceArgumentValue (Full.node <$> argumentNodes) coerceArgumentValue inputType (Transform.Int integer) = coerceInputLiteral inputType (Type.Int integer) coerceArgumentValue inputType (Transform.Boolean boolean) =
+ src/Language/GraphQL/Execute/Internal.hs view
@@ -0,0 +1,31 @@+{- 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 DuplicateRecordFields #-}+{-# LANGUAGE ExplicitForAll #-}+{-# LANGUAGE NamedFieldPuns #-}++module Language.GraphQL.Execute.Internal+ ( addError+ , singleError+ ) where++import Control.Monad.Trans.State (modify)+import Control.Monad.Catch (MonadCatch)+import Data.Sequence ((|>))+import qualified Data.Text as Text+import qualified Language.GraphQL.AST as Full+import Language.GraphQL.Error (CollectErrsT, Error(..), Resolution(..))+import Prelude hiding (null)++addError :: MonadCatch m => forall a. a -> Error -> CollectErrsT m a+addError returnValue error' = modify appender >> pure returnValue+ where+ appender :: Resolution m -> Resolution m+ appender resolution@Resolution{ errors } = resolution+ { errors = errors |> error'+ }++singleError :: [Full.Location] -> String -> Error+singleError errorLocations message = Error (Text.pack message) errorLocations []
+ src/Language/GraphQL/Execute/OrderedMap.hs view
@@ -0,0 +1,148 @@+{- 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 ExplicitForAll #-}++-- | This module contains a map data structure, that preserves insertion order.+-- Some definitions conflict with functions from prelude, so this module should+-- probably be imported qualified.+module Language.GraphQL.Execute.OrderedMap+ ( OrderedMap+ , elems+ , empty+ , insert+ , foldlWithKey'+ , keys+ , lookup+ , replace+ , singleton+ , size+ , toList+ , traverseMaybe+ ) where++import qualified Data.Foldable as Foldable+import Data.HashMap.Strict (HashMap, (!))+import qualified Data.HashMap.Strict as HashMap+import Data.Text (Text)+import Data.Vector (Vector)+import qualified Data.Vector as Vector+import Prelude hiding (filter, lookup)++-- | This map associates values with the given text keys. Insertion order is+-- preserved. When inserting a value with a key, that is already available in+-- the map, the existing value isn't overridden, but combined with the new value+-- using its 'Semigroup' instance.+--+-- Internally this map uses an array with keys to preserve the order and an+-- unorded map with key-value pairs.+data OrderedMap v = OrderedMap (Vector Text) (HashMap Text v)+ deriving (Eq)++instance Functor OrderedMap where+ fmap f (OrderedMap vector hashMap) = OrderedMap vector $ fmap f hashMap++instance Foldable OrderedMap where+ foldr f = foldrWithKey $ const f+ null (OrderedMap vector _) = Vector.null vector++instance Semigroup v => Semigroup (OrderedMap v) where+ (<>) = foldlWithKey'+ $ \accumulator key value -> insert key value accumulator++instance Semigroup v => Monoid (OrderedMap v) where+ mempty = empty++instance Traversable OrderedMap where+ traverse f (OrderedMap vector hashMap) = OrderedMap vector+ <$> traverse f hashMap++instance Show v => Show (OrderedMap v) where+ showsPrec precedence map' = showParen (precedence > 10)+ $ showString "fromList " . shows (toList map')++-- * Construction++-- | Constructs a map with a single element.+singleton :: forall v. Text -> v -> OrderedMap v+singleton key value = OrderedMap (Vector.singleton key)+ $ HashMap.singleton key value++-- | Constructs an empty map.+empty :: forall v. OrderedMap v+empty = OrderedMap mempty mempty++-- * Traversal++-- | Reduces this map by applying a binary operator from right to left to all+-- elements, using the given starting value.+foldrWithKey :: forall v a. (Text -> v -> a -> a) -> a -> OrderedMap v -> a+foldrWithKey f initial (OrderedMap vector hashMap) = foldr go initial vector+ where+ go key = f key (hashMap ! key)++-- | Reduces this map by applying a binary operator from left to right to all+-- elements, using the given starting value.+foldlWithKey' :: forall v a. (a -> Text -> v -> a) -> a -> OrderedMap v -> a+foldlWithKey' f initial (OrderedMap vector hashMap) =+ Vector.foldl' go initial vector+ where+ go accumulator key = f accumulator key (hashMap ! key)++-- | Traverse over the elements and collect the 'Just' results.+traverseMaybe+ :: Applicative f+ => forall a+ . (a -> f (Maybe b))+ -> OrderedMap a+ -> f (OrderedMap b)+traverseMaybe f orderedMap = foldlWithKey' filter empty+ <$> traverse f orderedMap+ where+ filter accumulator key (Just value) = replace key value accumulator+ filter accumulator _ Nothing = accumulator++-- * Lists++-- | Converts this map to the list of key-value pairs.+toList :: forall v. OrderedMap v -> [(Text, v)]+toList = foldrWithKey ((.) (:) . (,)) []++-- | Returns a list with all keys in this map.+keys :: forall v. OrderedMap v -> [Text]+keys (OrderedMap vector _) = Foldable.toList vector++-- | Returns a list with all elements in this map.+elems :: forall v. OrderedMap v -> [v]+elems = fmap snd . toList++-- * Basic interface++-- | Associates the specified value with the specified key in this map. If this+-- map previously contained a mapping for the key, the existing and new values+-- are combined.+insert :: Semigroup v => Text -> v -> OrderedMap v -> OrderedMap v+insert key value (OrderedMap vector hashMap)+ | Just available <- HashMap.lookup key hashMap = OrderedMap vector+ $ HashMap.insert key (available <> value) hashMap+ | otherwise = OrderedMap (Vector.snoc vector key)+ $ HashMap.insert key value hashMap++-- | Associates the specified value with the specified key in this map. If this+-- map previously contained a mapping for the key, the existing value is+-- replaced by the new one.+replace :: Text -> v -> OrderedMap v -> OrderedMap v+replace key value (OrderedMap vector hashMap)+ | HashMap.member key hashMap = OrderedMap vector+ $ HashMap.insert key value hashMap+ | otherwise = OrderedMap (Vector.snoc vector key)+ $ HashMap.insert key value hashMap++-- | Gives the size of this map, i.e. number of elements in it.+size :: forall v. OrderedMap v -> Int+size (OrderedMap vector _) = Vector.length vector++-- | Looks up a value in this map by key.+lookup :: forall v. Text -> OrderedMap v -> Maybe v+lookup key (OrderedMap _ hashMap) = HashMap.lookup key hashMap
src/Language/GraphQL/Execute/Subscribe.hs view
@@ -9,62 +9,78 @@ ) where import Conduit+import Control.Arrow (left) import Control.Monad.Catch (Exception(..), MonadCatch(..)) import Control.Monad.Trans.Reader (ReaderT(..), runReaderT) import Data.HashMap.Strict (HashMap) import qualified Data.HashMap.Strict as HashMap-import qualified Data.Map.Strict as Map import qualified Data.List.NonEmpty as NonEmpty import Data.Sequence (Seq(..))-import Data.Text (Text)-import qualified Data.Text as Text-import Language.GraphQL.AST (Name)+import qualified Language.GraphQL.AST as Full import Language.GraphQL.Execute.Coerce import Language.GraphQL.Execute.Execution+import Language.GraphQL.Execute.Internal+import qualified Language.GraphQL.Execute.OrderedMap as OrderedMap import qualified Language.GraphQL.Execute.Transform as Transform import Language.GraphQL.Error+ ( Error(..)+ , ResolverException+ , Response+ , ResponseEventStream+ , runCollectErrs+ ) import qualified Language.GraphQL.Type.Definition as Definition import qualified Language.GraphQL.Type as Type import qualified Language.GraphQL.Type.Out as Out import Language.GraphQL.Type.Schema --- This is actually executeMutation, but we don't distinguish between queries--- and mutations yet. subscribe :: (MonadCatch m, Serialize a)- => HashMap Name (Type m)+ => HashMap Full.Name (Type m) -> Out.ObjectType m+ -> Full.Location -> Seq (Transform.Selection m)- -> m (Either Text (ResponseEventStream m a))-subscribe types' objectType fields = do- sourceStream <- createSourceEventStream types' objectType fields- traverse (mapSourceToResponseEvent types' objectType fields) sourceStream+ -> m (Either Error (ResponseEventStream m a))+subscribe types' objectType objectLocation fields = do+ sourceStream <-+ createSourceEventStream types' objectType objectLocation fields+ let traverser =+ mapSourceToResponseEvent types' objectType objectLocation fields+ traverse traverser sourceStream mapSourceToResponseEvent :: (MonadCatch m, Serialize a)- => HashMap Name (Type m)+ => HashMap Full.Name (Type m) -> Out.ObjectType m+ -> Full.Location -> Seq (Transform.Selection m) -> Out.SourceEventStream m -> m (ResponseEventStream m a)-mapSourceToResponseEvent types' subscriptionType fields sourceStream = pure+mapSourceToResponseEvent types' subscriptionType objectLocation fields sourceStream+ = pure $ sourceStream- .| mapMC (executeSubscriptionEvent types' subscriptionType fields)+ .| mapMC (executeSubscriptionEvent types' subscriptionType objectLocation fields) createSourceEventStream :: MonadCatch m- => HashMap Name (Type m)+ => HashMap Full.Name (Type m) -> Out.ObjectType m+ -> Full.Location -> Seq (Transform.Selection m)- -> m (Either Text (Out.SourceEventStream m))-createSourceEventStream _types subscriptionType@(Out.ObjectType _ _ _ fieldTypes) fields- | [fieldGroup] <- Map.elems groupedFieldSet- , Transform.Field _ fieldName arguments' _ <- NonEmpty.head fieldGroup+ -> m (Either Error (Out.SourceEventStream m))+createSourceEventStream _types subscriptionType objectLocation fields+ | [fieldGroup] <- OrderedMap.elems groupedFieldSet+ , Transform.Field _ fieldName arguments' _ errorLocation <- NonEmpty.head fieldGroup+ , Out.ObjectType _ _ _ fieldTypes <- subscriptionType , resolverT <- fieldTypes HashMap.! fieldName , Out.EventStreamResolver fieldDefinition _ resolver <- resolverT , Out.Field _ _fieldType argumentDefinitions <- fieldDefinition = case coerceArgumentValues argumentDefinitions arguments' of- Nothing -> pure $ Left "Argument coercion failed."- Just argumentValues ->- resolveFieldEventStream Type.Null argumentValues resolver- | otherwise = pure $ Left "Subscription contains more than one field."+ Left _ -> pure+ $ Left+ $ Error "Argument coercion failed." [errorLocation] []+ Right argumentValues -> left (singleError [errorLocation])+ <$> resolveFieldEventStream Type.Null argumentValues resolver+ | otherwise = pure+ $ Left+ $ Error "Subscription contains more than one field." [objectLocation] [] where groupedFieldSet = collectFields subscriptionType fields @@ -72,26 +88,26 @@ => Type.Value -> Type.Subs -> Out.Subscribe m- -> m (Either Text (Out.SourceEventStream m))+ -> m (Either String (Out.SourceEventStream m)) resolveFieldEventStream result args resolver = catch (Right <$> runReaderT resolver context) handleEventStreamError where handleEventStreamError :: MonadCatch m => ResolverException- -> m (Either Text (Out.SourceEventStream m))- handleEventStreamError = pure . Left . Text.pack . displayException+ -> m (Either String (Out.SourceEventStream m))+ handleEventStreamError = pure . Left . displayException context = Type.Context { Type.arguments = Type.Arguments args , Type.values = result } --- This is actually executeMutation, but we don't distinguish between queries--- and mutations yet. executeSubscriptionEvent :: (MonadCatch m, Serialize a)- => HashMap Name (Type m)+ => HashMap Full.Name (Type m) -> Out.ObjectType m+ -> Full.Location -> Seq (Transform.Selection m) -> Definition.Value -> m (Response a)-executeSubscriptionEvent types' objectType fields initialValue =- runCollectErrs types' $ executeSelectionSet initialValue objectType fields+executeSubscriptionEvent types' objectType objectLocation fields initialValue+ = runCollectErrs types'+ $ executeSelectionSet initialValue objectType objectLocation fields
src/Language/GraphQL/Execute/Transform.hs view
@@ -1,3 +1,7 @@+{- 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 ExplicitForAll #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-}@@ -25,7 +29,6 @@ , QueryError(..) , Selection(..) , document- , queryError ) where import Control.Monad (foldM, unless)@@ -71,16 +74,18 @@ | SelectionField (Field m) -- | GraphQL has 3 operation types: queries, mutations and subscribtions.------ Currently only queries and mutations are supported. data Operation m- = Query (Maybe Text) (Seq (Selection m))- | Mutation (Maybe Text) (Seq (Selection m))- | Subscription (Maybe Text) (Seq (Selection m))+ = Query (Maybe Text) (Seq (Selection m)) Full.Location+ | Mutation (Maybe Text) (Seq (Selection m)) Full.Location+ | Subscription (Maybe Text) (Seq (Selection m)) Full.Location -- | Single GraphQL field. data Field m = Field- (Maybe Full.Name) Full.Name (HashMap Full.Name Input) (Seq (Selection m))+ (Maybe Full.Name)+ Full.Name+ (HashMap Full.Name (Full.Node Input))+ (Seq (Selection m))+ Full.Location -- | Contains the operation to be executed along with its root type. data Document m = Document@@ -92,16 +97,26 @@ [Full.VariableDefinition] [Full.Directive] Full.SelectionSet+ Full.Location -- | Query error types. data QueryError = OperationNotFound Text | OperationNameRequired | CoercionError- | TransformationError | EmptyDocument | UnsupportedRootOperation +instance Show QueryError where+ show (OperationNotFound operationName) = unwords+ ["Operation", Text.unpack operationName, "couldn't be found in the document."]+ show OperationNameRequired = "Missing operation name."+ show CoercionError = "Coercion error."+ show EmptyDocument =+ "The document doesn't contain any executable operations."+ show UnsupportedRootOperation =+ "Root operation type couldn't be found in the schema."+ data Input = Int Int32 | Float Double@@ -114,17 +129,6 @@ | Variable Type.Value deriving (Eq, Show) -queryError :: QueryError -> Text-queryError (OperationNotFound operationName) = Text.unwords- ["Operation", operationName, "couldn't be found in the document."]-queryError OperationNameRequired = "Missing operation name."-queryError CoercionError = "Coercion error."-queryError TransformationError = "Schema transformation error."-queryError EmptyDocument =- "The document doesn't contain any executable operations."-queryError UnsupportedRootOperation =- "Root operation type couldn't be found in the schema."- getOperation :: Maybe Full.Name -> NonEmpty OperationDefinition@@ -135,7 +139,7 @@ | Just operation' <- find matchingName operations = pure operation' | otherwise = Left $ OperationNotFound operationName where- matchingName (OperationDefinition _ name _ _ _) =+ matchingName (OperationDefinition _ name _ _ _ _) = name == Just operationName coerceVariableValues :: Coerce.VariableValue a@@ -145,7 +149,7 @@ -> HashMap.HashMap Full.Name a -> Either QueryError Type.Subs coerceVariableValues types operationDefinition variableValues =- let OperationDefinition _ _ variableDefinitions _ _ = operationDefinition+ let OperationDefinition _ _ variableDefinitions _ _ _ = operationDefinition in maybe (Left CoercionError) Right $ foldr forEach (Just HashMap.empty) variableDefinitions where@@ -173,7 +177,7 @@ constValue (Full.ConstBoolean b) = Type.Boolean b constValue Full.ConstNull = Type.Null constValue (Full.ConstEnum e) = Type.Enum e-constValue (Full.ConstList l) = Type.List $ constValue <$> l+constValue (Full.ConstList list) = Type.List $ constValue . Full.node <$> list constValue (Full.ConstObject o) = Type.Object $ HashMap.fromList $ constObjectField <$> o where@@ -203,14 +207,14 @@ , types = referencedTypes } case chosenOperation of- OperationDefinition Full.Query _ _ _ _ ->+ OperationDefinition Full.Query _ _ _ _ _ -> pure $ Document referencedTypes (Schema.query schema) $ operation chosenOperation replacement- OperationDefinition Full.Mutation _ _ _ _+ OperationDefinition Full.Mutation _ _ _ _ _ | Just mutationType <- Schema.mutation schema -> pure $ Document referencedTypes mutationType $ operation chosenOperation replacement- OperationDefinition Full.Subscription _ _ _ _+ OperationDefinition Full.Subscription _ _ _ _ _ | Just subscriptionType <- Schema.subscription schema -> pure $ Document referencedTypes subscriptionType $ operation chosenOperation replacement@@ -235,10 +239,10 @@ (operations, HashMap.insert name fragment fragments') defragment' _ acc = acc transform = \case- Full.OperationDefinition type' name variables directives' selections _ ->- OperationDefinition type' name variables directives' selections- Full.SelectionSet selectionSet _ ->- OperationDefinition Full.Query Nothing mempty mempty selectionSet+ Full.OperationDefinition type' name variables directives' selections location ->+ OperationDefinition type' name variables directives' selections location+ Full.SelectionSet selectionSet location ->+ OperationDefinition Full.Query Nothing mempty mempty selectionSet location -- * Operation @@ -247,12 +251,12 @@ = runIdentity $ evalStateT (collectFragments >> transform operationDefinition) replacement where- transform (OperationDefinition Full.Query name _ _ sels) =- Query name <$> appendSelection sels- transform (OperationDefinition Full.Mutation name _ _ sels) =- Mutation name <$> appendSelection sels- transform (OperationDefinition Full.Subscription name _ _ sels) =- Subscription name <$> appendSelection sels+ transform (OperationDefinition Full.Query name _ _ sels location) =+ flip (Query name) location <$> appendSelection sels+ transform (OperationDefinition Full.Mutation name _ _ sels location) =+ flip (Mutation name) location <$> appendSelection sels+ transform (OperationDefinition Full.Subscription name _ _ sels location) =+ flip (Subscription name) location <$> appendSelection sels -- * Selection @@ -268,15 +272,20 @@ inlineFragment fragmentSelection field :: Full.Field -> State (Replacement m) (Maybe (Field m))-field (Full.Field alias name arguments' directives' selections _) = do+field (Full.Field alias name arguments' directives' selections location) = do fieldArguments <- foldM go HashMap.empty arguments' fieldSelections <- appendSelection selections fieldDirectives <- Definition.selection <$> directives directives'- let field' = Field alias name fieldArguments fieldSelections+ let field' = Field alias name fieldArguments fieldSelections location pure $ field' <$ fieldDirectives where- go arguments (Full.Argument name' (Full.Node value' _) _) =- inputField arguments name' value'+ go arguments (Full.Argument name' (Full.Node value' _) location') = do+ objectFieldValue <- input value'+ case objectFieldValue of+ Just fieldValue ->+ let argumentNode = Full.Node fieldValue location'+ in pure $ HashMap.insert name' argumentNode arguments+ Nothing -> pure arguments fragmentSpread :: Full.FragmentSpread@@ -380,7 +389,7 @@ value (Full.Boolean boolean) = pure $ Type.Boolean boolean value Full.Null = pure Type.Null value (Full.Enum enum) = pure $ Type.Enum enum-value (Full.List list) = Type.List <$> traverse value list+value (Full.List list) = Type.List <$> traverse (value . Full.node) list value (Full.Object object) = Type.Object . HashMap.fromList <$> traverse objectField object where@@ -396,7 +405,7 @@ input (Full.Boolean boolean) = pure $ pure $ Boolean boolean input Full.Null = pure $ pure Null input (Full.Enum enum) = pure $ pure $ Enum enum-input (Full.List list) = pure . List <$> traverse value list+input (Full.List list) = pure . List <$> traverse (value . Full.node) list input (Full.Object object) = do objectFields <- foldM objectField HashMap.empty object pure $ pure $ Object objectFields
src/Language/GraphQL/Type.hs view
@@ -21,6 +21,6 @@ ) where import Language.GraphQL.Type.Definition-import Language.GraphQL.Type.Schema (Schema, schema)+import Language.GraphQL.Type.Schema (Schema, schema, schemaWithTypes) import qualified Language.GraphQL.Type.In as In import qualified Language.GraphQL.Type.Out as Out
src/Language/GraphQL/Type/Internal.hs view
@@ -12,6 +12,7 @@ , Directives , Schema(..) , Type(..)+ , description , directives , doesFragmentTypeApply , implementations@@ -55,41 +56,43 @@ -- | A Schema is created by supplying the root types of each type of operation, -- query and mutation (optional). A schema definition is then supplied to the -- validator and executor.------ __Note:__ When the schema is constructed, by default only the types that--- are reachable by traversing the root types are included, other types must--- be explicitly referenced. data Schema m = Schema- (Out.ObjectType m)- (Maybe (Out.ObjectType m))- (Maybe (Out.ObjectType m))- Directives- (HashMap Full.Name (Type m))+ (Maybe Text) -- ^ Description.+ (Out.ObjectType m) -- ^ Query.+ (Maybe (Out.ObjectType m)) -- ^ Mutation.+ (Maybe (Out.ObjectType m)) -- ^ Subscription.+ Directives -- ^ Directives+ (HashMap Full.Name (Type m)) -- ^ Types.+ -- Interface implementations (used only for faster access). (HashMap Full.Name [Type m]) +-- | Schema description.+description :: forall m. Schema m -> Maybe Text+description (Schema description' _ _ _ _ _ _) = description'+ -- | 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'+implementations (Schema _ _ _ _ _ _ implementations') = implementations' -- | These types may describe the parent context of a selection set. data CompositeType m
src/Language/GraphQL/Type/Schema.hs view
@@ -9,11 +9,13 @@ -- functions for defining and manipulating schemas. module Language.GraphQL.Type.Schema ( schema+ , schemaWithTypes , module Language.GraphQL.Type.Internal ) where import Data.HashMap.Strict (HashMap) import qualified Data.HashMap.Strict as HashMap+import Data.Text (Text) import Language.GraphQL.AST.DirectiveLocation (DirectiveLocation(..)) import qualified Language.GraphQL.AST.DirectiveLocation as DirectiveLocation import qualified Language.GraphQL.AST as Full@@ -22,6 +24,7 @@ , Directives , Schema , Type(..)+ , description , directives , implementations , mutation@@ -35,17 +38,47 @@ import qualified Language.GraphQL.Type.Out as Out -- | Schema constructor.+--+-- __Note:__ When the schema is constructed, by default only the types that+-- are reachable by traversing the root types are included, other types must+-- be explicitly referenced using 'schemaWithTypes' instead. schema :: forall m . Out.ObjectType m -- ^ Query type. -> Maybe (Out.ObjectType m) -- ^ Mutation type. -> Maybe (Out.ObjectType m) -- ^ Subscription type. -> Directives -- ^ Directive definitions. -> Schema m -- ^ Schema.-schema queryRoot mutationRoot subscriptionRoot directiveDefinitions =- Internal.Schema queryRoot mutationRoot subscriptionRoot+schema queryRoot mutationRoot subscriptionRoot =+ schemaWithTypes Nothing queryRoot mutationRoot subscriptionRoot mempty++-- | Constructs a complete schema, including user-defined types not referenced+-- in the schema directly (for example interface implementations).+schemaWithTypes :: forall m+ . Maybe Text -- ^ Schema description+ -> Out.ObjectType m -- ^ Query type.+ -> Maybe (Out.ObjectType m) -- ^ Mutation type.+ -> Maybe (Out.ObjectType m) -- ^ Subscription type.+ -> [Type m] -- ^ Additional types.+ -> Directives -- ^ Directive definitions.+ -> Schema m -- ^ Schema.+schemaWithTypes description' queryRoot mutationRoot subscriptionRoot types' directiveDefinitions =+ Internal.Schema description' queryRoot mutationRoot subscriptionRoot allDirectives collectedTypes collectedImplementations where- collectedTypes = collectReferencedTypes queryRoot mutationRoot subscriptionRoot+ allTypes = foldr addTypeDefinition HashMap.empty types'+ addTypeDefinition type'@(ScalarType (Definition.ScalarType typeName _)) accumulator =+ HashMap.insert typeName type' accumulator+ addTypeDefinition type'@(EnumType (Definition.EnumType typeName _ _)) accumulator =+ HashMap.insert typeName type' accumulator+ addTypeDefinition type'@(ObjectType (Out.ObjectType typeName _ _ _)) accumulator =+ HashMap.insert typeName type' accumulator+ addTypeDefinition type'@(InputObjectType (In.InputObjectType typeName _ _)) accumulator =+ HashMap.insert typeName type' accumulator+ addTypeDefinition type'@(InterfaceType (Out.InterfaceType typeName _ _ _)) accumulator =+ HashMap.insert typeName type' accumulator+ addTypeDefinition type'@(UnionType (Out.UnionType typeName _ _)) accumulator =+ HashMap.insert typeName type' accumulator+ collectedTypes = collectReferencedTypes queryRoot mutationRoot subscriptionRoot allTypes collectedImplementations = collectImplementations collectedTypes allDirectives = HashMap.union directiveDefinitions defaultDirectives defaultDirectives = HashMap.fromList@@ -98,11 +131,12 @@ -> Maybe (Out.ObjectType m) -> Maybe (Out.ObjectType m) -> HashMap Full.Name (Type m)-collectReferencedTypes queryRoot mutationRoot subscriptionRoot =- let queryTypes = traverseObjectType queryRoot HashMap.empty+ -> HashMap Full.Name (Type m)+collectReferencedTypes queryRoot mutationRoot subscriptionRoot extraTypes =+ let queryTypes = traverseObjectType queryRoot extraTypes mutationTypes = maybe queryTypes (`traverseObjectType` queryTypes) mutationRoot- in maybe mutationTypes (`traverseObjectType` queryTypes) subscriptionRoot+ in maybe mutationTypes (`traverseObjectType` mutationTypes) subscriptionRoot where collect traverser typeName element foundTypes | HashMap.member typeName foundTypes = foundTypes
src/Language/GraphQL/Validate.hs view
@@ -4,7 +4,6 @@ {-# LANGUAGE LambdaCase #-} {-# LANGUAGE RecordWildCards #-}-{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} -- | GraphQL validator.@@ -315,9 +314,6 @@ go inputObjectType value'@(Full.Node (Full.ConstObject fields) _) = foldMap (forEach inputObjectType) (Seq.fromList fields) |> rule inputObjectType value'- go listType value'@(Full.Node (Full.ConstList values) location')- = embedListLocation go listType values location'- |> rule listType value' go anotherValue value' = pure $ rule anotherValue value' forEach inputObjectType Full.ObjectField{value = value', ..} = go (valueTypeByName name inputObjectType) value'@@ -421,20 +417,6 @@ where valueType (In.Argument _ valueType' _) = valueType' --- valueTypeFromList :: Maybe In.Type -> Maybe In.Type-embedListLocation :: forall a m- . (Maybe In.Type -> Full.Node a -> Seq m)- -> Maybe In.Type- -> [a]- -> Full.Location- -> Seq m-embedListLocation go listType values location'- = foldMap (go $ valueTypeFromList listType) - $ flip Full.Node location' <$> Seq.fromList values- where- valueTypeFromList (Just (In.ListBaseType baseType)) = Just baseType- valueTypeFromList _ = Nothing- value :: forall m . Validation.Rule m -> Maybe In.Type@@ -445,9 +427,6 @@ go inputObjectType value'@(Full.Node (Full.Object fields) _) = foldMap (forEach inputObjectType) (Seq.fromList fields) |> rule inputObjectType value'- go listType value'@(Full.Node (Full.List values) location')- = embedListLocation go listType values location'- |> rule listType value' go anotherValue value' = pure $ rule anotherValue value' forEach inputObjectType Full.ObjectField{value = value', ..} = go (valueTypeByName name inputObjectType) value'
src/Language/GraphQL/Validate/Rules.hs view
@@ -1502,15 +1502,6 @@ 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@@ -1527,6 +1518,17 @@ , locations = [location'] } +unwrapInType :: In.Type -> Maybe In.Type+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+ -- | Literal values must be compatible with the type expected in the position -- they are found as per the coercion rules. --@@ -1540,7 +1542,7 @@ go (Just inputType) value | Just constValue <- toConstNode value = lift $ check inputType constValue- go _ _ = lift mempty+ go _ _ = lift mempty -- This rule checks only literals. toConstNode Full.Node{..} = flip Full.Node location <$> toConst node toConst (Full.Variable _) = Nothing toConst (Full.Int integer) = Just $ Full.ConstInt integer@@ -1550,7 +1552,7 @@ toConst Full.Null = Just Full.ConstNull toConst (Full.Enum enum) = Just $ Full.ConstEnum enum toConst (Full.List values) =- Just $ Full.ConstList $ catMaybes $ toConst <$> values+ Just $ Full.ConstList $ catMaybes $ toConstNode <$> values toConst (Full.Object fields) = Just $ Full.ConstObject $ catMaybes $ constObjectField <$> fields constObjectField Full.ObjectField{..}@@ -1582,24 +1584,37 @@ , 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+ -- Skip, objects are checked recursively by the validation traverser.+ | In.InputObjectType{} <- objectType+ , Full.ConstObject{} <- node = mempty check (In.ListBaseType listType) constValue@Full.Node{ .. }- | Full.ConstList listValues <- node =- foldMap (check listType) $ flip Full.Node location <$> listValues+ | Full.ConstList values <- node =+ foldMap (checkNull listType) values | otherwise = check listType constValue check inputType Full.Node{ .. } = pure $ Error { message = concat [ "Value "- , show node, " cannot be coerced to type \""+ , 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+ checkNull inputType constValue =+ let checkResult = check inputType constValue+ in case null checkResult of+ True+ | Just unwrappedType <- unwrapInType inputType+ , Full.Node{ node = Full.ConstNull, .. } <- constValue ->+ pure $ Error+ { message = concat+ [ "List of non-null values of type \""+ , show unwrappedType+ , "\" cannot contain null values."+ ]+ , locations = [location]+ }+ | otherwise -> mempty+ _ -> checkResult+
tests/Language/GraphQL/AST/ParserSpec.hs view
@@ -6,6 +6,7 @@ import Data.List.NonEmpty (NonEmpty(..)) import Language.GraphQL.AST.Document+import qualified Language.GraphQL.AST.DirectiveLocation as DirLoc import Language.GraphQL.AST.Parser import Test.Hspec (Spec, describe, it) import Test.Hspec.Megaparsec (shouldParse, shouldFailOn, shouldSucceedOn)@@ -118,6 +119,56 @@ | FIELD | FRAGMENT_SPREAD |]++ it "parses two minimal directive definitions" $+ let directive nm loc =+ TypeSystemDefinition+ (DirectiveDefinition+ (Description Nothing)+ nm+ (ArgumentsDefinition [])+ (loc :| []))+ example1 =+ directive "example1"+ (DirLoc.TypeSystemDirectiveLocation DirLoc.FieldDefinition)+ (Location {line = 2, column = 17})+ example2 =+ directive "example2"+ (DirLoc.ExecutableDirectiveLocation DirLoc.Field)+ (Location {line = 3, column = 17})+ testSchemaExtension = example1 :| [ example2 ]+ query = [r|+ directive @example1 on FIELD_DEFINITION+ directive @example2 on FIELD+ |]+ in parse document "" query `shouldParse` testSchemaExtension++ it "parses a directive definition with a default empty list argument" $+ let directive nm loc args =+ TypeSystemDefinition+ (DirectiveDefinition+ (Description Nothing)+ nm+ (ArgumentsDefinition+ [ InputValueDefinition+ (Description Nothing)+ argName+ argType+ argValue+ []+ | (argName, argType, argValue) <- args])+ (loc :| []))+ defn =+ directive "test"+ (DirLoc.TypeSystemDirectiveLocation DirLoc.FieldDefinition)+ [("foo",+ TypeList (TypeNamed "String"),+ Just+ $ Node (ConstList [])+ $ Location {line = 1, column = 33})]+ (Location {line = 1, column = 1})+ query = [r|directive @test(foo: [String] = []) on FIELD_DEFINITION|]+ in parse document "" query `shouldParse` (defn :| [ ]) it "parses schema extension with a new directive" $ parse document "" `shouldSucceedOn`[r|
tests/Language/GraphQL/ErrorSpec.hs view
@@ -8,17 +8,29 @@ ) where import qualified Data.Aeson as Aeson-import qualified Data.Sequence as Seq+import Data.List.NonEmpty (NonEmpty (..)) import Language.GraphQL.Error-import Test.Hspec ( Spec- , describe- , it- , shouldBe- )+import Test.Hspec+ ( Spec+ , describe+ , it+ , shouldBe+ )+import Text.Megaparsec (PosState(..))+import Text.Megaparsec.Error (ParseError(..), ParseErrorBundle(..))+import Text.Megaparsec.Pos (SourcePos(..), mkPos) spec :: Spec-spec = describe "singleError" $- it "constructs an error with the given message" $- let errors'' = Seq.singleton $ Error "Message." [] []- expected = Response Aeson.Null errors''- in singleError "Message." `shouldBe` expected+spec = describe "parseError" $+ it "generates response with a single error" $ do+ let parseErrors = TrivialError 0 Nothing mempty :| []+ posState = PosState+ { pstateInput = ""+ , pstateOffset = 0+ , pstateSourcePos = SourcePos "" (mkPos 1) (mkPos 1)+ , pstateTabWidth = mkPos 1+ , pstateLinePrefix = ""+ }+ Response Aeson.Null actual <-+ parseError (ParseErrorBundle parseErrors posState)+ length actual `shouldBe` 1
+ tests/Language/GraphQL/Execute/OrderedMapSpec.hs view
@@ -0,0 +1,72 @@+{- 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 #-}++module Language.GraphQL.Execute.OrderedMapSpec+ ( spec+ ) where++import Language.GraphQL.Execute.OrderedMap (OrderedMap)+import qualified Language.GraphQL.Execute.OrderedMap as OrderedMap+import Test.Hspec (Spec, describe, it, shouldBe, shouldSatisfy)++spec :: Spec+spec =+ describe "OrderedMap" $ do+ it "creates an empty map" $+ (mempty :: OrderedMap String) `shouldSatisfy` null++ it "creates a singleton" $+ let value :: String+ value = "value"+ in OrderedMap.size (OrderedMap.singleton "key" value) `shouldBe` 1++ it "combines inserted vales" $+ let key = "key"+ map1 = OrderedMap.singleton key ("1" :: String)+ map2 = OrderedMap.singleton key ("2" :: String)+ in OrderedMap.lookup key (map1 <> map2) `shouldBe` Just "12"++ it "shows the map" $+ let actual = show+ $ OrderedMap.insert "key1" "1"+ $ OrderedMap.singleton "key2" ("2" :: String)+ expected = "fromList [(\"key2\",\"2\"),(\"key1\",\"1\")]"+ in actual `shouldBe` expected++ it "traverses a map of just values" $+ let actual = sequence+ $ OrderedMap.insert "key1" (Just "2")+ $ OrderedMap.singleton "key2" $ Just ("1" :: String)+ expected = Just+ $ OrderedMap.insert "key1" "2"+ $ OrderedMap.singleton "key2" ("1" :: String)+ in actual `shouldBe` expected++ it "traverses a map with a Nothing" $+ let actual = sequence+ $ OrderedMap.insert "key1" Nothing+ $ OrderedMap.singleton "key2" $ Just ("1" :: String)+ expected = Nothing+ in actual `shouldBe` expected++ it "combines two maps preserving the order of the second one" $+ let map1 :: OrderedMap String+ map1 = OrderedMap.insert "key2" "2"+ $ OrderedMap.singleton "key1" "1"+ map2 :: OrderedMap String+ map2 = OrderedMap.insert "key4" "4"+ $ OrderedMap.singleton "key3" "3"+ expected = OrderedMap.insert "key4" "4"+ $ OrderedMap.insert "key3" "3"+ $ OrderedMap.insert "key2" "2"+ $ OrderedMap.singleton "key1" "1"+ in (map1 <> map2) `shouldBe` expected++ it "replaces existing values" $+ let key = "key"+ actual = OrderedMap.replace key ("2" :: String)+ $ OrderedMap.singleton key ("1" :: String)+ in OrderedMap.lookup key actual `shouldBe` Just "2"
tests/Language/GraphQL/ExecuteSpec.hs view
@@ -8,35 +8,88 @@ ( spec ) where -import Control.Exception (SomeException)+import Control.Exception (Exception(..), SomeException)+import Control.Monad.Catch (throwM) import Data.Aeson ((.=)) import qualified Data.Aeson as Aeson import Data.Aeson.Types (emptyObject) import Data.Conduit import Data.HashMap.Strict (HashMap) import qualified Data.HashMap.Strict as HashMap-import Language.GraphQL.AST (Document, Name)+import Data.Typeable (cast)+import Language.GraphQL.AST (Document, Location(..), Name) import Language.GraphQL.AST.Parser (document) import Language.GraphQL.Error-import Language.GraphQL.Execute-import Language.GraphQL.Type as Type-import Language.GraphQL.Type.Out as Out+import Language.GraphQL.Execute (execute)+import qualified Language.GraphQL.Type.Schema as Schema+import Language.GraphQL.Type+import qualified Language.GraphQL.Type.In as In+import qualified Language.GraphQL.Type.Out as Out+import Prelude hiding (id) import Test.Hspec (Spec, context, describe, it, shouldBe) import Text.Megaparsec (parse) import Text.RawString.QQ (r) +data PhilosopherException = PhilosopherException+ deriving Show++instance Exception PhilosopherException where+ toException = toException. ResolverException+ fromException e = do+ ResolverException resolverException <- fromException e+ cast resolverException+ philosopherSchema :: Schema (Either SomeException)-philosopherSchema = schema queryType Nothing (Just subscriptionType) mempty+philosopherSchema =+ schemaWithTypes Nothing queryType Nothing subscriptionRoot extraTypes mempty+ where+ subscriptionRoot = Just subscriptionType+ extraTypes =+ [ Schema.ObjectType bookType+ , Schema.ObjectType bookCollectionType+ ] queryType :: Out.ObjectType (Either SomeException) queryType = Out.ObjectType "Query" Nothing []- $ HashMap.singleton "philosopher" - $ ValueResolver philosopherField- $ pure $ Type.Object mempty+ $ HashMap.fromList+ [ ("philosopher", ValueResolver philosopherField philosopherResolver)+ , ("genres", ValueResolver genresField genresResolver)+ ] where philosopherField =- Out.Field Nothing (Out.NonNullObjectType philosopherType) HashMap.empty+ Out.Field Nothing (Out.NonNullObjectType philosopherType)+ $ HashMap.singleton "id"+ $ In.Argument Nothing (In.NamedScalarType id) Nothing+ philosopherResolver = pure $ Object mempty+ genresField =+ let fieldType = Out.ListType $ Out.NonNullScalarType string+ in Out.Field Nothing fieldType HashMap.empty+ genresResolver :: Resolve (Either SomeException)+ genresResolver = throwM PhilosopherException +musicType :: Out.ObjectType (Either SomeException)+musicType = Out.ObjectType "Music" Nothing []+ $ HashMap.fromList resolvers+ where+ resolvers =+ [ ("instrument", ValueResolver instrumentField instrumentResolver)+ ]+ instrumentResolver = pure $ String "piano"+ instrumentField = Out.Field Nothing (Out.NonNullScalarType string) HashMap.empty++poetryType :: Out.ObjectType (Either SomeException)+poetryType = Out.ObjectType "Poetry" Nothing []+ $ HashMap.fromList resolvers+ where+ resolvers =+ [ ("genre", ValueResolver genreField genreResolver)+ ]+ genreResolver = pure $ String "Futurism"+ genreField = Out.Field Nothing (Out.NonNullScalarType string) HashMap.empty++interestType :: Out.UnionType (Either SomeException)+interestType = Out.UnionType "Interest" Nothing [musicType, poetryType]+ philosopherType :: Out.ObjectType (Either SomeException) philosopherType = Out.ObjectType "Philosopher" Nothing [] $ HashMap.fromList resolvers@@ -44,19 +97,68 @@ resolvers = [ ("firstName", ValueResolver firstNameField firstNameResolver) , ("lastName", ValueResolver lastNameField lastNameResolver)+ , ("school", ValueResolver schoolField schoolResolver)+ , ("interest", ValueResolver interestField interestResolver)+ , ("majorWork", ValueResolver majorWorkField majorWorkResolver)+ , ("century", ValueResolver centuryField centuryResolver) ] firstNameField = Out.Field Nothing (Out.NonNullScalarType string) HashMap.empty- firstNameResolver = pure $ Type.String "Friedrich"+ firstNameResolver = pure $ String "Friedrich" lastNameField = Out.Field Nothing (Out.NonNullScalarType string) HashMap.empty- lastNameResolver = pure $ Type.String "Nietzsche"+ lastNameResolver = pure $ String "Nietzsche"+ schoolField+ = Out.Field Nothing (Out.NonNullEnumType schoolType) HashMap.empty+ schoolResolver = pure $ Enum "EXISTENTIALISM"+ interestField+ = Out.Field Nothing (Out.NonNullUnionType interestType) HashMap.empty+ interestResolver = pure+ $ Object+ $ HashMap.fromList [("instrument", "piano")]+ majorWorkField+ = Out.Field Nothing (Out.NonNullInterfaceType workType) HashMap.empty+ majorWorkResolver = pure+ $ Object+ $ HashMap.fromList+ [ ("title", "Also sprach Zarathustra: Ein Buch für Alle und Keinen")+ ]+ centuryField =+ Out.Field Nothing (Out.NonNullScalarType int) HashMap.empty+ centuryResolver = pure $ Float 18.5 +workType :: Out.InterfaceType (Either SomeException)+workType = Out.InterfaceType "Work" Nothing []+ $ HashMap.fromList fields+ where+ fields = [("title", titleField)]+ titleField = Out.Field Nothing (Out.NonNullScalarType string) HashMap.empty++bookType :: Out.ObjectType (Either SomeException)+bookType = Out.ObjectType "Book" Nothing [workType]+ $ HashMap.fromList resolvers+ where+ resolvers =+ [ ("title", ValueResolver titleField titleResolver)+ ]+ titleField = Out.Field Nothing (Out.NonNullScalarType string) HashMap.empty+ titleResolver = pure "Also sprach Zarathustra: Ein Buch für Alle und Keinen"++bookCollectionType :: Out.ObjectType (Either SomeException)+bookCollectionType = Out.ObjectType "Book" Nothing [workType]+ $ HashMap.fromList resolvers+ where+ resolvers =+ [ ("title", ValueResolver titleField titleResolver)+ ]+ titleField = Out.Field Nothing (Out.NonNullScalarType string) HashMap.empty+ titleResolver = pure "The Three Critiques"+ subscriptionType :: Out.ObjectType (Either SomeException) subscriptionType = Out.ObjectType "Subscription" Nothing [] $ HashMap.singleton "newQuote"- $ EventStreamResolver quoteField (pure $ Type.Object mempty)- $ pure $ yield $ Type.Object mempty+ $ EventStreamResolver quoteField (pure $ Object mempty)+ $ pure $ yield $ Object mempty where quoteField = Out.Field Nothing (Out.NonNullObjectType quoteType) HashMap.empty@@ -70,6 +172,13 @@ quoteField = Out.Field Nothing (Out.NonNullScalarType string) HashMap.empty +schoolType :: EnumType+schoolType = EnumType "School" Nothing $ HashMap.fromList+ [ ("NOMINALISM", EnumValue Nothing)+ , ("REALISM", EnumValue Nothing)+ , ("IDEALISM", EnumValue Nothing)+ ]+ type EitherStreamOrValue = Either (ResponseEventStream (Either SomeException) Aeson.Value) (Response Aeson.Value)@@ -118,6 +227,99 @@ Right (Right actual) = either (pure . parseError) execute' $ parse document "" "{ philosopher { firstName } philosopher { lastName } }" in actual `shouldBe` expected++ it "errors on invalid output enum values" $+ let data'' = Aeson.object+ [ "philosopher" .= Aeson.object+ [ "school" .= Aeson.Null+ ]+ ]+ executionErrors = pure $ Error+ { message = "Enum value completion failed."+ , locations = [Location 1 17]+ , path = []+ }+ expected = Response data'' executionErrors+ Right (Right actual) = either (pure . parseError) execute'+ $ parse document "" "{ philosopher { school } }"+ in actual `shouldBe` expected++ it "gives location information for non-null unions" $+ let data'' = Aeson.object+ [ "philosopher" .= Aeson.object+ [ "interest" .= Aeson.Null+ ]+ ]+ executionErrors = pure $ Error+ { message = "Union value completion failed."+ , locations = [Location 1 17]+ , path = []+ }+ expected = Response data'' executionErrors+ Right (Right actual) = either (pure . parseError) execute'+ $ parse document "" "{ philosopher { interest } }"+ in actual `shouldBe` expected++ it "gives location information for invalid interfaces" $+ let data'' = Aeson.object+ [ "philosopher" .= Aeson.object+ [ "majorWork" .= Aeson.Null+ ]+ ]+ executionErrors = pure $ Error+ { message = "Interface value completion failed."+ , locations = [Location 1 17]+ , path = []+ }+ expected = Response data'' executionErrors+ Right (Right actual) = either (pure . parseError) execute'+ $ parse document "" "{ philosopher { majorWork { title } } }"+ in actual `shouldBe` expected++ it "gives location information for invalid scalar arguments" $+ let data'' = Aeson.object+ [ "philosopher" .= Aeson.Null+ ]+ executionErrors = pure $ Error+ { message = "Argument coercing failed."+ , locations = [Location 1 15]+ , path = []+ }+ expected = Response data'' executionErrors+ Right (Right actual) = either (pure . parseError) execute'+ $ parse document "" "{ philosopher(id: true) { lastName } }"+ in actual `shouldBe` expected++ it "gives location information for failed result coercion" $+ let data'' = Aeson.object+ [ "philosopher" .= Aeson.object+ [ "century" .= Aeson.Null+ ]+ ]+ executionErrors = pure $ Error+ { message = "Result coercion failed."+ , locations = [Location 1 26]+ , path = []+ }+ expected = Response data'' executionErrors+ Right (Right actual) = either (pure . parseError) execute'+ $ parse document "" "{ philosopher(id: \"1\") { century } }"+ in actual `shouldBe` expected++ it "gives location information for failed result coercion" $+ let data'' = Aeson.object+ [ "genres" .= Aeson.Null+ ]+ executionErrors = pure $ Error+ { message = "PhilosopherException"+ , locations = [Location 1 3]+ , path = []+ }+ expected = Response data'' executionErrors+ Right (Right actual) = either (pure . parseError) execute'+ $ parse document "" "{ genres }"+ in actual `shouldBe` expected+ context "Subscription" $ it "subscribes" $ let data'' = Aeson.object
tests/Language/GraphQL/Validate/RulesSpec.hs view
@@ -49,16 +49,18 @@ catType = ObjectType "Cat" Nothing [petType] $ HashMap.fromList [ ("name", nameResolver) , ("nickname", nicknameResolver)- , ("doesKnowCommand", doesKnowCommandResolver)+ , ("doesKnowCommands", doesKnowCommandsResolver) , ("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+ doesKnowCommandsType = In.NonNullListType+ $ In.NonNullEnumType catCommandType+ doesKnowCommandsField = Field Nothing (Out.NonNullScalarType boolean)+ $ HashMap.singleton "catCommands"+ $ In.Argument Nothing doesKnowCommandsType Nothing+ doesKnowCommandsResolver = ValueResolver doesKnowCommandsField $ pure $ Boolean True nameResolver :: Resolver IO@@ -845,7 +847,7 @@ } in validate queryString `shouldBe` [expected] - context "providedRequiredArgumentsRule" $+ context "providedRequiredArgumentsRule" $ do it "checks for (non-)nullable arguments" $ let queryString = [r| {@@ -866,17 +868,17 @@ context "variablesInAllowedPositionRule" $ do it "rejects wrongly typed variable arguments" $ let queryString = [r|- query catCommandArgQuery($catCommandArg: CatCommand) {- cat {- doesKnowCommand(catCommand: $catCommandArg)+ query dogCommandArgQuery($dogCommandArg: DogCommand) {+ dog {+ doesKnowCommand(dogCommand: $dogCommandArg) } } |] expected = Error { message =- "Variable \"$catCommandArg\" of type \- \\"CatCommand\" used in position expecting type \- \\"!CatCommand\"."+ "Variable \"$dogCommandArg\" of type \+ \\"DogCommand\" used in position expecting type \+ \\"!DogCommand\"." , locations = [AST.Location 2 44] } in validate queryString `shouldBe` [expected]@@ -897,7 +899,7 @@ } in validate queryString `shouldBe` [expected] - context "valuesOfCorrectTypeRule" $+ context "valuesOfCorrectTypeRule" $ do it "rejects values of incorrect types" $ let queryString = [r| {@@ -910,5 +912,51 @@ { message = "Value 3 cannot be coerced to type \"Boolean\"." , locations = [AST.Location 4 52]+ }+ in validate queryString `shouldBe` [expected]++ it "uses the location of a single list value" $+ let queryString = [r|+ {+ cat {+ doesKnowCommands(catCommands: [3])+ }+ }+ |]+ expected = Error+ { message =+ "Value 3 cannot be coerced to type \"!CatCommand\"."+ , locations = [AST.Location 4 54]+ }+ in validate queryString `shouldBe` [expected]++ it "validates input object properties once" $+ let queryString = [r|+ {+ findDog(complex: { name: 3 }) {+ name+ }+ }+ |]+ expected = Error+ { message =+ "Value 3 cannot be coerced to type \"!String\"."+ , locations = [AST.Location 3 46]+ }+ in validate queryString `shouldBe` [expected]++ it "checks for required list members" $+ let queryString = [r|+ {+ cat {+ doesKnowCommands(catCommands: [null])+ }+ }+ |]+ expected = Error+ { message =+ "List of non-null values of type \"CatCommand\" \+ \cannot contain null values."+ , locations = [AST.Location 4 54] } in validate queryString `shouldBe` [expected]