51 Commits

Author SHA1 Message Date
9232e08eb9 Release 0.7.0.0 2020-05-11 12:34:48 +02:00
500cff20eb Separate Query and Mutation resolvers
Fixes #33 .
2020-05-10 18:32:58 +02:00
387d158bd1 Write contrinbuting guidelines 2020-04-12 08:32:39 +02:00
2760bd8ee1 Don't encode controls as block strings
Fixes #39.

String containing control sequences should be encoded as simple strings
even if they contain newlines, since the block strings can contain only
SourceCharacters.
2020-04-10 11:19:36 +02:00
30d6a0a58d encode null value as "null" 2020-03-31 10:04:34 +03:00
613e929d91 Update to Stack 15.x 2020-02-20 05:16:14 +01:00
c0e5e30e76 Document schema AST
Fixes #8.
2020-02-14 06:20:05 +01:00
67bebf853c Replace MonadIO constraint with just Monad
And make the tests use Identity instead of IO.
2020-02-01 20:46:35 +01:00
e8b82122c6 Try all extension parsers 2020-01-28 11:08:28 +01:00
a6bd2370b6 Parse type extensions
Signed-off-by: Eugen Wissner <belka@caraus.de>
2020-01-26 11:55:15 +01:00
b4a3c98114 Parse schema extensions 2020-01-25 16:45:39 +01:00
cb5270b197 Update copyright 2020-01-21 23:27:21 +01:00
3ef27f9d11 Add "extend symbol" lexer to parse extensions 2020-01-17 12:29:06 +01:00
ba710a3c96 Parse complete TypeSystemDefinition 2020-01-15 20:20:50 +01:00
d257d05d4e Parse enum and input object type definitions 2020-01-13 08:21:02 +01:00
adffa185bb Parse interface type definition 2020-01-13 08:21:02 +01:00
f4ed06741d Parse union definitions 2020-01-13 08:21:02 +01:00
8efb08fda1 Parse ObjectDefinition 2020-01-13 08:21:02 +01:00
d9a2937b55 Parse SchemaDefinition 2020-01-13 08:18:30 +01:00
f4f076fa59 Reduce usage of the opt parser
opt directives = some directive
All other occurrences of opt parse an optional list withing some
delimiters (braces, parens).
2020-01-13 08:11:22 +01:00
6d951491be Replace Parser.manyNE with NonEmpty.some 2020-01-12 07:19:28 +01:00
dd8f312cb3 Rewrite argument list to argument map 2020-01-01 10:58:11 +01:00
d82d5a36b3 Retrieve resolver arguments from the reader 2019-12-31 08:29:03 +01:00
44dc80bb37 Replace substitution function with a map
It makes using variables with queries more approachable, but some work
still has to be done.
- The type `Subs` should be renamed and moved out of `Schema`, together with
`AST.Core.Value` probably.
- Some kind of conversion should be possible from a user-defined input
type T to the Value. So the final HashMap should have a type like
`HashMap name a`, where a is an instance of a potential typeclass
InputType.
2019-12-30 18:26:24 +01:00
fdf5914626 Move AST to AST.Document 2019-12-28 07:07:58 +01:00
78ee76f9d5 Define schema AST.
Large parts of the schema aren't exported publically. They will be made
public during writing the parser.

Fixes #6.
2019-12-27 09:14:12 +01:00
56d88310df Add definition module 2019-12-26 13:07:21 +01:00
e3a495a778 Add changelog header and versioning policy 2019-12-26 13:05:17 +01:00
62f3c34bfe Replace AST.Selection data constructors 2019-12-25 06:45:29 +01:00
bdf711d69f Release 0.6.1.0 2019-12-23 06:35:32 +01:00
b215e1a4a7 Pretify multi-line string arguments as block strings
Fixes #10.
2019-12-21 09:25:05 +01:00
1e55f17e7e Encode Unicode. Fix #34 2019-12-20 07:58:09 +01:00
9a5d54c035 Escape non-source characters in the encoder 2019-12-19 06:59:27 +01:00
0cbe69736b Move Execute.Directive to Type.Directive
Just to roughly follow the structure of the reference implementation.
2019-12-18 09:03:18 +01:00
4c0d226030 Move Transform to Language.GraphQL.Execute
Language.GraphQL.AST.Transform is an internal module. Even though it
works with the AST, it is a part of the execution process, it translates
the original parser tree into a simpler one, so the executor has less
work to do. Language.GraphQL.AST should contain only the parser and be
independent from other packages, so it can be used on its own.
2019-12-07 09:46:00 +01:00
3c1a5c800f Support directives (skip and include)
Fixes #24.
2019-12-06 22:52:24 +01:00
fc9ad9c4a1 Consider __typename when evaluating fragments
Fixes #30.
2019-12-02 07:43:19 +01:00
def52ddc20 Fix strings not consuming spaces
Fixes #28
2019-11-28 19:09:26 +11:00
3497784984 Release 0.6.0.0 2019-11-27 08:26:51 +01:00
587aab005e Add a reader instance to the resolvers
The Reader contains a Name/Value hashmap, which will contain resolver
arguments.
2019-11-23 09:49:12 +01:00
625d7100ca Try type parsers in a different order 2019-11-22 08:00:50 +01:00
73e21661b4 Fix failed parsing on multiple required arguments
Fixes #25.
2019-11-21 08:51:42 +01:00
7b92e5bcfd Rewrite selections into a Sequence. Fix #21 2019-11-16 11:41:40 +01:00
115aa02672 Fail on cyclic fragments, fix #22 2019-11-14 20:40:09 +01:00
31c516927d Support nested fragments in any order
Fix #19.
2019-11-12 10:47:10 +01:00
1dd6b7b013 Support nested fragments
... without forward lookup.
2019-11-09 23:24:31 +01:00
b77da3d492 AST.Transform: Pass down a reader
The reader contains variable substitution functions and fragments.
2019-11-07 06:34:36 +01:00
73fc334bf8 Move related modules to Language.GraphQL.AST
Fixes #18.

- `Language.GraphQL.Encoder` moved to `Language.GraphQL.AST.Encoder`.
- `Language.GraphQL.Parser` moved to `Language.GraphQL.AST.Parser`.
- `Language.GraphQL.Lexer` moved to `Language.GraphQL.AST.Lexer`.
- All `Language.GraphQL.AST.Value` data constructor prefixes were removed. The
  module should be imported qualified.
- All `Language.GraphQL.AST.Core.Value` data constructor prefixes were removed.
  The module should be imported qualified.
- `Language.GraphQL.AST.Transform` is now isn't exposed publically anymore.
2019-11-03 11:00:18 +01:00
417ff5da7d Propagate Maybe in the transform tree
Since the transform tree can already find some errors, it may fail here
and there. Almost all functions return a Maybe to signalize an error.
Will be replaced with an Either of course.
2019-11-02 06:24:21 +01:00
0e3b6184be Save fragments in a hash map
Fixes #20.
2019-10-31 07:32:51 +01:00
51d39b69e8 Remove deprecated functions and aliases 2019-10-25 09:07:45 +02:00
38 changed files with 2637 additions and 1264 deletions

3
.gitignore vendored
View File

@ -9,3 +9,6 @@
cabal.sandbox.config
cabal.project.local
/graphql.cabal
# GHC
*.hi

View File

@ -1,6 +1,116 @@
# 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
`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
resolver is missing, it is assumed that the type condition is satisfied (all
fragments are included).
- Escaping characters during encoding.
### Added
- Directive support (@skip and @include).
- Pretifying multi-line string arguments as block strings.
## [0.6.0.0] - 2019-11-27
### Changed
- `Language.GraphQL.Encoder` moved to `Language.GraphQL.AST.Encoder`.
- `Language.GraphQL.Parser` moved to `Language.GraphQL.AST.Parser`.
- `Language.GraphQL.Lexer` moved to `Language.GraphQL.AST.Lexer`.
- All `Language.GraphQL.AST.Value` data constructor prefixes were removed. The
module should be imported qualified.
- All `Language.GraphQL.AST.Core.Value` data constructor prefixes were removed.
The module should be imported qualified.
- `Language.GraphQL.AST.Core.Object` is now just a HashMap.
- `Language.GraphQL.AST.Transform` is isn't exposed publically anymore.
- `Language.GraphQL.Schema.resolve` accepts a selection `Seq` (`Data.Sequence`)
instead of a list. Selections are stored as sequences internally as well.
- Add a reader instance to the resolver's monad stack. The Reader contains
a Name/Value hashmap, which will contain resolver arguments.
### Added
- Nested fragment support.
### Fixed
- Consume ignored tokens after `$` and `!`. I mistakenly assumed that
`$variable` is a single token, same as `Type!` is a single token. This is not
the case, for example `Variable` is defined as `$ Name`, so these are two
tokens, therefore whitespaces and commas after `$` and `!` should be
consumed.
### Improved
- `Language.GraphQL.AST.Parser.type_`: Try type parsers in a variable
definition in a different order to avoid using `but`.
### Removed
- `Language.GraphQL.AST.Arguments`. Use `[Language.GraphQL.AST.Argument]`
instead.
- `Language.GraphQL.AST.Directives`. Use `[Language.GraphQL.AST.Directives]`
instead.
- `Language.GraphQL.AST.VariableDefinitions`. Use
`[Language.GraphQL.AST.VariableDefinition]` instead.
- `Language.GraphQL.AST.FragmentName`. Use `Language.GraphQL.AST.Name` instead.
- `Language.GraphQL.Execute.Schema` - It was a resolver list, not a schema.
- `Language.GraphQL.Schema`: `enum`, `enumA`, `wrappedEnum` and `wrappedEnumA`.
Use `scalar`, `scalarA`, `wrappedScalar` and `wrappedScalarA` instead.
## [0.5.1.0] - 2019-10-22
### Deprecated
- `Language.GraphQL.AST.Arguments`. Use `[Language.GraphQL.AST.Argument]`
@ -105,6 +215,10 @@ 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
[0.5.0.1]: https://github.com/caraus-ecms/graphql/compare/v0.5.0.0...v0.5.0.1
[0.5.0.0]: https://github.com/caraus-ecms/graphql/compare/v0.4.0.0...v0.5.0.0

31
CONTRIBUTING.md Normal file
View File

@ -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
```

