aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore3
-rw-r--r--CHANGELOG.md59
-rw-r--r--CONTRIBUTING.md31
-rw-r--r--LICENSE2
-rw-r--r--README.md9
-rw-r--r--docs/tutorial/tutorial.lhs24
-rw-r--r--package.yaml6
-rwxr-xr-xsemaphoreci.sh5
-rw-r--r--src/Language/GraphQL.hs18
-rw-r--r--src/Language/GraphQL/AST.hs185
-rw-r--r--src/Language/GraphQL/AST/Core.hs12
-rw-r--r--src/Language/GraphQL/AST/DirectiveLocation.hs41
-rw-r--r--src/Language/GraphQL/AST/Document.hs486
-rw-r--r--src/Language/GraphQL/AST/Encoder.hs116
-rw-r--r--src/Language/GraphQL/AST/Lexer.hs42
-rw-r--r--src/Language/GraphQL/AST/Parser.hs435
-rw-r--r--src/Language/GraphQL/Error.hs21
-rw-r--r--src/Language/GraphQL/Execute.hs51
-rw-r--r--src/Language/GraphQL/Execute/Transform.hs107
-rw-r--r--src/Language/GraphQL/Schema.hs121
-rw-r--r--src/Language/GraphQL/Trans.hs25
-rw-r--r--stack.yaml2
-rw-r--r--tests/Language/GraphQL/AST/EncoderSpec.hs107
-rw-r--r--tests/Language/GraphQL/AST/LexerSpec.hs11
-rw-r--r--tests/Language/GraphQL/AST/ParserSpec.hs108
-rw-r--r--tests/Test/DirectiveSpec.hs18
-rw-r--r--tests/Test/FragmentSpec.hs16
-rw-r--r--tests/Test/StarWars/Data.hs13
-rw-r--r--tests/Test/StarWars/QuerySpec.hs182
-rw-r--r--tests/Test/StarWars/Schema.hs55
30 files changed, 1659 insertions, 652 deletions
diff --git a/.gitignore b/.gitignore
index 3594704..a074daa 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,3 +9,6 @@
cabal.sandbox.config
cabal.project.local
/graphql.cabal
+
+# GHC
+*.hi
diff --git a/CHANGELOG.md b/CHANGELOG.md
index af211ba..03b0831 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,9 +1,62 @@
-# Change Log
+# Changelog
All notable changes to this project will be documented in this file.
+The format is based on
+[Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
+and this project adheres to
+[Haskell Package Versioning Policy](https://pvp.haskell.org/).
+
+## [Unreleased]
+
+## [0.7.0.0] - 2020-05-11
+### Fixed
+- Result of null encoding
+- Block strings encoding
+- Result of tab and newline encoding
+
+### Added
+- AST for the GraphQL schema.
+- Type system definition parser.
+- `Trans.argument`.
+- Schema extension parser.
+- Contributing guidelines.
+- `Schema.resolversToMap` (intended for to be used internally).
+
+### Changed
+- Rename `AST.Definition` into `AST.Document.ExecutableDefinition`.
+ `AST.Document.TypeSystemDefinition` and `AST.Document.TypeSystemExtension`
+ can also be definitions.
+- Move all AST data to `AST.Document` and reexport them.
+- Rename `AST.OperationSelectionSet` to `AST.Document.SelectionSet`.
+- Make `Schema.Subs` a `Data.HashMap.Strict` (was a function
+ `key -> Maybe value` before).
+- Make `AST.Lexer.at` a text (symbol) parser. It was a char before and is
+ `symbol "@"` now.
+- Replace `MonadIO` with a plain `Monad`. Since the tests don't use IO,
+ set the inner monad to `Identity`.
+- `NonEmpty (Resolver m)` is now `HashMap Text (NonEmpty (Resolver m))`. Root
+ operation type can be any type, therefore a hashmap is needed. Since types
+ cannot be empty, we save the list of resolvers in the type as a non-empty
+ list. Currently only "Query" and "Mutation" are supported as types. For more
+ schema support is required. The executor checks now if the type in the query
+ matches the type of the provided root resolvers.
+
+### Removed
+- `AST.Field`, `AST.InlineFragment` and `AST.FragmentSpread`.
+ These types are only used in `AST.Selection` and `AST.Selection` contains now
+ 3 corresponding data constructors, `Field`, `InlineFragment` and
+ `FragmentSpread`, instead of separate types. It simplifies pattern matching
+ and doesn't make the code less typesafe.
+- `Schema.scalarA`.
+- `Schema.wrappedScalarA`.
+- `Schema.wrappedObjectA`.
+- `Schema.objectA`.
+- `AST.Argument`. Replaced with `AST.Arguments` which holds all arguments as a
+ key/value map.
+
## [0.6.1.0] - 2019-12-23
### Fixed
-- Parsing multiple string arguments, such as
+- Parsing multiple string arguments, such as
`login(username: "username", password: "password")` would fail on the comma
due to strings not having a space consumer.
- Fragment spread is evaluated based on the `__typename` resolver. If the
@@ -162,6 +215,8 @@ All notable changes to this project will be documented in this file.
### Added
- Data types for the GraphQL language.
+[Unreleased]: https://github.com/caraus-ecms/graphql/compare/v0.6.1.0...HEAD
+[0.7.0.0]: https://github.com/caraus-ecms/graphql/compare/v0.6.1.0...v0.7.0.0
[0.6.1.0]: https://github.com/caraus-ecms/graphql/compare/v0.6.0.0...v0.6.1.0
[0.6.0.0]: https://github.com/caraus-ecms/graphql/compare/v0.5.1.0...v0.6.0.0
[0.5.1.0]: https://github.com/caraus-ecms/graphql/compare/v0.5.0.1...v0.5.1.0
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..a1441f5
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,31 @@
+# Contributing guidelines
+
+## Testing
+
+To ensure all code changes adhere to existing code quality standards, some
+automatic checks can be run locally.
+
+Ensure that the code builds without warnings and passes the tests:
+
+```sh
+stack test --pedantic
+```
+
+And also run the linter on your code:
+
+```sh
+stack build hlint
+stack exec hlint -- src tests
+```
+
+Build the documentation and check if you get any warnings:
+
+```sh
+stack haddock
+```
+
+Validate that literate Haskell (tutorials) files compile without any warnings:
+
+```sh
+stack ghc -- -Wall -fno-code docs/tutorial/*.lhs
+```
diff --git a/LICENSE b/LICENSE
index c93baee..6447301 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-Copyright 2019 Eugen Wissner, Germany
+Copyright 2019-2020 Eugen Wissner, Germany
Copyright 2015-2017 J. Daniel Navarro
All rights reserved.
diff --git a/README.md b/README.md
index 136b9dd..f44e429 100644
--- a/README.md
+++ b/README.md
@@ -24,11 +24,18 @@ For the list of currently missing features see issues marked as
## Documentation
API documentation is available through
-[hackage](https://hackage.haskell.org/package/graphql).
+[Hackage](https://hackage.haskell.org/package/graphql).
You'll also find a small tutorial with some examples under
[docs/tutorial](https://github.com/caraus-ecms/graphql/tree/master/docs/tutorial).
+## Further information
+
+- [Contributing guidelines](CONTRIBUTING.md).
+- [Changelog](CHANGELOG.md) – this one contains the most recent changes;
+ individual changelogs for specific versions can be found on
+ [Hackage](https://hackage.haskell.org/package/graphql).
+
## Contact
Suggestions, contributions and bug reports are welcome.
diff --git a/docs/tutorial/tutorial.lhs b/docs/tutorial/tutorial.lhs
index d017ddd..edaf7f2 100644
--- a/docs/tutorial/tutorial.lhs
+++ b/docs/tutorial/tutorial.lhs
@@ -12,20 +12,19 @@ We have written a small tutorial to help you (and ourselves) understand the grap
Since this file is a literate haskell file, we start by importing some dependencies.
> {-# LANGUAGE OverloadedStrings #-}
-> {-# LANGUAGE LambdaCase #-}
> module Main where
>
> import Control.Monad.IO.Class (liftIO)
-> import Control.Monad.Trans.Except (throwE)
> import Data.Aeson (encode)
> import Data.ByteString.Lazy.Char8 (putStrLn)
+> import Data.HashMap.Strict (HashMap)
+> import qualified Data.HashMap.Strict as HashMap
> import Data.List.NonEmpty (NonEmpty(..))
> import Data.Text (Text)
> import Data.Time (getCurrentTime)
>
> import Language.GraphQL
> import qualified Language.GraphQL.Schema as Schema
-> import Language.GraphQL.Trans (ActionT(..))
>
> import Prelude hiding (putStrLn)
@@ -36,8 +35,8 @@ example from [graphql.js](https://github.com/graphql/graphql-js).
First we build a GraphQL schema.
-> schema1 :: NonEmpty (Schema.Resolver IO)
-> schema1 = hello :| []
+> schema1 :: HashMap Text (NonEmpty (Schema.Resolver IO))
+> schema1 = HashMap.singleton "Query" $ hello :| []
>
> hello :: Schema.Resolver IO
> hello = Schema.scalar "hello" (return ("it's me" :: Text))
@@ -66,14 +65,13 @@ returning
For this example, we're going to be using time.
-> schema2 :: NonEmpty (Schema.Resolver IO)
-> schema2 = time :| []
+> schema2 :: HashMap Text (NonEmpty (Schema.Resolver IO))
+> schema2 = HashMap.singleton "Query" $ time :| []
>
> time :: Schema.Resolver IO
-> time = Schema.scalarA "time" $ \case
-> [] -> do t <- liftIO getCurrentTime
-> return $ show t
-> _ -> ActionT $ throwE "Invalid arguments."
+> time = Schema.scalar "time" $ do
+> t <- liftIO getCurrentTime
+> return $ show t
This defines a simple schema with one type and one field,
which resolves to the current time.
@@ -126,8 +124,8 @@ This will fail
Now that we have two resolvers, we can define a schema which uses them both.
-> schema3 :: NonEmpty (Schema.Resolver IO)
-> schema3 = hello :| [time]
+> schema3 :: HashMap Text (NonEmpty (Schema.Resolver IO))
+> schema3 = HashMap.singleton "Query" $ hello :| [time]
>
> query3 :: Text
> query3 = "query timeAndHello { time hello }"
diff --git a/package.yaml b/package.yaml
index aaf496c..3ae0895 100644
--- a/package.yaml
+++ b/package.yaml
@@ -1,5 +1,5 @@
name: graphql
-version: 0.6.1.0
+version: 0.7.0.0
synopsis: Haskell GraphQL implementation
description:
This package provides a rudimentary parser for the
@@ -8,7 +8,7 @@ maintainer: belka@caraus.de
github: caraus-ecms/graphql
category: Language
copyright:
-- (c) 2019 Eugen Wissner
+- (c) 2019-2020 Eugen Wissner
- (c) 2015-2017 J. Daniel Navarro
author:
- Danny Navarro <j@dannynavarro.net>
@@ -30,6 +30,7 @@ dependencies:
- base >= 4.7 && < 5
- containers
- megaparsec
+- parser-combinators
- text
- transformers
- unordered-containers
@@ -53,4 +54,5 @@ tests:
- hspec
- hspec-expectations
- hspec-megaparsec
+ - QuickCheck
- raw-strings-qq
diff --git a/semaphoreci.sh b/semaphoreci.sh
index c14bd56..d7c8a93 100755
--- a/semaphoreci.sh
+++ b/semaphoreci.sh
@@ -8,7 +8,12 @@ setup() {
then
curl -L https://get.haskellstack.org/stable/linux-x86_64.tar.gz | tar xz --wildcards --strip-components=1 -C $SEMAPHORE_CACHE_DIR '*/stack'
fi
+ if [ -e "$SEMAPHORE_CACHE_DIR/graphql.cabal" ]
+ then
+ cp -a $SEMAPHORE_CACHE_DIR/graphql.cabal graphql.cabal
+ fi
$STACK --no-terminal setup
+ cp -a graphql.cabal $SEMAPHORE_CACHE_DIR/graphql.cabal
}
setup_test() {
diff --git a/src/Language/GraphQL.hs b/src/Language/GraphQL.hs
index afce8aa..73f9bdc 100644
--- a/src/Language/GraphQL.hs
+++ b/src/Language/GraphQL.hs
@@ -4,10 +4,10 @@ module Language.GraphQL
, graphqlSubs
) where
-import Control.Monad.IO.Class (MonadIO)
import qualified Data.Aeson as Aeson
import Data.List.NonEmpty (NonEmpty)
-import qualified Data.Text as T
+import Data.HashMap.Strict (HashMap)
+import Data.Text (Text)
import Language.GraphQL.Error
import Language.GraphQL.Execute
import Language.GraphQL.AST.Parser
@@ -16,19 +16,19 @@ import Text.Megaparsec (parse)
-- | If the text parses correctly as a @GraphQL@ query the query is
-- executed using the given 'Schema.Resolver's.
-graphql :: MonadIO m
- => NonEmpty (Schema.Resolver m) -- ^ Resolvers.
- -> T.Text -- ^ Text representing a @GraphQL@ request document.
+graphql :: Monad m
+ => HashMap Text (NonEmpty (Schema.Resolver m)) -- ^ Resolvers.
+ -> Text -- ^ Text representing a @GraphQL@ request document.
-> m Aeson.Value -- ^ Response.
-graphql = flip graphqlSubs $ const Nothing
+graphql = flip graphqlSubs mempty
-- | 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
-- 'Schema.Resolver's.
-graphqlSubs :: MonadIO m
- => NonEmpty (Schema.Resolver m) -- ^ Resolvers.
+graphqlSubs :: Monad m
+ => HashMap Text (NonEmpty (Schema.Resolver m)) -- ^ Resolvers.
-> Schema.Subs -- ^ Variable substitution function.
- -> T.Text -- ^ Text representing a @GraphQL@ request document.
+ -> Text -- ^ Text representing a @GraphQL@ request document.
-> m Aeson.Value -- ^ Response.
graphqlSubs schema f
= either parseError (execute schema f)
diff --git a/src/Language/GraphQL/AST.hs b/src/Language/GraphQL/AST.hs
index 44bf969..aba6dfd 100644
--- a/src/Language/GraphQL/AST.hs
+++ b/src/Language/GraphQL/AST.hs
@@ -1,185 +1,6 @@
--- | This module defines an abstract syntax tree for the @GraphQL@ language based on
--- <https://facebook.github.io/graphql/ Facebook's GraphQL Specification>.
---
--- Target AST for Parser.
+-- | Target AST for Parser.
module Language.GraphQL.AST
- ( Alias
- , Argument(..)
- , Definition(..)
- , Directive(..)
- , Document
- , Field(..)
- , FragmentDefinition(..)
- , FragmentSpread(..)
- , InlineFragment(..)
- , Name
- , NonNullType(..)
- , ObjectField(..)
- , OperationDefinition(..)
- , OperationType(..)
- , Selection(..)
- , SelectionSet
- , SelectionSetOpt
- , Type(..)
- , TypeCondition
- , Value(..)
- , VariableDefinition(..)
+ ( module Language.GraphQL.AST.Document
) where
-import Data.Int (Int32)
-import Data.List.NonEmpty (NonEmpty)
-import Data.Text (Text)
-
--- * Document
-
--- | GraphQL document.
-type Document = NonEmpty Definition
-
--- | Name
-type Name = Text
-
--- | Directive.
-data Directive = Directive Name [Argument] deriving (Eq, Show)
-
--- * Operations
-
--- | Top-level definition of a document, either an operation or a fragment.
-data Definition
- = DefinitionOperation OperationDefinition
- | DefinitionFragment FragmentDefinition
- deriving (Eq, Show)
-
--- | Operation definition.
-data OperationDefinition
- = OperationSelectionSet SelectionSet
- | OperationDefinition OperationType
- (Maybe Name)
- [VariableDefinition]
- [Directive]
- SelectionSet
- deriving (Eq, Show)
-
--- | GraphQL has 3 operation types: queries, mutations and subscribtions.
---
--- Currently only queries and mutations are supported.
-data OperationType = Query | Mutation deriving (Eq, Show)
-
--- * Selections
-
--- | "Top-level" selection, selection on an operation or fragment.
-type SelectionSet = NonEmpty Selection
-
--- | Field selection.
-type SelectionSetOpt = [Selection]
-
--- | Single selection element.
-data Selection
- = SelectionField Field
- | SelectionFragmentSpread FragmentSpread
- | SelectionInlineFragment InlineFragment
- deriving (Eq, Show)
-
--- * Field
-
--- | Single GraphQL field.
---
--- The only required property of a field is its name. Optionally it can also
--- have an alias, arguments or a list of subfields.
---
--- Given the following query:
---
--- @
--- {
--- zuck: user(id: 4) {
--- id
--- name
--- }
--- }
--- @
---
--- * "user", "id" and "name" are field names.
--- * "user" has two subfields, "id" and "name".
--- * "zuck" is an alias for "user". "id" and "name" have no aliases.
--- * "id: 4" is an argument for "user". "id" and "name" don't have any
--- arguments.
-data Field
- = Field (Maybe Alias) Name [Argument] [Directive] SelectionSetOpt
- deriving (Eq, Show)
-
--- | Alternative field name.
---
--- @
--- {
--- smallPic: profilePic(size: 64)
--- bigPic: profilePic(size: 1024)
--- }
--- @
---
--- Here "smallPic" and "bigPic" are aliases for the same field, "profilePic",
--- used to distinquish between profile pictures with different arguments
--- (sizes).
-type Alias = Name
-
--- | Single argument.
---
--- @
--- {
--- user(id: 4) {
--- name
--- }
--- }
--- @
---
--- Here "id" is an argument for the field "user" and its value is 4.
-data Argument = Argument Name Value deriving (Eq,Show)
-
--- * Fragments
-
--- | Fragment spread.
-data FragmentSpread = FragmentSpread Name [Directive] deriving (Eq, Show)
-
--- | Inline fragment.
-data InlineFragment = InlineFragment (Maybe TypeCondition) [Directive] SelectionSet
- deriving (Eq, Show)
-
--- | Fragment definition.
-data FragmentDefinition
- = FragmentDefinition Name TypeCondition [Directive] SelectionSet
- deriving (Eq, Show)
-
--- * Inputs
-
--- | Input value.
-data Value = Variable Name
- | Int Int32
- | Float Double
- | String Text
- | Boolean Bool
- | Null
- | Enum Name
- | List [Value]
- | Object [ObjectField]
- deriving (Eq, Show)
-
--- | Key-value pair.
---
--- A list of 'ObjectField's represents a GraphQL object type.
-data ObjectField = ObjectField Name Value deriving (Eq, Show)
-
--- | Variable definition.
-data VariableDefinition = VariableDefinition Name Type (Maybe Value)
- deriving (Eq, Show)
-
--- | Type condition.
-type TypeCondition = Name
-
--- | Type representation.
-data Type = TypeNamed Name
- | TypeList Type
- | TypeNonNull NonNullType
- deriving (Eq, Show)
-
--- | Helper type to represent Non-Null types and lists of such types.
-data NonNullType = NonNullTypeNamed Name
- | NonNullTypeList Type
- deriving (Eq, Show)
+import Language.GraphQL.AST.Document
diff --git a/src/Language/GraphQL/AST/Core.hs b/src/Language/GraphQL/AST/Core.hs
index 7ba4830..084ae21 100644
--- a/src/Language/GraphQL/AST/Core.hs
+++ b/src/Language/GraphQL/AST/Core.hs
@@ -1,7 +1,6 @@
-- | This is the AST meant to be executed.
module Language.GraphQL.AST.Core
( Alias
- , Argument(..)
, Arguments(..)
, Directive(..)
, Document
@@ -35,16 +34,19 @@ data Operation
-- | Single GraphQL field.
data Field
- = Field (Maybe Alias) Name [Argument] (Seq Selection)
+ = Field (Maybe Alias) Name Arguments (Seq Selection)
deriving (Eq, Show)
--- | Single argument.
-data Argument = Argument Name Value deriving (Eq, Show)
-
-- | Argument list.
newtype Arguments = Arguments (HashMap Name Value)
deriving (Eq, Show)
+instance Semigroup Arguments where
+ (Arguments x) <> (Arguments y) = Arguments $ x <> y
+
+instance Monoid Arguments where
+ mempty = Arguments mempty
+
-- | Directive.
data Directive = Directive Name Arguments
deriving (Eq, Show)
diff --git a/src/Language/GraphQL/AST/DirectiveLocation.hs b/src/Language/GraphQL/AST/DirectiveLocation.hs
new file mode 100644
index 0000000..5b7a36f
--- /dev/null
+++ b/src/Language/GraphQL/AST/DirectiveLocation.hs
@@ -0,0 +1,41 @@
+-- | Various parts of a GraphQL document can be annotated with directives.
+-- This module describes locations in a document where directives can appear.
+module Language.GraphQL.AST.DirectiveLocation
+ ( DirectiveLocation(..)
+ , ExecutableDirectiveLocation(..)
+ , TypeSystemDirectiveLocation(..)
+ ) where
+
+-- | All directives can be splitted in two groups: directives used to annotate
+-- various parts of executable definitions and the ones used in the schema
+-- definition.
+data DirectiveLocation
+ = ExecutableDirectiveLocation ExecutableDirectiveLocation
+ | TypeSystemDirectiveLocation TypeSystemDirectiveLocation
+ deriving (Eq, Show)
+
+-- | Where directives can appear in an executable definition, like a query.
+data ExecutableDirectiveLocation
+ = Query
+ | Mutation
+ | Subscription
+ | Field
+ | FragmentDefinition
+ | FragmentSpread
+ | InlineFragment
+ deriving (Eq, Show)
+
+-- | Where directives can appear in a type system definition.
+data TypeSystemDirectiveLocation
+ = Schema
+ | Scalar
+ | Object
+ | FieldDefinition
+ | ArgumentDefinition
+ | Interface
+ | Union
+ | Enum
+ | EnumValue
+ | InputObject
+ | InputFieldDefinition
+ deriving (Eq, Show)
diff --git a/src/Language/GraphQL/AST/Document.hs b/src/Language/GraphQL/AST/Document.hs
new file mode 100644
index 0000000..3b13691
--- /dev/null
+++ b/src/Language/GraphQL/AST/Document.hs
@@ -0,0 +1,486 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+-- | This module defines an abstract syntax tree for the @GraphQL@ language. It
+-- follows closely the structure given in the specification. Please refer to
+-- <https://facebook.github.io/graphql/ Facebook's GraphQL Specification>.
+-- for more information.
+module Language.GraphQL.AST.Document
+ ( Alias
+ , Argument(..)
+ , ArgumentsDefinition(..)
+ , Definition(..)
+ , Description(..)
+ , Directive(..)
+ , Document
+ , EnumValueDefinition(..)
+ , ExecutableDefinition(..)
+ , FieldDefinition(..)
+ , FragmentDefinition(..)
+ , ImplementsInterfaces(..)
+ , InputValueDefinition(..)
+ , Name
+ , NamedType
+ , NonNullType(..)
+ , ObjectField(..)
+ , OperationDefinition(..)
+ , OperationType(..)
+ , OperationTypeDefinition(..)
+ , SchemaExtension(..)
+ , Selection(..)
+ , SelectionSet
+ , SelectionSetOpt
+ , Type(..)
+ , TypeCondition
+ , TypeDefinition(..)
+ , TypeExtension(..)
+ , TypeSystemDefinition(..)
+ , TypeSystemExtension(..)
+ , UnionMemberTypes(..)
+ , Value(..)
+ , VariableDefinition(..)
+ ) where
+
+import Data.Foldable (toList)
+import Data.Int (Int32)
+import Data.List.NonEmpty (NonEmpty)
+import Data.Text (Text)
+import qualified Data.Text as Text
+import Language.GraphQL.AST.DirectiveLocation
+
+-- * Language
+
+-- ** Source Text
+
+-- | Name.
+type Name = Text
+
+-- ** Document
+
+-- | GraphQL document.
+type Document = NonEmpty Definition
+
+-- | All kinds of definitions that can occur in a GraphQL document.
+data Definition
+ = ExecutableDefinition ExecutableDefinition
+ | TypeSystemDefinition TypeSystemDefinition
+ | TypeSystemExtension TypeSystemExtension
+ deriving (Eq, Show)
+
+-- | Top-level definition of a document, either an operation or a fragment.
+data ExecutableDefinition
+ = DefinitionOperation OperationDefinition
+ | DefinitionFragment FragmentDefinition
+ deriving (Eq, Show)
+
+-- ** Operations
+
+-- | Operation definition.
+data OperationDefinition
+ = SelectionSet SelectionSet
+ | OperationDefinition
+ OperationType
+ (Maybe Name)
+ [VariableDefinition]
+ [Directive]
+ SelectionSet
+ deriving (Eq, Show)
+
+-- | GraphQL has 3 operation types:
+--
+-- * query - a read-only fetch.
+-- * mutation - a write operation followed by a fetch.
+-- * subscription - a long-lived request that fetches data in response to
+-- source events.
+--
+-- Currently only queries and mutations are supported.
+data OperationType = Query | Mutation deriving (Eq, Show)
+
+-- ** Selection Sets
+
+-- | "Top-level" selection, selection on an operation or fragment.
+type SelectionSet = NonEmpty Selection
+
+-- | Field selection.
+type SelectionSetOpt = [Selection]
+
+-- | Selection is a single entry in a selection set. It can be a single field,
+-- fragment spread or inline fragment.
+--
+-- The only required property of a field is its name. Optionally it can also
+-- have an alias, arguments, directives and a list of subfields.
+--
+-- In the following query "user" is a field with two subfields, "id" and "name":
+--
+-- @
+-- {
+-- user {
+-- id
+-- name
+-- }
+-- }
+-- @
+--
+-- A fragment spread refers to a fragment defined outside the operation and is
+-- expanded at the execution time.
+--
+-- @
+-- {
+-- user {
+-- ...userFragment
+-- }
+-- }
+--
+-- fragment userFragment on UserType {
+-- id
+-- name
+-- }
+-- @
+--
+-- Inline fragments are similar but they don't have any name and the type
+-- condition ("on UserType") is optional.
+--
+-- @
+-- {
+-- user {
+-- ... on UserType {
+-- id
+-- name
+-- }
+-- }
+-- @
+data Selection
+ = Field (Maybe Alias) Name [Argument] [Directive] SelectionSetOpt
+ | FragmentSpread Name [Directive]
+ | InlineFragment (Maybe TypeCondition) [Directive] SelectionSet
+ deriving (Eq, Show)
+
+-- ** Arguments
+
+-- | Single argument.
+--
+-- @
+-- {
+-- user(id: 4) {
+-- name
+-- }
+-- }
+-- @
+--
+-- Here "id" is an argument for the field "user" and its value is 4.
+data Argument = Argument Name Value deriving (Eq,Show)
+
+-- ** Field Alias
+
+-- | Alternative field name.
+--
+-- @
+-- {
+-- smallPic: profilePic(size: 64)
+-- bigPic: profilePic(size: 1024)
+-- }
+-- @
+--
+-- Here "smallPic" and "bigPic" are aliases for the same field, "profilePic",
+-- used to distinquish between profile pictures with different arguments
+-- (sizes).
+type Alias = Name
+
+-- ** Fragments
+
+-- | Fragment definition.
+data FragmentDefinition
+ = FragmentDefinition Name TypeCondition [Directive] SelectionSet
+ deriving (Eq, Show)
+
+-- | Type condition.
+type TypeCondition = Name
+
+-- ** Input Values
+
+-- | Input value.
+data Value
+ = Variable Name
+ | Int Int32
+ | Float Double
+ | String Text
+ | Boolean Bool
+ | Null
+ | Enum Name
+ | List [Value]
+ | Object [ObjectField]
+ deriving (Eq, Show)
+
+-- | Key-value pair.
+--
+-- A list of 'ObjectField's represents a GraphQL object type.
+data ObjectField = ObjectField Name Value deriving (Eq, Show)
+
+-- ** Variables
+
+-- | Variable definition.
+data VariableDefinition = VariableDefinition Name Type (Maybe Value)
+ deriving (Eq, Show)
+
+-- ** Type References
+
+-- | Type representation.
+data Type
+ = TypeNamed Name
+ | TypeList Type
+ | TypeNonNull NonNullType
+ deriving (Eq, Show)
+
+-- | Represents type names.
+type NamedType = Name
+
+-- | Helper type to represent Non-Null types and lists of such types.
+data NonNullType
+ = NonNullTypeNamed Name
+ | NonNullTypeList Type
+ deriving (Eq, Show)
+
+-- ** Directives
+
+-- | Directive.
+--
+-- Directives begin with "@", can accept arguments, and can be applied to the
+-- most GraphQL elements, providing additional information.
+data Directive = Directive Name [Argument] deriving (Eq, Show)
+
+-- * Type System
+
+-- | Type system can define a schema, a type or a directive.
+--
+-- @
+-- schema {
+-- query: Query
+-- }
+--
+-- directive @example on FIELD_DEFINITION
+--
+-- type Query {
+-- field: String @example
+-- }
+-- @
+--
+-- This example defines a custom directive "@example", which is applied to a
+-- field definition of the type definition "Query". On the top the schema
+-- is defined by taking advantage of the type "Query".
+data TypeSystemDefinition
+ = SchemaDefinition [Directive] (NonEmpty OperationTypeDefinition)
+ | TypeDefinition TypeDefinition
+ | DirectiveDefinition
+ Description Name ArgumentsDefinition (NonEmpty DirectiveLocation)
+ deriving (Eq, Show)
+
+-- ** Type System Extensions
+
+-- | Extension for a type system definition. Only schema and type definitions
+-- can be extended.
+data TypeSystemExtension
+ = SchemaExtension SchemaExtension
+ | TypeExtension TypeExtension
+ deriving (Eq, Show)
+
+-- ** Schema
+
+-- | Root operation type definition.
+--
+-- Defining root operation types is not required since they have defaults. So
+-- the default query root type is "Query", and the default mutation root type
+-- is "Mutation". But these defaults can be changed for a specific schema. In
+-- the following code the query root type is changed to "MyQueryRootType", and
+-- the mutation root type to "MyMutationRootType":
+--
+-- @
+-- schema {
+-- query: MyQueryRootType
+-- mutation: MyMutationRootType
+-- }
+-- @
+data OperationTypeDefinition
+ = OperationTypeDefinition OperationType NamedType
+ deriving (Eq, Show)
+
+-- | Extension of the schema definition by further operations or directives.
+data SchemaExtension
+ = SchemaOperationExtension [Directive] (NonEmpty OperationTypeDefinition)
+ | SchemaDirectivesExtension (NonEmpty Directive)
+ deriving (Eq, Show)
+
+-- ** Descriptions
+
+-- | GraphQL has built-in capability to document service APIs. Documentation
+-- is a GraphQL string that precedes a particular definition and contains
+-- Markdown. Any GraphQL definition can be documented this way.
+--
+-- @
+-- """
+-- Supported languages.
+-- """
+-- enum Language {
+-- "English"
+-- EN
+--
+-- "Russian"
+-- RU
+-- }
+-- @
+newtype Description = Description (Maybe Text)
+ deriving (Eq, Show)
+
+-- ** Types
+
+-- | Type definitions describe various user-defined types.
+data TypeDefinition
+ = ScalarTypeDefinition Description Name [Directive]
+ | ObjectTypeDefinition
+ Description
+ Name
+ (ImplementsInterfaces [])
+ [Directive]
+ [FieldDefinition]
+ | InterfaceTypeDefinition Description Name [Directive] [FieldDefinition]
+ | UnionTypeDefinition Description Name [Directive] (UnionMemberTypes [])
+ | EnumTypeDefinition Description Name [Directive] [EnumValueDefinition]
+ | InputObjectTypeDefinition
+ Description Name [Directive] [InputValueDefinition]
+ deriving (Eq, Show)
+
+-- | Extensions for custom, already defined types.
+data TypeExtension
+ = ScalarTypeExtension Name (NonEmpty Directive)
+ | ObjectTypeFieldsDefinitionExtension
+ Name (ImplementsInterfaces []) [Directive] (NonEmpty FieldDefinition)
+ | ObjectTypeDirectivesExtension
+ Name (ImplementsInterfaces []) (NonEmpty Directive)
+ | ObjectTypeImplementsInterfacesExtension
+ Name (ImplementsInterfaces NonEmpty)
+ | InterfaceTypeFieldsDefinitionExtension
+ Name [Directive] (NonEmpty FieldDefinition)
+ | InterfaceTypeDirectivesExtension Name (NonEmpty Directive)
+ | UnionTypeUnionMemberTypesExtension
+ Name [Directive] (UnionMemberTypes NonEmpty)
+ | UnionTypeDirectivesExtension Name (NonEmpty Directive)
+ | EnumTypeEnumValuesDefinitionExtension
+ Name [Directive] (NonEmpty EnumValueDefinition)
+ | EnumTypeDirectivesExtension Name (NonEmpty Directive)
+ | InputObjectTypeInputFieldsDefinitionExtension
+ Name [Directive] (NonEmpty InputValueDefinition)
+ | InputObjectTypeDirectivesExtension Name (NonEmpty Directive)
+ deriving (Eq, Show)
+
+-- ** Objects
+
+-- | Defines a list of interfaces implemented by the given object type.
+--
+-- @
+-- type Business implements NamedEntity & ValuedEntity {
+-- name: String
+-- }
+-- @
+--
+-- Here the object type "Business" implements two interfaces: "NamedEntity" and
+-- "ValuedEntity".
+newtype ImplementsInterfaces t = ImplementsInterfaces (t NamedType)
+
+instance Foldable t => Eq (ImplementsInterfaces t) where
+ (ImplementsInterfaces xs) == (ImplementsInterfaces ys)
+ = toList xs == toList ys
+
+instance Foldable t => Show (ImplementsInterfaces t) where
+ show (ImplementsInterfaces interfaces) = Text.unpack
+ $ Text.append "implements"
+ $ Text.intercalate " & "
+ $ toList interfaces
+
+-- | Definition of a single field in a type.
+--
+-- @
+-- type Person {
+-- name: String
+-- picture(width: Int, height: Int): Url
+-- }
+-- @
+--
+-- "name" and "picture", including their arguments and types, are field
+-- definitions.
+data FieldDefinition
+ = FieldDefinition Description Name ArgumentsDefinition Type [Directive]
+ deriving (Eq, Show)
+
+-- | A list of values passed to a field.
+--
+-- @
+-- type Person {
+-- name: String
+-- picture(width: Int, height: Int): Url
+-- }
+-- @
+--
+-- "Person" has two fields, "name" and "picture". "name" doesn't have any
+-- arguments, so 'ArgumentsDefinition' contains an empty list. "picture"
+-- contains definitions for 2 arguments: "width" and "height".
+newtype ArgumentsDefinition = ArgumentsDefinition [InputValueDefinition]
+ deriving (Eq, Show)
+
+instance Semigroup ArgumentsDefinition where
+ (ArgumentsDefinition xs) <> (ArgumentsDefinition ys) =
+ ArgumentsDefinition $ xs <> ys
+
+instance Monoid ArgumentsDefinition where
+ mempty = ArgumentsDefinition []
+
+-- | Defines an input value.
+--
+-- * Input values can define field arguments, see 'ArgumentsDefinition'.
+-- * They can also be used as field definitions in an input type.
+--
+-- @
+-- input Point2D {
+-- x: Float
+-- y: Float
+-- }
+-- @
+--
+-- The input type "Point2D" contains two value definitions: "x" and "y".
+data InputValueDefinition
+ = InputValueDefinition Description Name Type (Maybe Value) [Directive]
+ deriving (Eq, Show)
+
+-- ** Unions
+
+-- | List of types forming a union.
+--
+-- @
+-- union SearchResult = Person | Photo
+-- @
+--
+-- "Person" and "Photo" are member types of the union "SearchResult".
+newtype UnionMemberTypes t = UnionMemberTypes (t NamedType)
+
+instance Foldable t => Eq (UnionMemberTypes t) where
+ (UnionMemberTypes xs) == (UnionMemberTypes ys) = toList xs == toList ys
+
+instance Foldable t => Show (UnionMemberTypes t) where
+ show (UnionMemberTypes memberTypes) = Text.unpack
+ $ Text.intercalate " | "
+ $ toList memberTypes
+
+-- ** Enums
+
+-- | Single value in an enum definition.
+--
+-- @
+-- enum Direction {
+-- NORTH
+-- EAST
+-- SOUTH
+-- WEST
+-- }
+-- @
+--
+-- "NORTH, "EAST", "SOUTH", and "WEST" are value definitions of an enum type
+-- definition "Direction".
+data EnumValueDefinition = EnumValueDefinition Description Name [Directive]
+ deriving (Eq, Show)
diff --git a/src/Language/GraphQL/AST/Encoder.hs b/src/Language/GraphQL/AST/Encoder.hs
index 508212a..69f5599 100644
--- a/src/Language/GraphQL/AST/Encoder.hs
+++ b/src/Language/GraphQL/AST/Encoder.hs
@@ -15,7 +15,6 @@ module Language.GraphQL.AST.Encoder
import Data.Char (ord)
import Data.Foldable (fold)
-import Data.Monoid ((<>))
import qualified Data.List.NonEmpty as NonEmpty
import Data.Text (Text)
import qualified Data.Text as Text
@@ -26,6 +25,7 @@ import qualified Data.Text.Lazy.Builder as Builder
import Data.Text.Lazy.Builder.Int (decimal, hexadecimal)
import Data.Text.Lazy.Builder.RealFloat (realFloat)
import qualified Language.GraphQL.AST as Full
+import Language.GraphQL.AST.Document
-- | Instructs the encoder whether the GraphQL document should be minified or
-- pretty printed.
@@ -43,16 +43,18 @@ pretty = Pretty 0
minified :: Formatter
minified = Minified
--- | Converts a 'Full.Document' into a string.
-document :: Formatter -> Full.Document -> Lazy.Text
+-- | Converts a Document' into a string.
+document :: Formatter -> Document -> Lazy.Text
document formatter defs
| Pretty _ <- formatter = Lazy.Text.intercalate "\n" encodeDocument
| Minified <-formatter = Lazy.Text.snoc (mconcat encodeDocument) '\n'
where
- encodeDocument = NonEmpty.toList $ definition formatter <$> defs
+ encodeDocument = foldr executableDefinition [] defs
+ executableDefinition (ExecutableDefinition x) acc = definition formatter x : acc
+ executableDefinition _ acc = acc
--- | Converts a 'Full.Definition' into a string.
-definition :: Formatter -> Full.Definition -> Lazy.Text
+-- | Converts a t'Full.ExecutableDefinition' into a string.
+definition :: Formatter -> ExecutableDefinition -> Lazy.Text
definition formatter x
| Pretty _ <- formatter = Lazy.Text.snoc (encodeDefinition x) '\n'
| Minified <- formatter = encodeDefinition x
@@ -62,14 +64,16 @@ definition formatter x
encodeDefinition (Full.DefinitionFragment fragment)
= fragmentDefinition formatter fragment
+-- | Converts a 'Full.OperationDefinition into a string.
operationDefinition :: Formatter -> Full.OperationDefinition -> Lazy.Text
-operationDefinition formatter (Full.OperationSelectionSet sels)
+operationDefinition formatter (Full.SelectionSet sels)
= selectionSet formatter sels
operationDefinition formatter (Full.OperationDefinition Full.Query name vars dirs sels)
= "query " <> node formatter name vars dirs sels
operationDefinition formatter (Full.OperationDefinition Full.Mutation name vars dirs sels)
= "mutation " <> node formatter name vars dirs sels
+-- | Converts a Full.Query or Full.Mutation into a string.
node :: Formatter ->
Maybe Full.Name ->
[Full.VariableDefinition] ->
@@ -110,17 +114,21 @@ selectionSet formatter
selectionSetOpt :: Formatter -> Full.SelectionSetOpt -> Lazy.Text
selectionSetOpt formatter = bracesList formatter $ selection formatter
+indentSymbol :: Lazy.Text
+indentSymbol = " "
+
indent :: (Integral a) => a -> Lazy.Text
-indent indentation = Lazy.Text.replicate (fromIntegral indentation) " "
+indent indentation = Lazy.Text.replicate (fromIntegral indentation) indentSymbol
selection :: Formatter -> Full.Selection -> Lazy.Text
selection formatter = Lazy.Text.append indent' . encodeSelection
where
- encodeSelection (Full.SelectionField field') = field incrementIndent field'
- encodeSelection (Full.SelectionInlineFragment fragment) =
- inlineFragment incrementIndent fragment
- encodeSelection (Full.SelectionFragmentSpread spread) =
- fragmentSpread incrementIndent spread
+ encodeSelection (Full.Field alias name args directives' selections) =
+ field incrementIndent alias name args directives' selections
+ encodeSelection (Full.InlineFragment typeCondition directives' selections) =
+ inlineFragment incrementIndent typeCondition directives' selections
+ encodeSelection (Full.FragmentSpread name directives') =
+ fragmentSpread incrementIndent name directives'
incrementIndent
| Pretty indentation <- formatter = Pretty $ indentation + 1
| otherwise = Minified
@@ -131,8 +139,15 @@ selection formatter = Lazy.Text.append indent' . encodeSelection
colon :: Formatter -> Lazy.Text
colon formatter = eitherFormat formatter ": " ":"
-field :: Formatter -> Full.Field -> Lazy.Text
-field formatter (Full.Field alias name args dirs set)
+-- | Converts Full.Field into a string
+field :: Formatter ->
+ Maybe Full.Name ->
+ Full.Name ->
+ [Full.Argument] ->
+ [Full.Directive] ->
+ [Full.Selection] ->
+ Lazy.Text
+field formatter alias name args dirs set
= optempty prependAlias (fold alias)
<> Lazy.Text.fromStrict name
<> optempty (arguments formatter) args
@@ -154,13 +169,18 @@ argument formatter (Full.Argument name value')
-- * Fragments
-fragmentSpread :: Formatter -> Full.FragmentSpread -> Lazy.Text
-fragmentSpread formatter (Full.FragmentSpread name ds)
- = "..." <> Lazy.Text.fromStrict name <> optempty (directives formatter) ds
+fragmentSpread :: Formatter -> Full.Name -> [Full.Directive] -> Lazy.Text
+fragmentSpread formatter name directives'
+ = "..." <> Lazy.Text.fromStrict name
+ <> optempty (directives formatter) directives'
-inlineFragment :: Formatter -> Full.InlineFragment -> Lazy.Text
-inlineFragment formatter (Full.InlineFragment tc dirs sels)
- = "... on "
+inlineFragment ::
+ Formatter ->
+ Maybe Full.TypeCondition ->
+ [Full.Directive] ->
+ Full.SelectionSet ->
+ Lazy.Text
+inlineFragment formatter tc dirs sels = "... on "
<> Lazy.Text.fromStrict (fold tc)
<> directives formatter dirs
<> eitherFormat formatter " " mempty
@@ -191,7 +211,7 @@ value _ (Full.Variable x) = variable x
value _ (Full.Int x) = Builder.toLazyText $ decimal x
value _ (Full.Float x) = Builder.toLazyText $ realFloat x
value _ (Full.Boolean x) = booleanValue x
-value _ Full.Null = mempty
+value _ Full.Null = "null"
value formatter (Full.String string) = stringValue formatter string
value _ (Full.Enum x) = Lazy.Text.fromStrict x
value formatter (Full.List x) = listValue formatter x
@@ -201,26 +221,40 @@ booleanValue :: Bool -> Lazy.Text
booleanValue True = "true"
booleanValue False = "false"
+quote :: Builder.Builder
+quote = Builder.singleton '\"'
+
+oneLine :: Text -> Builder
+oneLine string = quote <> Text.foldr (mappend . escape) quote string
+
stringValue :: Formatter -> Text -> Lazy.Text
stringValue Minified string = Builder.toLazyText
- $ quote <> Text.foldr (mappend . escape') quote string
- where
- quote = Builder.singleton '\"'
- escape' '\n' = Builder.fromString "\\n"
- escape' char = escape char
-stringValue (Pretty indentation) string = byStringType $ Text.lines string
- where
- byStringType [] = "\"\""
- byStringType [line] = Builder.toLazyText
- $ quote <> Text.foldr (mappend . escape) quote line
- byStringType lines' = "\"\"\"\n"
- <> Lazy.Text.unlines (transformLine <$> lines')
- <> indent indentation
- <> "\"\"\""
- transformLine = (indent (indentation + 1) <>)
- . Lazy.Text.fromStrict
- . Text.replace "\"\"\"" "\\\"\"\""
- quote = Builder.singleton '\"'
+ $ quote <> Text.foldr (mappend . escape) quote string
+stringValue (Pretty indentation) string =
+ if hasEscaped string
+ then stringValue Minified string
+ else Builder.toLazyText $ encoded lines'
+ where
+ isWhiteSpace char = char == ' ' || char == '\t'
+ isNewline char = char == '\n' || char == '\r'
+ hasEscaped = Text.any (not . isAllowed)
+ isAllowed char =
+ char == '\t' || isNewline char || (char >= '\x0020' && char /= '\x007F')
+
+ tripleQuote = Builder.fromText "\"\"\""
+ start = tripleQuote <> Builder.singleton '\n'
+ end = Builder.fromLazyText (indent indentation) <> tripleQuote
+
+ strip = Text.dropWhile isWhiteSpace . Text.dropWhileEnd isWhiteSpace
+ lines' = map Builder.fromText $ Text.split isNewline (Text.replace "\r\n" "\n" $ strip string)
+ encoded [] = oneLine string
+ encoded [_] = oneLine string
+ encoded lines'' = start <> transformLines lines'' <> end
+ transformLines = foldr ((\line acc -> line <> Builder.singleton '\n' <> acc) . transformLine) mempty
+ transformLine line =
+ if Lazy.Text.null (Builder.toLazyText line)
+ then line
+ else Builder.fromLazyText (indent (indentation + 1)) <> line
escape :: Char -> Builder
escape char'
@@ -228,7 +262,9 @@ escape char'
| char' == '\"' = Builder.fromString "\\\""
| char' == '\b' = Builder.fromString "\\b"
| char' == '\f' = Builder.fromString "\\f"
+ | char' == '\n' = Builder.fromString "\\n"
| char' == '\r' = Builder.fromString "\\r"
+ | char' == '\t' = Builder.fromString "\\t"
| char' < '\x0010' = unicode "\\u000" char'
| char' < '\x0020' = unicode "\\u00" char'
| otherwise = Builder.singleton char'
diff --git a/src/Language/GraphQL/AST/Lexer.hs b/src/Language/GraphQL/AST/Lexer.hs
index f95070c..0ba55e3 100644
--- a/src/Language/GraphQL/AST/Lexer.hs
+++ b/src/Language/GraphQL/AST/Lexer.hs
@@ -15,6 +15,7 @@ module Language.GraphQL.AST.Lexer
, dollar
, comment
, equals
+ , extend
, integer
, float
, lexeme
@@ -28,20 +29,16 @@ module Language.GraphQL.AST.Lexer
, unicodeBOM
) where
-import Control.Applicative ( Alternative(..)
- , liftA2
- )
-import Data.Char ( chr
- , digitToInt
- , isAsciiLower
- , isAsciiUpper
- , ord
- )
+import Control.Applicative (Alternative(..), liftA2)
+import Data.Char (chr, digitToInt, isAsciiLower, isAsciiUpper, ord)
import Data.Foldable (foldl')
import Data.List (dropWhileEnd)
+import qualified Data.List.NonEmpty as NonEmpty
+import Data.List.NonEmpty (NonEmpty(..))
import Data.Proxy (Proxy(..))
import Data.Void (Void)
import Text.Megaparsec ( Parsec
+ , (<?>)
, between
, chunk
, chunkToTokens
@@ -56,11 +53,9 @@ import Text.Megaparsec ( Parsec
, takeWhile1P
, try
)
-import Text.Megaparsec.Char ( char
- , digitChar
- , space1
- )
+import Text.Megaparsec.Char (char, digitChar, space1)
import qualified Text.Megaparsec.Char.Lexer as Lexer
+import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.Lazy as TL
@@ -97,8 +92,8 @@ dollar :: Parser T.Text
dollar = symbol "$"
-- | Parser for "@".
-at :: Parser Char
-at = char '@'
+at :: Parser Text
+at = symbol "@"
-- | Parser for "&".
amp :: Parser T.Text
@@ -134,7 +129,7 @@ braces = between (symbol "{") (symbol "}")
-- | Parser for strings.
string :: Parser T.Text
-string = between "\"" "\"" stringValue <* spaceConsumer
+string = between "\"" "\"" stringValue <* spaceConsumer
where
stringValue = T.pack <$> many stringCharacter
stringCharacter = satisfy isStringCharacter1
@@ -143,7 +138,7 @@ string = between "\"" "\"" stringValue <* spaceConsumer
-- | Parser for block strings.
blockString :: Parser T.Text
-blockString = between "\"\"\"" "\"\"\"" stringValue <* spaceConsumer
+blockString = between "\"\"\"" "\"\"\"" stringValue <* spaceConsumer
where
stringValue = do
byLine <- sepBy (many blockStringCharacter) lineTerminator
@@ -226,3 +221,16 @@ escapeSequence = do
-- | Parser for the "Byte Order Mark".
unicodeBOM :: Parser ()
unicodeBOM = optional (char '\xfeff') >> pure ()
+
+-- | Parses "extend" followed by a 'symbol'. It is used by schema extensions.
+extend :: forall a. Text -> String -> NonEmpty (Parser a) -> Parser a
+extend token extensionLabel parsers
+ = foldr combine headParser (NonEmpty.tail parsers)
+ <?> extensionLabel
+ where
+ headParser = tryExtension $ NonEmpty.head parsers
+ combine current accumulated = accumulated <|> tryExtension current
+ tryExtension extensionParser = try
+ $ symbol "extend"
+ *> symbol token
+ *> extensionParser \ No newline at end of file
diff --git a/src/Language/GraphQL/AST/Parser.hs b/src/Language/GraphQL/AST/Parser.hs
index 1505615..3449903 100644
--- a/src/Language/GraphQL/AST/Parser.hs
+++ b/src/Language/GraphQL/AST/Parser.hs
@@ -6,63 +6,358 @@ module Language.GraphQL.AST.Parser
( document
) where
-import Control.Applicative ( Alternative(..)
- , optional
- )
+import Control.Applicative (Alternative(..), optional)
+import Control.Applicative.Combinators (sepBy1)
+import qualified Control.Applicative.Combinators.NonEmpty as NonEmpty
import Data.List.NonEmpty (NonEmpty(..))
-import Language.GraphQL.AST
+import Data.Text (Text)
+import qualified Language.GraphQL.AST.DirectiveLocation as Directive
+import Language.GraphQL.AST.DirectiveLocation
+ ( DirectiveLocation
+ , ExecutableDirectiveLocation
+ , TypeSystemDirectiveLocation
+ )
+import Language.GraphQL.AST.Document
import Language.GraphQL.AST.Lexer
-import Text.Megaparsec ( lookAhead
- , option
- , try
- , (<?>)
- )
+import Text.Megaparsec (lookAhead, option, try, (<?>))
-- | Parser for the GraphQL documents.
document :: Parser Document
-document = unicodeBOM >> spaceConsumer >> lexeme (manyNE definition)
+document = unicodeBOM
+ >> spaceConsumer
+ >> lexeme (NonEmpty.some definition)
definition :: Parser Definition
-definition = DefinitionOperation <$> operationDefinition
- <|> DefinitionFragment <$> fragmentDefinition
- <?> "definition error!"
+definition = ExecutableDefinition <$> executableDefinition
+ <|> TypeSystemDefinition <$> typeSystemDefinition
+ <|> TypeSystemExtension <$> typeSystemExtension
+ <?> "Definition"
+
+executableDefinition :: Parser ExecutableDefinition
+executableDefinition = DefinitionOperation <$> operationDefinition
+ <|> DefinitionFragment <$> fragmentDefinition
+ <?> "ExecutableDefinition"
+
+typeSystemDefinition :: Parser TypeSystemDefinition
+typeSystemDefinition = schemaDefinition
+ <|> TypeDefinition <$> typeDefinition
+ <|> directiveDefinition
+ <?> "TypeSystemDefinition"
+
+typeSystemExtension :: Parser TypeSystemExtension
+typeSystemExtension = SchemaExtension <$> schemaExtension
+ <|> TypeExtension <$> typeExtension
+ <?> "TypeSystemExtension"
+
+directiveDefinition :: Parser TypeSystemDefinition
+directiveDefinition = DirectiveDefinition
+ <$> description
+ <* symbol "directive"
+ <* at
+ <*> name
+ <*> argumentsDefinition
+ <* symbol "on"
+ <*> directiveLocations
+ <?> "DirectiveDefinition"
+
+directiveLocations :: Parser (NonEmpty DirectiveLocation)
+directiveLocations = optional pipe
+ *> directiveLocation `NonEmpty.sepBy1` pipe
+
+directiveLocation :: Parser DirectiveLocation
+directiveLocation
+ = Directive.ExecutableDirectiveLocation <$> executableDirectiveLocation
+ <|> Directive.TypeSystemDirectiveLocation <$> typeSystemDirectiveLocation
+
+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"
+
+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"
+
+typeDefinition :: Parser TypeDefinition
+typeDefinition = scalarTypeDefinition
+ <|> objectTypeDefinition
+ <|> interfaceTypeDefinition
+ <|> unionTypeDefinition
+ <|> enumTypeDefinition
+ <|> inputObjectTypeDefinition
+ <?> "TypeDefinition"
+
+typeExtension :: Parser TypeExtension
+typeExtension = scalarTypeExtension
+ <|> objectTypeExtension
+ <|> interfaceTypeExtension
+ <|> unionTypeExtension
+ <|> enumTypeExtension
+ <|> inputObjectTypeExtension
+ <?> "TypeExtension"
+
+scalarTypeDefinition :: Parser TypeDefinition
+scalarTypeDefinition = ScalarTypeDefinition
+ <$> description
+ <* symbol "scalar"
+ <*> name
+ <*> directives
+ <?> "ScalarTypeDefinition"
+
+scalarTypeExtension :: Parser TypeExtension
+scalarTypeExtension = extend "scalar" "ScalarTypeExtension"
+ $ (ScalarTypeExtension <$> name <*> NonEmpty.some directive) :| []
+
+objectTypeDefinition :: Parser TypeDefinition
+objectTypeDefinition = ObjectTypeDefinition
+ <$> description
+ <* symbol "type"
+ <*> name
+ <*> option (ImplementsInterfaces []) (implementsInterfaces sepBy1)
+ <*> directives
+ <*> braces (many fieldDefinition)
+ <?> "ObjectTypeDefinition"
+
+objectTypeExtension :: Parser TypeExtension
+objectTypeExtension = extend "type" "ObjectTypeExtension"
+ $ fieldsDefinitionExtension :|
+ [ directivesExtension
+ , implementsInterfacesExtension
+ ]
+ where
+ fieldsDefinitionExtension = ObjectTypeFieldsDefinitionExtension
+ <$> name
+ <*> option (ImplementsInterfaces []) (implementsInterfaces sepBy1)
+ <*> directives
+ <*> braces (NonEmpty.some fieldDefinition)
+ directivesExtension = ObjectTypeDirectivesExtension
+ <$> name
+ <*> option (ImplementsInterfaces []) (implementsInterfaces sepBy1)
+ <*> NonEmpty.some directive
+ implementsInterfacesExtension = ObjectTypeImplementsInterfacesExtension
+ <$> name
+ <*> implementsInterfaces NonEmpty.sepBy1
+
+description :: Parser Description
+description = Description
+ <$> optional (string <|> blockString)
+ <?> "Description"
+
+unionTypeDefinition :: Parser TypeDefinition
+unionTypeDefinition = UnionTypeDefinition
+ <$> description
+ <* symbol "union"
+ <*> name
+ <*> directives
+ <*> option (UnionMemberTypes []) (unionMemberTypes sepBy1)
+ <?> "UnionTypeDefinition"
+
+unionTypeExtension :: Parser TypeExtension
+unionTypeExtension = extend "union" "UnionTypeExtension"
+ $ unionMemberTypesExtension :| [directivesExtension]
+ where
+ unionMemberTypesExtension = UnionTypeUnionMemberTypesExtension
+ <$> name
+ <*> directives
+ <*> unionMemberTypes NonEmpty.sepBy1
+ directivesExtension = UnionTypeDirectivesExtension
+ <$> name
+ <*> NonEmpty.some directive
+
+unionMemberTypes ::
+ Foldable t =>
+ (Parser Text -> Parser Text -> Parser (t NamedType)) ->
+ Parser (UnionMemberTypes t)
+unionMemberTypes sepBy' = UnionMemberTypes
+ <$ equals
+ <* optional pipe
+ <*> name `sepBy'` pipe
+ <?> "UnionMemberTypes"
+
+interfaceTypeDefinition :: Parser TypeDefinition
+interfaceTypeDefinition = InterfaceTypeDefinition
+ <$> description
+ <* symbol "interface"
+ <*> name
+ <*> directives
+ <*> braces (many fieldDefinition)
+ <?> "InterfaceTypeDefinition"
+
+interfaceTypeExtension :: Parser TypeExtension
+interfaceTypeExtension = extend "interface" "InterfaceTypeExtension"
+ $ fieldsDefinitionExtension :| [directivesExtension]
+ where
+ fieldsDefinitionExtension = InterfaceTypeFieldsDefinitionExtension
+ <$> name
+ <*> directives
+ <*> braces (NonEmpty.some fieldDefinition)
+ directivesExtension = InterfaceTypeDirectivesExtension
+ <$> name
+ <*> NonEmpty.some directive
+
+enumTypeDefinition :: Parser TypeDefinition
+enumTypeDefinition = EnumTypeDefinition
+ <$> description
+ <* symbol "enum"
+ <*> name
+ <*> directives
+ <*> listOptIn braces enumValueDefinition
+ <?> "EnumTypeDefinition"
+
+enumTypeExtension :: Parser TypeExtension
+enumTypeExtension = extend "enum" "EnumTypeExtension"
+ $ enumValuesDefinitionExtension :| [directivesExtension]
+ where
+ enumValuesDefinitionExtension = EnumTypeEnumValuesDefinitionExtension
+ <$> name
+ <*> directives
+ <*> braces (NonEmpty.some enumValueDefinition)
+ directivesExtension = EnumTypeDirectivesExtension
+ <$> name
+ <*> NonEmpty.some directive
+
+inputObjectTypeDefinition :: Parser TypeDefinition
+inputObjectTypeDefinition = InputObjectTypeDefinition
+ <$> description
+ <* symbol "input"
+ <*> name
+ <*> directives
+ <*> listOptIn braces inputValueDefinition
+ <?> "InputObjectTypeDefinition"
+
+inputObjectTypeExtension :: Parser TypeExtension
+inputObjectTypeExtension = extend "input" "InputObjectTypeExtension"
+ $ inputFieldsDefinitionExtension :| [directivesExtension]
+ where
+ inputFieldsDefinitionExtension = InputObjectTypeInputFieldsDefinitionExtension
+ <$> name
+ <*> directives
+ <*> braces (NonEmpty.some inputValueDefinition)
+ directivesExtension = InputObjectTypeDirectivesExtension
+ <$> name
+ <*> NonEmpty.some directive
+
+enumValueDefinition :: Parser EnumValueDefinition
+enumValueDefinition = EnumValueDefinition
+ <$> description
+ <*> enumValue
+ <*> directives
+ <?> "EnumValueDefinition"
+
+implementsInterfaces ::
+ Foldable t =>
+ (Parser Text -> Parser Text -> Parser (t NamedType)) ->
+ Parser (ImplementsInterfaces t)
+implementsInterfaces sepBy' = ImplementsInterfaces
+ <$ symbol "implements"
+ <* optional amp
+ <*> name `sepBy'` amp
+ <?> "ImplementsInterfaces"
+
+inputValueDefinition :: Parser InputValueDefinition
+inputValueDefinition = InputValueDefinition
+ <$> description
+ <*> name
+ <* colon
+ <*> type'
+ <*> defaultValue
+ <*> directives
+ <?> "InputValueDefinition"
+
+argumentsDefinition :: Parser ArgumentsDefinition
+argumentsDefinition = ArgumentsDefinition
+ <$> listOptIn parens inputValueDefinition
+ <?> "ArgumentsDefinition"
+
+fieldDefinition :: Parser FieldDefinition
+fieldDefinition = FieldDefinition
+ <$> description
+ <*> name
+ <*> argumentsDefinition
+ <* colon
+ <*> type'
+ <*> directives
+ <?> "FieldDefinition"
+
+schemaDefinition :: Parser TypeSystemDefinition
+schemaDefinition = SchemaDefinition
+ <$ symbol "schema"
+ <*> directives
+ <*> operationTypeDefinitions
+ <?> "SchemaDefinition"
+
+operationTypeDefinitions :: Parser (NonEmpty OperationTypeDefinition)
+operationTypeDefinitions = braces $ NonEmpty.some operationTypeDefinition
+
+schemaExtension :: Parser SchemaExtension
+schemaExtension = extend "schema" "SchemaExtension"
+ $ schemaOperationExtension :| [directivesExtension]
+ where
+ directivesExtension = SchemaDirectivesExtension
+ <$> NonEmpty.some directive
+ schemaOperationExtension = SchemaOperationExtension
+ <$> directives
+ <*> operationTypeDefinitions
+
+operationTypeDefinition :: Parser OperationTypeDefinition
+operationTypeDefinition = OperationTypeDefinition
+ <$> operationType <* colon
+ <*> name
+ <?> "OperationTypeDefinition"
operationDefinition :: Parser OperationDefinition
-operationDefinition = OperationSelectionSet <$> selectionSet
- <|> OperationDefinition <$> operationType
- <*> optional name
- <*> opt variableDefinitions
- <*> opt directives
- <*> selectionSet
- <?> "operationDefinition error"
+operationDefinition = SelectionSet <$> selectionSet
+ <|> operationDefinition'
+ <?> "operationDefinition error"
+ where
+ operationDefinition'
+ = OperationDefinition <$> operationType
+ <*> optional name
+ <*> variableDefinitions
+ <*> directives
+ <*> selectionSet
operationType :: Parser OperationType
operationType = Query <$ symbol "query"
<|> Mutation <$ symbol "mutation"
- <?> "operationType error"
+ -- <?> Keep default error message
-- * SelectionSet
selectionSet :: Parser SelectionSet
-selectionSet = braces $ manyNE selection
+selectionSet = braces $ NonEmpty.some selection
selectionSetOpt :: Parser SelectionSetOpt
-selectionSetOpt = braces $ some selection
+selectionSetOpt = listOptIn braces selection
selection :: Parser Selection
-selection = SelectionField <$> field
- <|> try (SelectionFragmentSpread <$> fragmentSpread)
- <|> SelectionInlineFragment <$> inlineFragment
- <?> "selection error!"
+selection = field
+ <|> try fragmentSpread
+ <|> inlineFragment
+ <?> "selection error!"
-- * Field
-field :: Parser Field
-field = Field <$> optional alias
- <*> name
- <*> opt arguments
- <*> opt directives
- <*> opt selectionSetOpt
+field :: Parser Selection
+field = Field
+ <$> optional alias
+ <*> name
+ <*> arguments
+ <*> directives
+ <*> selectionSetOpt
alias :: Parser Alias
alias = try $ name <* colon
@@ -70,30 +365,32 @@ alias = try $ name <* colon
-- * Arguments
arguments :: Parser [Argument]
-arguments = parens $ some argument
+arguments = listOptIn parens argument
argument :: Parser Argument
argument = Argument <$> name <* colon <*> value
-- * Fragments
-fragmentSpread :: Parser FragmentSpread
-fragmentSpread = FragmentSpread <$ spread
- <*> fragmentName
- <*> opt directives
+fragmentSpread :: Parser Selection
+fragmentSpread = FragmentSpread
+ <$ spread
+ <*> fragmentName
+ <*> directives
-inlineFragment :: Parser InlineFragment
-inlineFragment = InlineFragment <$ spread
- <*> optional typeCondition
- <*> opt directives
- <*> selectionSet
+inlineFragment :: Parser Selection
+inlineFragment = InlineFragment
+ <$ spread
+ <*> optional typeCondition
+ <*> directives
+ <*> selectionSet
fragmentDefinition :: Parser FragmentDefinition
fragmentDefinition = FragmentDefinition
<$ symbol "fragment"
<*> name
<*> typeCondition
- <*> opt directives
+ <*> directives
<*> selectionSet
fragmentName :: Parser Name
@@ -121,68 +418,68 @@ value = Variable <$> variable
booleanValue = True <$ symbol "true"
<|> False <$ symbol "false"
- enumValue :: Parser Name
- enumValue = but (symbol "true") *> but (symbol "false") *> but (symbol "null") *> name
-
listValue :: Parser [Value]
listValue = brackets $ some value
objectValue :: Parser [ObjectField]
objectValue = braces $ some objectField
+enumValue :: Parser Name
+enumValue = but (symbol "true") *> but (symbol "false") *> but (symbol "null") *> name
+
objectField :: Parser ObjectField
-objectField = ObjectField <$> name <* symbol ":" <*> value
+objectField = ObjectField <$> name <* colon <*> value
-- * Variables
variableDefinitions :: Parser [VariableDefinition]
-variableDefinitions = parens $ some variableDefinition
+variableDefinitions = listOptIn parens variableDefinition
variableDefinition :: Parser VariableDefinition
-variableDefinition = VariableDefinition <$> variable
- <* colon
- <*> type_
- <*> optional defaultValue
+variableDefinition = VariableDefinition
+ <$> variable
+ <* colon
+ <*> type'
+ <*> defaultValue
+ <?> "VariableDefinition"
+
variable :: Parser Name
variable = dollar *> name
-defaultValue :: Parser Value
-defaultValue = equals *> value
+defaultValue :: Parser (Maybe Value)
+defaultValue = optional (equals *> value) <?> "DefaultValue"
-- * Input Types
-type_ :: Parser Type
-type_ = try (TypeNonNull <$> nonNullType)
- <|> TypeList <$> brackets type_
+type' :: Parser Type
+type' = try (TypeNonNull <$> nonNullType)
+ <|> TypeList <$> brackets type'
<|> TypeNamed <$> name
- <?> "type_ error!"
+ <?> "Type"
nonNullType :: Parser NonNullType
nonNullType = NonNullTypeNamed <$> name <* bang
- <|> NonNullTypeList <$> brackets type_ <* bang
+ <|> NonNullTypeList <$> brackets type' <* bang
<?> "nonNullType error!"
-- * Directives
directives :: Parser [Directive]
-directives = some directive
+directives = many directive
directive :: Parser Directive
directive = Directive
- <$ at
- <*> name
- <*> opt arguments
+ <$ at
+ <*> name
+ <*> arguments
-- * Internal
-opt :: Monoid a => Parser a -> Parser a
-opt = option mempty
+listOptIn :: (Parser [a] -> Parser [a]) -> Parser a -> Parser [a]
+listOptIn surround = option [] . surround . some
-- Hack to reverse parser success
but :: Parser a -> Parser ()
but pn = False <$ lookAhead pn <|> pure True >>= \case
False -> empty
True -> pure ()
-
-manyNE :: Alternative f => f a -> f (NonEmpty a)
-manyNE p = (:|) <$> p <*> many p
diff --git a/src/Language/GraphQL/Error.hs b/src/Language/GraphQL/Error.hs
index 41a2a9c..91911b7 100644
--- a/src/Language/GraphQL/Error.hs
+++ b/src/Language/GraphQL/Error.hs
@@ -20,18 +20,20 @@ import Control.Monad.Trans.State ( StateT
, modify
, runStateT
)
-import Text.Megaparsec ( ParseErrorBundle(..)
- , SourcePos(..)
- , errorOffset
- , parseErrorTextPretty
- , reachOffset
- , unPos
- )
+import Text.Megaparsec
+ ( ParseErrorBundle(..)
+ , PosState(..)
+ , SourcePos(..)
+ , errorOffset
+ , parseErrorTextPretty
+ , reachOffset
+ , unPos
+ )
-- | Wraps a parse error into a list of errors.
parseError :: Applicative f => ParseErrorBundle Text Void -> f Aeson.Value
parseError ParseErrorBundle{..} =
- pure $ Aeson.object [("errors", Aeson.toJSON $ fst $ foldl go ([], bundlePosState) bundleErrors)]
+ pure $ Aeson.object [("errors", Aeson.toJSON $ fst $ foldl go ([], bundlePosState) bundleErrors)]
where
errorObject s SourcePos{..} = Aeson.object
[ ("message", Aeson.toJSON $ init $ parseErrorTextPretty s)
@@ -39,7 +41,8 @@ parseError ParseErrorBundle{..} =
, ("column", Aeson.toJSON $ unPos sourceColumn)
]
go (result, state) x =
- let (sourcePosition, _, newState) = reachOffset (errorOffset x) state
+ let (_, newState) = reachOffset (errorOffset x) state
+ sourcePosition = pstateSourcePos newState
in (errorObject x sourcePosition : result, newState)
-- | A wrapper to pass error messages around.
diff --git a/src/Language/GraphQL/Execute.hs b/src/Language/GraphQL/Execute.hs
index 74f33f9..204d08c 100644
--- a/src/Language/GraphQL/Execute.hs
+++ b/src/Language/GraphQL/Execute.hs
@@ -6,14 +6,14 @@ module Language.GraphQL.Execute
, executeWithName
) where
-import Control.Monad.IO.Class (MonadIO)
import qualified Data.Aeson as Aeson
-import Data.Foldable (toList)
import Data.List.NonEmpty (NonEmpty(..))
-import qualified Data.List.NonEmpty as NE
+import qualified Data.List.NonEmpty as NonEmpty
+import Data.HashMap.Strict (HashMap)
+import qualified Data.HashMap.Strict as HashMap
import Data.Text (Text)
import qualified Data.Text as Text
-import qualified Language.GraphQL.AST as AST
+import Language.GraphQL.AST.Document
import qualified Language.GraphQL.AST.Core as AST.Core
import qualified Language.GraphQL.Execute.Transform as Transform
import Language.GraphQL.Error
@@ -24,13 +24,14 @@ import qualified Language.GraphQL.Schema as Schema
--
-- Returns the result of the query against the schema wrapped in a /data/
-- field, or errors wrapped in an /errors/ field.
-execute :: MonadIO m
- => NonEmpty (Schema.Resolver m) -- ^ Resolvers.
+execute :: Monad m
+ => HashMap Text (NonEmpty (Schema.Resolver m)) -- ^ Resolvers.
-> Schema.Subs -- ^ Variable substitution function.
- -> AST.Document -- @GraphQL@ document.
+ -> Document -- @GraphQL@ document.
-> m Aeson.Value
execute schema subs doc =
- maybe transformError (document schema Nothing) $ Transform.document subs doc
+ maybe transformError (document schema Nothing)
+ $ Transform.document subs doc
where
transformError = return $ singleError "Schema transformation error."
@@ -40,24 +41,25 @@ execute schema subs doc =
--
-- Returns the result of the query against the schema wrapped in a /data/
-- field, or errors wrapped in an /errors/ field.
-executeWithName :: MonadIO m
- => NonEmpty (Schema.Resolver m) -- ^ Resolvers
+executeWithName :: Monad m
+ => HashMap Text (NonEmpty (Schema.Resolver m)) -- ^ Resolvers
-> Text -- ^ Operation name.
-> Schema.Subs -- ^ Variable substitution function.
- -> AST.Document -- ^ @GraphQL@ Document.
+ -> Document -- ^ @GraphQL@ Document.
-> m Aeson.Value
executeWithName schema name subs doc =
- maybe transformError (document schema $ Just name) $ Transform.document subs doc
+ maybe transformError (document schema $ Just name)
+ $ Transform.document subs doc
where
transformError = return $ singleError "Schema transformation error."
-document :: MonadIO m
- => NonEmpty (Schema.Resolver m)
+document :: Monad m
+ => HashMap Text (NonEmpty (Schema.Resolver m))
-> Maybe Text
-> AST.Core.Document
-> m Aeson.Value
document schema Nothing (op :| []) = operation schema op
-document schema (Just name) operations = case NE.dropWhile matchingName operations of
+document schema (Just name) operations = case NonEmpty.dropWhile matchingName operations of
[] -> return $ singleError
$ Text.unwords ["Operation", name, "couldn't be found in the document."]
(op:_) -> operation schema op
@@ -67,11 +69,18 @@ document schema (Just name) operations = case NE.dropWhile matchingName operatio
matchingName _ = False
document _ _ _ = return $ singleError "Missing operation name."
-operation :: MonadIO m
- => NonEmpty (Schema.Resolver m)
+operation :: Monad m
+ => HashMap Text (NonEmpty (Schema.Resolver m))
-> AST.Core.Operation
-> m Aeson.Value
-operation schema (AST.Core.Query _ flds)
- = runCollectErrs (Schema.resolve (toList schema) flds)
-operation schema (AST.Core.Mutation _ flds)
- = runCollectErrs (Schema.resolve (toList schema) flds)
+operation schema = schemaOperation
+ where
+ runResolver fields = runCollectErrs
+ . flip Schema.resolve fields
+ . Schema.resolversToMap
+ resolve fields queryType = maybe lookupError (runResolver fields)
+ $ HashMap.lookup queryType schema
+ lookupError = pure
+ $ singleError "Root operation type couldn't be found in the schema."
+ schemaOperation (AST.Core.Query _ fields) = resolve fields "Query"
+ schemaOperation (AST.Core.Mutation _ fields) = resolve fields "Mutation"
diff --git a/src/Language/GraphQL/Execute/Transform.hs b/src/Language/GraphQL/Execute/Transform.hs
index 882b324..5a9eef8 100644
--- a/src/Language/GraphQL/Execute/Transform.hs
+++ b/src/Language/GraphQL/Execute/Transform.hs
@@ -11,7 +11,7 @@ module Language.GraphQL.Execute.Transform
import Control.Arrow (first)
import Control.Monad (foldM, unless)
import Control.Monad.Trans.Class (lift)
-import Control.Monad.Trans.Reader (ReaderT, ask, runReaderT)
+import Control.Monad.Trans.Reader (ReaderT, asks, runReaderT)
import Control.Monad.Trans.State (StateT, evalStateT, gets, modify)
import Data.HashMap.Strict (HashMap)
import qualified Data.HashMap.Strict as HashMap
@@ -19,6 +19,7 @@ import qualified Data.List.NonEmpty as NonEmpty
import Data.Sequence (Seq, (<|), (><))
import qualified Language.GraphQL.AST as Full
import qualified Language.GraphQL.AST.Core as Core
+import Language.GraphQL.AST.Document (Definition(..), Document)
import qualified Language.GraphQL.Schema as Schema
import qualified Language.GraphQL.Type.Directive as Directive
@@ -35,18 +36,19 @@ liftJust = lift . lift . Just
-- | Rewrites the original syntax tree into an intermediate representation used
-- for query execution.
-document :: Schema.Subs -> Full.Document -> Maybe Core.Document
+document :: Schema.Subs -> Document -> Maybe Core.Document
document subs document' =
flip runReaderT subs
$ evalStateT (collectFragments >> operations operationDefinitions)
$ Replacement HashMap.empty fragmentTable
where
(fragmentTable, operationDefinitions) = foldr defragment mempty document'
- defragment (Full.DefinitionOperation definition) acc =
+ defragment (ExecutableDefinition (Full.DefinitionOperation definition)) acc =
(definition :) <$> acc
- defragment (Full.DefinitionFragment definition) acc =
+ defragment (ExecutableDefinition (Full.DefinitionFragment definition)) acc =
let (Full.FragmentDefinition name _ _ _) = definition
in first (HashMap.insert name definition) acc
+ defragment _ acc = acc
-- * Operation
@@ -56,26 +58,47 @@ operations operations' = do
lift . lift $ NonEmpty.nonEmpty coreOperations
operation :: Full.OperationDefinition -> TransformT Core.Operation
-operation (Full.OperationSelectionSet sels) =
- operation $ Full.OperationDefinition Full.Query mempty mempty mempty sels
--- TODO: Validate Variable definitions with substituter
-operation (Full.OperationDefinition Full.Query name _vars _dirs sels) =
- Core.Query name <$> appendSelection sels
-operation (Full.OperationDefinition Full.Mutation name _vars _dirs sels) =
- Core.Mutation name <$> appendSelection sels
+operation (Full.SelectionSet sels)
+ = operation $ Full.OperationDefinition Full.Query mempty mempty mempty sels
+operation (Full.OperationDefinition Full.Query name _vars _dirs sels)
+ = Core.Query name <$> appendSelection sels
+operation (Full.OperationDefinition Full.Mutation name _vars _dirs sels)
+ = Core.Mutation name <$> appendSelection sels
-- * Selection
selection ::
Full.Selection ->
TransformT (Either (Seq Core.Selection) Core.Selection)
-selection (Full.SelectionField field') =
- maybe (Left mempty) (Right . Core.SelectionField) <$> field field'
-selection (Full.SelectionFragmentSpread fragment) =
- maybe (Left mempty) (Right . Core.SelectionFragment)
- <$> fragmentSpread fragment
-selection (Full.SelectionInlineFragment fragment) =
- inlineFragment fragment
+selection (Full.Field alias name arguments' directives' selections) =
+ maybe (Left mempty) (Right . Core.SelectionField) <$> do
+ fieldArguments <- arguments arguments'
+ fieldSelections <- appendSelection selections
+ fieldDirectives <- Directive.selection <$> directives directives'
+ let field' = Core.Field alias name fieldArguments fieldSelections
+ pure $ field' <$ fieldDirectives
+selection (Full.FragmentSpread name directives') =
+ maybe (Left mempty) (Right . Core.SelectionFragment) <$> do
+ spreadDirectives <- Directive.selection <$> directives directives'
+ fragments' <- gets fragments
+ fragment <- maybe lookupDefinition liftJust (HashMap.lookup name fragments')
+ pure $ fragment <$ spreadDirectives
+ where
+ lookupDefinition = do
+ fragmentDefinitions' <- gets fragmentDefinitions
+ found <- lift . lift $ HashMap.lookup name fragmentDefinitions'
+ fragmentDefinition found
+selection (Full.InlineFragment type' directives' selections) = do
+ fragmentDirectives <- Directive.selection <$> directives directives'
+ case fragmentDirectives of
+ Nothing -> pure $ Left mempty
+ _ -> do
+ fragmentSelectionSet <- appendSelection selections
+ pure $ maybe Left selectionFragment type' fragmentSelectionSet
+ where
+ selectionFragment typeName = Right
+ . Core.SelectionFragment
+ . Core.Fragment typeName
appendSelection ::
Traversable t =>
@@ -104,33 +127,6 @@ collectFragments = do
_ <- fragmentDefinition nextValue
collectFragments
-inlineFragment ::
- Full.InlineFragment ->
- TransformT (Either (Seq Core.Selection) Core.Selection)
-inlineFragment (Full.InlineFragment type' directives' selectionSet) = do
- fragmentDirectives <- Directive.selection <$> directives directives'
- case fragmentDirectives of
- Nothing -> pure $ Left mempty
- _ -> do
- fragmentSelectionSet <- appendSelection selectionSet
- pure $ maybe Left selectionFragment type' fragmentSelectionSet
- where
- selectionFragment typeName = Right
- . Core.SelectionFragment
- . Core.Fragment typeName
-
-fragmentSpread :: Full.FragmentSpread -> TransformT (Maybe Core.Fragment)
-fragmentSpread (Full.FragmentSpread name directives') = do
- spreadDirectives <- Directive.selection <$> directives directives'
- fragments' <- gets fragments
- fragment <- maybe lookupDefinition liftJust (HashMap.lookup name fragments')
- pure $ fragment <$ spreadDirectives
- where
- lookupDefinition = do
- fragmentDefinitions' <- gets fragmentDefinitions
- found <- lift . lift $ HashMap.lookup name fragmentDefinitions'
- fragmentDefinition found
-
fragmentDefinition ::
Full.FragmentDefinition ->
TransformT Core.Fragment
@@ -147,28 +143,15 @@ fragmentDefinition (Full.FragmentDefinition name type' _ selections) = do
let newFragments = HashMap.insert name newValue fragments'
in Replacement newFragments fragmentDefinitions'
-field :: Full.Field -> TransformT (Maybe Core.Field)
-field (Full.Field alias name arguments' directives' selections) = do
- fieldArguments <- traverse argument arguments'
- fieldSelections <- appendSelection selections
- fieldDirectives <- Directive.selection <$> directives directives'
- let field' = Core.Field alias name fieldArguments fieldSelections
- pure $ field' <$ fieldDirectives
-
arguments :: [Full.Argument] -> TransformT Core.Arguments
arguments = fmap Core.Arguments . foldM go HashMap.empty
where
- go arguments' argument' = do
- (Core.Argument name value') <- argument argument'
- return $ HashMap.insert name value' arguments'
-
-argument :: Full.Argument -> TransformT Core.Argument
-argument (Full.Argument n v) = Core.Argument n <$> value v
+ go arguments' (Full.Argument name value') = do
+ substitutedValue <- value value'
+ return $ HashMap.insert name substitutedValue arguments'
value :: Full.Value -> TransformT Core.Value
-value (Full.Variable n) = do
- substitute' <- lift ask
- lift . lift $ substitute' n
+value (Full.Variable name) = lift (asks $ HashMap.lookup name) >>= lift . lift
value (Full.Int i) = pure $ Core.Int i
value (Full.Float f) = pure $ Core.Float f
value (Full.String x) = pure $ Core.String x
diff --git a/src/Language/GraphQL/Schema.hs b/src/Language/GraphQL/Schema.hs
index fa8bf78..c678e48 100644
--- a/src/Language/GraphQL/Schema.hs
+++ b/src/Language/GraphQL/Schema.hs
@@ -3,28 +3,23 @@
-- | This module provides a representation of a @GraphQL@ Schema in addition to
-- functions for defining and manipulating schemas.
module Language.GraphQL.Schema
- ( Resolver
+ ( Resolver(..)
, Subs
, object
- , objectA
- , scalar
- , scalarA
, resolve
+ , resolversToMap
+ , scalar
, wrappedObject
- , wrappedObjectA
, wrappedScalar
- , wrappedScalarA
-- * AST Reexports
, Field
- , Argument(..)
, Value(..)
) where
-import Control.Monad.IO.Class (MonadIO(..))
import Control.Monad.Trans.Class (lift)
import Control.Monad.Trans.Except (runExceptT)
import Control.Monad.Trans.Reader (runReaderT)
-import Data.Foldable (find, fold)
+import Data.Foldable (fold, toList)
import Data.Maybe (fromMaybe)
import qualified Data.Aeson as Aeson
import Data.HashMap.Strict (HashMap)
@@ -38,81 +33,80 @@ import Language.GraphQL.Trans
import qualified Language.GraphQL.Type as Type
-- | Resolves a 'Field' into an @Aeson.@'Data.Aeson.Types.Object' with error
--- information (if an error has occurred). @m@ is usually expected to be an
--- instance of 'MonadIO'.
+-- information (if an error has occurred). @m@ is an arbitrary monad, usually
+-- 'IO'.
data Resolver m = Resolver
Text -- ^ Name
(Field -> CollectErrsT m Aeson.Object) -- ^ Resolver
--- | Variable substitution function.
-type Subs = Name -> Maybe Value
+-- | Converts resolvers to a map.
+resolversToMap
+ :: (Foldable f, Functor f)
+ => f (Resolver m)
+ -> HashMap Text (Field -> CollectErrsT m Aeson.Object)
+resolversToMap = HashMap.fromList . toList . fmap toKV
+ where
+ toKV (Resolver name f) = (name, f)
+
+-- | Contains variables for the query. The key of the map is a variable name,
+-- and the value is the variable value.
+type Subs = HashMap Name Value
-- | Create a new 'Resolver' with the given 'Name' from the given 'Resolver's.
-object :: MonadIO m => Name -> ActionT m [Resolver m] -> Resolver m
-object name = objectA name . const
-
--- | Like 'object' but also taking 'Argument's.
-objectA :: MonadIO m
- => Name -> ([Argument] -> ActionT m [Resolver m]) -> Resolver m
-objectA name f = Resolver name $ resolveFieldValue f resolveRight
+object :: Monad m => Name -> ActionT m [Resolver m] -> Resolver m
+object name f = Resolver name $ resolveFieldValue f resolveRight
where
- resolveRight fld@(Field _ _ _ flds) resolver = withField (resolve resolver flds) fld
+ resolveRight fld@(Field _ _ _ flds) resolver
+ = withField (resolve (resolversToMap resolver) flds) fld
--- | Like 'object' but also taking 'Argument's and can be null or a list of objects.
-wrappedObjectA :: MonadIO m
- => Name -> ([Argument] -> ActionT m (Type.Wrapping [Resolver m])) -> Resolver m
-wrappedObjectA name f = Resolver name $ resolveFieldValue f resolveRight
+-- | Like 'object' but can be null or a list of objects.
+wrappedObject ::
+ Monad m =>
+ Name ->
+ ActionT m (Type.Wrapping [Resolver m]) ->
+ Resolver m
+wrappedObject name f = Resolver name $ resolveFieldValue f resolveRight
where
resolveRight fld@(Field _ _ _ sels) resolver
- = withField (traverse (`resolve` sels) resolver) fld
-
--- | Like 'object' but can be null or a list of objects.
-wrappedObject :: MonadIO m
- => Name -> ActionT m (Type.Wrapping [Resolver m]) -> Resolver m
-wrappedObject name = wrappedObjectA name . const
+ = withField (traverse (resolveMap sels) resolver) fld
+ resolveMap = flip (resolve . resolversToMap)
-- | A scalar represents a primitive value, like a string or an integer.
-scalar :: (MonadIO m, Aeson.ToJSON a) => Name -> ActionT m a -> Resolver m
-scalar name = scalarA name . const
-
--- | Like 'scalar' but also taking 'Argument's.
-scalarA :: (MonadIO m, Aeson.ToJSON a)
- => Name -> ([Argument] -> ActionT m a) -> Resolver m
-scalarA name f = Resolver name $ resolveFieldValue f resolveRight
+scalar :: (Monad m, Aeson.ToJSON a) => Name -> ActionT m a -> Resolver m
+scalar name f = Resolver name $ resolveFieldValue f resolveRight
where
resolveRight fld result = withField (return result) fld
--- | Like 'scalar' but also taking 'Argument's and can be null or a list of scalars.
-wrappedScalarA :: (MonadIO m, Aeson.ToJSON a)
- => Name -> ([Argument] -> ActionT m (Type.Wrapping a)) -> Resolver m
-wrappedScalarA name f = Resolver name $ resolveFieldValue f resolveRight
+-- | Like 'scalar' but can be null or a list of scalars.
+wrappedScalar ::
+ (Monad m, Aeson.ToJSON a) =>
+ Name ->
+ ActionT m (Type.Wrapping a) ->
+ Resolver m
+wrappedScalar name f = Resolver name $ resolveFieldValue f resolveRight
where
resolveRight fld (Type.Named result) = withField (return result) fld
resolveRight fld Type.Null
= return $ HashMap.singleton (aliasOrName fld) Aeson.Null
resolveRight fld (Type.List result) = withField (return result) fld
--- | Like 'scalar' but can be null or a list of scalars.
-wrappedScalar :: (MonadIO m, Aeson.ToJSON a)
- => Name -> ActionT m (Type.Wrapping a) -> Resolver m
-wrappedScalar name = wrappedScalarA name . const
-
-resolveFieldValue :: MonadIO m
- => ([Argument] -> ActionT m a)
- -> (Field -> a -> CollectErrsT m (HashMap Text Aeson.Value))
- -> Field
- -> CollectErrsT m (HashMap Text Aeson.Value)
+resolveFieldValue ::
+ Monad m =>
+ ActionT m a ->
+ (Field -> a -> CollectErrsT m Aeson.Object) ->
+ Field ->
+ CollectErrsT m (HashMap Text Aeson.Value)
resolveFieldValue f resolveRight fld@(Field _ _ args _) = do
- result <- lift $ reader . runExceptT . runActionT $ f args
+ result <- lift $ reader . runExceptT . runActionT $ f
either resolveLeft (resolveRight fld) result
where
- reader = flip runReaderT $ Context mempty
+ reader = flip runReaderT $ Context {arguments=args}
resolveLeft err = do
_ <- addErrMsg err
return $ HashMap.singleton (aliasOrName fld) Aeson.Null
--- | Helper function to facilitate 'Argument' handling.
-withField :: (MonadIO m, Aeson.ToJSON a)
+-- | Helper function to facilitate error handling and result emitting.
+withField :: (Monad m, Aeson.ToJSON a)
=> CollectErrsT m a -> Field -> CollectErrsT m (HashMap Text Aeson.Value)
withField v fld
= HashMap.singleton (aliasOrName fld) . Aeson.toJSON <$> runAppendErrs v
@@ -120,23 +114,22 @@ withField v fld
-- | Takes a list of 'Resolver's and a list of 'Field's and applies each
-- 'Resolver' to each 'Field'. Resolves into a value containing the
-- resolved 'Field', or a null value and error information.
-resolve :: MonadIO m
- => [Resolver m] -> Seq Selection -> CollectErrsT m Aeson.Value
+resolve :: Monad m
+ => HashMap Text (Field -> CollectErrsT m Aeson.Object)
+ -> Seq Selection
+ -> CollectErrsT m Aeson.Value
resolve resolvers = fmap (Aeson.toJSON . fold) . traverse tryResolvers
where
- resolveTypeName (Resolver "__typename" f) = do
+ resolveTypeName f = do
value <- f $ Field Nothing "__typename" mempty mempty
return $ HashMap.lookupDefault "" "__typename" value
- resolveTypeName _ = return ""
tryResolvers (SelectionField fld@(Field _ name _ _))
- = maybe (errmsg fld) (tryResolver fld) $ find (compareResolvers name) resolvers
+ = fromMaybe (errmsg fld) $ HashMap.lookup name resolvers <*> Just fld
tryResolvers (SelectionFragment (Fragment typeCondition selections')) = do
- that <- traverse resolveTypeName (find (compareResolvers "__typename") resolvers)
+ that <- traverse resolveTypeName $ HashMap.lookup "__typename" resolvers
if maybe True (Aeson.String typeCondition ==) that
then fmap fold . traverse tryResolvers $ selections'
else return mempty
- compareResolvers name (Resolver name' _) = name == name'
- tryResolver fld (Resolver _ resolver) = resolver fld
errmsg fld@(Field _ name _ _) = do
addErrMsg $ T.unwords ["field", name, "not resolved."]
return $ HashMap.singleton (aliasOrName fld) Aeson.Null
diff --git a/src/Language/GraphQL/Trans.hs b/src/Language/GraphQL/Trans.hs
index 4232e75..09c012b 100644
--- a/src/Language/GraphQL/Trans.hs
+++ b/src/Language/GraphQL/Trans.hs
@@ -1,7 +1,8 @@
-- | Monad transformer stack used by the @GraphQL@ resolvers.
module Language.GraphQL.Trans
( ActionT(..)
- , Context(Context)
+ , Context(..)
+ , argument
) where
import Control.Applicative (Alternative(..))
@@ -9,13 +10,17 @@ import Control.Monad (MonadPlus(..))
import Control.Monad.IO.Class (MonadIO(..))
import Control.Monad.Trans.Class (MonadTrans(..))
import Control.Monad.Trans.Except (ExceptT)
-import Control.Monad.Trans.Reader (ReaderT)
-import Data.HashMap.Strict (HashMap)
+import Control.Monad.Trans.Reader (ReaderT, asks)
+import qualified Data.HashMap.Strict as HashMap
+import Data.Maybe (fromMaybe)
import Data.Text (Text)
-import Language.GraphQL.AST.Core (Name, Value)
+import Language.GraphQL.AST.Core
+import Prelude hiding (lookup)
-- | Resolution context holds resolver arguments.
-newtype Context = Context (HashMap Name Value)
+newtype Context = Context
+ { arguments :: Arguments
+ }
-- | Monad transformer stack used by the resolvers to provide error handling
-- and resolution context (resolver arguments).
@@ -47,3 +52,13 @@ instance Monad m => Alternative (ActionT m) where
instance Monad m => MonadPlus (ActionT m) where
mzero = empty
mplus = (<|>)
+
+-- | Retrieves an argument by its name. If the argument with this name couldn't
+-- be found, returns 'Value.Null' (i.e. the argument is assumed to
+-- be optional then).
+argument :: Monad m => Name -> ActionT m Value
+argument argumentName = do
+ argumentValue <- ActionT $ lift $ asks $ lookup . arguments
+ pure $ fromMaybe Null argumentValue
+ where
+ lookup (Arguments argumentMap) = HashMap.lookup argumentName argumentMap
diff --git a/stack.yaml b/stack.yaml
index f7d770d..ecf2cde 100644
--- a/stack.yaml
+++ b/stack.yaml
@@ -1,4 +1,4 @@
-resolver: lts-14.18
+resolver: lts-15.12
packages:
- .
diff --git a/tests/Language/GraphQL/AST/EncoderSpec.hs b/tests/Language/GraphQL/AST/EncoderSpec.hs
index 0067c83..71ee948 100644
--- a/tests/Language/GraphQL/AST/EncoderSpec.hs
+++ b/tests/Language/GraphQL/AST/EncoderSpec.hs
@@ -6,37 +6,124 @@ module Language.GraphQL.AST.EncoderSpec
import Language.GraphQL.AST
import Language.GraphQL.AST.Encoder
-import Test.Hspec (Spec, context, describe, it, shouldBe)
+import Test.Hspec (Spec, context, describe, it, shouldBe, shouldStartWith, shouldEndWith, shouldNotContain)
+import Test.QuickCheck (choose, oneof, forAll)
import Text.RawString.QQ (r)
+import Data.Text.Lazy (cons, toStrict, unpack)
spec :: Spec
spec = do
describe "value" $ do
+ context "null value" $ do
+ let testNull formatter = value formatter Null `shouldBe` "null"
+ it "minified" $ testNull minified
+ it "pretty" $ testNull pretty
+
context "minified" $ do
it "escapes \\" $
value minified (String "\\") `shouldBe` "\"\\\\\""
- it "escapes quotes" $
+ it "escapes double quotes" $
value minified (String "\"") `shouldBe` "\"\\\"\""
+ it "escapes \\f" $
+ value minified (String "\f") `shouldBe` "\"\\f\""
+ it "escapes \\n" $
+ value minified (String "\n") `shouldBe` "\"\\n\""
+ it "escapes \\r" $
+ value minified (String "\r") `shouldBe` "\"\\r\""
+ it "escapes \\t" $
+ value minified (String "\t") `shouldBe` "\"\\t\""
it "escapes backspace" $
value minified (String "a\bc") `shouldBe` "\"a\\bc\""
- it "escapes Unicode" $
- value minified (String "\0") `shouldBe` "\"\\u0000\""
+ context "escapes Unicode for chars less than 0010" $ do
+ it "Null" $ value minified (String "\x0000") `shouldBe` "\"\\u0000\""
+ it "bell" $ value minified (String "\x0007") `shouldBe` "\"\\u0007\""
+ context "escapes Unicode for char less than 0020" $ do
+ it "DLE" $ value minified (String "\x0010") `shouldBe` "\"\\u0010\""
+ it "EM" $ value minified (String "\x0019") `shouldBe` "\"\\u0019\""
+ context "encodes without escape" $ do
+ it "space" $ value minified (String "\x0020") `shouldBe` "\" \""
+ it "~" $ value minified (String "\x007E") `shouldBe` "\"~\""
context "pretty" $ do
it "uses strings for short string values" $
value pretty (String "Short text") `shouldBe` "\"Short text\""
- it "uses block strings for text with new lines" $
+ it "uses block strings for text with new lines, with newline symbol" $
value pretty (String "Line 1\nLine 2")
- `shouldBe` "\"\"\"\n Line 1\n Line 2\n\"\"\""
- it "escapes \\ in short strings" $
- value pretty (String "\\") `shouldBe` "\"\\\\\""
+ `shouldBe` [r|"""
+ Line 1
+ Line 2
+"""|]
+ it "uses block strings for text with new lines, with CR symbol" $
+ value pretty (String "Line 1\rLine 2")
+ `shouldBe` [r|"""
+ Line 1
+ Line 2
+"""|]
+ it "uses block strings for text with new lines, with CR symbol followed by newline" $
+ value pretty (String "Line 1\r\nLine 2")
+ `shouldBe` [r|"""
+ Line 1
+ Line 2
+"""|]
+ it "encodes as one line string if has escaped symbols" $ do
+ let
+ genNotAllowedSymbol = oneof
+ [ choose ('\x0000', '\x0008')
+ , choose ('\x000B', '\x000C')
+ , choose ('\x000E', '\x001F')
+ , pure '\x007F'
+ ]
+
+ forAll genNotAllowedSymbol $ \x -> do
+ let
+ rawValue = "Short \n" <> cons x "text"
+ encoded = value pretty (String $ toStrict rawValue)
+ shouldStartWith (unpack encoded) "\""
+ shouldEndWith (unpack encoded) "\""
+ shouldNotContain (unpack encoded) "\"\"\""
+
+ it "Hello world" $ value pretty (String "Hello,\n World!\n\nYours,\n GraphQL.")
+ `shouldBe` [r|"""
+ Hello,
+ World!
+
+ Yours,
+ GraphQL.
+"""|]
+
+ it "has only newlines" $ value pretty (String "\n") `shouldBe` [r|"""
+
+
+"""|]
+ it "has newlines and one symbol at the begining" $
+ value pretty (String "a\n\n") `shouldBe` [r|"""
+ a
+
+
+"""|]
+ it "has newlines and one symbol at the end" $
+ value pretty (String "\n\na") `shouldBe` [r|"""
+
+
+ a
+"""|]
+ it "has newlines and one symbol in the middle" $
+ value pretty (String "\na\n") `shouldBe` [r|"""
+
+ a
+
+"""|]
+ it "skip trailing whitespaces" $ value pretty (String " Short\ntext ")
+ `shouldBe` [r|"""
+ Short
+ text
+"""|]
describe "definition" $
it "indents block strings in arguments" $
let arguments = [Argument "message" (String "line1\nline2")]
field = Field Nothing "field" arguments [] []
- set = OperationSelectionSet $ pure $ SelectionField field
- operation = DefinitionOperation set
+ operation = DefinitionOperation $ SelectionSet $ pure field
in definition pretty operation `shouldBe` [r|{
field(message: """
line1
diff --git a/tests/Language/GraphQL/AST/LexerSpec.hs b/tests/Language/GraphQL/AST/LexerSpec.hs
index a0acb9f..0b4cb31 100644
--- a/tests/Language/GraphQL/AST/LexerSpec.hs
+++ b/tests/Language/GraphQL/AST/LexerSpec.hs
@@ -8,7 +8,7 @@ import Data.Text (Text)
import Data.Void (Void)
import Language.GraphQL.AST.Lexer
import Test.Hspec (Spec, context, describe, it)
-import Test.Hspec.Megaparsec (shouldParse, shouldSucceedOn)
+import Test.Hspec.Megaparsec (shouldParse, shouldFailOn, shouldSucceedOn)
import Text.Megaparsec (ParseErrorBundle, parse)
import Text.RawString.QQ (r)
@@ -77,7 +77,7 @@ spec = describe "Lexer" $ do
parse spread "" "..." `shouldParse` "..."
parse colon "" ":" `shouldParse` ":"
parse equals "" "=" `shouldParse` "="
- parse at "" "@" `shouldParse` '@'
+ parse at "" "@" `shouldParse` "@"
runBetween brackets `shouldSucceedOn` "[]"
runBetween braces `shouldSucceedOn` "{}"
parse pipe "" "|" `shouldParse` "|"
@@ -87,6 +87,13 @@ spec = describe "Lexer" $ do
parse blockString "" [r|""""""|] `shouldParse` ""
it "lexes ampersand" $
parse amp "" "&" `shouldParse` "&"
+ it "lexes schema extensions" $
+ parseExtend "schema" `shouldSucceedOn` "extend schema"
+ it "fails if the given token doesn't match" $
+ parseExtend "schema" `shouldFailOn` "extend shema"
+
+parseExtend :: Text -> (Text -> Either (ParseErrorBundle Text Void) ())
+parseExtend extension = parse (extend extension "" $ pure $ pure ()) ""
runBetween :: (Parser () -> Parser ()) -> Text -> Either (ParseErrorBundle Text Void) ()
runBetween parser = parse (parser $ pure ()) ""
diff --git a/tests/Language/GraphQL/AST/ParserSpec.hs b/tests/Language/GraphQL/AST/ParserSpec.hs
index 420d3cf..4fae5b1 100644
--- a/tests/Language/GraphQL/AST/ParserSpec.hs
+++ b/tests/Language/GraphQL/AST/ParserSpec.hs
@@ -4,9 +4,11 @@ module Language.GraphQL.AST.ParserSpec
( spec
) where
+import Data.List.NonEmpty (NonEmpty(..))
+import Language.GraphQL.AST.Document
import Language.GraphQL.AST.Parser
import Test.Hspec (Spec, describe, it)
-import Test.Hspec.Megaparsec (shouldSucceedOn)
+import Test.Hspec.Megaparsec (shouldParse, shouldSucceedOn)
import Text.Megaparsec (parse)
import Text.RawString.QQ (r)
@@ -28,17 +30,115 @@ spec = describe "Parser" $ do
it "accepts two required arguments" $
parse document "" `shouldSucceedOn` [r|
mutation auth($username: String!, $password: String!){
- test
+ test
}|]
it "accepts two string arguments" $
parse document "" `shouldSucceedOn` [r|
mutation auth{
- test(username: "username", password: "password")
+ test(username: "username", password: "password")
}|]
it "accepts two block string arguments" $
parse document "" `shouldSucceedOn` [r|
mutation auth{
- test(username: """username""", password: """password""")
+ test(username: """username""", password: """password""")
}|]
+
+ it "parses minimal schema definition" $
+ parse document "" `shouldSucceedOn` [r|schema { query: Query }|]
+
+ it "parses minimal scalar definition" $
+ parse document "" `shouldSucceedOn` [r|scalar Time|]
+
+ it "parses ImplementsInterfaces" $
+ parse document "" `shouldSucceedOn` [r|
+ type Person implements NamedEntity & ValuedEntity {
+ name: String
+ }
+ |]
+
+ it "parses a type without ImplementsInterfaces" $
+ parse document "" `shouldSucceedOn` [r|
+ type Person {
+ name: String
+ }
+ |]
+
+ it "parses ArgumentsDefinition in an ObjectDefinition" $
+ parse document "" `shouldSucceedOn` [r|
+ type Person {
+ name(first: String, last: String): String
+ }
+ |]
+
+ it "parses minimal union type definition" $
+ parse document "" `shouldSucceedOn` [r|
+ union SearchResult = Photo | Person
+ |]
+
+ it "parses minimal interface type definition" $
+ parse document "" `shouldSucceedOn` [r|
+ interface NamedEntity {
+ name: String
+ }
+ |]
+
+ it "parses minimal enum type definition" $
+ parse document "" `shouldSucceedOn` [r|
+ enum Direction {
+ NORTH
+ EAST
+ SOUTH
+ WEST
+ }
+ |]
+
+ it "parses minimal enum type definition" $
+ parse document "" `shouldSucceedOn` [r|
+ enum Direction {
+ NORTH
+ EAST
+ SOUTH
+ WEST
+ }
+ |]
+
+ it "parses minimal input object type definition" $
+ parse document "" `shouldSucceedOn` [r|
+ input Point2D {
+ x: Float
+ y: Float
+ }
+ |]
+
+ it "parses minimal input enum definition with an optional pipe" $
+ parse document "" `shouldSucceedOn` [r|
+ directive @example on
+ | FIELD
+ | FRAGMENT_SPREAD
+ |]
+
+ it "parses schema extension with a new directive" $
+ parse document "" `shouldSucceedOn`[r|
+ extend schema @newDirective
+ |]
+
+ it "parses schema extension with an operation type definition" $
+ parse document "" `shouldSucceedOn` [r|extend schema { query: Query }|]
+
+ it "parses schema extension with an operation type and directive" $
+ let newDirective = Directive "newDirective" []
+ testSchemaExtension = TypeSystemExtension
+ $ SchemaExtension
+ $ SchemaOperationExtension [newDirective]
+ $ OperationTypeDefinition Query "Query" :| []
+ query = [r|extend schema @newDirective { query: Query }|]
+ in parse document "" query `shouldParse` (testSchemaExtension :| [])
+
+ it "parses an object extension" $
+ parse document "" `shouldSucceedOn` [r|
+ extend type Story {
+ isHiddenLocally: Boolean
+ }
+ |] \ No newline at end of file
diff --git a/tests/Test/DirectiveSpec.hs b/tests/Test/DirectiveSpec.hs
index 2224bc5..3b9da19 100644
--- a/tests/Test/DirectiveSpec.hs
+++ b/tests/Test/DirectiveSpec.hs
@@ -5,14 +5,18 @@ module Test.DirectiveSpec
) where
import Data.Aeson (Value, object, (.=))
+import Data.HashMap.Strict (HashMap)
+import qualified Data.HashMap.Strict as HashMap
import Data.List.NonEmpty (NonEmpty(..))
+import Data.Text (Text)
import Language.GraphQL
import qualified Language.GraphQL.Schema as Schema
import Test.Hspec (Spec, describe, it, shouldBe)
import Text.RawString.QQ (r)
-experimentalResolver :: Schema.Resolver IO
-experimentalResolver = Schema.scalar "experimentalField" $ pure (5 :: Int)
+experimentalResolver :: HashMap Text (NonEmpty (Schema.Resolver IO))
+experimentalResolver = HashMap.singleton "Query"
+ $ Schema.scalar "experimentalField" (pure (5 :: Int)) :| []
emptyObject :: Value
emptyObject = object
@@ -29,7 +33,7 @@ spec =
}
|]
- actual <- graphql (experimentalResolver :| []) query
+ actual <- graphql experimentalResolver query
actual `shouldBe` emptyObject
it "should not skip fields if @skip is false" $ do
@@ -44,7 +48,7 @@ spec =
]
]
- actual <- graphql (experimentalResolver :| []) query
+ actual <- graphql experimentalResolver query
actual `shouldBe` expected
it "should skip fields if @include is false" $ do
@@ -54,7 +58,7 @@ spec =
}
|]
- actual <- graphql (experimentalResolver :| []) query
+ actual <- graphql experimentalResolver query
actual `shouldBe` emptyObject
it "should be able to @skip a fragment spread" $ do
@@ -68,7 +72,7 @@ spec =
}
|]
- actual <- graphql (experimentalResolver :| []) query
+ actual <- graphql experimentalResolver query
actual `shouldBe` emptyObject
it "should be able to @skip an inline fragment" $ do
@@ -80,5 +84,5 @@ spec =
}
|]
- actual <- graphql (experimentalResolver :| []) query
+ actual <- graphql experimentalResolver query
actual `shouldBe` emptyObject
diff --git a/tests/Test/FragmentSpec.hs b/tests/Test/FragmentSpec.hs
index de10d63..74293a9 100644
--- a/tests/Test/FragmentSpec.hs
+++ b/tests/Test/FragmentSpec.hs
@@ -51,7 +51,7 @@ spec :: Spec
spec = do
describe "Inline fragment executor" $ do
it "chooses the first selection if the type matches" $ do
- actual <- graphql (garment "Hat" :| []) inlineQuery
+ actual <- graphql (HashMap.singleton "Query" $ garment "Hat" :| []) inlineQuery
let expected = object
[ "data" .= object
[ "garment" .= object
@@ -62,7 +62,7 @@ spec = do
in actual `shouldBe` expected
it "chooses the last selection if the type matches" $ do
- actual <- graphql (garment "Shirt" :| []) inlineQuery
+ actual <- graphql (HashMap.singleton "Query" $ garment "Shirt" :| []) inlineQuery
let expected = object
[ "data" .= object
[ "garment" .= object
@@ -83,7 +83,7 @@ spec = do
}|]
resolvers = Schema.object "garment" $ return [circumference, size]
- actual <- graphql (resolvers :| []) query
+ actual <- graphql (HashMap.singleton "Query" $ resolvers :| []) query
let expected = object
[ "data" .= object
[ "garment" .= object
@@ -101,7 +101,7 @@ spec = do
}
}|]
- actual <- graphql (size :| []) query
+ actual <- graphql (HashMap.singleton "Query" $ size :| []) query
actual `shouldNotSatisfy` hasErrors
describe "Fragment spread executor" $ do
@@ -116,7 +116,7 @@ spec = do
}
|]
- actual <- graphql (circumference :| []) query
+ actual <- graphql (HashMap.singleton "Query" $ circumference :| []) query
let expected = object
[ "data" .= object
[ "circumference" .= (60 :: Int)
@@ -141,7 +141,7 @@ spec = do
}
|]
- actual <- graphql (garment "Hat" :| []) query
+ actual <- graphql (HashMap.singleton "Query" $ garment "Hat" :| []) query
let expected = object
[ "data" .= object
[ "garment" .= object
@@ -162,7 +162,7 @@ spec = do
}
|]
- actual <- graphql (circumference :| []) query
+ actual <- graphql (HashMap.singleton "Query" $ circumference :| []) query
actual `shouldSatisfy` hasErrors
it "considers type condition" $ do
@@ -187,5 +187,5 @@ spec = do
]
]
]
- actual <- graphql (garment "Hat" :| []) query
+ actual <- graphql (HashMap.singleton "Query" $ garment "Hat" :| []) query
actual `shouldBe` expected
diff --git a/tests/Test/StarWars/Data.hs b/tests/Test/StarWars/Data.hs
index 4854f8f..9466991 100644
--- a/tests/Test/StarWars/Data.hs
+++ b/tests/Test/StarWars/Data.hs
@@ -8,7 +8,6 @@ module Test.StarWars.Data
, getEpisode
, getFriends
, getHero
- , getHeroIO
, getHuman
, id_
, homePlanet
@@ -17,11 +16,8 @@ module Test.StarWars.Data
, typeName
) where
-import Data.Monoid (mempty)
-import Control.Applicative ( Alternative(..)
- , liftA2
- )
-import Control.Monad.IO.Class (MonadIO(..))
+import Data.Functor.Identity (Identity)
+import Control.Applicative (Alternative(..), liftA2)
import Control.Monad.Trans.Except (throwE)
import Data.Maybe (catMaybes)
import Data.Text (Text)
@@ -71,7 +67,7 @@ appearsIn :: Character -> [Int]
appearsIn (Left x) = _appearsIn . _droidChar $ x
appearsIn (Right x) = _appearsIn . _humanChar $ x
-secretBackstory :: MonadIO m => Character -> ActionT m Text
+secretBackstory :: Character -> ActionT Identity Text
secretBackstory = const $ ActionT $ throwE "secretBackstory is secret."
typeName :: Character -> Text
@@ -166,9 +162,6 @@ getHero :: Int -> Character
getHero 5 = luke
getHero _ = artoo
-getHeroIO :: Int -> IO Character
-getHeroIO = pure . getHero
-
getHuman :: Alternative f => ID -> f Character
getHuman = fmap Right . getHuman'
diff --git a/tests/Test/StarWars/QuerySpec.hs b/tests/Test/StarWars/QuerySpec.hs
index 0f6a2ef..45fcf42 100644
--- a/tests/Test/StarWars/QuerySpec.hs
+++ b/tests/Test/StarWars/QuerySpec.hs
@@ -5,20 +5,15 @@ module Test.StarWars.QuerySpec
) where
import qualified Data.Aeson as Aeson
-import Data.Aeson ( object
- , (.=)
- )
+import Data.Aeson ((.=))
+import Data.Functor.Identity (Identity(..))
+import qualified Data.HashMap.Strict as HashMap
import Data.Text (Text)
import Language.GraphQL
import Language.GraphQL.Schema (Subs)
import Text.RawString.QQ (r)
-import Test.Hspec.Expectations ( Expectation
- , shouldBe
- )
-import Test.Hspec ( Spec
- , describe
- , it
- )
+import Test.Hspec.Expectations (Expectation, shouldBe)
+import Test.Hspec (Spec, describe, it)
import Test.StarWars.Schema
-- * Test
@@ -34,7 +29,11 @@ spec = describe "Star Wars Query Tests" $ do
}
}
|]
- $ object [ "data" .= object ["hero" .= object ["id" .= ("2001" :: Text)]]]
+ $ Aeson.object
+ [ "data" .= Aeson.object
+ [ "hero" .= Aeson.object ["id" .= ("2001" :: Text)]
+ ]
+ ]
it "R2-D2 ID and friends" $ testQuery
[r| query HeroNameAndFriendsQuery {
hero {
@@ -46,14 +45,14 @@ spec = describe "Star Wars Query Tests" $ do
}
}
|]
- $ object [ "data" .= object [
- "hero" .= object
+ $ Aeson.object [ "data" .= Aeson.object [
+ "hero" .= Aeson.object
[ "id" .= ("2001" :: Text)
, r2d2Name
, "friends" .=
- [ object [lukeName]
- , object [hanName]
- , object [leiaName]
+ [ Aeson.object [lukeName]
+ , Aeson.object [hanName]
+ , Aeson.object [leiaName]
]
]
]]
@@ -73,37 +72,37 @@ spec = describe "Star Wars Query Tests" $ do
}
}
|]
- $ object [ "data" .= object [
- "hero" .= object [
+ $ Aeson.object [ "data" .= Aeson.object [
+ "hero" .= Aeson.object [
"name" .= ("R2-D2" :: Text)
, "friends" .= [
- object [
+ Aeson.object [
"name" .= ("Luke Skywalker" :: Text)
, "appearsIn" .= ["NEWHOPE","EMPIRE","JEDI" :: Text]
, "friends" .= [
- object [hanName]
- , object [leiaName]
- , object [c3poName]
- , object [r2d2Name]
+ Aeson.object [hanName]
+ , Aeson.object [leiaName]
+ , Aeson.object [c3poName]
+ , Aeson.object [r2d2Name]
]
]
- , object [
+ , Aeson.object [
hanName
, "appearsIn" .= [ "NEWHOPE","EMPIRE","JEDI" :: Text]
- , "friends" .= [
- object [lukeName]
- , object [leiaName]
- , object [r2d2Name]
+ , "friends" .=
+ [ Aeson.object [lukeName]
+ , Aeson.object [leiaName]
+ , Aeson.object [r2d2Name]
]
]
- , object [
+ , Aeson.object [
leiaName
, "appearsIn" .= [ "NEWHOPE","EMPIRE","JEDI" :: Text]
- , "friends" .= [
- object [lukeName]
- , object [hanName]
- , object [c3poName]
- , object [r2d2Name]
+ , "friends" .=
+ [ Aeson.object [lukeName]
+ , Aeson.object [hanName]
+ , Aeson.object [c3poName]
+ , Aeson.object [r2d2Name]
]
]
]
@@ -116,40 +115,40 @@ spec = describe "Star Wars Query Tests" $ do
}
}
|]
- $ object [ "data" .= object [
- "human" .= object [lukeName]
- ]]
+ $ Aeson.object [ "data" .= Aeson.object
+ [ "human" .= Aeson.object [lukeName]
+ ]]
it "Luke ID with variable" $ testQueryParams
- (\v -> if v == "someId" then Just "1000" else Nothing)
+ (HashMap.singleton "someId" "1000")
[r| query FetchSomeIDQuery($someId: String!) {
human(id: $someId) {
name
}
}
|]
- $ object [ "data" .= object [
- "human" .= object [lukeName]
+ $ Aeson.object [ "data" .= Aeson.object [
+ "human" .= Aeson.object [lukeName]
]]
it "Han ID with variable" $ testQueryParams
- (\v -> if v == "someId" then Just "1002" else Nothing)
+ (HashMap.singleton "someId" "1002")
[r| query FetchSomeIDQuery($someId: String!) {
human(id: $someId) {
name
}
}
|]
- $ object [ "data" .= object [
- "human" .= object [hanName]
+ $ Aeson.object [ "data" .= Aeson.object [
+ "human" .= Aeson.object [hanName]
]]
it "Invalid ID" $ testQueryParams
- (\v -> if v == "id" then Just "Not a valid ID" else Nothing)
+ (HashMap.singleton "id" "Not a valid ID")
[r| query humanQuery($id: String!) {
human(id: $id) {
name
}
}
- |] $ object ["data" .= object ["human" .= Aeson.Null]]
+ |] $ Aeson.object ["data" .= Aeson.object ["human" .= Aeson.Null]]
it "Luke aliased" $ testQuery
[r| query FetchLukeAliased {
luke: human(id: "1000") {
@@ -157,8 +156,8 @@ spec = describe "Star Wars Query Tests" $ do
}
}
|]
- $ object [ "data" .= object [
- "luke" .= object [lukeName]
+ $ Aeson.object [ "data" .= Aeson.object [
+ "luke" .= Aeson.object [lukeName]
]]
it "R2-D2 ID and friends aliased" $ testQuery
[r| query HeroNameAndFriendsQuery {
@@ -171,14 +170,14 @@ spec = describe "Star Wars Query Tests" $ do
}
}
|]
- $ object [ "data" .= object [
- "hero" .= object [
+ $ Aeson.object [ "data" .= Aeson.object [
+ "hero" .= Aeson.object [
"id" .= ("2001" :: Text)
, r2d2Name
- , "friends" .= [
- object ["friendName" .= ("Luke Skywalker" :: Text)]
- , object ["friendName" .= ("Han Solo" :: Text)]
- , object ["friendName" .= ("Leia Organa" :: Text)]
+ , "friends" .=
+ [ Aeson.object ["friendName" .= ("Luke Skywalker" :: Text)]
+ , Aeson.object ["friendName" .= ("Han Solo" :: Text)]
+ , Aeson.object ["friendName" .= ("Leia Organa" :: Text)]
]
]
]]
@@ -192,9 +191,9 @@ spec = describe "Star Wars Query Tests" $ do
}
}
|]
- $ object [ "data" .= object [
- "luke" .= object [lukeName]
- , "leia" .= object [leiaName]
+ $ Aeson.object [ "data" .= Aeson.object
+ [ "luke" .= Aeson.object [lukeName]
+ , "leia" .= Aeson.object [leiaName]
]]
describe "Fragments for complex queries" $ do
@@ -210,9 +209,9 @@ spec = describe "Star Wars Query Tests" $ do
}
}
|]
- $ object [ "data" .= object [
- "luke" .= object [lukeName, tatooine]
- , "leia" .= object [leiaName, alderaan]
+ $ Aeson.object [ "data" .= Aeson.object [
+ "luke" .= Aeson.object [lukeName, tatooine]
+ , "leia" .= Aeson.object [leiaName, alderaan]
]]
it "Fragment for duplicate content" $ testQuery
[r| query UseFragment {
@@ -228,9 +227,9 @@ spec = describe "Star Wars Query Tests" $ do
homePlanet
}
|]
- $ object [ "data" .= object [
- "luke" .= object [lukeName, tatooine]
- , "leia" .= object [leiaName, alderaan]
+ $ Aeson.object [ "data" .= Aeson.object [
+ "luke" .= Aeson.object [lukeName, tatooine]
+ , "leia" .= Aeson.object [leiaName, alderaan]
]]
describe "__typename" $ do
@@ -242,8 +241,11 @@ spec = describe "Star Wars Query Tests" $ do
}
}
|]
- $ object ["data" .= object [
- "hero" .= object ["__typename" .= ("Droid" :: Text), r2d2Name]
+ $ Aeson.object ["data" .= Aeson.object [
+ "hero" .= Aeson.object
+ [ "__typename" .= ("Droid" :: Text)
+ , r2d2Name
+ ]
]]
it "Luke is a human" $ testQuery
[r| query CheckTypeOfLuke {
@@ -253,8 +255,11 @@ spec = describe "Star Wars Query Tests" $ do
}
}
|]
- $ object ["data" .= object [
- "hero" .= object ["__typename" .= ("Human" :: Text), lukeName]
+ $ Aeson.object ["data" .= Aeson.object [
+ "hero" .= Aeson.object
+ [ "__typename" .= ("Human" :: Text)
+ , lukeName
+ ]
]]
describe "Errors in resolvers" $ do
@@ -267,15 +272,15 @@ spec = describe "Star Wars Query Tests" $ do
}
}
|]
- $ object
- [ "data" .= object
- [ "hero" .= object
+ $ Aeson.object
+ [ "data" .= Aeson.object
+ [ "hero" .= Aeson.object
[ "name" .= ("R2-D2" :: Text)
, "secretBackstory" .= Aeson.Null
]
]
, "errors" .=
- [ object
+ [ Aeson.object
["message" .= ("secretBackstory is secret." :: Text)]
]
]
@@ -290,19 +295,19 @@ spec = describe "Star Wars Query Tests" $ do
}
}
|]
- $ object ["data" .= object
- [ "hero" .= object
+ $ Aeson.object ["data" .= Aeson.object
+ [ "hero" .= Aeson.object
[ "name" .= ("R2-D2" :: Text)
, "friends" .=
- [ object
+ [ Aeson.object
[ "name" .= ("Luke Skywalker" :: Text)
, "secretBackstory" .= Aeson.Null
]
- , object
+ , Aeson.object
[ "name" .= ("Han Solo" :: Text)
, "secretBackstory" .= Aeson.Null
]
- , object
+ , Aeson.object
[ "name" .= ("Leia Organa" :: Text)
, "secretBackstory" .= Aeson.Null
]
@@ -310,9 +315,15 @@ spec = describe "Star Wars Query Tests" $ do
]
]
, "errors" .=
- [ object ["message" .= ("secretBackstory is secret." :: Text)]
- , object ["message" .= ("secretBackstory is secret." :: Text)]
- , object ["message" .= ("secretBackstory is secret." :: Text)]
+ [ Aeson.object
+ [ "message" .= ("secretBackstory is secret." :: Text)
+ ]
+ , Aeson.object
+ [ "message" .= ("secretBackstory is secret." :: Text)
+ ]
+ , Aeson.object
+ [ "message" .= ("secretBackstory is secret." :: Text)
+ ]
]
]
it "error on secretBackstory with alias" $ testQuery
@@ -323,15 +334,17 @@ spec = describe "Star Wars Query Tests" $ do
}
}
|]
- $ object
- [ "data" .= object
- [ "mainHero" .= object
+ $ Aeson.object
+ [ "data" .= Aeson.object
+ [ "mainHero" .= Aeson.object
[ "name" .= ("R2-D2" :: Text)
, "story" .= Aeson.Null
]
]
, "errors" .=
- [ object ["message" .= ("secretBackstory is secret." :: Text)]
+ [ Aeson.object
+ [ "message" .= ("secretBackstory is secret." :: Text)
+ ]
]
]
@@ -345,7 +358,8 @@ spec = describe "Star Wars Query Tests" $ do
alderaan = "homePlanet" .= ("Alderaan" :: Text)
testQuery :: Text -> Aeson.Value -> Expectation
-testQuery q expected = graphql schema q >>= flip shouldBe expected
+testQuery q expected = runIdentity (graphql schema q) `shouldBe` expected
testQueryParams :: Subs -> Text -> Aeson.Value -> Expectation
-testQueryParams f q expected = graphqlSubs schema f q >>= flip shouldBe expected
+testQueryParams f q expected =
+ runIdentity (graphqlSubs schema f q) `shouldBe` expected
diff --git a/tests/Test/StarWars/Schema.hs b/tests/Test/StarWars/Schema.hs
index 7b98747..cd25599 100644
--- a/tests/Test/StarWars/Schema.hs
+++ b/tests/Test/StarWars/Schema.hs
@@ -1,4 +1,3 @@
-{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
module Test.StarWars.Schema
( character
@@ -10,9 +9,12 @@ module Test.StarWars.Schema
import Control.Monad.Trans.Except (throwE)
import Control.Monad.Trans.Class (lift)
-import Control.Monad.IO.Class (MonadIO(..))
+import Data.Functor.Identity (Identity)
+import Data.HashMap.Strict (HashMap)
+import qualified Data.HashMap.Strict as HashMap
import Data.List.NonEmpty (NonEmpty(..))
import Data.Maybe (catMaybes)
+import Data.Text (Text)
import qualified Language.GraphQL.Schema as Schema
import Language.GraphQL.Trans
import qualified Language.GraphQL.Type as Type
@@ -20,32 +22,37 @@ import Test.StarWars.Data
-- See https://github.com/graphql/graphql-js/blob/master/src/__tests__/starWarsSchema.js
-schema :: MonadIO m => NonEmpty (Schema.Resolver m)
-schema = hero :| [human, droid]
+schema :: HashMap Text (NonEmpty (Schema.Resolver Identity))
+schema = HashMap.singleton "Query" $ hero :| [human, droid]
-hero :: MonadIO m => Schema.Resolver m
-hero = Schema.objectA "hero" $ \case
- [] -> character artoo
- [Schema.Argument "episode" (Schema.Enum "NEWHOPE")] -> character $ getHero 4
- [Schema.Argument "episode" (Schema.Enum "EMPIRE" )] -> character $ getHero 5
- [Schema.Argument "episode" (Schema.Enum "JEDI" )] -> character $ getHero 6
- _ -> ActionT $ throwE "Invalid arguments."
+hero :: Schema.Resolver Identity
+hero = Schema.object "hero" $ do
+ episode <- argument "episode"
+ character $ case episode of
+ Schema.Enum "NEWHOPE" -> getHero 4
+ Schema.Enum "EMPIRE" -> getHero 5
+ Schema.Enum "JEDI" -> getHero 6
+ _ -> artoo
-human :: MonadIO m => Schema.Resolver m
-human = Schema.wrappedObjectA "human" $ \case
- [Schema.Argument "id" (Schema.String i)] -> do
- humanCharacter <- lift $ return $ getHuman i >>= Just
- case humanCharacter of
- Nothing -> return Type.Null
- Just e -> Type.Named <$> character e
- _ -> ActionT $ throwE "Invalid arguments."
+human :: Schema.Resolver Identity
+human = Schema.wrappedObject "human" $ do
+ id' <- argument "id"
+ case id' of
+ Schema.String i -> do
+ humanCharacter <- lift $ return $ getHuman i >>= Just
+ case humanCharacter of
+ Nothing -> return Type.Null
+ Just e -> Type.Named <$> character e
+ _ -> ActionT $ throwE "Invalid arguments."
-droid :: MonadIO m => Schema.Resolver m
-droid = Schema.objectA "droid" $ \case
- [Schema.Argument "id" (Schema.String i)] -> character =<< liftIO (getDroid i)
- _ -> ActionT $ throwE "Invalid arguments."
+droid :: Schema.Resolver Identity
+droid = Schema.object "droid" $ do
+ id' <- argument "id"
+ case id' of
+ Schema.String i -> character =<< getDroid i
+ _ -> ActionT $ throwE "Invalid arguments."
-character :: MonadIO m => Character -> ActionT m [Schema.Resolver m]
+character :: Character -> ActionT Identity [Schema.Resolver Identity]
character char = return
[ Schema.scalar "id" $ return $ id_ char
, Schema.scalar "name" $ return $ name char