aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md12
-rw-r--r--graphql.cabal13
-rw-r--r--src/Language/GraphQL.hs45
-rw-r--r--src/Language/GraphQL/AST/Document.hs8
-rw-r--r--src/Language/GraphQL/AST/Parser.hs4
-rw-r--r--src/Language/GraphQL/Execute.hs51
-rw-r--r--tests/Language/GraphQL/AST/Arbitrary.hs99
-rw-r--r--tests/Language/GraphQL/AST/ParserSpec.hs105
-rw-r--r--tests/Language/GraphQL/ExecuteSpec.hs76
-rw-r--r--tests/Schemas/HeroSchema.hs70
10 files changed, 411 insertions, 72 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index bfd8aa2..69663e9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,17 @@ The format is based on
and this project adheres to
[Haskell Package Versioning Policy](https://pvp.haskell.org/).
+## [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
### Added
- `Serialize` instance for `Type.Definition.Value`.
@@ -466,6 +477,7 @@ and this project adheres to
### Added
- Data types for the GraphQL language.
+[1.0.3.0]: https://www.caraus.tech/projects/pub-graphql/repository/23/diff?rev=1.0.3.0&rev_to=v1.0.2.0
[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.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
diff --git a/graphql.cabal b/graphql.cabal
index daf020f..c50029e 100644
--- a/graphql.cabal
+++ b/graphql.cabal
@@ -1,7 +1,7 @@
cabal-version: 2.4
name: graphql
-version: 1.0.2.0
+version: 1.0.3.0
synopsis: Haskell GraphQL implementation
description: Haskell <https://spec.graphql.org/June2018/ GraphQL> implementation.
category: Language
@@ -22,8 +22,7 @@ extra-source-files:
README.md
tested-with:
GHC == 8.10.7,
- GHC == 9.0.1,
- GHC == 9.2.1
+ GHC == 9.2.2
source-repository head
type: git
@@ -72,7 +71,7 @@ library
megaparsec >= 9.0 && < 10,
parser-combinators >= 1.3 && < 2,
template-haskell >= 2.16 && < 3,
- text ^>= 1.2.4,
+ text >= 1.2 && < 3,
transformers ^>= 0.5.6,
unordered-containers ^>= 0.2.14,
vector ^>= 0.12.3
@@ -93,12 +92,14 @@ test-suite graphql-test
Language.GraphQL.AST.EncoderSpec
Language.GraphQL.AST.LexerSpec
Language.GraphQL.AST.ParserSpec
+ Language.GraphQL.AST.Arbitrary
Language.GraphQL.ErrorSpec
Language.GraphQL.Execute.CoerceSpec
Language.GraphQL.Execute.OrderedMapSpec
Language.GraphQL.ExecuteSpec
Language.GraphQL.Type.OutSpec
Language.GraphQL.Validate.RulesSpec
+ Schemas.HeroSchema
hs-source-dirs:
tests
ghc-options: -threaded -rtsopts -with-rtsopts=-N -Wall
@@ -113,5 +114,7 @@ test-suite graphql-test
hspec-megaparsec ^>= 2.2.0,
megaparsec,
text,
- unordered-containers
+ unordered-containers,
+ containers,
+ vector
default-language: Haskell2010
diff --git a/src/Language/GraphQL.hs b/src/Language/GraphQL.hs
index 20bb123..b64cf42 100644
--- a/src/Language/GraphQL.hs
+++ b/src/Language/GraphQL.hs
@@ -1,9 +1,8 @@
{-# LANGUAGE CPP #-}
-
-#ifdef WITH_JSON
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
+#ifdef WITH_JSON
-- | This module provides the functions to parse and execute @GraphQL@ queries.
module Language.GraphQL
( graphql
@@ -79,6 +78,46 @@ graphqlSubs schema operationName variableValues document' =
#else
-- | This module provides the functions to parse and execute @GraphQL@ queries.
module Language.GraphQL
- (
+ ( graphql
) 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
diff --git a/src/Language/GraphQL/AST/Document.hs b/src/Language/GraphQL/AST/Document.hs
index a698d2e..ea640df 100644
--- a/src/Language/GraphQL/AST/Document.hs
+++ b/src/Language/GraphQL/AST/Document.hs
@@ -49,6 +49,8 @@ module Language.GraphQL.AST.Document
, Value(..)
, VariableDefinition(..)
, escape
+ , showVariableName
+ , showVariable
) where
import Data.Char (ord)
@@ -339,6 +341,12 @@ data VariableDefinition =
VariableDefinition Name Type (Maybe (Node ConstValue)) Location
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 representation.
diff --git a/src/Language/GraphQL/AST/Parser.hs b/src/Language/GraphQL/AST/Parser.hs
index 19251ab..e19823c 100644
--- a/src/Language/GraphQL/AST/Parser.hs
+++ b/src/Language/GraphQL/AST/Parser.hs
@@ -450,8 +450,8 @@ value = Full.Variable <$> variable
<|> Full.Null <$ nullValue
<|> Full.String <$> stringValue
<|> Full.Enum <$> try enumValue
- <|> Full.List <$> brackets (some $ valueNode value)
- <|> Full.Object <$> braces (some $ objectField $ valueNode value)
+ <|> Full.List <$> brackets (many $ valueNode value)
+ <|> Full.Object <$> braces (many $ objectField $ valueNode value)
<?> "Value"
constValue :: Parser Full.ConstValue
diff --git a/src/Language/GraphQL/Execute.hs b/src/Language/GraphQL/Execute.hs
index 476cc50..5ceb616 100644
--- a/src/Language/GraphQL/Execute.hs
+++ b/src/Language/GraphQL/Execute.hs
@@ -61,6 +61,7 @@ import Language.GraphQL.Error
, ResponseEventStream
)
import Prelude hiding (null)
+import Language.GraphQL.AST.Document (showVariableName)
newtype ExecutorT m a = ExecutorT
{ 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 = 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 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) =
- let queryErrorMessage = Text.concat
- [ "Operation \""
- , Text.pack operationName
- , "\" not found."
+ let queryErrorMessage = Text.unlines
+ [ Text.concat
+ [ "Operation \""
+ , Text.pack operationName
+ , "\" is not found in the named operations you've provided. "
+ ]
+ , operationNameErrorText
]
in Error{ message = queryErrorMessage, locations = [], path = [] }
queryError (CoercionError variableDefinition) =
- let Full.VariableDefinition variableName _ _ location = variableDefinition
+ let (Full.VariableDefinition _ _ _ location) = variableDefinition
queryErrorMessage = Text.concat
- [ "Failed to coerce the variable \""
- , variableName
- , "\"."
+ [ "Failed to coerce the variable "
+ , Text.pack $ Full.showVariable variableDefinition
+ , "."
]
in Error{ message = queryErrorMessage, locations = [location], path = [] }
queryError (UnknownInputType variableDefinition) =
- let Full.VariableDefinition variableName variableTypeName _ location = variableDefinition
+ let Full.VariableDefinition _ variableTypeName _ location = variableDefinition
queryErrorMessage = Text.concat
- [ "Variable \""
- , variableName
- , "\" has unknown type \""
+ [ "Variable "
+ , Text.pack $ showVariableName variableDefinition
+ , " has unknown type "
, Text.pack $ show variableTypeName
- , "\"."
+ , "."
]
in Error{ message = queryErrorMessage, locations = [location], path = [] }
@@ -375,6 +386,7 @@ executeField objectValue fields (viewResolver -> resolverPair) errorPath =
, Handler (resolverHandler fieldLocation)
]
where
+ fieldErrorPath = fieldsSegment fields : errorPath
inputCoercionHandler :: (MonadCatch m, Serialize a)
=> Full.Location
-> InputCoercionException
@@ -402,17 +414,16 @@ executeField objectValue fields (viewResolver -> resolverPair) errorPath =
then throwM e
else returnError newError
exceptionHandler errorLocation e =
- let newPath = fieldsSegment fields : errorPath
- newError = constructError e errorLocation newPath
+ let newError = constructError e errorLocation fieldErrorPath
in if Out.isNonNullType fieldType
- then throwM $ FieldException errorLocation newPath e
+ then throwM $ FieldException errorLocation fieldErrorPath e
else returnError newError
returnError newError = tell (Seq.singleton newError) >> pure null
go fieldName inputArguments = do
argumentValues <- coerceArgumentValues argumentTypes inputArguments
resolvedValue <-
resolveFieldValue resolveFunction objectValue fieldName argumentValues
- completeValue fieldType fields errorPath resolvedValue
+ completeValue fieldType fields fieldErrorPath resolvedValue
(resolverField, resolveFunction) = resolverPair
Out.Field _ fieldType argumentTypes = resolverField
@@ -445,6 +456,7 @@ resolveAbstractType abstractType values'
_ -> pure Nothing
| otherwise = pure Nothing
+-- https://spec.graphql.org/October2021/#sec-Value-Completion
completeValue :: (MonadCatch m, Serialize a)
=> Out.Type m
-> NonEmpty (Transform.Field m)
@@ -476,8 +488,7 @@ completeValue outputType@(Out.EnumBaseType enumType) _ _ (Type.Enum enum) =
$ ValueCompletionException (show outputType)
$ Type.Enum enum
completeValue (Out.ObjectBaseType objectType) fields errorPath result
- = executeSelectionSet (mergeSelectionSets fields) objectType result
- $ fieldsSegment fields : errorPath
+ = executeSelectionSet (mergeSelectionSets fields) objectType result errorPath
completeValue outputType@(Out.InterfaceBaseType interfaceType) fields errorPath result
| Type.Object objectMap <- result = do
let abstractType = Type.Internal.AbstractInterfaceType interfaceType
diff --git a/tests/Language/GraphQL/AST/Arbitrary.hs b/tests/Language/GraphQL/AST/Arbitrary.hs
new file mode 100644
index 0000000..8d0544e
--- /dev/null
+++ b/tests/Language/GraphQL/AST/Arbitrary.hs
@@ -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'
diff --git a/tests/Language/GraphQL/AST/ParserSpec.hs b/tests/Language/GraphQL/AST/ParserSpec.hs
index 702beab..13faa21 100644
--- a/tests/Language/GraphQL/AST/ParserSpec.hs
+++ b/tests/Language/GraphQL/AST/ParserSpec.hs
@@ -5,46 +5,78 @@ module Language.GraphQL.AST.ParserSpec
) where
import Data.List.NonEmpty (NonEmpty(..))
+import Data.Text (Text)
+import qualified Data.Text as Text
import Language.GraphQL.AST.Document
import qualified Language.GraphQL.AST.DirectiveLocation as DirLoc
import Language.GraphQL.AST.Parser
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 Text.Megaparsec (parse)
+import Test.QuickCheck (property, NonEmptyList (..), mapSize)
+import Language.GraphQL.AST.Arbitrary
spec :: Spec
spec = describe "Parser" $ do
it "accepts BOM header" $
parse document "" `shouldSucceedOn` "\xfeff{foo}"
- it "accepts block strings as argument" $
- parse document "" `shouldSucceedOn` [gql|{
- hello(text: """Argument""")
- }|]
-
- it "accepts strings as argument" $
- parse document "" `shouldSucceedOn` [gql|{
- hello(text: "Argument")
- }|]
-
- it "accepts two required arguments" $
- parse document "" `shouldSucceedOn` [gql|
- mutation auth($username: String!, $password: String!){
- test
- }|]
-
- it "accepts two string arguments" $
- parse document "" `shouldSucceedOn` [gql|
- mutation auth{
- test(username: "username", password: "password")
- }|]
-
- it "accepts two block string arguments" $
- parse document "" `shouldSucceedOn` [gql|
- mutation auth{
- test(username: """username""", password: """password""")
- }|]
+ context "Arguments" $ do
+ it "accepts block strings as argument" $
+ parse document "" `shouldSucceedOn` [gql|{
+ hello(text: """Argument""")
+ }|]
+
+ it "accepts strings as argument" $
+ parse document "" `shouldSucceedOn` [gql|{
+ 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" $
+ parse document "" `shouldSucceedOn` [gql|
+ mutation auth($username: String!, $password: String!){
+ test
+ }|]
+
+ it "accepts two string arguments" $
+ parse document "" `shouldSucceedOn` [gql|
+ mutation auth{
+ test(username: "username", password: "password")
+ }|]
+
+ it "accepts two block string arguments" $
+ parse document "" `shouldSucceedOn` [gql|
+ mutation auth{
+ 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" $
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" $
parse document "" `shouldSucceedOn` [gql|
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" $
parse document "" `shouldSucceedOn` [gql|
"""
diff --git a/tests/Language/GraphQL/ExecuteSpec.hs b/tests/Language/GraphQL/ExecuteSpec.hs
index 5eafb2e..73d62b4 100644
--- a/tests/Language/GraphQL/ExecuteSpec.hs
+++ b/tests/Language/GraphQL/ExecuteSpec.hs
@@ -4,6 +4,7 @@
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
+
module Language.GraphQL.ExecuteSpec
( spec
) where
@@ -20,12 +21,17 @@ import Language.GraphQL.Error
import Language.GraphQL.Execute (execute)
import Language.GraphQL.TH
import qualified Language.GraphQL.Type.Schema as Schema
+import qualified Language.GraphQL.Type as Type
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 Schemas.HeroSchema (heroSchema)
+import Data.Maybe (fromJust)
+import qualified Data.Sequence as Seq
+import qualified Data.Text as Text
data PhilosopherException = PhilosopherException
deriving Show
@@ -178,7 +184,7 @@ quoteType = Out.ObjectType "Quote" Nothing []
quoteField =
Out.Field Nothing (Out.NonNullScalarType string) HashMap.empty
-schoolType :: EnumType
+schoolType :: Type.EnumType
schoolType = EnumType "School" Nothing $ HashMap.fromList
[ ("NOMINALISM", EnumValue Nothing)
, ("REALISM", EnumValue Nothing)
@@ -186,12 +192,12 @@ schoolType = EnumType "School" Nothing $ HashMap.fromList
]
type EitherStreamOrValue = Either
- (ResponseEventStream (Either SomeException) Value)
- (Response Value)
+ (ResponseEventStream (Either SomeException) Type.Value)
+ (Response Type.Value)
execute' :: Document -> Either SomeException EitherStreamOrValue
execute' =
- execute philosopherSchema Nothing (mempty :: HashMap Name Value)
+ execute philosopherSchema Nothing (mempty :: HashMap Name Type.Value)
spec :: Spec
spec =
@@ -335,6 +341,68 @@ spec =
$ parse document "" "{ philosopher(id: \"1\") { firstLanguage } }"
in actual `shouldBe` expected
+ context "queryError" $ do
+ let
+ namedQuery name = "query " <> name <> " { philosopher(id: \"1\") { interest } }"
+ twoQueries = namedQuery "A" <> " " <> namedQuery "B"
+ startsWith :: Text.Text -> Text.Text -> Bool
+ startsWith xs ys = Text.take (Text.length ys) xs == ys
+
+ it "throws operation name is required error" $
+ let expectedErrorMessage :: Text.Text
+ expectedErrorMessage = "Operation name is required"
+ Right (Right (Response _ executionErrors)) = either (pure . parseError) execute' $ parse document "" twoQueries
+ Error msg _ _ = Seq.index executionErrors 0
+ in msg `startsWith` expectedErrorMessage `shouldBe` True
+
+ it "throws operation not found error" $
+ let expectedErrorMessage :: Text.Text
+ expectedErrorMessage = "Operation \"C\" is not found"
+ execute'' :: Document -> Either SomeException EitherStreamOrValue
+ execute'' = execute philosopherSchema (Just "C") (mempty :: HashMap Name Type.Value)
+ Right (Right (Response _ executionErrors)) = either (pure . parseError) execute''
+ $ parse document "" twoQueries
+ Error msg _ _ = Seq.index executionErrors 0
+ in msg `startsWith` expectedErrorMessage `shouldBe` True
+
+ it "throws variable coercion error" $
+ let data'' = Null
+ executionErrors = pure $ Error
+ { message = "Failed to coerce the variable $id: String."
+ , locations =[Location 1 7]
+ , path = []
+ }
+ expected = Response data'' executionErrors
+ executeWithVars :: Document -> Either SomeException EitherStreamOrValue
+ executeWithVars = execute philosopherSchema Nothing (HashMap.singleton "id" (Type.Int 1))
+ Right (Right actual) = either (pure . parseError) executeWithVars
+ $ parse document "" "query($id: String) { philosopher(id: \"1\") { firstLanguage } }"
+ in 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
+ Right (Right actual) = either (pure . parseError) execute'
+ $ parse document "" "query($id: Cat) { philosopher(id: \"1\") { firstLanguage } }"
+ in actual `shouldBe` expected
+
+ context "Error path" $ do
+ let executeHero :: Document -> Either SomeException EitherStreamOrValue
+ executeHero = execute heroSchema Nothing (HashMap.empty :: HashMap Name Type.Value)
+
+ it "at the beggining of the list" $
+ let Right (Right actual) = either (pure . parseError) executeHero
+ $ parse document "" "{ hero(id: \"1\") { friends { name } } }"
+ 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" $
it "subscribes" $
let data'' = Object
diff --git a/tests/Schemas/HeroSchema.hs b/tests/Schemas/HeroSchema.hs
new file mode 100644
index 0000000..71b7a10
--- /dev/null
+++ b/tests/Schemas/HeroSchema.hs
@@ -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(..), SomeException)
+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 (Either SomeException)
+heroSchema =
+ schemaWithTypes Nothing queryType Nothing Nothing [] mempty
+
+type ObjectType = Out.ObjectType (Either SomeException)
+
+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 (Either SomeException)
+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))
+ ]