View File

@ -1,4 +1,4 @@
Copyright 2019 Eugen Wissner, Germany
Copyright 2019-2020 Eugen Wissner, Germany
Copyright 2015-2017 J. Daniel Navarro
All rights reserved.

View File

@ -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.

View File

@ -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 }"

View File

@ -1,5 +1,5 @@
name: graphql
version: 0.5.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>
@ -28,13 +28,18 @@ data-files:
dependencies:
- aeson
- base >= 4.7 && < 5
- containers
- megaparsec
- parser-combinators
- text
- transformers
- unordered-containers
library:
source-dirs: src
other-modules:
- Language.GraphQL.Execute.Transform
- Language.GraphQL.Type.Directive
tests:
tasty:
@ -49,4 +54,5 @@ tests:
- hspec
- hspec-expectations
- hspec-megaparsec
- QuickCheck
- raw-strings-qq

View File

@ -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() {

View File

@ -4,31 +4,31 @@ 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.Parser
import Language.GraphQL.AST.Parser
import qualified Language.GraphQL.Schema as Schema
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)

View File

@ -1,167 +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(..)
, Arguments
, Definition(..)
, Directive(..)
, Directives
, Document
, Field(..)
, FragmentDefinition(..)
, FragmentName
, FragmentSpread(..)
, InlineFragment(..)
, Name
, NonNullType(..)
, ObjectField(..)
, OperationDefinition(..)
, OperationType(..)
, Selection(..)
, SelectionSet
, SelectionSetOpt
, Type(..)
, TypeCondition
, Value(..)
, VariableDefinition(..)
, VariableDefinitions
( module Language.GraphQL.AST.Document
) where
import Data.Int (Int32)
import Data.List.NonEmpty (NonEmpty)
import Data.Text (Text)
import Language.GraphQL.AST.Core ( Alias
, Name
, TypeCondition
)
-- * Document
-- | GraphQL document.
type Document = NonEmpty Definition
-- * 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 a operation.
type SelectionSet = NonEmpty Selection
-- | Field selection.
type SelectionSetOpt = [Selection]
-- | Single selection element.
data Selection
= SelectionField Field
| SelectionFragmentSpread FragmentSpread
| SelectionInlineFragment InlineFragment
deriving (Eq, Show)
-- * Field
-- | GraphQL field.
data Field
= Field (Maybe Alias) Name [Argument] [Directive] SelectionSetOpt
deriving (Eq, Show)
-- * Arguments
-- | Argument list.
{-# DEPRECATED Arguments "Use [Argument] instead" #-}
type Arguments = [Argument]
-- | Argument.
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)
{-# DEPRECATED FragmentName "Use Name instead" #-}
type FragmentName = Name
-- * Input values
-- | Input value.
data Value = ValueVariable Name
| ValueInt Int32
| ValueFloat Double
| ValueString Text
| ValueBoolean Bool
| ValueNull
| ValueEnum Name
| ValueList [Value]
| ValueObject [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 list.
{-# DEPRECATED VariableDefinitions "Use [VariableDefinition] instead" #-}
type VariableDefinitions = [VariableDefinition]
-- | Variable definition.
data VariableDefinition = VariableDefinition Name Type (Maybe Value)
deriving (Eq, Show)
-- * Input types
-- | 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)
-- * Directives
-- | Directive list.
{-# DEPRECATED Directives "Use [Directive] instead" #-}
type Directives = [Directive]
-- | Directive.
data Directive = Directive Name [Argument] deriving (Eq, Show)
import Language.GraphQL.AST.Document

View File

@ -1,12 +1,12 @@
-- | This is the AST meant to be executed.
module Language.GraphQL.AST.Core
( Alias
, Argument(..)
, Arguments(..)
, Directive(..)
, Document
, Field(..)
, Fragment(..)
, Name
, ObjectField(..)
, Operation(..)
, Selection(..)
, TypeCondition
@ -14,12 +14,12 @@ module Language.GraphQL.AST.Core
) where
import Data.Int (Int32)
import Data.HashMap.Strict (HashMap)
import Data.List.NonEmpty (NonEmpty)
import Data.String
import Data.Sequence (Seq)
import Data.String (IsString(..))
import Data.Text (Text)
-- | Name
type Name = Text
import Language.GraphQL.AST (Alias, Name, TypeCondition)
-- | GraphQL document is a non-empty list of operations.
type Document = NonEmpty Operation
@ -28,87 +28,32 @@ type Document = NonEmpty Operation
--
-- Currently only queries and mutations are supported.
data Operation
= Query (Maybe Text) (NonEmpty Selection)
| Mutation (Maybe Text) (NonEmpty Selection)
= Query (Maybe Text) (Seq Selection)
| Mutation (Maybe Text) (Seq Selection)
deriving (Eq, Show)
-- | A single GraphQL field.
--
-- 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 "name". "id" and "name don't have any
-- arguments.
data Field = Field (Maybe Alias) Name [Argument] [Selection] 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)
-- | Represents accordingly typed GraphQL values.
data Value
= ValueInt Int32
-- GraphQL Float is double precision
| ValueFloat Double
| ValueString Text
| ValueBoolean Bool
| ValueNull
| ValueEnum Name
| ValueList [Value]
| ValueObject [ObjectField]
-- | Single GraphQL field.
data Field
= Field (Maybe Alias) Name Arguments (Seq Selection)
deriving (Eq, Show)
instance IsString Value where
fromString = ValueString . fromString
-- | Argument list.
newtype Arguments = Arguments (HashMap Name Value)
deriving (Eq, Show)
-- | Key-value pair.
--
-- A list of 'ObjectField's represents a GraphQL object type.
data ObjectField = ObjectField Name Value deriving (Eq, Show)
instance Semigroup Arguments where
(Arguments x) <> (Arguments y) = Arguments $ x <> y
-- | Type condition.
type TypeCondition = Name
instance Monoid Arguments where
mempty = Arguments mempty
-- | Directive.
data Directive = Directive Name Arguments
deriving (Eq, Show)
-- | Represents fragments and inline fragments.
data Fragment
= Fragment TypeCondition (NonEmpty Selection)
= Fragment TypeCondition (Seq Selection)
deriving (Eq, Show)
-- | Single selection element.
@ -116,3 +61,18 @@ data Selection
= SelectionFragment Fragment
| SelectionField Field
deriving (Eq, Show)
-- | Represents accordingly typed GraphQL values.
data Value
= Int Int32
| Float Double -- ^ GraphQL Float is double precision
| String Text
| Boolean Bool
| Null
| Enum Name
| List [Value]
| Object (HashMap Name Value)
deriving (Eq, Show)
instance IsString Value where
fromString = String . fromString

View File

@ -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)

View File

@ -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)

View File

