14 Commits

17 changed files with 558 additions and 171 deletions

View File

@ -6,6 +6,30 @@ The format is based on
and this project adheres to and this project adheres to
[Haskell Package Versioning Policy](https://pvp.haskell.org/). [Haskell Package Versioning Policy](https://pvp.haskell.org/).
## [1.1.0.0] - 2022-12-24
### Changed
- Removed deprecated `Language.GraphQL.Error` functions: `addErr`, `addErrMsg`,
`singleError`.
- Deprecate `Resolution`, `CollectErrsT` and `runCollectErrs` in the `Error`
module. It was already noted in the documentation that these symbols are
deprecated, now a pragma is added.
- `Language.GraphQL`: Added information about the *json* flag and switching to
*graphql-spice* for JSON support.
### Added
- Partial schema printing: operation type encoder.
## [1.0.3.0] - 2022-03-27
### Fixed
- Index position in error path. (Index and Segment paths of a field have been
swapped).
- Parsing empty list as an argument.
### Added
- quickCheck Parser test for arguments. Arbitrary instances for Language.GraphQL.AST.Document.
- Enhanced query error messages. Add tests for these cases.
- Allow version 2.0 of the text package.
## [1.0.2.0] - 2021-12-26 ## [1.0.2.0] - 2021-12-26
### Added ### Added
- `Serialize` instance for `Type.Definition.Value`. - `Serialize` instance for `Type.Definition.Value`.
@ -466,7 +490,9 @@ and this project adheres to
### Added ### Added
- Data types for the GraphQL language. - Data types for the GraphQL language.
[1.0.2.0]: https://www.caraus.tech/projects/pub-graphql/repository/23/diff?rev=1.0.2.0&rev_to=v1.0.1.0 [1.1.0.0]: https://www.caraus.tech/projects/pub-graphql/repository/23/diff?rev=v1.1.0.0&rev_to=v1.0.3.0
[1.0.3.0]: https://www.caraus.tech/projects/pub-graphql/repository/23/diff?rev=v1.0.3.0&rev_to=v1.0.2.0
[1.0.2.0]: https://www.caraus.tech/projects/pub-graphql/repository/23/diff?rev=v1.0.2.0&rev_to=v1.0.1.0
[1.0.1.0]: https://www.caraus.tech/projects/pub-graphql/repository/23/diff?rev=v1.0.1.0&rev_to=v1.0.0.0 [1.0.1.0]: https://www.caraus.tech/projects/pub-graphql/repository/23/diff?rev=v1.0.1.0&rev_to=v1.0.0.0
[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 [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.1.0]: https://www.caraus.tech/projects/pub-graphql/repository/23/diff?rev=v0.11.1.0&rev_to=v0.11.0.0

View File

@ -1,7 +1,7 @@
cabal-version: 2.4 cabal-version: 2.4
name: graphql name: graphql
version: 1.0.2.0 version: 1.1.0.0
synopsis: Haskell GraphQL implementation synopsis: Haskell GraphQL implementation
description: Haskell <https://spec.graphql.org/June2018/ GraphQL> implementation. description: Haskell <https://spec.graphql.org/June2018/ GraphQL> implementation.
category: Language category: Language
@ -11,7 +11,7 @@ author: Danny Navarro <j@dannynavarro.net>,
Matthías Páll Gissurarson <mpg@mpg.is>, Matthías Páll Gissurarson <mpg@mpg.is>,
Sólrún Halla Einarsdóttir <she@mpg.is> Sólrún Halla Einarsdóttir <she@mpg.is>
maintainer: belka@caraus.de maintainer: belka@caraus.de
copyright: (c) 2019-2021 Eugen Wissner, copyright: (c) 2019-2022 Eugen Wissner,
(c) 2015-2017 J. Daniel Navarro (c) 2015-2017 J. Daniel Navarro
license: MPL-2.0 AND BSD-3-Clause license: MPL-2.0 AND BSD-3-Clause
license-files: LICENSE, license-files: LICENSE,
@ -22,8 +22,7 @@ extra-source-files:
README.md README.md
tested-with: tested-with:
GHC == 8.10.7, GHC == 8.10.7,
GHC == 9.0.1, GHC == 9.2.4
GHC == 9.2.1
source-repository head source-repository head
type: git type: git
@ -31,7 +30,7 @@ source-repository head
flag Json flag Json
description: Whether to build against @aeson 1.x@ description: Whether to build against @aeson 1.x@
default: True default: False
manual: True manual: True
library library
@ -72,7 +71,7 @@ library
megaparsec >= 9.0 && < 10, megaparsec >= 9.0 && < 10,
parser-combinators >= 1.3 && < 2, parser-combinators >= 1.3 && < 2,
template-haskell >= 2.16 && < 3, template-haskell >= 2.16 && < 3,
text ^>= 1.2.4, text >= 1.2 && < 3,
transformers ^>= 0.5.6, transformers ^>= 0.5.6,
unordered-containers ^>= 0.2.14, unordered-containers ^>= 0.2.14,
vector ^>= 0.12.3 vector ^>= 0.12.3
@ -93,12 +92,14 @@ test-suite graphql-test
Language.GraphQL.AST.EncoderSpec Language.GraphQL.AST.EncoderSpec
Language.GraphQL.AST.LexerSpec Language.GraphQL.AST.LexerSpec
Language.GraphQL.AST.ParserSpec Language.GraphQL.AST.ParserSpec
Language.GraphQL.AST.Arbitrary
Language.GraphQL.ErrorSpec Language.GraphQL.ErrorSpec
Language.GraphQL.Execute.CoerceSpec Language.GraphQL.Execute.CoerceSpec
Language.GraphQL.Execute.OrderedMapSpec Language.GraphQL.Execute.OrderedMapSpec
Language.GraphQL.ExecuteSpec Language.GraphQL.ExecuteSpec
Language.GraphQL.Type.OutSpec Language.GraphQL.Type.OutSpec
Language.GraphQL.Validate.RulesSpec Language.GraphQL.Validate.RulesSpec
Schemas.HeroSchema
hs-source-dirs: hs-source-dirs:
tests tests
ghc-options: -threaded -rtsopts -with-rtsopts=-N -Wall ghc-options: -threaded -rtsopts -with-rtsopts=-N -Wall
@ -110,8 +111,11 @@ test-suite graphql-test
exceptions, exceptions,
graphql, graphql,
hspec ^>= 2.9.1, hspec ^>= 2.9.1,
hspec-expectations ^>= 0.8.2,
hspec-megaparsec ^>= 2.2.0, hspec-megaparsec ^>= 2.2.0,
megaparsec, megaparsec,
text, text,
unordered-containers unordered-containers,
containers,
vector
default-language: Haskell2010 default-language: Haskell2010

View File

@ -1,10 +1,31 @@
{-# LANGUAGE CPP #-} {-# LANGUAGE CPP #-}
#ifdef WITH_JSON
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE RecordWildCards #-}
#ifdef WITH_JSON
-- | This module provides the functions to parse and execute @GraphQL@ queries. -- | This module provides the functions to parse and execute @GraphQL@ queries.
--
-- The content of this module depends on the value of the __json__ flag, which
-- is currently on by default. This behavior will change in the future, the flag
-- will be switched off by default and then removed.
--
-- This documentation is generated with the enabled __json__ flag and functions
-- described here support JSON and are deprecated. JSON instances are provided
-- now by an additional package, __graphql-spice__. To start using the new
-- package create __cabal.project__ in the root directory of your project with
-- the following contents:
--
-- @
-- packages: .
-- constraints: graphql -json
-- @
--
-- Then add __graphql-spice__ as dependency.
--
-- The new version of this module defines only one function, @graphql@, which
-- works with the internal GraphQL value representation used by this lbirary.
-- Refer to @Language.GraphQL.JSON.graphql@ in __graphql-spice__ for the
-- function that accepts and returns JSON.
module Language.GraphQL module Language.GraphQL
( graphql ( graphql
, graphqlSubs , graphqlSubs
@ -24,6 +45,7 @@ import qualified Language.GraphQL.Validate as Validate
import Language.GraphQL.Type.Schema (Schema) import Language.GraphQL.Type.Schema (Schema)
import Text.Megaparsec (parse) import Text.Megaparsec (parse)
{-# DEPRECATED graphql "Use graphql-spice package instead" #-}
-- | If the text parses correctly as a @GraphQL@ query the query is -- | If the text parses correctly as a @GraphQL@ query the query is
-- executed using the given 'Schema'. -- executed using the given 'Schema'.
graphql :: MonadCatch m graphql :: MonadCatch m
@ -32,6 +54,7 @@ graphql :: MonadCatch m
-> m (Either (ResponseEventStream m Aeson.Value) Aeson.Object) -- ^ Response. -> m (Either (ResponseEventStream m Aeson.Value) Aeson.Object) -- ^ Response.
graphql schema = graphqlSubs schema mempty mempty graphql schema = graphqlSubs schema mempty mempty
{-# DEPRECATED graphqlSubs "Use graphql-spice package instead" #-}
-- | If the text parses correctly as a @GraphQL@ query the substitution is -- | If the text parses correctly as a @GraphQL@ query the substitution is
-- applied to the query and the query is then executed using to the given -- applied to the query and the query is then executed using to the given
-- 'Schema'. -- 'Schema'.
@ -79,6 +102,46 @@ graphqlSubs schema operationName variableValues document' =
#else #else
-- | This module provides the functions to parse and execute @GraphQL@ queries. -- | This module provides the functions to parse and execute @GraphQL@ queries.
module Language.GraphQL module Language.GraphQL
( ( graphql
) where ) where
import Control.Monad.Catch (MonadCatch)
import Data.HashMap.Strict (HashMap)
import qualified Data.Sequence as Seq
import Data.Text (Text)
import qualified Data.Text as Text
import qualified Language.GraphQL.AST as Full
import Language.GraphQL.Error
import Language.GraphQL.Execute
import qualified Language.GraphQL.Validate as Validate
import Language.GraphQL.Type.Schema (Schema)
import Prelude hiding (null)
import Text.Megaparsec (parse)
-- | If the text parses correctly as a @GraphQL@ query the query is
-- executed using the given 'Schema'.
--
-- An operation name can be given if the document contains multiple operations.
graphql :: (MonadCatch m, VariableValue a, Serialize b)
=> Schema m -- ^ Resolvers.
-> Maybe Text -- ^ Operation name.
-> HashMap Full.Name a -- ^ Variable substitution function.
-> Text -- ^ Text representing a @GraphQL@ request document.
-> m (Either (ResponseEventStream m b) (Response b)) -- ^ Response.
graphql schema operationName variableValues document' =
case parse Full.document "" document' of
Left errorBundle -> pure <$> parseError errorBundle
Right parsed ->
case validate parsed of
Seq.Empty -> execute schema operationName variableValues parsed
errors -> pure $ pure
$ Response null
$ fromValidationError <$> errors
where
validate = Validate.document schema Validate.specifiedRules
fromValidationError Validate.Error{..} = Error
{ message = Text.pack message
, locations = locations
, path = []
}
#endif #endif

View File

@ -49,6 +49,8 @@ module Language.GraphQL.AST.Document
, Value(..) , Value(..)
, VariableDefinition(..) , VariableDefinition(..)
, escape , escape
, showVariableName
, showVariable
) where ) where
import Data.Char (ord) import Data.Char (ord)
@ -339,6 +341,12 @@ data VariableDefinition =
VariableDefinition Name Type (Maybe (Node ConstValue)) Location VariableDefinition Name Type (Maybe (Node ConstValue)) Location
deriving (Eq, Show) deriving (Eq, Show)
showVariableName :: VariableDefinition -> String
showVariableName (VariableDefinition name _ _ _) = "$" <> Text.unpack name
showVariable :: VariableDefinition -> String
showVariable var@(VariableDefinition _ type' _ _) = showVariableName var <> ":" <> " " <> show type'
-- ** Type References -- ** Type References
-- | Type representation. -- | Type representation.

View File

@ -11,6 +11,7 @@ module Language.GraphQL.AST.Encoder
, directive , directive
, document , document
, minified , minified
, operationType
, pretty , pretty
, type' , type'
, value , value
@ -34,7 +35,7 @@ import qualified Language.GraphQL.AST.Document as Full
-- Use 'pretty' or 'minified' to construct the formatter. -- Use 'pretty' or 'minified' to construct the formatter.
data Formatter data Formatter
= Minified = Minified
| Pretty Word | Pretty !Word
-- | Constructs a formatter for pretty printing. -- | Constructs a formatter for pretty printing.
pretty :: Formatter pretty :: Formatter
@ -101,7 +102,7 @@ variableDefinition formatter variableDefinition' =
in variable variableName in variable variableName
<> eitherFormat formatter ": " ":" <> eitherFormat formatter ": " ":"
<> type' variableType <> type' variableType
<> maybe mempty (defaultValue formatter) (Full.node <$> defaultValue') <> maybe mempty (defaultValue formatter . Full.node) defaultValue'
defaultValue :: Formatter -> Full.ConstValue -> Lazy.Text defaultValue :: Formatter -> Full.ConstValue -> Lazy.Text
defaultValue formatter val defaultValue formatter val
@ -294,6 +295,12 @@ nonNullType :: Full.NonNullType -> Lazy.Text
nonNullType (Full.NonNullTypeNamed x) = Lazy.Text.fromStrict x <> "!" nonNullType (Full.NonNullTypeNamed x) = Lazy.Text.fromStrict x <> "!"
nonNullType (Full.NonNullTypeList x) = listType x <> "!" nonNullType (Full.NonNullTypeList x) = listType x <> "!"
-- | Produces lowercase operation type: query, mutation or subscription.
operationType :: Formatter -> Full.OperationType -> Lazy.Text
operationType _formatter Full.Query = "query"
operationType _formatter Full.Mutation = "mutation"
operationType _formatter Full.Subscription = "subscription"
-- * Internal -- * Internal
between :: Char -> Char -> Lazy.Text -> Lazy.Text between :: Char -> Char -> Lazy.Text -> Lazy.Text

View File

@ -58,6 +58,7 @@ import qualified Text.Megaparsec.Char.Lexer as Lexer
import Data.Text (Text) import Data.Text (Text)
import qualified Data.Text as T import qualified Data.Text as T
import qualified Data.Text.Lazy as TL import qualified Data.Text.Lazy as TL
import Control.Monad (void)
-- | Standard parser. -- | Standard parser.
-- Accepts the type of the parsed token. -- Accepts the type of the parsed token.
@ -93,7 +94,7 @@ dollar = symbol "$"
-- | Parser for "@". -- | Parser for "@".
at :: Parser () at :: Parser ()
at = symbol "@" >> pure () at = void $ symbol "@"
-- | Parser for "&". -- | Parser for "&".
amp :: Parser T.Text amp :: Parser T.Text
@ -101,7 +102,7 @@ amp = symbol "&"
-- | Parser for ":". -- | Parser for ":".
colon :: Parser () colon :: Parser ()
colon = symbol ":" >> pure () colon = void $ symbol ":"
-- | Parser for "=". -- | Parser for "=".
equals :: Parser T.Text equals :: Parser T.Text
@ -220,7 +221,7 @@ escapeSequence = do
-- | Parser for the "Byte Order Mark". -- | Parser for the "Byte Order Mark".
unicodeBOM :: Parser () unicodeBOM :: Parser ()
unicodeBOM = optional (char '\xfeff') >> pure () unicodeBOM = void $ optional $ char '\xfeff'
-- | Parses "extend" followed by a 'symbol'. It is used by schema extensions. -- | Parses "extend" followed by a 'symbol'. It is used by schema extensions.
extend :: forall a. Text -> String -> NonEmpty (Parser a) -> Parser a extend :: forall a. Text -> String -> NonEmpty (Parser a) -> Parser a

View File

@ -450,8 +450,8 @@ value = Full.Variable <$> variable
<|> Full.Null <$ nullValue <|> Full.Null <$ nullValue
<|> Full.String <$> stringValue <|> Full.String <$> stringValue
<|> Full.Enum <$> try enumValue <|> Full.Enum <$> try enumValue
<|> Full.List <$> brackets (some $ valueNode value) <|> Full.List <$> brackets (many $ valueNode value)
<|> Full.Object <$> braces (some $ objectField $ valueNode value) <|> Full.Object <$> braces (many $ objectField $ valueNode value)
<?> "Value" <?> "Value"
constValue :: Parser Full.ConstValue constValue :: Parser Full.ConstValue

View File

@ -15,16 +15,13 @@ module Language.GraphQL.Error
, ResolverException(..) , ResolverException(..)
, Response(..) , Response(..)
, ResponseEventStream , ResponseEventStream
, addErr
, addErrMsg
, parseError , parseError
, runCollectErrs , runCollectErrs
, singleError
) where ) where
import Conduit import Conduit
import Control.Exception (Exception(..)) import Control.Exception (Exception(..))
import Control.Monad.Trans.State (StateT, modify, runStateT) import Control.Monad.Trans.State (StateT, runStateT)
import Data.HashMap.Strict (HashMap) import Data.HashMap.Strict (HashMap)
import Data.Sequence (Seq(..), (|>)) import Data.Sequence (Seq(..), (|>))
import qualified Data.Sequence as Seq import qualified Data.Sequence as Seq
@ -103,11 +100,9 @@ instance Exception ResolverException
-- * Deprecated -- * Deprecated
{-# DEPRECATED runCollectErrs "runCollectErrs was part of the old executor and isn't used anymore" #-}
-- | Runs the given query computation, but collects the errors into an error -- | Runs the given query computation, but collects the errors into an error
-- list, which is then sent back with the data. -- list, which is then sent back with the data.
--
-- /runCollectErrs was part of the old executor and isn't used anymore, it will
-- be deprecated in the future and removed./
runCollectErrs :: (Monad m, Serialize a) runCollectErrs :: (Monad m, Serialize a)
=> HashMap Name (Schema.Type m) => HashMap Name (Schema.Type m)
-> CollectErrsT m a -> CollectErrsT m a
@ -117,40 +112,13 @@ runCollectErrs types' res = do
$ Resolution{ errors = Seq.empty, types = types' } $ Resolution{ errors = Seq.empty, types = types' }
pure $ Response dat errors pure $ Response dat errors
{-# DEPRECATED Resolution "Resolution was part of the old executor and isn't used anymore" #-}
-- | Executor context. -- | Executor context.
--
-- /Resolution was part of the old executor and isn't used anymore, it will be
-- deprecated in the future and removed./
data Resolution m = Resolution data Resolution m = Resolution
{ errors :: Seq Error { errors :: Seq Error
, types :: HashMap Name (Schema.Type m) , types :: HashMap Name (Schema.Type m)
} }
{-# DEPRECATED CollectErrsT "CollectErrsT was part of the old executor and isn't used anymore" #-}
-- | A wrapper to pass error messages around. -- | A wrapper to pass error messages around.
--
-- /CollectErrsT was part of the old executor and isn't used anymore, it will be
-- deprecated in the future and removed./
type CollectErrsT m = StateT (Resolution m) m 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 $ 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

View File

@ -61,6 +61,7 @@ import Language.GraphQL.Error
, ResponseEventStream , ResponseEventStream
) )
import Prelude hiding (null) import Prelude hiding (null)
import Language.GraphQL.AST.Document (showVariableName)
newtype ExecutorT m a = ExecutorT newtype ExecutorT m a = ExecutorT
{ runExecutorT :: ReaderT (HashMap Full.Name (Type m)) (WriterT (Seq Error) m) a { runExecutorT :: ReaderT (HashMap Full.Name (Type m)) (WriterT (Seq Error) m) a
@ -190,32 +191,42 @@ data QueryError
tell :: Monad m => Seq Error -> ExecutorT m () tell :: Monad m => Seq Error -> ExecutorT m ()
tell = ExecutorT . lift . Writer.tell tell = ExecutorT . lift . Writer.tell
operationNameErrorText :: Text
operationNameErrorText = Text.unlines
[ "Named operations must be provided with the name of the desired operation."
, "See https://spec.graphql.org/June2018/#sec-Language.Document description."
]
queryError :: QueryError -> Error queryError :: QueryError -> Error
queryError OperationNameRequired = queryError OperationNameRequired =
Error{ message = "Operation name is required.", locations = [], path = [] } let queryErrorMessage = "Operation name is required. " <> operationNameErrorText
in Error{ message = queryErrorMessage, locations = [], path = [] }
queryError (OperationNotFound operationName) = queryError (OperationNotFound operationName) =
let queryErrorMessage = Text.concat let queryErrorMessage = Text.unlines
[ Text.concat
[ "Operation \"" [ "Operation \""
, Text.pack operationName , Text.pack operationName
, "\" not found." , "\" is not found in the named operations you've provided. "
]
, operationNameErrorText
] ]
in Error{ message = queryErrorMessage, locations = [], path = [] } in Error{ message = queryErrorMessage, locations = [], path = [] }
queryError (CoercionError variableDefinition) = queryError (CoercionError variableDefinition) =
let Full.VariableDefinition variableName _ _ location = variableDefinition let (Full.VariableDefinition _ _ _ location) = variableDefinition
queryErrorMessage = Text.concat queryErrorMessage = Text.concat
[ "Failed to coerce the variable \"" [ "Failed to coerce the variable "
, variableName , Text.pack $ Full.showVariable variableDefinition
, "\"." , "."
] ]
in Error{ message = queryErrorMessage, locations = [location], path = [] } in Error{ message = queryErrorMessage, locations = [location], path = [] }
queryError (UnknownInputType variableDefinition) = queryError (UnknownInputType variableDefinition) =
let Full.VariableDefinition variableName variableTypeName _ location = variableDefinition let Full.VariableDefinition _ variableTypeName _ location = variableDefinition
queryErrorMessage = Text.concat queryErrorMessage = Text.concat
[ "Variable \"" [ "Variable "
, variableName , Text.pack $ showVariableName variableDefinition
, "\" has unknown type \"" , " has unknown type "
, Text.pack $ show variableTypeName , Text.pack $ show variableTypeName
, "\"." , "."
] ]
in Error{ message = queryErrorMessage, locations = [location], path = [] } in Error{ message = queryErrorMessage, locations = [location], path = [] }
@ -375,6 +386,7 @@ executeField objectValue fields (viewResolver -> resolverPair) errorPath =
, Handler (resolverHandler fieldLocation) , Handler (resolverHandler fieldLocation)
] ]
where where
fieldErrorPath = fieldsSegment fields : errorPath
inputCoercionHandler :: (MonadCatch m, Serialize a) inputCoercionHandler :: (MonadCatch m, Serialize a)
=> Full.Location => Full.Location
-> InputCoercionException -> InputCoercionException
@ -402,17 +414,16 @@ executeField objectValue fields (viewResolver -> resolverPair) errorPath =
then throwM e then throwM e
else returnError newError else returnError newError
exceptionHandler errorLocation e = exceptionHandler errorLocation e =
let newPath = fieldsSegment fields : errorPath let newError = constructError e errorLocation fieldErrorPath
newError = constructError e errorLocation newPath
in if Out.isNonNullType fieldType in if Out.isNonNullType fieldType
then throwM $ FieldException errorLocation newPath e then throwM $ FieldException errorLocation fieldErrorPath e
else returnError newError else returnError newError
returnError newError = tell (Seq.singleton newError) >> pure null returnError newError = tell (Seq.singleton newError) >> pure null
go fieldName inputArguments = do go fieldName inputArguments = do
argumentValues <- coerceArgumentValues argumentTypes inputArguments argumentValues <- coerceArgumentValues argumentTypes inputArguments
resolvedValue <- resolvedValue <-
resolveFieldValue resolveFunction objectValue fieldName argumentValues resolveFieldValue resolveFunction objectValue fieldName argumentValues
completeValue fieldType fields errorPath resolvedValue completeValue fieldType fields fieldErrorPath resolvedValue
(resolverField, resolveFunction) = resolverPair (resolverField, resolveFunction) = resolverPair
Out.Field _ fieldType argumentTypes = resolverField Out.Field _ fieldType argumentTypes = resolverField
@ -445,6 +456,7 @@ resolveAbstractType abstractType values'
_ -> pure Nothing _ -> pure Nothing
| otherwise = pure Nothing | otherwise = pure Nothing
-- https://spec.graphql.org/October2021/#sec-Value-Completion
completeValue :: (MonadCatch m, Serialize a) completeValue :: (MonadCatch m, Serialize a)
=> Out.Type m => Out.Type m
-> NonEmpty (Transform.Field m) -> NonEmpty (Transform.Field m)
@ -476,8 +488,7 @@ completeValue outputType@(Out.EnumBaseType enumType) _ _ (Type.Enum enum) =
$ ValueCompletionException (show outputType) $ ValueCompletionException (show outputType)
$ Type.Enum enum $ Type.Enum enum
completeValue (Out.ObjectBaseType objectType) fields errorPath result completeValue (Out.ObjectBaseType objectType) fields errorPath result
= executeSelectionSet (mergeSelectionSets fields) objectType result = executeSelectionSet (mergeSelectionSets fields) objectType result errorPath
$ fieldsSegment fields : errorPath
completeValue outputType@(Out.InterfaceBaseType interfaceType) fields errorPath result completeValue outputType@(Out.InterfaceBaseType interfaceType) fields errorPath result
| Type.Object objectMap <- result = do | Type.Object objectMap <- result = do
let abstractType = Type.Internal.AbstractInterfaceType interfaceType let abstractType = Type.Internal.AbstractInterfaceType interfaceType

View File

@ -8,6 +8,11 @@
{-# LANGUAGE CPP #-} {-# LANGUAGE CPP #-}
-- | Types and functions used for input and result coercion. -- | Types and functions used for input and result coercion.
--
-- JSON instances in this module are only available with the __json__
-- flag that is currently on by default, but will be disabled in the future.
-- Refer to the documentation in the 'Language.GraphQL' module and to
-- the __graphql-spice__ package.
module Language.GraphQL.Execute.Coerce module Language.GraphQL.Execute.Coerce
( Output(..) ( Output(..)
, Serialize(..) , Serialize(..)

View File

@ -54,7 +54,7 @@ import Data.HashMap.Strict (HashMap)
import Data.HashSet (HashSet) import Data.HashSet (HashSet)
import qualified Data.HashSet as HashSet import qualified Data.HashSet as HashSet
import Data.List (groupBy, sortBy, sortOn) import Data.List (groupBy, sortBy, sortOn)
import Data.Maybe (catMaybes, fromMaybe, isJust, isNothing, mapMaybe) import Data.Maybe (fromMaybe, isJust, isNothing, mapMaybe)
import Data.List.NonEmpty (NonEmpty(..)) import Data.List.NonEmpty (NonEmpty(..))
import Data.Ord (comparing) import Data.Ord (comparing)
import Data.Sequence (Seq(..), (|>)) import Data.Sequence (Seq(..), (|>))
@ -1551,9 +1551,9 @@ valuesOfCorrectTypeRule = ValueRule go constGo
toConst Full.Null = Just Full.ConstNull toConst Full.Null = Just Full.ConstNull
toConst (Full.Enum enum) = Just $ Full.ConstEnum enum toConst (Full.Enum enum) = Just $ Full.ConstEnum enum
toConst (Full.List values) = toConst (Full.List values) =
Just $ Full.ConstList $ catMaybes $ toConstNode <$> values Just $ Full.ConstList $ mapMaybe toConstNode values
toConst (Full.Object fields) = Just $ Full.ConstObject toConst (Full.Object fields) = Just $ Full.ConstObject
$ catMaybes $ constObjectField <$> fields $ mapMaybe constObjectField fields
constObjectField Full.ObjectField{..} constObjectField Full.ObjectField{..}
| Just constValue <- toConstNode value = | Just constValue <- toConstNode value =
Just $ Full.ObjectField name constValue location Just $ Full.ObjectField name constValue location

View File

@ -9,7 +9,7 @@
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
-- | Test helpers. -- | Test helpers.
module Test.Hspec.GraphQL module Test.Hspec.GraphQL {-# DEPRECATED "Use graphql-spice package instead" #-}
( shouldResolve ( shouldResolve
, shouldResolveTo , shouldResolveTo
) where ) where
@ -43,7 +43,7 @@ shouldResolve executor query = do
_ -> expectationFailure _ -> expectationFailure
"the query is expected to resolve to a value, but it resolved to an event stream" "the query is expected to resolve to a value, but it resolved to an event stream"
#else #else
module Test.Hspec.GraphQL module Test.Hspec.GraphQL {-# DEPRECATED "Use graphql-spice package instead" #-}
( (
) where ) where
#endif #endif

View File

@ -0,0 +1,99 @@
{-# LANGUAGE OverloadedStrings #-}
module Language.GraphQL.AST.Arbitrary where
import qualified Language.GraphQL.AST.Document as Doc
import Test.QuickCheck.Arbitrary (Arbitrary (arbitrary))
import Test.QuickCheck (oneof, elements, listOf, resize, NonEmptyList (..))
import Test.QuickCheck.Gen (Gen (..))
import Data.Text (Text, pack)
newtype AnyPrintableChar = AnyPrintableChar { getAnyPrintableChar :: Char } deriving (Eq, Show)
alpha :: String
alpha = ['a'..'z'] <> ['A'..'Z']
num :: String
num = ['0'..'9']
instance Arbitrary AnyPrintableChar where
arbitrary = AnyPrintableChar <$> elements chars
where
chars = alpha <> num <> ['_']
newtype AnyPrintableText = AnyPrintableText { getAnyPrintableText :: Text } deriving (Eq, Show)
instance Arbitrary AnyPrintableText where
arbitrary = do
nonEmptyStr <- getNonEmpty <$> (arbitrary :: Gen (NonEmptyList AnyPrintableChar))
pure $ AnyPrintableText (pack $ map getAnyPrintableChar nonEmptyStr)
-- https://spec.graphql.org/June2018/#Name
newtype AnyName = AnyName { getAnyName :: Text } deriving (Eq, Show)
instance Arbitrary AnyName where
arbitrary = do
firstChar <- elements $ alpha <> ['_']
rest <- (arbitrary :: Gen [AnyPrintableChar])
pure $ AnyName (pack $ firstChar : map getAnyPrintableChar rest)
newtype AnyLocation = AnyLocation { getAnyLocation :: Doc.Location } deriving (Eq, Show)
instance Arbitrary AnyLocation where
arbitrary = AnyLocation <$> (Doc.Location <$> arbitrary <*> arbitrary)
newtype AnyNode a = AnyNode { getAnyNode :: Doc.Node a } deriving (Eq, Show)
instance Arbitrary a => Arbitrary (AnyNode a) where
arbitrary = do
(AnyLocation location') <- arbitrary
node' <- flip Doc.Node location' <$> arbitrary
pure $ AnyNode node'
newtype AnyObjectField a = AnyObjectField { getAnyObjectField :: Doc.ObjectField a } deriving (Eq, Show)
instance Arbitrary a => Arbitrary (AnyObjectField a) where
arbitrary = do
name' <- getAnyName <$> arbitrary
value' <- getAnyNode <$> arbitrary
location' <- getAnyLocation <$> arbitrary
pure $ AnyObjectField $ Doc.ObjectField name' value' location'
newtype AnyValue = AnyValue { getAnyValue :: Doc.Value } deriving (Eq, Show)
instance Arbitrary AnyValue where
arbitrary = AnyValue <$> oneof
[ variableGen
, Doc.Int <$> arbitrary
, Doc.Float <$> arbitrary
, Doc.String <$> (getAnyPrintableText <$> arbitrary)
, Doc.Boolean <$> arbitrary
, MkGen $ \_ _ -> Doc.Null
, Doc.Enum <$> (getAnyName <$> arbitrary)
, Doc.List <$> listGen
, Doc.Object <$> objectGen
]
where
variableGen :: Gen Doc.Value
variableGen = Doc.Variable <$> (getAnyName <$> arbitrary)
listGen :: Gen [Doc.Node Doc.Value]
listGen = (resize 5 . listOf) nodeGen
nodeGen = do
node' <- getAnyNode <$> (arbitrary :: Gen (AnyNode AnyValue))
pure (getAnyValue <$> node')
objectGen :: Gen [Doc.ObjectField Doc.Value]
objectGen = resize 1 $ do
list <- getNonEmpty <$> (arbitrary :: Gen (NonEmptyList (AnyObjectField AnyValue)))
pure $ map (fmap getAnyValue . getAnyObjectField) list
newtype AnyArgument a = AnyArgument { getAnyArgument :: Doc.Argument } deriving (Eq, Show)
instance Arbitrary a => Arbitrary (AnyArgument a) where
arbitrary = do
name' <- getAnyName <$> arbitrary
(AnyValue value') <- arbitrary
(AnyLocation location') <- arbitrary
pure $ AnyArgument $ Doc.Argument name' (Doc.Node value' location') location'
printArgument :: AnyArgument AnyValue -> Text
printArgument (AnyArgument (Doc.Argument name' (Doc.Node value' _) _)) = name' <> ": " <> (pack . show) value'

View File

@ -173,3 +173,8 @@ spec = do
|] '\n' |] '\n'
actual = definition pretty operation actual = definition pretty operation
in actual `shouldBe` expected in actual `shouldBe` expected
describe "operationType" $
it "produces lowercase mutation operation type" $
let actual = operationType pretty Full.Mutation
in actual `shouldBe` "mutation"

View File

@ -5,19 +5,24 @@ module Language.GraphQL.AST.ParserSpec
) where ) where
import Data.List.NonEmpty (NonEmpty(..)) import Data.List.NonEmpty (NonEmpty(..))
import Data.Text (Text)
import qualified Data.Text as Text
import Language.GraphQL.AST.Document import Language.GraphQL.AST.Document
import qualified Language.GraphQL.AST.DirectiveLocation as DirLoc import qualified Language.GraphQL.AST.DirectiveLocation as DirLoc
import Language.GraphQL.AST.Parser import Language.GraphQL.AST.Parser
import Language.GraphQL.TH import Language.GraphQL.TH
import Test.Hspec (Spec, describe, it) import Test.Hspec (Spec, describe, it, context)
import Test.Hspec.Megaparsec (shouldParse, shouldFailOn, shouldSucceedOn) import Test.Hspec.Megaparsec (shouldParse, shouldFailOn, shouldSucceedOn)
import Text.Megaparsec (parse) import Text.Megaparsec (parse)
import Test.QuickCheck (property, NonEmptyList (..), mapSize)
import Language.GraphQL.AST.Arbitrary
spec :: Spec spec :: Spec
spec = describe "Parser" $ do spec = describe "Parser" $ do
it "accepts BOM header" $ it "accepts BOM header" $
parse document "" `shouldSucceedOn` "\xfeff{foo}" parse document "" `shouldSucceedOn` "\xfeff{foo}"
context "Arguments" $ do
it "accepts block strings as argument" $ it "accepts block strings as argument" $
parse document "" `shouldSucceedOn` [gql|{ parse document "" `shouldSucceedOn` [gql|{
hello(text: """Argument""") hello(text: """Argument""")
@ -28,6 +33,26 @@ spec = describe "Parser" $ do
hello(text: "Argument") hello(text: "Argument")
}|] }|]
it "accepts int as argument1" $
parse document "" `shouldSucceedOn` [gql|{
user(id: 4)
}|]
it "accepts boolean as argument" $
parse document "" `shouldSucceedOn` [gql|{
hello(flag: true) { field1 }
}|]
it "accepts float as argument" $
parse document "" `shouldSucceedOn` [gql|{
body(height: 172.5) { height }
}|]
it "accepts empty list as argument" $
parse document "" `shouldSucceedOn` [gql|{
query(list: []) { field1 }
}|]
it "accepts two required arguments" $ it "accepts two required arguments" $
parse document "" `shouldSucceedOn` [gql| parse document "" `shouldSucceedOn` [gql|
mutation auth($username: String!, $password: String!){ mutation auth($username: String!, $password: String!){
@ -46,6 +71,13 @@ spec = describe "Parser" $ do
test(username: """username""", password: """password""") test(username: """username""", password: """password""")
}|] }|]
it "accepts any arguments" $ mapSize (const 10) $ property $ \xs ->
let
query' :: Text
arguments = map printArgument $ getNonEmpty (xs :: NonEmptyList (AnyArgument AnyValue))
query' = "query(" <> Text.intercalate ", " arguments <> ")" in
parse document "" `shouldSucceedOn` ("{ " <> query' <> " }")
it "parses minimal schema definition" $ it "parses minimal schema definition" $
parse document "" `shouldSucceedOn` [gql|schema { query: Query }|] parse document "" `shouldSucceedOn` [gql|schema { query: Query }|]
@ -95,16 +127,6 @@ spec = describe "Parser" $ do
} }
|] |]
it "parses minimal enum type definition" $
parse document "" `shouldSucceedOn` [gql|
enum Direction {
NORTH
EAST
SOUTH
WEST
}
|]
it "parses minimal input object type definition" $ it "parses minimal input object type definition" $
parse document "" `shouldSucceedOn` [gql| parse document "" `shouldSucceedOn` [gql|
input Point2D { input Point2D {
@ -202,6 +224,13 @@ spec = describe "Parser" $ do
} }
|] |]
it "rejects empty selection set" $
parse document "" `shouldFailOn` [gql|
query {
innerField {}
}
|]
it "parses documents beginning with a comment" $ it "parses documents beginning with a comment" $
parse document "" `shouldSucceedOn` [gql| parse document "" `shouldSucceedOn` [gql|
""" """

View File

@ -2,13 +2,18 @@
v. 2.0. If a copy of the MPL was not distributed with this file, You can 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/. -} obtain one at https://mozilla.org/MPL/2.0/. -}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE QuasiQuotes #-}
module Language.GraphQL.ExecuteSpec module Language.GraphQL.ExecuteSpec
( spec ( spec
) where ) where
import Control.Exception (Exception(..), SomeException) import Control.Exception (Exception(..))
import Control.Monad.Catch (throwM) import Control.Monad.Catch (throwM)
import Data.Conduit import Data.Conduit
import Data.HashMap.Strict (HashMap) import Data.HashMap.Strict (HashMap)
@ -20,12 +25,23 @@ import Language.GraphQL.Error
import Language.GraphQL.Execute (execute) import Language.GraphQL.Execute (execute)
import Language.GraphQL.TH import Language.GraphQL.TH
import qualified Language.GraphQL.Type.Schema as Schema import qualified Language.GraphQL.Type.Schema as Schema
import qualified Language.GraphQL.Type as Type
import Language.GraphQL.Type import Language.GraphQL.Type
import qualified Language.GraphQL.Type.In as In import qualified Language.GraphQL.Type.In as In
import qualified Language.GraphQL.Type.Out as Out import qualified Language.GraphQL.Type.Out as Out
import Prelude hiding (id) import Prelude hiding (id)
import Test.Hspec (Spec, context, describe, it, shouldBe) import Test.Hspec (Spec, context, describe, it, shouldBe)
import Text.Megaparsec (parse) import Text.Megaparsec (parse, errorBundlePretty)
import Schemas.HeroSchema (heroSchema)
import Data.Maybe (fromJust)
import qualified Data.Sequence as Seq
import Data.Text (Text)
import qualified Data.Text as Text
import Test.Hspec.Expectations
( Expectation
, expectationFailure
)
import Data.Either (fromRight)
data PhilosopherException = PhilosopherException data PhilosopherException = PhilosopherException
deriving Show deriving Show
@ -36,7 +52,7 @@ instance Exception PhilosopherException where
ResolverException resolverException <- fromException e ResolverException resolverException <- fromException e
cast resolverException cast resolverException
philosopherSchema :: Schema (Either SomeException) philosopherSchema :: Schema IO
philosopherSchema = philosopherSchema =
schemaWithTypes Nothing queryType Nothing subscriptionRoot extraTypes mempty schemaWithTypes Nothing queryType Nothing subscriptionRoot extraTypes mempty
where where
@ -46,7 +62,7 @@ philosopherSchema =
, Schema.ObjectType bookCollectionType , Schema.ObjectType bookCollectionType
] ]
queryType :: Out.ObjectType (Either SomeException) queryType :: Out.ObjectType IO
queryType = Out.ObjectType "Query" Nothing [] queryType = Out.ObjectType "Query" Nothing []
$ HashMap.fromList $ HashMap.fromList
[ ("philosopher", ValueResolver philosopherField philosopherResolver) [ ("philosopher", ValueResolver philosopherField philosopherResolver)
@ -62,14 +78,14 @@ queryType = Out.ObjectType "Query" Nothing []
genresField = genresField =
let fieldType = Out.ListType $ Out.NonNullScalarType string let fieldType = Out.ListType $ Out.NonNullScalarType string
in Out.Field Nothing fieldType HashMap.empty in Out.Field Nothing fieldType HashMap.empty
genresResolver :: Resolve (Either SomeException) genresResolver :: Resolve IO
genresResolver = throwM PhilosopherException genresResolver = throwM PhilosopherException
countField = countField =
let fieldType = Out.NonNullScalarType int let fieldType = Out.NonNullScalarType int
in Out.Field Nothing fieldType HashMap.empty in Out.Field Nothing fieldType HashMap.empty
countResolver = pure "" countResolver = pure ""
musicType :: Out.ObjectType (Either SomeException) musicType :: Out.ObjectType IO
musicType = Out.ObjectType "Music" Nothing [] musicType = Out.ObjectType "Music" Nothing []
$ HashMap.fromList resolvers $ HashMap.fromList resolvers
where where
@ -79,7 +95,7 @@ musicType = Out.ObjectType "Music" Nothing []
instrumentResolver = pure $ String "piano" instrumentResolver = pure $ String "piano"
instrumentField = Out.Field Nothing (Out.NonNullScalarType string) HashMap.empty instrumentField = Out.Field Nothing (Out.NonNullScalarType string) HashMap.empty
poetryType :: Out.ObjectType (Either SomeException) poetryType :: Out.ObjectType IO
poetryType = Out.ObjectType "Poetry" Nothing [] poetryType = Out.ObjectType "Poetry" Nothing []
$ HashMap.fromList resolvers $ HashMap.fromList resolvers
where where
@ -89,10 +105,10 @@ poetryType = Out.ObjectType "Poetry" Nothing []
genreResolver = pure $ String "Futurism" genreResolver = pure $ String "Futurism"
genreField = Out.Field Nothing (Out.NonNullScalarType string) HashMap.empty genreField = Out.Field Nothing (Out.NonNullScalarType string) HashMap.empty
interestType :: Out.UnionType (Either SomeException) interestType :: Out.UnionType IO
interestType = Out.UnionType "Interest" Nothing [musicType, poetryType] interestType = Out.UnionType "Interest" Nothing [musicType, poetryType]
philosopherType :: Out.ObjectType (Either SomeException) philosopherType :: Out.ObjectType IO
philosopherType = Out.ObjectType "Philosopher" Nothing [] philosopherType = Out.ObjectType "Philosopher" Nothing []
$ HashMap.fromList resolvers $ HashMap.fromList resolvers
where where
@ -133,14 +149,14 @@ philosopherType = Out.ObjectType "Philosopher" Nothing []
= Out.Field Nothing (Out.NonNullScalarType string) HashMap.empty = Out.Field Nothing (Out.NonNullScalarType string) HashMap.empty
firstLanguageResolver = pure Null firstLanguageResolver = pure Null
workType :: Out.InterfaceType (Either SomeException) workType :: Out.InterfaceType IO
workType = Out.InterfaceType "Work" Nothing [] workType = Out.InterfaceType "Work" Nothing []
$ HashMap.fromList fields $ HashMap.fromList fields
where where
fields = [("title", titleField)] fields = [("title", titleField)]
titleField = Out.Field Nothing (Out.NonNullScalarType string) HashMap.empty titleField = Out.Field Nothing (Out.NonNullScalarType string) HashMap.empty
bookType :: Out.ObjectType (Either SomeException) bookType :: Out.ObjectType IO
bookType = Out.ObjectType "Book" Nothing [workType] bookType = Out.ObjectType "Book" Nothing [workType]
$ HashMap.fromList resolvers $ HashMap.fromList resolvers
where where
@ -150,7 +166,7 @@ bookType = Out.ObjectType "Book" Nothing [workType]
titleField = Out.Field Nothing (Out.NonNullScalarType string) HashMap.empty titleField = Out.Field Nothing (Out.NonNullScalarType string) HashMap.empty
titleResolver = pure "Also sprach Zarathustra: Ein Buch für Alle und Keinen" titleResolver = pure "Also sprach Zarathustra: Ein Buch für Alle und Keinen"
bookCollectionType :: Out.ObjectType (Either SomeException) bookCollectionType :: Out.ObjectType IO
bookCollectionType = Out.ObjectType "Book" Nothing [workType] bookCollectionType = Out.ObjectType "Book" Nothing [workType]
$ HashMap.fromList resolvers $ HashMap.fromList resolvers
where where
@ -160,7 +176,7 @@ bookCollectionType = Out.ObjectType "Book" Nothing [workType]
titleField = Out.Field Nothing (Out.NonNullScalarType string) HashMap.empty titleField = Out.Field Nothing (Out.NonNullScalarType string) HashMap.empty
titleResolver = pure "The Three Critiques" titleResolver = pure "The Three Critiques"
subscriptionType :: Out.ObjectType (Either SomeException) subscriptionType :: Out.ObjectType IO
subscriptionType = Out.ObjectType "Subscription" Nothing [] subscriptionType = Out.ObjectType "Subscription" Nothing []
$ HashMap.singleton "newQuote" $ HashMap.singleton "newQuote"
$ EventStreamResolver quoteField (pure $ Object mempty) $ EventStreamResolver quoteField (pure $ Object mempty)
@ -169,7 +185,7 @@ subscriptionType = Out.ObjectType "Subscription" Nothing []
quoteField = quoteField =
Out.Field Nothing (Out.NonNullObjectType quoteType) HashMap.empty Out.Field Nothing (Out.NonNullObjectType quoteType) HashMap.empty
quoteType :: Out.ObjectType (Either SomeException) quoteType :: Out.ObjectType IO
quoteType = Out.ObjectType "Quote" Nothing [] quoteType = Out.ObjectType "Quote" Nothing []
$ HashMap.singleton "quote" $ HashMap.singleton "quote"
$ ValueResolver quoteField $ ValueResolver quoteField
@ -178,7 +194,7 @@ quoteType = Out.ObjectType "Quote" Nothing []
quoteField = quoteField =
Out.Field Nothing (Out.NonNullScalarType string) HashMap.empty Out.Field Nothing (Out.NonNullScalarType string) HashMap.empty
schoolType :: EnumType schoolType :: Type.EnumType
schoolType = EnumType "School" Nothing $ HashMap.fromList schoolType = EnumType "School" Nothing $ HashMap.fromList
[ ("NOMINALISM", EnumValue Nothing) [ ("NOMINALISM", EnumValue Nothing)
, ("REALISM", EnumValue Nothing) , ("REALISM", EnumValue Nothing)
@ -186,12 +202,48 @@ schoolType = EnumType "School" Nothing $ HashMap.fromList
] ]
type EitherStreamOrValue = Either type EitherStreamOrValue = Either
(ResponseEventStream (Either SomeException) Value) (ResponseEventStream IO Type.Value)
(Response Value) (Response Type.Value)
execute' :: Document -> Either SomeException EitherStreamOrValue -- Asserts that a query resolves to a value.
execute' = shouldResolveTo :: Text.Text -> Response Type.Value -> Expectation
execute philosopherSchema Nothing (mempty :: HashMap Name Value) shouldResolveTo querySource expected =
case parse document "" querySource of
(Right parsedDocument) ->
execute philosopherSchema Nothing (mempty :: HashMap Name Type.Value) parsedDocument >>= go
(Left errorBundle) -> expectationFailure $ errorBundlePretty errorBundle
where
go = \case
Right result -> shouldBe result expected
Left _ -> expectationFailure
"the query is expected to resolve to a value, but it resolved to an event stream"
-- Asserts that the executor produces an error that starts with a string.
shouldContainError :: Either (ResponseEventStream IO Type.Value) (Response Type.Value)
-> Text
-> Expectation
shouldContainError streamOrValue expected =
case streamOrValue of
Right response -> respond response
Left _ -> expectationFailure
"the query is expected to resolve to a value, but it resolved to an event stream"
where
startsWith :: Text.Text -> Text.Text -> Bool
startsWith xs ys = Text.take (Text.length ys) xs == ys
respond :: Response Type.Value -> Expectation
respond Response{ errors }
| any ((`startsWith` expected) . message) errors = pure ()
| otherwise = expectationFailure
"the query is expected to execute with errors, but the response doesn't contain any errors"
parseAndExecute :: Schema IO
-> Maybe Text
-> HashMap Name Type.Value
-> Text
-> IO (Either (ResponseEventStream IO Type.Value) (Response Type.Value))
parseAndExecute schema' operation variables
= either (pure . parseError) (execute schema' operation variables)
. parse document ""
spec :: Spec spec :: Spec
spec = spec =
@ -207,9 +259,7 @@ spec =
} }
|] |]
expected = Response (Object mempty) mempty expected = Response (Object mempty) mempty
Right (Right actual) = either (pure . parseError) execute' in sourceQuery `shouldResolveTo` expected
$ parse document "" sourceQuery
in actual `shouldBe` expected
context "Query" $ do context "Query" $ do
it "skips unknown fields" $ it "skips unknown fields" $
@ -219,9 +269,8 @@ spec =
$ HashMap.singleton "firstName" $ HashMap.singleton "firstName"
$ String "Friedrich" $ String "Friedrich"
expected = Response data'' mempty expected = Response data'' mempty
Right (Right actual) = either (pure . parseError) execute' sourceQuery = "{ philosopher { firstName surname } }"
$ parse document "" "{ philosopher { firstName surname } }" in sourceQuery `shouldResolveTo` expected
in actual `shouldBe` expected
it "merges selections" $ it "merges selections" $
let data'' = Object let data'' = Object
$ HashMap.singleton "philosopher" $ HashMap.singleton "philosopher"
@ -231,9 +280,8 @@ spec =
, ("lastName", String "Nietzsche") , ("lastName", String "Nietzsche")
] ]
expected = Response data'' mempty expected = Response data'' mempty
Right (Right actual) = either (pure . parseError) execute' sourceQuery = "{ philosopher { firstName } philosopher { lastName } }"
$ parse document "" "{ philosopher { firstName } philosopher { lastName } }" in sourceQuery `shouldResolveTo` expected
in actual `shouldBe` expected
it "errors on invalid output enum values" $ it "errors on invalid output enum values" $
let data'' = Object $ HashMap.singleton "philosopher" Null let data'' = Object $ HashMap.singleton "philosopher" Null
@ -244,9 +292,8 @@ spec =
, path = [Segment "philosopher", Segment "school"] , path = [Segment "philosopher", Segment "school"]
} }
expected = Response data'' executionErrors expected = Response data'' executionErrors
Right (Right actual) = either (pure . parseError) execute' sourceQuery = "{ philosopher { school } }"
$ parse document "" "{ philosopher { school } }" in sourceQuery `shouldResolveTo` expected
in actual `shouldBe` expected
it "gives location information for non-null unions" $ it "gives location information for non-null unions" $
let data'' = Object $ HashMap.singleton "philosopher" Null let data'' = Object $ HashMap.singleton "philosopher" Null
@ -257,9 +304,8 @@ spec =
, path = [Segment "philosopher", Segment "interest"] , path = [Segment "philosopher", Segment "interest"]
} }
expected = Response data'' executionErrors expected = Response data'' executionErrors
Right (Right actual) = either (pure . parseError) execute' sourceQuery = "{ philosopher { interest } }"
$ parse document "" "{ philosopher { interest } }" in sourceQuery `shouldResolveTo` expected
in actual `shouldBe` expected
it "gives location information for invalid interfaces" $ it "gives location information for invalid interfaces" $
let data'' = Object $ HashMap.singleton "philosopher" Null let data'' = Object $ HashMap.singleton "philosopher" Null
@ -271,9 +317,8 @@ spec =
, path = [Segment "philosopher", Segment "majorWork"] , path = [Segment "philosopher", Segment "majorWork"]
} }
expected = Response data'' executionErrors expected = Response data'' executionErrors
Right (Right actual) = either (pure . parseError) execute' sourceQuery = "{ philosopher { majorWork { title } } }"
$ parse document "" "{ philosopher { majorWork { title } } }" in sourceQuery `shouldResolveTo` expected
in actual `shouldBe` expected
it "gives location information for invalid scalar arguments" $ it "gives location information for invalid scalar arguments" $
let data'' = Object $ HashMap.singleton "philosopher" Null let data'' = Object $ HashMap.singleton "philosopher" Null
@ -284,9 +329,8 @@ spec =
, path = [Segment "philosopher"] , path = [Segment "philosopher"]
} }
expected = Response data'' executionErrors expected = Response data'' executionErrors
Right (Right actual) = either (pure . parseError) execute' sourceQuery = "{ philosopher(id: true) { lastName } }"
$ parse document "" "{ philosopher(id: true) { lastName } }" in sourceQuery `shouldResolveTo` expected
in actual `shouldBe` expected
it "gives location information for failed result coercion" $ it "gives location information for failed result coercion" $
let data'' = Object $ HashMap.singleton "philosopher" Null let data'' = Object $ HashMap.singleton "philosopher" Null
@ -296,9 +340,8 @@ spec =
, path = [Segment "philosopher", Segment "century"] , path = [Segment "philosopher", Segment "century"]
} }
expected = Response data'' executionErrors expected = Response data'' executionErrors
Right (Right actual) = either (pure . parseError) execute' sourceQuery = "{ philosopher(id: \"1\") { century } }"
$ parse document "" "{ philosopher(id: \"1\") { century } }" in sourceQuery `shouldResolveTo` expected
in actual `shouldBe` expected
it "gives location information for failed result coercion" $ it "gives location information for failed result coercion" $
let data'' = Object $ HashMap.singleton "genres" Null let data'' = Object $ HashMap.singleton "genres" Null
@ -308,9 +351,8 @@ spec =
, path = [Segment "genres"] , path = [Segment "genres"]
} }
expected = Response data'' executionErrors expected = Response data'' executionErrors
Right (Right actual) = either (pure . parseError) execute' sourceQuery = "{ genres }"
$ parse document "" "{ genres }" in sourceQuery `shouldResolveTo` expected
in actual `shouldBe` expected
it "sets data to null if a root field isn't nullable" $ it "sets data to null if a root field isn't nullable" $
let executionErrors = pure $ Error let executionErrors = pure $ Error
@ -319,9 +361,8 @@ spec =
, path = [Segment "count"] , path = [Segment "count"]
} }
expected = Response Null executionErrors expected = Response Null executionErrors
Right (Right actual) = either (pure . parseError) execute' sourceQuery = "{ count }"
$ parse document "" "{ count }" in sourceQuery `shouldResolveTo` expected
in actual `shouldBe` expected
it "detects nullability errors" $ it "detects nullability errors" $
let data'' = Object $ HashMap.singleton "philosopher" Null let data'' = Object $ HashMap.singleton "philosopher" Null
@ -331,19 +372,69 @@ spec =
, path = [Segment "philosopher", Segment "firstLanguage"] , path = [Segment "philosopher", Segment "firstLanguage"]
} }
expected = Response data'' executionErrors expected = Response data'' executionErrors
Right (Right actual) = either (pure . parseError) execute' sourceQuery = "{ philosopher(id: \"1\") { firstLanguage } }"
$ parse document "" "{ philosopher(id: \"1\") { firstLanguage } }" in sourceQuery `shouldResolveTo` expected
in actual `shouldBe` expected
context "queryError" $ do
let namedQuery name = "query " <> name <> " { philosopher(id: \"1\") { interest } }"
twoQueries = namedQuery "A" <> " " <> namedQuery "B"
it "throws operation name is required error" $ do
let expectedErrorMessage = "Operation name is required"
actual <- parseAndExecute philosopherSchema Nothing mempty twoQueries
actual `shouldContainError` expectedErrorMessage
it "throws operation not found error" $ do
let expectedErrorMessage = "Operation \"C\" is not found"
actual <- parseAndExecute philosopherSchema (Just "C") mempty twoQueries
actual `shouldContainError` expectedErrorMessage
it "throws variable coercion error" $ do
let data'' = Null
executionErrors = pure $ Error
{ message = "Failed to coerce the variable $id: String."
, locations =[Location 1 7]
, path = []
}
expected = Response data'' executionErrors
executeWithVars = execute philosopherSchema Nothing (HashMap.singleton "id" (Type.Int 1))
Right actual <- either (pure . parseError) executeWithVars
$ parse document "" "query($id: String) { philosopher(id: \"1\") { firstLanguage } }"
actual `shouldBe` expected
it "throws variable unkown input type error" $
let data'' = Null
executionErrors = pure $ Error
{ message = "Variable $id has unknown type Cat."
, locations =[Location 1 7]
, path = []
}
expected = Response data'' executionErrors
sourceQuery = "query($id: Cat) { philosopher(id: \"1\") { firstLanguage } }"
in sourceQuery `shouldResolveTo` expected
context "Error path" $ do
let executeHero :: Document -> IO EitherStreamOrValue
executeHero = execute heroSchema Nothing (HashMap.empty :: HashMap Name Type.Value)
it "at the beggining of the list" $ do
Right actual <- either (pure . parseError) executeHero
$ parse document "" "{ hero(id: \"1\") { friends { name } } }"
let Response _ errors' = actual
Error _ _ path' = fromJust $ Seq.lookup 0 errors'
expected = [Segment "hero", Segment "friends", Index 0, Segment "name"]
in path' `shouldBe` expected
context "Subscription" $ context "Subscription" $
it "subscribes" $ it "subscribes" $ do
let data'' = Object let data'' = Object
$ HashMap.singleton "newQuote" $ HashMap.singleton "newQuote"
$ Object $ Object
$ HashMap.singleton "quote" $ HashMap.singleton "quote"
$ String "Naturam expelles furca, tamen usque recurret." $ String "Naturam expelles furca, tamen usque recurret."
expected = Response data'' mempty expected = Response data'' mempty
Right (Left stream) = either (pure . parseError) execute' Left stream <- execute philosopherSchema Nothing (mempty :: HashMap Name Type.Value)
$ fromRight (error "Parse error")
$ parse document "" "subscription { newQuote { quote } }" $ parse document "" "subscription { newQuote { quote } }"
Right (Just actual) = runConduit $ stream .| await Just actual <- runConduit $ stream .| await
in actual `shouldBe` expected actual `shouldBe` expected