@ -0,0 +1,344 @@
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ExplicitForAll #-}
-- | This module defines a minifier and a printer for the @GraphQL@ language.
module Language.GraphQL.AST.Encoder
( Formatter
, definition
, directive
, document
, minified
, pretty
, type'
, value
) where
import Data.Char (ord)
import Data.Foldable (fold)
import qualified Data.List.NonEmpty as NonEmpty
import Data.Text (Text)
import qualified Data.Text as Text
import qualified Data.Text.Lazy as Lazy (Text)
import qualified Data.Text.Lazy as Lazy.Text
import Data.Text.Lazy.Builder (Builder)
import qualified Data.Text.Lazy.Builder as Builder
import Data.Text.Lazy.Builder.Int (decimal, hexadecimal)
import Data.Text.Lazy.Builder.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.
--
-- Use 'pretty' or 'minified' to construct the formatter.
data Formatter
= Minified
| Pretty Word
-- | Constructs a formatter for pretty printing.
pretty :: Formatter
pretty = Pretty 0
-- | Constructs a formatter for minifying.
minified :: Formatter
minified = Minified
-- | 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 = foldr executableDefinition [] defs
executableDefinition (ExecutableDefinition x) acc = definition formatter x : acc
executableDefinition _ acc = acc
-- | 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
where
encodeDefinition (Full.DefinitionOperation operation)
= operationDefinition formatter operation
encodeDefinition (Full.DefinitionFragment fragment)
= fragmentDefinition formatter fragment
-- | Converts a 'Full.OperationDefinition into a string.
operationDefinition :: Formatter -> Full.OperationDefinition -> Lazy.Text
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] ->
[Full.Directive] ->
Full.SelectionSet ->
Lazy.Text
node formatter name vars dirs sels
= Lazy.Text.fromStrict (fold name)
<> optempty (variableDefinitions formatter) vars
<> optempty (directives formatter) dirs
<> eitherFormat formatter " " mempty
<> selectionSet formatter sels
variableDefinitions :: Formatter -> [Full.VariableDefinition] -> Lazy.Text
variableDefinitions formatter
= parensCommas formatter $ variableDefinition formatter
variableDefinition :: Formatter -> Full.VariableDefinition -> Lazy.Text
variableDefinition formatter (Full.VariableDefinition var ty dv)
= variable var
<> eitherFormat formatter ": " ":"
<> type' ty
<> maybe mempty (defaultValue formatter) dv
defaultValue :: Formatter -> Full.Value -> Lazy.Text
defaultValue formatter val
= eitherFormat formatter " = " "="
<> value formatter val
variable :: Full.Name -> Lazy.Text
variable var = "$" <> Lazy.Text.fromStrict var
selectionSet :: Formatter -> Full.SelectionSet -> Lazy.Text
selectionSet formatter
= bracesList formatter (selection formatter)
. NonEmpty.toList
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) indentSymbol
selection :: Formatter -> Full.Selection -> Lazy.Text
selection formatter = Lazy.Text.append indent' . encodeSelection
where
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
indent'
| Pretty indentation <- formatter = indent $ indentation + 1
| otherwise = ""
colon :: Formatter -> Lazy.Text
colon formatter = eitherFormat formatter ": " ":"
-- | 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
<> optempty (directives formatter) dirs
<> optempty selectionSetOpt' set
where
prependAlias aliasName = Lazy.Text.fromStrict aliasName <> colon formatter
selectionSetOpt' = (eitherFormat formatter " " "" <>)
. selectionSetOpt formatter
arguments :: Formatter -> [Full.Argument] -> Lazy.Text
arguments formatter = parensCommas formatter $ argument formatter
argument :: Formatter -> Full.Argument -> Lazy.Text
argument formatter (Full.Argument name value')
= Lazy.Text.fromStrict name
<> colon formatter
<> value formatter value'
-- * Fragments
fragmentSpread :: Formatter -> Full.Name -> [Full.Directive] -> Lazy.Text
fragmentSpread formatter name directives'
= "..." <> Lazy.Text.fromStrict name
<> optempty (directives formatter) directives'
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
<> selectionSet formatter sels
fragmentDefinition :: Formatter -> Full.FragmentDefinition -> Lazy.Text
fragmentDefinition formatter (Full.FragmentDefinition name tc dirs sels)
= "fragment " <> Lazy.Text.fromStrict name
<> " on " <> Lazy.Text.fromStrict tc
<> optempty (directives formatter) dirs
<> eitherFormat formatter " " mempty
<> selectionSet formatter sels
-- * Miscellaneous
-- | Converts a 'Full.Directive' into a string.
directive :: Formatter -> Full.Directive -> Lazy.Text
directive formatter (Full.Directive name args)
= "@" <> Lazy.Text.fromStrict name <> optempty (arguments formatter) args
directives :: Formatter -> [Full.Directive] -> Lazy.Text
directives Minified = spaces (directive Minified)
directives formatter = Lazy.Text.cons ' ' . spaces (directive formatter)
-- | Converts a 'Full.Value' into a string.
value :: Formatter -> Full.Value -> Lazy.Text
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 = "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
value formatter (Full.Object x) = objectValue formatter x
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
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'
| char' == '\\' = Builder.fromString "\\\\"
| char' == '\"' = Builder.fromString "\\\""
| char' == '\b' = Builder.fromString "\\b"
| char' == '\f' = Builder.fromString "\\f"
| char' == '\n' = Builder.fromString "\\n"
| char' == '\r' = Builder.fromString "\\r"
| char' == '\t' = Builder.fromString "\\t"
| char' < '\x0010' = unicode "\\u000" char'
| char' < '\x0020' = unicode "\\u00" char'
| otherwise = Builder.singleton char'
where
unicode prefix = mappend (Builder.fromString prefix) . (hexadecimal . ord)
listValue :: Formatter -> [Full.Value] -> Lazy.Text
listValue formatter = bracketsCommas formatter $ value formatter
objectValue :: Formatter -> [Full.ObjectField] -> Lazy.Text
objectValue formatter = intercalate $ objectField formatter
where
intercalate f
= braces
. Lazy.Text.intercalate (eitherFormat formatter ", " ",")
. fmap f
objectField :: Formatter -> Full.ObjectField -> Lazy.Text
objectField formatter (Full.ObjectField name value') =
Lazy.Text.fromStrict name <> colon formatter <> value formatter value'
-- | Converts a 'Full.Type' a type into a string.
type' :: Full.Type -> Lazy.Text
type' (Full.TypeNamed x) = Lazy.Text.fromStrict x
type' (Full.TypeList x) = listType x
type' (Full.TypeNonNull x) = nonNullType x
listType :: Full.Type -> Lazy.Text
listType x = brackets (type' x)
nonNullType :: Full.NonNullType -> Lazy.Text
nonNullType (Full.NonNullTypeNamed x) = Lazy.Text.fromStrict x <> "!"
nonNullType (Full.NonNullTypeList x) = listType x <> "!"
-- * Internal
between :: Char -> Char -> Lazy.Text -> Lazy.Text
between open close = Lazy.Text.cons open . (`Lazy.Text.snoc` close)
parens :: Lazy.Text -> Lazy.Text
parens = between '(' ')'
brackets :: Lazy.Text -> Lazy.Text
brackets = between '[' ']'
braces :: Lazy.Text -> Lazy.Text
braces = between '{' '}'
spaces :: forall a. (a -> Lazy.Text) -> [a] -> Lazy.Text
spaces f = Lazy.Text.intercalate "\SP" . fmap f
parensCommas :: forall a. Formatter -> (a -> Lazy.Text) -> [a] -> Lazy.Text
parensCommas formatter f
= parens
. Lazy.Text.intercalate (eitherFormat formatter ", " ",")
. fmap f
bracketsCommas :: Formatter -> (a -> Lazy.Text) -> [a] -> Lazy.Text
bracketsCommas formatter f
= brackets
. Lazy.Text.intercalate (eitherFormat formatter ", " ",")
. fmap f
bracesList :: forall a. Formatter -> (a -> Lazy.Text) -> [a] -> Lazy.Text
bracesList (Pretty intendation) f xs
= Lazy.Text.snoc (Lazy.Text.intercalate "\n" content) '\n'
<> (Lazy.Text.snoc $ Lazy.Text.replicate (fromIntegral intendation) " ") '}'
where
content = "{" : fmap f xs
bracesList Minified f xs = braces $ Lazy.Text.intercalate "," $ fmap f xs
optempty :: (Eq a, Monoid a, Monoid b) => (a -> b) -> a -> b
optempty f xs = if xs == mempty then mempty else f xs
eitherFormat :: forall a. Formatter -> a -> a -> a
eitherFormat (Pretty _) x _ = x
eitherFormat Minified _ x = x

View File

@ -3,7 +3,7 @@
-- | This module defines a bunch of small parsers used to parse individual
-- lexemes.
module Language.GraphQL.Lexer
module Language.GraphQL.AST.Lexer
( Parser
, amp
, at
@ -15,6 +15,7 @@ module Language.GraphQL.Lexer
, dollar
, comment
, equals
, extend
, integer
, float
, lexeme
@ -28,20 +29,16 @@ module Language.GraphQL.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
@ -89,16 +84,16 @@ symbol :: T.Text -> Parser T.Text
symbol = Lexer.symbol spaceConsumer
-- | Parser for "!".
bang :: Parser Char
bang = char '!'
bang :: Parser T.Text
bang = symbol "!"
-- | Parser for "$".
dollar :: Parser Char
dollar = char '$'
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
string = between "\"" "\"" stringValue <* spaceConsumer
where
stringValue = T.pack <$> many stringCharacter
stringCharacter = satisfy isStringCharacter1
@ -143,7 +138,7 @@ string = between "\"" "\"" stringValue
-- | Parser for block strings.
blockString :: Parser T.Text
blockString = between "\"\"\"" "\"\"\"" stringValue
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

View File

@ -0,0 +1,485 @@
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
-- | @GraphQL@ document parser.
module Language.GraphQL.AST.Parser
( document
) where
import Control.Applicative (Alternative(..), optional)
import Control.Applicative.Combinators (sepBy1)
import qualified Control.Applicative.Combinators.NonEmpty as NonEmpty
import Data.List.NonEmpty (NonEmpty(..))
import Data.Text (Text)
import qualified Language.GraphQL.AST.DirectiveLocation as Directive
import Language.GraphQL.AST.DirectiveLocation
( DirectiveLocation
, ExecutableDirectiveLocation
, TypeSystemDirectiveLocation
)
import Language.GraphQL.AST.Document
import Language.GraphQL.AST.Lexer
import Text.Megaparsec (lookAhead, option, try, (<?>))
-- | Parser for the GraphQL documents.
document :: Parser Document
document = unicodeBOM
>> spaceConsumer
>> lexeme (NonEmpty.some definition)
definition :: Parser Definition
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 = SelectionSet <$> selectionSet
<|> operationDefinition'
<?> "operationDefinition error"
where
operationDefinition'
= OperationDefinition <$> operationType
<*> optional name
<*> variableDefinitions
<*> directives
<*> selectionSet
operationType :: Parser OperationType
operationType = Query <$ symbol "query"
<|> Mutation <$ symbol "mutation"
-- <?> Keep default error message
-- * SelectionSet
selectionSet :: Parser SelectionSet
selectionSet = braces $ NonEmpty.some selection
selectionSetOpt :: Parser SelectionSetOpt
selectionSetOpt = listOptIn braces selection
selection :: Parser Selection
selection = field
<|> try fragmentSpread
<|> inlineFragment
<?> "selection error!"
-- * Field
field :: Parser Selection
field = Field
<$> optional alias
<*> name
<*> arguments
<*> directives
<*> selectionSetOpt
alias :: Parser Alias
alias = try $ name <* colon
-- * Arguments
arguments :: Parser [Argument]
arguments = listOptIn parens argument
argument :: Parser Argument
argument = Argument <$> name <* colon <*> value
-- * Fragments
fragmentSpread :: Parser Selection
fragmentSpread = FragmentSpread
<$ spread
<*> fragmentName
<*> directives
inlineFragment :: Parser Selection
inlineFragment = InlineFragment
<$ spread
<*> optional typeCondition
<*> directives
<*> selectionSet
fragmentDefinition :: Parser FragmentDefinition
fragmentDefinition = FragmentDefinition
<$ symbol "fragment"
<*> name
<*> typeCondition
<*> directives
<*> selectionSet
fragmentName :: Parser Name
fragmentName = but (symbol "on") *> name
typeCondition :: Parser TypeCondition
typeCondition = symbol "on" *> name
-- * Input Values
value :: Parser Value
value = Variable <$> variable
<|> Float <$> try float
<|> Int <$> integer
<|> Boolean <$> booleanValue
<|> Null <$ symbol "null"
<|> String <$> blockString
<|> String <$> string
<|> Enum <$> try enumValue
<|> List <$> listValue
<|> Object <$> objectValue
<?> "value error!"
where
booleanValue :: Parser Bool
booleanValue = True <$ symbol "true"
<|> False <$ symbol "false"
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 <* colon <*> value
-- * Variables
variableDefinitions :: Parser [VariableDefinition]
variableDefinitions = listOptIn parens variableDefinition
variableDefinition :: Parser VariableDefinition
variableDefinition = VariableDefinition
<$> variable
<* colon
<*> type'
<*> defaultValue
<?> "VariableDefinition"
variable :: Parser Name
variable = dollar *> name
defaultValue :: Parser (Maybe Value)
defaultValue = optional (equals *> value) <?> "DefaultValue"
-- * Input Types
type' :: Parser Type
type' = try (TypeNonNull <$> nonNullType)
<|> TypeList <$> brackets type'
<|> TypeNamed <$> name
<?> "Type"
nonNullType :: Parser NonNullType
nonNullType = NonNullTypeNamed <$> name <* bang
<|> NonNullTypeList <$> brackets type' <* bang
<?> "nonNullType error!"
-- * Directives
directives :: Parser [Directive]
directives = many directive
directive :: Parser Directive
directive = Directive
<$ at
<*> name
<*> arguments
-- * Internal
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 ()

View File

@ -1,136 +0,0 @@
{-# LANGUAGE OverloadedStrings #-}
-- | After the document is parsed, before getting executed the AST is
-- transformed into a similar, simpler AST. This module is responsible for
-- this transformation.
module Language.GraphQL.AST.Transform
( document
) where
import Control.Applicative (empty)
import Data.Bifunctor (first)
import Data.Either (partitionEithers)
import Data.Foldable (fold, foldMap)
import Data.List.NonEmpty (NonEmpty)
import qualified Data.List.NonEmpty as NonEmpty
import Data.Monoid (Alt(Alt,getAlt), (<>))
import qualified Language.GraphQL.AST as Full
import qualified Language.GraphQL.AST.Core as Core
import qualified Language.GraphQL.Schema as Schema
-- | Replaces a fragment name by a list of 'Core.Field'. If the name doesn't
-- match an empty list is returned.
type Fragmenter = Core.Name -> [Core.Field]
-- | Rewrites the original syntax tree into an intermediate representation used
-- for query execution.
document :: Schema.Subs -> Full.Document -> Maybe Core.Document
document subs doc = operations subs fr ops
where
(fr, ops) = first foldFrags
. partitionEithers
. NonEmpty.toList
$ defrag subs
<$> doc
foldFrags :: [Fragmenter] -> Fragmenter
foldFrags fs n = getAlt $ foldMap (Alt . ($ n)) fs
-- * Operation
-- TODO: Replace Maybe by MonadThrow CustomError
operations
:: Schema.Subs
-> Fragmenter
-> [Full.OperationDefinition]
-> Maybe Core.Document
operations subs fr = NonEmpty.nonEmpty . fmap (operation subs fr)
operation
:: Schema.Subs
-> Fragmenter
-> Full.OperationDefinition
-> Core.Operation
operation subs fr (Full.OperationSelectionSet sels) =
operation subs fr $ Full.OperationDefinition Full.Query empty empty empty sels
-- TODO: Validate Variable definitions with substituter
operation subs fr (Full.OperationDefinition Full.Query name _vars _dirs sels) =
Core.Query name $ appendSelection subs fr sels
operation subs fr (Full.OperationDefinition Full.Mutation name _vars _dirs sels) =
Core.Mutation name $ appendSelection subs fr sels
selection
:: Schema.Subs
-> Fragmenter
-> Full.Selection
-> Either [Core.Selection] Core.Selection
selection subs fr (Full.SelectionField fld)
= Right $ Core.SelectionField $ field subs fr fld
selection _ fr (Full.SelectionFragmentSpread (Full.FragmentSpread name _))
= Left $ Core.SelectionField <$> fr name
selection subs fr (Full.SelectionInlineFragment fragment)
| (Full.InlineFragment (Just typeCondition) _ selectionSet) <- fragment
= Right
$ Core.SelectionFragment
$ Core.Fragment typeCondition
$ appendSelection subs fr selectionSet
| (Full.InlineFragment Nothing _ selectionSet) <- fragment
= Left $ NonEmpty.toList $ appendSelection subs fr selectionSet
-- * Fragment replacement
-- | Extract Fragments into a single Fragmenter function and a Operation
-- Definition.
defrag
:: Schema.Subs
-> Full.Definition
-> Either Fragmenter Full.OperationDefinition
defrag _ (Full.DefinitionOperation op) =
Right op
defrag subs (Full.DefinitionFragment fragDef) =
Left $ fragmentDefinition subs fragDef
fragmentDefinition :: Schema.Subs -> Full.FragmentDefinition -> Fragmenter
fragmentDefinition subs (Full.FragmentDefinition name _tc _dirs sels) name'
| name == name' = selection' <$> do
selections <- NonEmpty.toList $ selection subs mempty <$> sels
either id pure selections
| otherwise = empty
where
selection' (Core.SelectionField field') = field'
selection' _ = error "Fragments within fragments are not supported yet"
field :: Schema.Subs -> Fragmenter -> Full.Field -> Core.Field
field subs fr (Full.Field a n args _dirs sels) =
Core.Field a n (fold $ argument subs `traverse` args) (foldr go empty sels)
where
go :: Full.Selection -> [Core.Selection] -> [Core.Selection]
go (Full.SelectionFragmentSpread (Full.FragmentSpread name _dirs)) = ((Core.SelectionField <$> fr name) <>)
go sel = (either id pure (selection subs fr sel) <>)
argument :: Schema.Subs -> Full.Argument -> Maybe Core.Argument
argument subs (Full.Argument n v) = Core.Argument n <$> value subs v
value :: Schema.Subs -> Full.Value -> Maybe Core.Value
value subs (Full.ValueVariable n) = subs n
value _ (Full.ValueInt i) = pure $ Core.ValueInt i
value _ (Full.ValueFloat f) = pure $ Core.ValueFloat f
value _ (Full.ValueString x) = pure $ Core.ValueString x
value _ (Full.ValueBoolean b) = pure $ Core.ValueBoolean b
value _ Full.ValueNull = pure Core.ValueNull
value _ (Full.ValueEnum e) = pure $ Core.ValueEnum e
value subs (Full.ValueList l) =
Core.ValueList <$> traverse (value subs) l
value subs (Full.ValueObject o) =
Core.ValueObject <$> traverse (objectField subs) o
objectField :: Schema.Subs -> Full.ObjectField -> Maybe Core.ObjectField
objectField subs (Full.ObjectField n v) = Core.ObjectField n <$> value subs v
appendSelection ::
Schema.Subs ->
Fragmenter ->
NonEmpty Full.Selection ->
NonEmpty Core.Selection
appendSelection subs fr = NonEmpty.fromList
. foldr (either (++) (:) . selection subs fr) []

View File

@ -1,277 +0,0 @@
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ExplicitForAll #-}
-- | This module defines a minifier and a printer for the @GraphQL@ language.
module Language.GraphQL.Encoder
( Formatter
, definition
, directive
, document
, minified
, pretty
, type'
, value
) where
import Data.Foldable (fold)
import Data.Monoid ((<>))
import qualified Data.List.NonEmpty as NonEmpty (toList)
import Data.Text.Lazy (Text)
import qualified Data.Text.Lazy as Text.Lazy
import Data.Text.Lazy.Builder (toLazyText)
import Data.Text.Lazy.Builder.Int (decimal)
import Data.Text.Lazy.Builder.RealFloat (realFloat)
import Language.GraphQL.AST
-- | Instructs the encoder whether a GraphQL should be minified or pretty
-- printed.
--
-- Use 'pretty' and 'minified' to construct the formatter.
data Formatter
= Minified
| Pretty Word
-- | Constructs a formatter for pretty printing.
pretty :: Formatter
pretty = Pretty 0
-- | Constructs a formatter for minifying.
minified :: Formatter
minified = Minified
-- | Converts a 'Document' into a string.
document :: Formatter -> Document -> Text
document formatter defs
| Pretty _ <- formatter = Text.Lazy.intercalate "\n" encodeDocument
| Minified <-formatter = Text.Lazy.snoc (mconcat encodeDocument) '\n'
where
encodeDocument = NonEmpty.toList $ definition formatter <$> defs
-- | Converts a 'Definition' into a string.
definition :: Formatter -> Definition -> Text
definition formatter x
| Pretty _ <- formatter = Text.Lazy.snoc (encodeDefinition x) '\n'
| Minified <- formatter = encodeDefinition x
where
encodeDefinition (DefinitionOperation operation)
= operationDefinition formatter operation
encodeDefinition (DefinitionFragment fragment)
= fragmentDefinition formatter fragment
operationDefinition :: Formatter -> OperationDefinition -> Text
operationDefinition formatter (OperationSelectionSet sels)
= selectionSet formatter sels
operationDefinition formatter (OperationDefinition Query name vars dirs sels)
= "query " <> node formatter name vars dirs sels
operationDefinition formatter (OperationDefinition Mutation name vars dirs sels)
= "mutation " <> node formatter name vars dirs sels
node :: Formatter
-> Maybe Name
-> [VariableDefinition]
-> [Directive]
-> SelectionSet
-> Text
node formatter name vars dirs sels
= Text.Lazy.fromStrict (fold name)
<> optempty (variableDefinitions formatter) vars
<> optempty (directives formatter) dirs
<> eitherFormat formatter " " mempty
<> selectionSet formatter sels
variableDefinitions :: Formatter -> [VariableDefinition] -> Text
variableDefinitions formatter
= parensCommas formatter $ variableDefinition formatter
variableDefinition :: Formatter -> VariableDefinition -> Text
variableDefinition formatter (VariableDefinition var ty dv)
= variable var
<> eitherFormat formatter ": " ":"
<> type' ty
<> maybe mempty (defaultValue formatter) dv
defaultValue :: Formatter -> Value -> Text
defaultValue formatter val
= eitherFormat formatter " = " "="
<> value formatter val
variable :: Name -> Text
variable var = "$" <> Text.Lazy.fromStrict var
selectionSet :: Formatter -> SelectionSet -> Text
selectionSet formatter
= bracesList formatter (selection formatter)
. NonEmpty.toList
selectionSetOpt :: Formatter -> SelectionSetOpt -> Text
selectionSetOpt formatter = bracesList formatter $ selection formatter
selection :: Formatter -> Selection -> Text
selection formatter = Text.Lazy.append indent . f
where
f (SelectionField x) = field incrementIndent x
f (SelectionInlineFragment x) = inlineFragment incrementIndent x
f (SelectionFragmentSpread x) = fragmentSpread incrementIndent x
incrementIndent
| Pretty n <- formatter = Pretty $ n + 1
| otherwise = Minified
indent
| Pretty n <- formatter = Text.Lazy.replicate (fromIntegral $ n + 1) " "
| otherwise = mempty
field :: Formatter -> Field -> Text
field formatter (Field alias name args dirs selso)
= optempty (`Text.Lazy.append` colon) (Text.Lazy.fromStrict $ fold alias)
<> Text.Lazy.fromStrict name
<> optempty (arguments formatter) args
<> optempty (directives formatter) dirs
<> selectionSetOpt'
where
colon = eitherFormat formatter ": " ":"
selectionSetOpt'
| null selso = mempty
| otherwise = eitherFormat formatter " " mempty <> selectionSetOpt formatter selso
arguments :: Formatter -> [Argument] -> Text
arguments formatter = parensCommas formatter $ argument formatter
argument :: Formatter -> Argument -> Text
argument formatter (Argument name v)
= Text.Lazy.fromStrict name
<> eitherFormat formatter ": " ":"
<> value formatter v
-- * Fragments
fragmentSpread :: Formatter -> FragmentSpread -> Text
fragmentSpread formatter (FragmentSpread name ds)
= "..." <> Text.Lazy.fromStrict name <> optempty (directives formatter) ds
inlineFragment :: Formatter -> InlineFragment -> Text
inlineFragment formatter (InlineFragment tc dirs sels)
= "... on "
<> Text.Lazy.fromStrict (fold tc)
<> directives formatter dirs
<> eitherFormat formatter " " mempty
<> selectionSet formatter sels
fragmentDefinition :: Formatter -> FragmentDefinition -> Text
fragmentDefinition formatter (FragmentDefinition name tc dirs sels)
= "fragment " <> Text.Lazy.fromStrict name
<> " on " <> Text.Lazy.fromStrict tc
<> optempty (directives formatter) dirs
<> eitherFormat formatter " " mempty
<> selectionSet formatter sels
-- * Miscellaneous
-- | Converts a 'Directive' into a string.
directive :: Formatter -> Directive -> Text
directive formatter (Directive name args)
= "@" <> Text.Lazy.fromStrict name <> optempty (arguments formatter) args
directives :: Formatter -> [Directive] -> Text
directives formatter@(Pretty _) = Text.Lazy.cons ' ' . spaces (directive formatter)
directives Minified = spaces (directive Minified)
-- | Converts a 'Value' into a string.
value :: Formatter -> Value -> Text
value _ (ValueVariable x) = variable x
value _ (ValueInt x) = toLazyText $ decimal x
value _ (ValueFloat x) = toLazyText $ realFloat x
value _ (ValueBoolean x) = booleanValue x
value _ ValueNull = mempty
value _ (ValueString x) = stringValue $ Text.Lazy.fromStrict x
value _ (ValueEnum x) = Text.Lazy.fromStrict x
value formatter (ValueList x) = listValue formatter x
value formatter (ValueObject x) = objectValue formatter x
booleanValue :: Bool -> Text
booleanValue True = "true"
booleanValue False = "false"
stringValue :: Text -> Text
stringValue
= quotes
. Text.Lazy.replace "\"" "\\\""
. Text.Lazy.replace "\\" "\\\\"
listValue :: Formatter -> [Value] -> Text
listValue formatter = bracketsCommas formatter $ value formatter
objectValue :: Formatter -> [ObjectField] -> Text
objectValue formatter = intercalate $ objectField formatter
where
intercalate f
= braces
. Text.Lazy.intercalate (eitherFormat formatter ", " ",")
. fmap f
objectField :: Formatter -> ObjectField -> Text
objectField formatter (ObjectField name v)
= Text.Lazy.fromStrict name <> colon <> value formatter v
where
colon
| Pretty _ <- formatter = ": "
| Minified <- formatter = ":"
-- | Converts a 'Type' a type into a string.
type' :: Type -> Text
type' (TypeNamed x) = Text.Lazy.fromStrict x
type' (TypeList x) = listType x
type' (TypeNonNull x) = nonNullType x
listType :: Type -> Text
listType x = brackets (type' x)
nonNullType :: NonNullType -> Text
nonNullType (NonNullTypeNamed x) = Text.Lazy.fromStrict x <> "!"
nonNullType (NonNullTypeList x) = listType x <> "!"
-- * Internal
between :: Char -> Char -> Text -> Text
between open close = Text.Lazy.cons open . (`Text.Lazy.snoc` close)
parens :: Text -> Text
parens = between '(' ')'
brackets :: Text -> Text
brackets = between '[' ']'
braces :: Text -> Text
braces = between '{' '}'
quotes :: Text -> Text
quotes = between '"' '"'
spaces :: forall a. (a -> Text) -> [a] -> Text
spaces f = Text.Lazy.intercalate "\SP" . fmap f
parensCommas :: forall a. Formatter -> (a -> Text) -> [a] -> Text
parensCommas formatter f
= parens
. Text.Lazy.intercalate (eitherFormat formatter ", " ",")
. fmap f
bracketsCommas :: Formatter -> (a -> Text) -> [a] -> Text
bracketsCommas formatter f
= brackets
. Text.Lazy.intercalate (eitherFormat formatter ", " ",")
. fmap f
bracesList :: forall a. Formatter -> (a -> Text) -> [a] -> Text
bracesList (Pretty intendation) f xs
= Text.Lazy.snoc (Text.Lazy.intercalate "\n" content) '\n'
<> (Text.Lazy.snoc $ Text.Lazy.replicate (fromIntegral intendation) " ") '}'
where
content = "{" : fmap f xs
bracesList Minified f xs = braces $ Text.Lazy.intercalate "," $ fmap f xs
optempty :: (Eq a, Monoid a, Monoid b) => (a -> b) -> a -> b
optempty f xs = if xs == mempty then mempty else f xs
eitherFormat :: forall a. Formatter -> a -> a -> a
eitherFormat (Pretty _) x _ = x
eitherFormat Minified _ x = x

View File

@ -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.

View File

@ -6,15 +6,16 @@ module Language.GraphQL.Execute
, executeWithName
) where
import Control.Monad.IO.Class (MonadIO)
import qualified Data.Aeson as Aeson
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.AST.Transform as Transform
import qualified Language.GraphQL.Execute.Transform as Transform
import Language.GraphQL.Error
import qualified Language.GraphQL.Schema as Schema
@ -23,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."
@ -39,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
@ -66,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 (NE.toList schema) (NE.toList flds))
operation schema (AST.Core.Mutation _ flds)
= runCollectErrs (Schema.resolve (NE.toList schema) (NE.toList 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"

View File

@ -0,0 +1,167 @@
{-# LANGUAGE ExplicitForAll #-}
{-# LANGUAGE TupleSections #-}
-- | After the document is parsed, before getting executed the AST is
-- transformed into a similar, simpler AST. This module is responsible for
-- this transformation.
module Language.GraphQL.Execute.Transform
( document
) where
import Control.Arrow (first)
import Control.Monad (foldM, unless)
import Control.Monad.Trans.Class (lift)
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
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
-- | Associates a fragment name with a list of 'Core.Field's.
data Replacement = Replacement
{ fragments :: HashMap Core.Name Core.Fragment
, fragmentDefinitions :: HashMap Full.Name Full.FragmentDefinition
}
type TransformT a = StateT Replacement (ReaderT Schema.Subs Maybe) a
liftJust :: forall a. a -> TransformT a
liftJust = lift . lift . Just
-- | Rewrites the original syntax tree into an intermediate representation used
-- for query execution.
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 (ExecutableDefinition (Full.DefinitionOperation definition)) acc =
(definition :) <$> acc
defragment (ExecutableDefinition (Full.DefinitionFragment definition)) acc =
let (Full.FragmentDefinition name _ _ _) = definition
in first (HashMap.insert name definition) acc
defragment _ acc = acc
-- * Operation
operations :: [Full.OperationDefinition] -> TransformT Core.Document
operations operations' = do
coreOperations <- traverse operation operations'
lift . lift $ NonEmpty.nonEmpty coreOperations
operation :: Full.OperationDefinition -> TransformT Core.Operation
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.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 =>
t Full.Selection ->
TransformT (Seq Core.Selection)
appendSelection = foldM go mempty
where
go acc sel = append acc <$> selection sel
append acc (Left list) = list >< acc
append acc (Right one) = one <| acc
directives :: [Full.Directive] -> TransformT [Core.Directive]
directives = traverse directive
where
directive (Full.Directive directiveName directiveArguments) =
Core.Directive directiveName <$> arguments directiveArguments
-- * Fragment replacement
-- | Extract fragment definitions into a single 'HashMap'.
collectFragments :: TransformT ()
collectFragments = do
fragDefs <- gets fragmentDefinitions
let nextValue = head $ HashMap.elems fragDefs
unless (HashMap.null fragDefs) $ do
_ <- fragmentDefinition nextValue
collectFragments
fragmentDefinition ::
Full.FragmentDefinition ->
TransformT Core.Fragment
fragmentDefinition (Full.FragmentDefinition name type' _ selections) = do
modify deleteFragmentDefinition
fragmentSelection <- appendSelection selections
let newValue = Core.Fragment type' fragmentSelection
modify $ insertFragment newValue
liftJust newValue
where
deleteFragmentDefinition (Replacement fragments' fragmentDefinitions') =
Replacement fragments' $ HashMap.delete name fragmentDefinitions'
insertFragment newValue (Replacement fragments' fragmentDefinitions') =
let newFragments = HashMap.insert name newValue fragments'
in Replacement newFragments fragmentDefinitions'
arguments :: [Full.Argument] -> TransformT Core.Arguments
arguments = fmap Core.Arguments . foldM go HashMap.empty
where
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 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
value (Full.Boolean b) = pure $ Core.Boolean b
value Full.Null = pure Core.Null
value (Full.Enum e) = pure $ Core.Enum e
value (Full.List l) =
Core.List <$> traverse value l
value (Full.Object o) =
Core.Object . HashMap.fromList <$> traverse objectField o
objectField :: Full.ObjectField -> TransformT (Core.Name, Core.Value)
objectField (Full.ObjectField name value') = (name,) <$> value value'

View File

@ -1,188 +0,0 @@
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
-- | @GraphQL@ document parser.
module Language.GraphQL.Parser
( document
) where
import Control.Applicative ( Alternative(..)
, optional
)
import Data.List.NonEmpty (NonEmpty(..))
import Language.GraphQL.AST
import Language.GraphQL.Lexer
import Text.Megaparsec ( lookAhead
, option
, try
, (<?>)
)
-- | Parser for the GraphQL documents.
document :: Parser Document
document = unicodeBOM >> spaceConsumer >> lexeme (manyNE definition)
definition :: Parser Definition
definition = DefinitionOperation <$> operationDefinition
<|> DefinitionFragment <$> fragmentDefinition
<?> "definition error!"
operationDefinition :: Parser OperationDefinition
operationDefinition = OperationSelectionSet <$> selectionSet
<|> OperationDefinition <$> operationType
<*> optional name
<*> opt variableDefinitions
<*> opt directives
<*> selectionSet
<?> "operationDefinition error"
operationType :: Parser OperationType
operationType = Query <$ symbol "query"
<|> Mutation <$ symbol "mutation"
<?> "operationType error"
-- * SelectionSet
selectionSet :: Parser SelectionSet
selectionSet = braces $ manyNE selection
selectionSetOpt :: Parser SelectionSetOpt
selectionSetOpt = braces $ some selection
selection :: Parser Selection
selection = SelectionField <$> field
<|> try (SelectionFragmentSpread <$> fragmentSpread)
<|> SelectionInlineFragment <$> inlineFragment
<?> "selection error!"
-- * Field
field :: Parser Field
field = Field <$> optional alias
<*> name
<*> opt arguments
<*> opt directives
<*> opt selectionSetOpt
alias :: Parser Alias
alias = try $ name <* colon
-- * Arguments
arguments :: Parser [Argument]
arguments = parens $ some argument
argument :: Parser Argument
argument = Argument <$> name <* colon <*> value
-- * Fragments
fragmentSpread :: Parser FragmentSpread
fragmentSpread = FragmentSpread <$ spread
<*> fragmentName
<*> opt directives
inlineFragment :: Parser InlineFragment
inlineFragment = InlineFragment <$ spread
<*> optional typeCondition
<*> opt directives
<*> selectionSet
fragmentDefinition :: Parser FragmentDefinition
fragmentDefinition = FragmentDefinition
<$ symbol "fragment"
<*> name
<*> typeCondition
<*> opt directives
<*> selectionSet
fragmentName :: Parser Name
fragmentName = but (symbol "on") *> name
typeCondition :: Parser TypeCondition
typeCondition = symbol "on" *> name
-- * Input Values
value :: Parser Value
value = ValueVariable <$> variable
<|> ValueFloat <$> try float
<|> ValueInt <$> integer
<|> ValueBoolean <$> booleanValue
<|> ValueNull <$ symbol "null"
<|> ValueString <$> blockString
<|> ValueString <$> string
<|> ValueEnum <$> try enumValue
<|> ValueList <$> listValue
<|> ValueObject <$> objectValue
<?> "value error!"
where
booleanValue :: Parser Bool
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
objectField :: Parser ObjectField
objectField = ObjectField <$> name <* symbol ":" <*> value
-- * Variables
variableDefinitions :: Parser [VariableDefinition]
variableDefinitions = parens $ some variableDefinition
variableDefinition :: Parser VariableDefinition
variableDefinition = VariableDefinition <$> variable
<* colon
<*> type_
<*> optional defaultValue
variable :: Parser Name
variable = dollar *> name
defaultValue :: Parser Value
defaultValue = equals *> value
-- * Input Types
type_ :: Parser Type
type_ = try (TypeNamed <$> name <* but "!")
<|> TypeList <$> brackets type_
<|> TypeNonNull <$> nonNullType
<?> "type_ error!"
nonNullType :: Parser NonNullType
nonNullType = NonNullTypeNamed <$> name <* bang
<|> NonNullTypeList <$> brackets type_ <* bang
<?> "nonNullType error!"
-- * Directives
directives :: Parser [Directive]
directives = some directive
directive :: Parser Directive
directive = Directive
<$ at
<*> name
<*> opt arguments
-- * Internal
opt :: Monoid a => Parser a -> Parser a
opt = option mempty
-- 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

View File

@ -3,148 +3,110 @@
-- | This module provides a representation of a @GraphQL@ Schema in addition to
-- functions for defining and manipulating schemas.
module Language.GraphQL.Schema
( Resolver
, Schema
( Resolver(..)
, Subs
, object
, objectA
, scalar
, scalarA
, enum
, enumA
, resolve
, wrappedEnum
, wrappedEnumA
, 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 Data.Foldable (find, fold)
import Data.List.NonEmpty (NonEmpty)
import Control.Monad.Trans.Reader (runReaderT)
import Data.Foldable (fold, toList)
import Data.Maybe (fromMaybe)
import qualified Data.Aeson as Aeson
import Data.HashMap.Strict (HashMap)
import qualified Data.HashMap.Strict as HashMap
import Data.Sequence (Seq)
import Data.Text (Text)
import qualified Data.Text as T
import Language.GraphQL.AST.Core
import Language.GraphQL.Error
import Language.GraphQL.Trans
import Language.GraphQL.Type
import Language.GraphQL.AST.Core
{-# DEPRECATED Schema "Use NonEmpty (Resolver m) instead" #-}
-- | A GraphQL schema.
-- @m@ is usually expected to be an instance of 'MonadIO'.
type Schema m = NonEmpty (Resolver m)
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
-- | Like 'object' but also taking 'Argument's and can be null or a list of objects.
wrappedObjectA :: MonadIO m
=> Name -> ([Argument] -> ActionT m (Wrapping [Resolver m])) -> Resolver m
wrappedObjectA name f = Resolver name $ resolveFieldValue f resolveRight
where
resolveRight fld@(Field _ _ _ sels) resolver
= withField (traverse (`resolve` sels) resolver) fld
resolveRight fld@(Field _ _ _ flds) resolver
= withField (resolve (resolversToMap resolver) flds) fld
-- | Like 'object' but can be null or a list of objects.
wrappedObject :: MonadIO m
=> Name -> ActionT m (Wrapping [Resolver m]) -> Resolver m
wrappedObject name = wrappedObjectA name . const
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 (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
-- | Lika '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 (Wrapping a)) -> Resolver m
wrappedScalarA name f = Resolver name $ resolveFieldValue f resolveRight
where
resolveRight fld (Named result) = withField (return result) fld
resolveRight fld Null
= return $ HashMap.singleton (aliasOrName fld) Aeson.Null
resolveRight fld (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 (Wrapping a) -> Resolver m
wrappedScalar name = wrappedScalarA name . const
{-# DEPRECATED enum "Use scalar instead" #-}
enum :: MonadIO m => Name -> ActionT m [Text] -> Resolver m
enum name = enumA name . const
{-# DEPRECATED enumA "Use scalarA instead" #-}
enumA :: MonadIO m => Name -> ([Argument] -> ActionT m [Text]) -> Resolver m
enumA name f = Resolver name $ resolveFieldValue f resolveRight
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 resolver = withField (return resolver) fld
{-# DEPRECATED wrappedEnumA "Use wrappedScalarA instead" #-}
wrappedEnumA :: MonadIO m
=> Name -> ([Argument] -> ActionT m (Wrapping [Text])) -> Resolver m
wrappedEnumA name f = Resolver name $ resolveFieldValue f resolveRight
where
resolveRight fld (Named resolver) = withField (return resolver) fld
resolveRight fld Null
resolveRight fld (Type.Named result) = withField (return result) fld
resolveRight fld Type.Null
= return $ HashMap.singleton (aliasOrName fld) Aeson.Null
resolveRight fld (List resolver) = withField (return resolver) fld
resolveRight fld (Type.List result) = withField (return result) fld
{-# DEPRECATED wrappedEnum "Use wrappedScalar instead" #-}
wrappedEnum :: MonadIO m => Name -> ActionT m (Wrapping [Text]) -> Resolver m
wrappedEnum name = wrappedEnumA 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 $ runExceptT . runActionT $ f args
result <- lift $ reader . runExceptT . runActionT $ f
either resolveLeft (resolveRight fld) result
where
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
@ -152,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] -> [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 <- maybe (return "") resolveTypeName (find (compareResolvers "__typename") resolvers)
if Aeson.String typeCondition == that
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

View File

@ -1,6 +1,8 @@
-- | Monad transformer stack used by the @GraphQL@ resolvers.
module Language.GraphQL.Trans
( ActionT(..)
, Context(..)
, argument
) where
import Control.Applicative (Alternative(..))
@ -8,10 +10,23 @@ 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, asks)
import qualified Data.HashMap.Strict as HashMap
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import Language.GraphQL.AST.Core
import Prelude hiding (lookup)
-- | Monad transformer stack used by the resolvers to provide error handling.
newtype ActionT m a = ActionT { runActionT :: ExceptT Text m a }
-- | Resolution context holds resolver arguments.
newtype Context = Context
{ arguments :: Arguments
}
-- | Monad transformer stack used by the resolvers to provide error handling
-- and resolution context (resolver arguments).
newtype ActionT m a = ActionT
{ runActionT :: ExceptT Text (ReaderT Context m) a
}
instance Functor m => Functor (ActionT m) where
fmap f = ActionT . fmap f . runActionT
@ -25,7 +40,7 @@ instance Monad m => Monad (ActionT m) where
(ActionT action) >>= f = ActionT $ action >>= runActionT . f
instance MonadTrans ActionT where
lift = ActionT . lift
lift = ActionT . lift . lift
instance MonadIO m => MonadIO (ActionT m) where
liftIO = lift . liftIO
@ -37,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

View File

@ -1,11 +1,9 @@
-- | Definitions for @GraphQL@ type system.
-- | Definitions for @GraphQL@ input types.
module Language.GraphQL.Type
( Wrapping(..)
) where
import Data.Aeson as Aeson ( ToJSON
, toJSON
)
import Data.Aeson as Aeson (ToJSON, toJSON)
import qualified Data.Aeson as Aeson
-- | GraphQL distinguishes between "wrapping" and "named" types. Each wrapping

View File

@ -0,0 +1,50 @@
{-# LANGUAGE OverloadedStrings #-}
module Language.GraphQL.Type.Directive
( selection
) where
import qualified Data.HashMap.Strict as HashMap
import Language.GraphQL.AST.Core
-- | Directive processing status.
data Status
= Skip -- ^ Skip the selection and stop directive processing
| Include Directive -- ^ The directive was processed, try other handlers
| Continue Directive -- ^ Directive handler mismatch, try other handlers
-- | Takes a list of directives, handles supported directives and excludes them
-- from the result. If the selection should be skipped, returns 'Nothing'.
selection :: [Directive] -> Maybe [Directive]
selection = foldr go (Just [])
where
go directive' directives' =
case (skip . include) (Continue directive') of
(Include _) -> directives'
Skip -> Nothing
(Continue x) -> (x :) <$> directives'
handle :: (Directive -> Status) -> Status -> Status
handle _ Skip = Skip
handle handler (Continue directive) = handler directive
handle handler (Include directive) = handler directive
-- * Directive implementations
skip :: Status -> Status
skip = handle skip'
where
skip' directive'@(Directive "skip" (Arguments arguments)) =
case HashMap.lookup "if" arguments of
(Just (Boolean True)) -> Skip
_ -> Include directive'
skip' directive' = Continue directive'
include :: Status -> Status
include = handle include'
where
include' directive'@(Directive "include" (Arguments arguments)) =
case HashMap.lookup "if" arguments of
(Just (Boolean True)) -> Include directive'
_ -> Skip
include' directive' = Continue directive'

View File

@ -1,4 +1,4 @@
resolver: lts-14.11
resolver: lts-15.12
packages:
- .

View File

@ -0,0 +1,133 @@
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
module Language.GraphQL.AST.EncoderSpec
( spec
) where
import Language.GraphQL.AST
import Language.GraphQL.AST.Encoder
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 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\""
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, with newline symbol" $
value pretty (String "Line 1\nLine 2")
`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 [] []
operation = DefinitionOperation $ SelectionSet $ pure field
in definition pretty operation `shouldBe` [r|{
field(message: """
line1
line2
""")
}
|]

View File

@ -1,14 +1,14 @@
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
module Language.GraphQL.LexerSpec
module Language.GraphQL.AST.LexerSpec
( spec
) where
import Data.Text (Text)
import Data.Void (Void)
import Language.GraphQL.Lexer
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)
@ -71,13 +71,13 @@ spec = describe "Lexer" $ do
parse float "" "-1.123e4567" `shouldParse` (-1.123e4567)
it "lexes punctuation" $ do
parse bang "" "!" `shouldParse` '!'
parse dollar "" "$" `shouldParse` '$'
parse bang "" "!" `shouldParse` "!"
parse dollar "" "$" `shouldParse` "$"
runBetween parens `shouldSucceedOn` "()"
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 ()) ""

View File

@ -0,0 +1,144 @@
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
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 (shouldParse, shouldSucceedOn)
import Text.Megaparsec (parse)
import Text.RawString.QQ (r)
spec :: Spec
spec = describe "Parser" $ do
it "accepts BOM header" $
parse document "" `shouldSucceedOn` "\xfeff{foo}"
it "accepts block strings as argument" $
parse document "" `shouldSucceedOn` [r|{
hello(text: """Argument""")
}|]
it "accepts strings as argument" $
parse document "" `shouldSucceedOn` [r|{
hello(text: "Argument")
}|]
it "accepts two required arguments" $
parse document "" `shouldSucceedOn` [r|
mutation auth($username: String!, $password: String!){
test
}|]
it "accepts two string arguments" $
parse document "" `shouldSucceedOn` [r|
mutation auth{
test(username: "username", password: "password")
}|]
it "accepts two block string arguments" $
parse document "" `shouldSucceedOn` [r|
mutation auth{
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
}
|]

View File

@ -1,21 +0,0 @@
{-# LANGUAGE OverloadedStrings #-}
module Language.GraphQL.EncoderSpec
( spec
) where
import Language.GraphQL.AST ( Value(..))
import Language.GraphQL.Encoder ( value
, minified
)
import Test.Hspec ( Spec
, describe
, it
, shouldBe
)
spec :: Spec
spec = describe "value" $ do
it "escapes \\" $
value minified (ValueString "\\") `shouldBe` "\"\\\\\""
it "escapes quotes" $
value minified (ValueString "\"") `shouldBe` "\"\\\"\""

View File

@ -1,26 +0,0 @@
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
module Language.GraphQL.ParserSpec
( spec
) where
import Language.GraphQL.Parser (document)
import Test.Hspec (Spec, describe, it)
import Test.Hspec.Megaparsec (shouldSucceedOn)
import Text.Megaparsec (parse)
import Text.RawString.QQ (r)
spec :: Spec
spec = describe "Parser" $ do
it "accepts BOM header" $
parse document "" `shouldSucceedOn` "\xfeff{foo}"
it "accepts block strings as argument" $
parse document "" `shouldSucceedOn` [r|{
hello(text: """Argument""")
}|]
it "accepts strings as argument" $
parse document "" `shouldSucceedOn` [r|{
hello(text: "Argument")
}|]

View File

@ -0,0 +1,88 @@
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
module Test.DirectiveSpec
( spec
) 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 :: HashMap Text (NonEmpty (Schema.Resolver IO))
experimentalResolver = HashMap.singleton "Query"
$ Schema.scalar "experimentalField" (pure (5 :: Int)) :| []
emptyObject :: Value
emptyObject = object
[ "data" .= object []
]
spec :: Spec
spec =
describe "Directive executor" $ do
it "should be able to @skip fields" $ do
let query = [r|
{
experimentalField @skip(if: true)
}
|]
actual <- graphql experimentalResolver query
actual `shouldBe` emptyObject
it "should not skip fields if @skip is false" $ do
let query = [r|
{
experimentalField @skip(if: false)
}
|]
expected = object
[ "data" .= object
[ "experimentalField" .= (5 :: Int)
]
]
actual <- graphql experimentalResolver query
actual `shouldBe` expected
it "should skip fields if @include is false" $ do
let query = [r|
{
experimentalField @include(if: false)
}
|]
actual <- graphql experimentalResolver query
actual `shouldBe` emptyObject
it "should be able to @skip a fragment spread" $ do
let query = [r|
{
...experimentalFragment @skip(if: true)
}
fragment experimentalFragment on ExperimentalType {
experimentalField
}
|]
actual <- graphql experimentalResolver query
actual `shouldBe` emptyObject
it "should be able to @skip an inline fragment" $ do
let query = [r|
{
... on ExperimentalType @skip(if: true) {
experimentalField
}
}
|]
actual <- graphql experimentalResolver query
actual `shouldBe` emptyObject

View File

@ -10,7 +10,13 @@ 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, shouldNotSatisfy)
import Test.Hspec ( Spec
, describe
, it
, shouldBe
, shouldSatisfy
, shouldNotSatisfy
)
import Text.RawString.QQ (r)
size :: Schema.Resolver IO
@ -37,61 +43,149 @@ inlineQuery = [r|{
}
}|]
hasErrors :: Value -> Bool
hasErrors (Object object') = HashMap.member "errors" object'
hasErrors _ = True
spec :: Spec
spec = describe "Inline fragment executor" $ do
it "chooses the first selection if the type matches" $ do
actual <- graphql (garment "Hat" :| []) inlineQuery
let expected = object
[ "data" .= object
[ "garment" .= object
spec = do
describe "Inline fragment executor" $ do
it "chooses the first selection if the type matches" $ do
actual <- graphql (HashMap.singleton "Query" $ garment "Hat" :| []) inlineQuery
let expected = object
[ "data" .= object
[ "garment" .= object
[ "circumference" .= (60 :: Int)
]
]
]
in actual `shouldBe` expected
it "chooses the last selection if the type matches" $ do
actual <- graphql (HashMap.singleton "Query" $ garment "Shirt" :| []) inlineQuery
let expected = object
[ "data" .= object
[ "garment" .= object
[ "size" .= ("L" :: Text)
]
]
]
in actual `shouldBe` expected
it "embeds inline fragments without type" $ do
let query = [r|{
garment {
circumference
... {
size
}
}
}|]
resolvers = Schema.object "garment" $ return [circumference, size]
actual <- graphql (HashMap.singleton "Query" $ resolvers :| []) query
let expected = object
[ "data" .= object
[ "garment" .= object
[ "circumference" .= (60 :: Int)
, "size" .= ("L" :: Text)
]
]
]
in actual `shouldBe` expected
it "evaluates fragments on Query" $ do
let query = [r|{
... {
size
}
}|]
actual <- graphql (HashMap.singleton "Query" $ size :| []) query
actual `shouldNotSatisfy` hasErrors
describe "Fragment spread executor" $ do
it "evaluates fragment spreads" $ do
let query = [r|
{
...circumferenceFragment
}
fragment circumferenceFragment on Hat {
circumference
}
|]
actual <- graphql (HashMap.singleton "Query" $ circumference :| []) query
let expected = object
[ "data" .= object
[ "circumference" .= (60 :: Int)
]
]
]
in actual `shouldBe` expected
in actual `shouldBe` expected
it "chooses the last selection if the type matches" $ do
actual <- graphql (garment "Shirt" :| []) inlineQuery
let expected = object
[ "data" .= object
[ "garment" .= object
[ "size" .= ("L" :: Text)
it "evaluates nested fragments" $ do
let query = [r|
{
garment {
...circumferenceFragment
}
}
fragment circumferenceFragment on Hat {
...hatFragment
}
fragment hatFragment on Hat {
circumference
}
|]
actual <- graphql (HashMap.singleton "Query" $ garment "Hat" :| []) query
let expected = object
[ "data" .= object
[ "garment" .= object
[ "circumference" .= (60 :: Int)
]
]
]
]
in actual `shouldBe` expected
in actual `shouldBe` expected
it "embeds inline fragments without type" $ do
let query = [r|{
garment {
circumference
... {
size
}
}
}|]
resolvers = Schema.object "garment" $ return [circumference, size]
it "rejects recursive fragments" $ do
let query = [r|
{
...circumferenceFragment
}
actual <- graphql (resolvers :| []) query
let expected = object
[ "data" .= object
[ "garment" .= object
[ "circumference" .= (60 :: Int)
, "size" .= ("L" :: Text)
fragment circumferenceFragment on Hat {
...circumferenceFragment
}
|]
actual <- graphql (HashMap.singleton "Query" $ circumference :| []) query
actual `shouldSatisfy` hasErrors
it "considers type condition" $ do
let query = [r|
{
garment {
...circumferenceFragment
...sizeFragment
}
}
fragment circumferenceFragment on Hat {
circumference
}
fragment sizeFragment on Shirt {
size
}
|]
expected = object
[ "data" .= object
[ "garment" .= object
[ "circumference" .= (60 :: Int)
]
]
]
]
in actual `shouldBe` expected
it "evaluates fragments on Query" $ do
let query = [r|{
... {
size
}
}|]
actual <- graphql (size :| []) query
actual `shouldNotSatisfy` hasErrors
where
hasErrors (Object object') = HashMap.member "errors" object'
hasErrors _ = True
actual <- graphql (HashMap.singleton "Query" $ garment "Hat" :| []) query
actual `shouldBe` expected

View File

@ -7,8 +7,8 @@ module Test.KitchenSinkSpec
import qualified Data.Text.IO as Text.IO
import qualified Data.Text.Lazy.IO as Text.Lazy.IO
import qualified Data.Text.Lazy as Lazy (Text)
import qualified Language.GraphQL.Encoder as Encoder
import qualified Language.GraphQL.Parser as Parser
import qualified Language.GraphQL.AST.Encoder as Encoder
import qualified Language.GraphQL.AST.Parser as Parser
import Paths_graphql (getDataFileName)
import Test.Hspec (Spec, describe, it)
import Test.Hspec.Megaparsec (parseSatisfies)

View File

@ -8,7 +8,6 @@ module Test.StarWars.Data
, getEpisode
, getFriends
, getHero
, getHeroIO
, getHuman
, id_
, homePlanet
@ -17,16 +16,13 @@ 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)
import Language.GraphQL.Trans
import Language.GraphQL.Type
import qualified Language.GraphQL.Type as Type
-- * Data
-- See https://github.com/graphql/graphql-js/blob/master/src/__tests__/starWarsData.js
@ -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'
@ -191,8 +184,8 @@ getDroid' _ = empty
getFriends :: Character -> [Character]
getFriends char = catMaybes $ liftA2 (<|>) getDroid getHuman <$> friends char
getEpisode :: Int -> Maybe (Wrapping Text)
getEpisode 4 = pure $ Named "NEWHOPE"
getEpisode 5 = pure $ Named "EMPIRE"
getEpisode 6 = pure $ Named "JEDI"
getEpisode :: Int -> Maybe (Type.Wrapping Text)
getEpisode 4 = pure $ Type.Named "NEWHOPE"
getEpisode 5 = pure $ Type.Named "EMPIRE"
getEpisode 6 = pure $ Type.Named "JEDI"
getEpisode _ = empty

View File

@ -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

View File

@ -1,4 +1,3 @@
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
module Test.StarWars.Schema
( character
@ -10,48 +9,56 @@ 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 Language.GraphQL.Type
import qualified Language.GraphQL.Type as Type
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.ValueEnum "NEWHOPE")] -> character $ getHero 4
[Schema.Argument "episode" (Schema.ValueEnum "EMPIRE" )] -> character $ getHero 5
[Schema.Argument "episode" (Schema.ValueEnum "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.ValueString i)] -> do
humanCharacter <- lift $ return $ getHuman i >>= Just
case humanCharacter of
Nothing -> return Null
Just e -> 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.ValueString 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
, Schema.wrappedObject "friends"
$ traverse character $ List $ Named <$> getFriends char
, Schema.wrappedScalar "appearsIn" $ return . List
$ traverse character $ Type.List $ Type.Named <$> getFriends char
, Schema.wrappedScalar "appearsIn" $ return . Type.List
$ catMaybes (getEpisode <$> appearsIn char)
, Schema.scalar "secretBackstory" $ secretBackstory char
, Schema.scalar "homePlanet" $ return $ either mempty homePlanet char