View File

@ -0,0 +1,70 @@
{- 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 Schemas.HeroSchema (heroSchema) where
import Control.Exception (Exception(..))
import Control.Monad.Catch (throwM)
import Language.GraphQL.Error (ResolverException (..))
import qualified Language.GraphQL.Type.In as In
import qualified Language.GraphQL.Type as Type
import Language.GraphQL.Type.Schema (schemaWithTypes)
import qualified Data.HashMap.Strict as HashMap
import Data.Typeable (cast)
import qualified Language.GraphQL.Type.Out as Out
data HeroException = HeroException
deriving Show
instance Exception HeroException where
toException = toException. ResolverException
fromException e = do
ResolverException resolverException <- fromException e
cast resolverException
heroSchema :: Type.Schema IO
heroSchema =
schemaWithTypes Nothing queryType Nothing Nothing [] mempty
type ObjectType = Out.ObjectType IO
queryType :: ObjectType
queryType = Out.ObjectType "Query" Nothing []
$ HashMap.fromList
[ ("hero", Out.ValueResolver heroField heroResolver)
]
where
heroField = Out.Field Nothing (Out.NamedObjectType heroType)
$ HashMap.singleton "id"
$ In.Argument Nothing (In.NamedScalarType Type.id) Nothing
heroResolver = pure $ Type.Object mempty
stringField :: Out.Field IO
stringField = Out.Field Nothing (Out.NonNullScalarType Type.string) HashMap.empty
heroType :: ObjectType
heroType = Out.ObjectType "Hero" Nothing [] $ HashMap.fromList resolvers
where
resolvers =
[ ("id", Out.ValueResolver stringField (pure $ Type.String "4111"))
, ("name", Out.ValueResolver stringField (pure $ Type.String "R2D2"))
, ("friends", Out.ValueResolver friendsField (pure $ Type.List [luke]))
]
friendsField = Out.Field Nothing (Out.ListType $ Out.NonNullObjectType lukeType) HashMap.empty
-- This list values are ignored because of current realisation (types and resolvers are the same entity)
-- The values from lukeType will be used
luke = Type.Object $ HashMap.fromList
[ ("id", "dfdfdf")
, ("name", "dfdfdff")
]
lukeType :: ObjectType
lukeType = Out.ObjectType "Luke" Nothing [] $ HashMap.fromList resolvers
where
resolvers =
[ ("id", Out.ValueResolver stringField (pure $ Type.String "1000"))
, ("name", Out.ValueResolver stringField (throwM HeroException))
]