70 Commits

Author SHA1 Message Date
dd6fdf69f6 Release 1.0.0.0 2021-07-04 09:57:17 +02:00
b99bb72272 Report subscription error locations 2021-07-02 09:28:03 +02:00
b580d1a988 Attach the field location to resolver exceptions 2021-06-27 13:42:58 +02:00
c601ccb4ad Add dependency version ranges
Also remove stack.yaml since it isn't used anymore. and adding libraries
to the snapshots doesn't seem to be as easy as I hoped.
2021-06-26 07:35:18 +02:00
96bb061666 Fail with a location for result coercion
The intermediate representation was further modified so that the
operation definitions contain location information. Probably I should
introduce a data type that generalizes fields and operations, so it
contains object type, location and the selection set, so the functions
don't accept so many arguments.
2021-06-24 09:29:24 +02:00
812f6967d4 Provide locations for argument errors
The executor still doesn't give an error per argument, but a single
error per field with locations for all arguments.
If a non-null argument isn't specified, only the error location of the
field is given. If some arguments cannot be coerced, only the locations
of these arguments are given, non-null arguments are ignored. This
should still be improved, so the executor returns all errors at once.
The transformation tree is changed, so that argument map contains
locations of the arguments (but not the locations of the argument values
yet).
2021-06-22 09:13:27 +02:00
6fe9eb72e4 Fix merging fields with arguments
executeField shouldn't assume that a selection has only one field with a
given name, but it should take the first field. The underlying cause is
a wrong pattern, which (because of the laziness) is executed only if the
field has arguments.
2021-06-18 06:51:14 +02:00
2ce2be5d91 Provide location information for interface errors 2021-06-17 08:15:27 +02:00
c311cb0070 Add constructor with additional schema types 2021-05-13 17:40:38 +02:00
1b7cd85216 Add location information to the intermediate tree 2021-05-12 06:51:59 +02:00
f671645043 Remove unused QueryError.TransformationError 2021-05-11 07:11:47 +02:00
1af95345d2 Deprecate internal error generation functions
The functions generating errors in the executor should be changed anyway
when we provide better error messages from the executor, with the error
location and response path. So public definitions of these functions are
deprecated now and they are replaced by more generic functions in the
executor code.
2021-05-10 09:43:39 +02:00
0d23df3da2 Provide an internal function to add errors
The old function, addErrMsg, takes only a string with an error
description, but more information is required for the execution errors:
locations and path. addErrMsg should be deprecated after the switching
to the new addError.
2021-05-09 12:42:02 +02:00
5a5f265fe4 Validate non-nullable values inside lists 2021-05-06 22:23:16 +02:00
2220f0ca56 Remove unused OverloadedStrings pragmas 2021-04-14 07:09:21 +02:00
5654b78935 Traverse input object properties once 2021-04-12 07:09:39 +02:00
d6dda14cfd Remove package.yaml
This reduces duplication between the modified cabal file and
package.yaml.
2021-04-07 10:12:40 +02:00
328e6acdee Emit list item errors once 2021-03-16 10:08:13 +01:00
4d762d6356 Add location information to list values 2021-03-14 12:19:30 +01:00
cbccb9ed0b Add -Wall flags to graphql.cabal 2021-02-22 08:30:36 +11:00
ca0f0bd32d Fix some issues with directive definitions
I found some issues with directive definitions:

- I couldn't use `on FIELD_DEFINITION`, I believe because `FIELD` was parsed
  first in `executableDirectiveLocation`. I've combined both
  `executableDirectiveLocation` and `typetypeSystemDirectiveLocation` into one
  function which can reorder them to ensure every directive location gets a fair
  chance at parsing.

Not actually to do with directives, some literals weren't being parsed
correctly.

- The GraphQL spec defines list to be `[]` or `[Value]`, but empty literal lists
  weren't being parsed correctly because of using `some` instead of `many`.

- The GraphQL spec defines objects to be `{}` or `{Name: Value}`, but empty
  literal objects had the same issue.
2021-02-21 23:35:34 +11:00
10e4d64052 Replace Map with OrderedMap 2021-02-19 08:09:04 +01:00
d74e27e903 traverseMaybe OrderedMap 2021-02-15 09:04:16 +01:00
90d36f66b9 Combine value inserted into the OrderedMap 2021-02-14 14:46:06 +01:00
c1a1b47aea Add OrderedMap prototype 2021-02-13 06:56:10 +01:00
1e8405a6d6 Document AST.Document.escape 2021-02-11 12:02:08 +01:00
2839b28590 Release 0.11.1.0 2021-02-07 08:10:46 +01:00
ed725ea514 Split validation rule tests in contexts 2021-02-06 12:54:27 +01:00
b27da54bf4 Provide custom Show instances for AST values 2021-02-04 08:12:12 +01:00
a034f2ce4d Validate values 2021-02-03 05:47:40 +01:00
ebf4f4d24e Update stack snapshot to 17.x. 2021-02-02 07:15:30 +01:00
1c7554c328 Validate variable usage is allowed in objects 2021-01-22 09:26:22 +01:00
c018657e25 Fix the type in messages when validating variables 2021-01-04 08:24:50 +01:00
71a5964c27 Rename variablesInAllowedPositionRule's variables
Name variablesInAllowedPositionRule's variables more meaningful.
2020-12-27 11:47:29 +01:00
22abf7ca58 Validate variable usages are allowed in arguments 2020-12-26 06:31:56 +01:00
5a6709030c Add show instances for AST type representation 2020-12-17 20:42:47 +01:00
2bcae9e0a7 Implement Show class for GraphQL type definitions
.. in the `Type` modules.
2020-12-14 22:36:27 +01:00
2dbc985dfc Validate fragment spreads are possible 2020-11-19 08:48:37 +01:00
86a0e00f7e Collect interface implementations 2020-11-17 08:10:32 +01:00
1f4eb6fb9b Implement basic "Field Selection Merging" rule 2020-11-15 10:11:09 +01:00
f5209481aa Extract collectFields function 2020-11-11 08:49:45 +01:00
445f33dcf3 Release 0.11.0.0 2020-11-07 09:05:47 +01:00
4a3b4cb16d Fix singleFieldSubscriptionsRule fragment lookup
singleFieldSubscriptionsRule picks up a wrong fragment definition.
2020-11-06 08:33:51 +01:00
7f0fb18716 Remove StarWars tests
Our own test suite is slowly getting sufficient.
2020-11-05 07:55:22 +01:00
afcf9aaa14 Write documentation out of the source tree
In a Wiki.
2020-11-02 08:24:48 +01:00
6e8d8a34a1 Reflect infrastructure and license changes 2020-10-30 07:06:36 +01:00
7c0b0ace4d Collect types once the schema is created 2020-10-07 05:24:51 +02:00
a91bc7f2d2 Validate required input fields 2020-10-05 14:51:21 +02:00
d5f518fe82 Validate required arguments 2020-10-03 07:34:34 +02:00
6daae8a521 Validate directives are in valid locations 2020-10-02 06:31:38 +02:00
56b63f1c3e Validate input object field names 2020-09-30 05:14:52 +02:00
466416d4b0 Validate directives are defined 2020-09-29 06:21:32 +02:00
4602eb1df3 Validate arguments are defined 2020-09-28 07:06:15 +02:00
ced9b815db Validate leaf selections 2020-09-26 09:06:30 +02:00
3373c94895 Validate field selections on composite types 2020-09-26 07:57:25 +02:00
9bfa2aa7e8 Validate input fields have unique names 2020-09-24 05:47:31 +02:00
e9a94147fb Validate variables are used 2020-09-22 04:42:25 +02:00
3e393004ae Validate all variables are defined 2020-09-21 07:28:40 +02:00
38c3097bcf Validate fragments are input types 2020-09-20 06:59:27 +02:00
21a7d9cce4 Validate variable names are unique 2020-09-19 18:18:26 +02:00
9a08aa5de7 Validate directives are unique per location 2020-09-18 07:32:58 +02:00
497b93c41b Validate arguments have unique names 2020-09-17 10:33:37 +02:00
6e644c5b4b Move path to the execution error
Since it isn't possible to get a path during validation, without
executing the query.
2020-09-16 09:12:49 +02:00
4c10ce9204 Use Seq as base monad in the validator
It is more natural to implement the logic: try to apply each rule to
each node.
2020-09-15 08:06:07 +02:00
08998dbd93 Validate fragments don't form cycles 2020-09-11 08:03:49 +02:00
c2c57b6363 Validate all fragments are used 2020-09-09 17:04:31 +02:00
f6ff0ab9c7 Validate fragments on composite types 2020-09-07 22:01:49 +02:00
d327d9d1ce Validate fragment spread type existence 2020-09-05 10:00:58 +02:00
14ed209828 Collect types from the subscription root 2020-09-04 19:12:19 +02:00
33318a3b01 Validate fragment spread target existence 2020-08-31 11:06:27 +02:00
47 changed files with 5320 additions and 2536 deletions

View File

@ -1,63 +0,0 @@
name: Haskell CI
on:
push: ~
pull_request:
branches: [master]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-haskell@v1
with:
enable-stack: true
stack-no-global: true
stack-version: latest
- name: Cache
uses: actions/cache@v2
with:
path: |
~/.stack
stack.yaml.lock
key: ${{ runner.os }}-test-${{ hashFiles('**/stack.yaml') }}
restore-keys: ${{ runner.os }}-test-
- name: Install dependencies
run: stack --no-terminal test --only-snapshot
- name: Run tests
run: stack --no-terminal test --pedantic
- name: Build the documentation
run: |
stack --no-terminal ghc -- -Wall -Werror -fno-code docs/tutorial/tutorial.lhs
stack --no-terminal haddock --no-haddock-deps
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-haskell@v1
with:
enable-stack: true
stack-no-global: true
stack-version: latest
- name: Cache
uses: actions/cache@v2
with:
path: |
~/.stack
stack.yaml.lock
key: ${{ runner.os }}-lint-${{ hashFiles('**/stack.yaml') }}
restore-keys: ${{ runner.os }}-lint-
- name: Build HLint
run: stack --no-terminal build hlint
- name: Install HLint
run: stack --no-terminal install hlint
- name: Lint
run: stack --no-terminal exec hlint -- src tests docs

View File

@ -6,28 +6,137 @@ The format is based on
and this project adheres to and this project adheres to
[Haskell Package Versioning Policy](https://pvp.haskell.org/). [Haskell Package Versioning Policy](https://pvp.haskell.org/).
## [1.0.0.0]
### Added
- `Language.GraphQL.Execute.OrderedMap` is a map data structure, that preserves
insertion order.
- `Language.GraphQL.Schema.schemaWithTypes` constructs a complete schema,
including an optional schema description and user-defined types not referenced
in the schema directly (for example interface implementations).
- `Language.GraphQL.Schema.description` returns the optional schema description.
- All errors that can be associated with a location in the query contain
location information.
### Fixed
- Parser now accepts empty lists and objects.
- Parser now accepts all directive locations.
- `valuesOfCorrectTypeRule` doesn't check lists recursively since the
validation traverser calls it on all list items.
- `valuesOfCorrectTypeRule` doesn't check objects recursively since the
validation traverser calls it on all object properties.
- Validation of non-nullable values inside lists.
- `executeField` shouldn't assume that a selection has only one field with a
given name, but it should take the first field. The underlying cause is a
wrong pattern, which (because of the laziness) is executed only if the field
has arguments.
### Changed
- `AST.Document.Value.List` and `AST.Document.ConstValue.ConstList` contain
location information for each list item.
- `Error`: `singleError`, `addErr` and `addErrMsg` are deprecated. They are
internal functions used by the executor for error handling.
## [0.11.1.0] - 2021-02-07
### Added
- `Validate.Rules`:
- `overlappingFieldsCanBeMergedRule`
- `possibleFragmentSpreadsRule`
- `variablesInAllowedPositionRule`
- `valuesOfCorrectTypeRule`
- `Type.Schema.implementations` contains a map from interfaces and objects to
interfaces they implement.
- Show instances for GraphQL type definitions in the `Type` modules.
- Custom Show instances for type and value representations in the AST.
- `AST.Document.escape` escapes a single character in a `StringValue`.
## [0.11.0.0] - 2020-11-07
### Changed
- `AST.Document.Selection` wraps additional new types: `Field`, `FragmentSpread`
and `InlineFragment`. Thus validation rules can be defined more concise.
- `AST.Document`: `Argument` and `Directive` contain token location.
- `AST.Document.Argument` contains the `Value` wrapped in the `Node`.
- `AST.Lexer.colon` and `AST.Lexer.at` ignore the result (it is always the
- same).
- `Validate.Validation`: `Validation.rules` was removed. `Validation.rules`
contained the list of rules, but the executed rules shouldn't know about other
rules. `rules` was a part of the `Validation` context to pass it easier
around, but since the rules are traversed once now and applied to all nodes in
the tree at the beginning, it isn't required anymore.
- `Validate.Validation.Error`: `path` is removed since it isn't possible to get
the path without executing the query.
- `Error.Error`: `path` added. It is currently always empty.
- `Validate.Validation.Path` was moved to `Error`.
- `Type.Schema.Schema`: data constructor is hidden, fields are accessible with
freestanding functions: `query`, `mutation`, `subscription`, `directives` and
`types`.
### Added
- `Validate.Validation.Rule` constructors:
- `SelectionRule`
- `FragmentRule`
- `FragmentSpreadRule`
- `ArgumentsRule`
- `DirectivesRule`
- `VariablesRule`
- `FieldRule`
- `Validate.Rules`:
- `fragmentsOnCompositeTypesRule`
- `fragmentSpreadTargetDefinedRule`
- `fragmentSpreadTypeExistenceRule`
- `noUnusedFragmentsRule`
- `noFragmentCyclesRule`
- `uniqueArgumentNamesRule`
- `uniqueDirectiveNamesRule`
- `uniqueVariableNamesRule`
- `variablesAreInputTypesRule`
- `noUndefinedVariablesRule`
- `noUndefinedVariablesRule`
- `noUnusedVariablesRule`
- `uniqueInputFieldNamesRule`
- `fieldsOnCorrectTypeRule`
- `scalarLeafsRule`
- `knownArgumentNamesRule`
- `knownDirectiveNamesRule`
- `directivesInValidLocationsRule`
- `providedRequiredArgumentsRule`
- `providedRequiredInputFieldsRule`
- `AST.Document.Field`.
- `AST.Document.FragmentSpread`.
- `AST.Document.InlineFragment`.
- `AST.Document.Node`.
- `Type.In.Arguments`: Type alias for an argument map.
- `Type.Schema.Directive` and `Type.Schema.Directives` are directive definition
representation.
- `Type.Schema.schema`: Schema constructor.
### Fixed
- Collecting existing types from the schema considers subscriptions.
### Removed
- `AST.Document.Alias`. Use `AST.Document.Name` instead.
## [0.10.0.0] - 2020-08-29 ## [0.10.0.0] - 2020-08-29
## Changed ### Changed
- `Test.Hspec.GraphQL.*`: replace `IO` in the resolver with any `MonadCatch`. - `Test.Hspec.GraphQL.*`: replace `IO` in the resolver with any `MonadCatch`.
- The `Location` argument of `AST.Document.Definition.ExecutableDefinition` was - The `Location` argument of `AST.Document.Definition.ExecutableDefinition` was
moved to `OperationDefinition` and `FragmentDefinition` since these are the moved to `OperationDefinition` and `FragmentDefinition` since these are the
actual elements that have a location in the document. actual elements that have a location in the document.
- `Validate.Rules` get the whole validation context (AST and schema). - `Validate.Rules` get the whole validation context (AST and schema).
## Added ### Added
- `Validate.Validation` contains data structures and functions used by the - `Validate.Validation` contains data structures and functions used by the
validator and concretet rules. validator and concretet rules.
- `Validate.Rules`: operation validation rules. - `Validate.Rules`: operation validation rules.
## [0.9.0.0] - 2020-07-24 ## [0.9.0.0] - 2020-07-24
## Fixed ### Fixed
- Location of a parse error is returned in a singleton array with key - Location of a parse error is returned in a singleton array with key
`locations`. `locations`.
- Parsing comments in the front of definitions. - Parsing comments in the front of definitions.
- Some missing labels were added to the parsers, some labels were fixed to - Some missing labels were added to the parsers, some labels were fixed to
refer to the AST nodes being parsed. refer to the AST nodes being parsed.
## Added ### Added
- `AST` reexports `AST.Parser`. - `AST` reexports `AST.Parser`.
- `AST.Document.Location` is a token location as a line and column pair. - `AST.Document.Location` is a token location as a line and column pair.
- `Execute` reexports `Execute.Coerce`. - `Execute` reexports `Execute.Coerce`.
@ -43,7 +152,7 @@ and this project adheres to
- `Test.Hspec.GraphQL` contains some test helpers. - `Test.Hspec.GraphQL` contains some test helpers.
- `Validate` contains the validator and standard rules. - `Validate` contains the validator and standard rules.
## Changed ### Changed
- `Type.Out.Resolver`: Interface fields don't have resolvers, object fields - `Type.Out.Resolver`: Interface fields don't have resolvers, object fields
have value resolvers, root subscription type resolvers need an additional have value resolvers, root subscription type resolvers need an additional
resolver that creates an event stream. `Resolver` represents these differences resolver that creates an event stream. `Resolver` represents these differences
@ -67,7 +176,7 @@ and this project adheres to
- The constraint of the base monad was changed to `MonadCatch` (and it implies - The constraint of the base monad was changed to `MonadCatch` (and it implies
`MonadThrow`). `MonadThrow`).
## Removed ### Removed
- `Trans.ActionT` is an unneeded layer of complexity. `Type.Out.Resolver` - `Trans.ActionT` is an unneeded layer of complexity. `Type.Out.Resolver`
represents possible resolver configurations. represents possible resolver configurations.
- `Execute.executeWithName`. `Execute.execute` takes the operation name and - `Execute.executeWithName`. `Execute.execute` takes the operation name and
@ -334,16 +443,19 @@ and this project adheres to
### Added ### Added
- Data types for the GraphQL language. - Data types for the GraphQL language.
[0.10.0.0]: https://github.com/caraus-ecms/graphql/compare/v0.9.0.0...v0.10.0.0 [1.0.0.0]: https://www.caraus.tech/projects/pub-graphql/repository/23/diff?rev=v1.0.0.0&rev_to=v0.11.1.0
[0.9.0.0]: https://github.com/caraus-ecms/graphql/compare/v0.8.0.0...v0.9.0.0 [0.11.1.0]: https://www.caraus.tech/projects/pub-graphql/repository/23/diff?rev=v0.11.1.0&rev_to=v0.11.0.0
[0.8.0.0]: https://github.com/caraus-ecms/graphql/compare/v0.7.0.0...v0.8.0.0 [0.11.0.0]: https://www.caraus.tech/projects/pub-graphql/repository/23/diff?rev=v0.11.0.0&rev_to=v0.10.0.0
[0.7.0.0]: https://github.com/caraus-ecms/graphql/compare/v0.6.1.0...v0.7.0.0 [0.10.0.0]: https://www.caraus.tech/projects/pub-graphql/repository/23/diff?rev=v0.10.0.0&rev_to=v0.9.0.0
[0.6.1.0]: https://github.com/caraus-ecms/graphql/compare/v0.6.0.0...v0.6.1.0 [0.9.0.0]: https://www.caraus.tech/projects/pub-graphql/repository/23/diff?rev=v0.9.0.0&rev_to=v0.8.0.0
[0.6.0.0]: https://github.com/caraus-ecms/graphql/compare/v0.5.1.0...v0.6.0.0 [0.8.0.0]: https://www.caraus.tech/projects/pub-graphql/repository/23/diff?rev=v0.8.0.0&rev_to=v0.7.0.0
[0.5.1.0]: https://github.com/caraus-ecms/graphql/compare/v0.5.0.1...v0.5.1.0 [0.7.0.0]: https://www.caraus.tech/projects/pub-graphql/repository/23/diff?rev=v0.7.0.0&rev_to=v0.6.1.0
[0.5.0.1]: https://github.com/caraus-ecms/graphql/compare/v0.5.0.0...v0.5.0.1 [0.6.1.0]: https://www.caraus.tech/projects/pub-graphql/repository/23/diff?rev=v0.6.1.0&rev_to=v0.6.0.0
[0.5.0.0]: https://github.com/caraus-ecms/graphql/compare/v0.4.0.0...v0.5.0.0 [0.6.0.0]: https://www.caraus.tech/projects/pub-graphql/repository/23/diff?rev=v0.6.0.0&rev_to=v0.5.1.0
[0.4.0.0]: https://github.com/caraus-ecms/graphql/compare/v0.3...v0.4.0.0 [0.5.1.0]: https://www.caraus.tech/projects/pub-graphql/repository/23/diff?rev=v0.5.1.0&rev_to=v0.5.0.1
[0.3]: https://github.com/caraus-ecms/graphql/compare/v0.2.1...v0.3 [0.5.0.1]: https://www.caraus.tech/projects/pub-graphql/repository/23/diff?rev=v0.5.0.1&rev_to=v0.5.0.0
[0.2.1]: https://github.com/caraus-ecms/graphql/compare/v0.2...v0.2.1 [0.5.0.0]: https://www.caraus.tech/projects/pub-graphql/repository/23/diff?rev=v0.5.0.0&rev_to=v0.4.0.0
[0.2]: https://github.com/caraus-ecms/graphql/compare/v0.1...v0.2 [0.4.0.0]: https://www.caraus.tech/projects/pub-graphql/repository/23/diff?rev=v0.4.0.0&rev_to=v0.3
[0.3]: https://www.caraus.tech/projects/pub-graphql/repository/23/diff?rev=v0.3&rev_to=v0.2.1
[0.2.1]: https://www.caraus.tech/projects/pub-graphql/repository/23/diff?rev=v0.2.1&rev_to=v0.2
[0.2]: https://www.caraus.tech/projects/pub-graphql/repository/23/diff?rev=v0.2&rev_to=v0.1

View File

@ -1,31 +0,0 @@
# 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
```

373
LICENSE.MPL Normal file
View File

@ -0,0 +1,373 @@
Mozilla Public License Version 2.0
==================================
1. Definitions
--------------
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
1.3. "Contribution"
means Covered Software of a particular Contributor.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
1.5. "Incompatible With Secondary Licenses"
means
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
1.10. "Modifications"
means any of the following:
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
(b) any new file in Source Code Form that contains any Covered
Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
2. License Grants and Conditions
--------------------------------
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
(a) for any code that a Contributor has removed from Covered Software;
or
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
3. Responsibilities
-------------------
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
5. Termination
--------------
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
************************************************************************
* *
* 6. Disclaimer of Warranty *
* ------------------------- *
* *
* Covered Software is provided under this License on an "as is" *
* basis, without warranty of any kind, either expressed, implied, or *
* statutory, including, without limitation, warranties that the *
* Covered Software is free of defects, merchantable, fit for a *
* particular purpose or non-infringing. The entire risk as to the *
* quality and performance of the Covered Software is with You. *
* Should any Covered Software prove defective in any respect, You *
* (not any Contributor) assume the cost of any necessary servicing, *
* repair, or correction. This disclaimer of warranty constitutes an *
* essential part of this License. No use of any Covered Software is *
* authorized under this License except under this disclaimer. *
* *
************************************************************************
************************************************************************
* *
* 7. Limitation of Liability *
* -------------------------- *
* *
* Under no circumstances and under no legal theory, whether tort *
* (including negligence), contract, or otherwise, shall any *
* Contributor, or anyone who distributes Covered Software as *
* permitted above, be liable to You for any direct, indirect, *
* special, incidental, or consequential damages of any character *
* including, without limitation, damages for lost profits, loss of *
* goodwill, work stoppage, computer failure or malfunction, or any *
* and all other commercial damages or losses, even if such party *
* shall have been informed of the possibility of such damages. This *
* limitation of liability shall not apply to liability for death or *
* personal injury resulting from such party's negligence to the *
* extent applicable law prohibits such limitation. Some *
* jurisdictions do not allow the exclusion or limitation of *
* incidental or consequential damages, so this exclusion and *
* limitation may not apply to You. *
* *
************************************************************************
8. Litigation
-------------
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
9. Miscellaneous
----------------
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
10. Versions of the License
---------------------------
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
-------------------------------------------
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.

145
README.md
View File

@ -1,148 +1,15 @@
# GraphQL implementation in Haskell # GraphQL implementation in Haskell
[![Hackage Version](https://img.shields.io/hackage/v/graphql.svg)](https://hackage.haskell.org/package/graphql)
[![Build Status](https://github.com/caraus-ecms/graphql/workflows/Haskell%20CI/badge.svg)](https://github.com/caraus-ecms/graphql/actions?query=workflow%3A%22Haskell+CI%22)
[![License](https://img.shields.io/badge/license-BSD--3--Clause-blue.svg)](https://raw.githubusercontent.com/caraus-ecms/graphql/master/LICENSE)
[![Simple Haskell](https://www.simplehaskell.org/badges/badge.svg)](https://www.simplehaskell.org) [![Simple Haskell](https://www.simplehaskell.org/badges/badge.svg)](https://www.simplehaskell.org)
[![CI/CD](https://img.shields.io/badge/CI-CD-brightgreen)](https://build.caraus.tech/go/pipelines)
This implementation is relatively low-level by design, it doesn't provide any See https://www.caraus.tech/projects/pub-graphql.
mappings between the GraphQL types and Haskell's type system and avoids
compile-time magic. It focuses on flexibility instead, so other solutions can
be built on top of it.
## State of the work Report issues on the
[bug tracker](https://www.caraus.tech/projects/pub-graphql/issues).
For now this library provides:
- Parser for the query and schema languages, as well as a printer for the query
language (minimizer and pretty-printer).
- Data structures to define a type system.
- Executor (queries, mutations and subscriptions are supported).
- Validation is work in progress.
- Introspection isn't available yet.
But the idea is to be a Haskell port of
[`graphql-js`](https://github.com/graphql/graphql-js).
For a more precise list of currently missing features see issues marked as
"[not implemented](https://github.com/caraus-ecms/graphql/labels/not%20implemented)".
## Documentation
API documentation is available through 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 Further documentation will be made available in the
[docs/tutorial](https://github.com/caraus-ecms/graphql/tree/master/docs/tutorial). [Wiki](https://www.caraus.tech/projects/pub-graphql/wiki).
### Getting started
We start with a simple GraphQL API that provides us with some famous and less
famous cites.
```graphql
"""
Root Query type.
"""
type Query {
"""
Provides a cite.
"""
cite: String!
}
```
This is called a GraphQL schema, it defines all queries supported by the API.
`Query` is the root query type. Every GraphQL API should define a query type.
`Query` has a single field `cite` that returns a `String`. The `!` after the
type denotes that the returned value cannot be `Null`. GraphQL fields are
nullable by default.
To be able to work with this schema, we are going to implement it in Haskell.
```haskell
{-# LANGUAGE OverloadedStrings #-}
import Control.Exception (SomeException)
import qualified Data.Aeson as Aeson
import qualified Data.ByteString.Lazy.Char8 as ByteString.Lazy.Char8
import qualified Data.HashMap.Strict as HashMap
import Language.GraphQL
import Language.GraphQL.Type
import qualified Language.GraphQL.Type.Out as Out
-- GraphQL supports 3 kinds of operations: queries, mutations and subscriptions.
-- Our first schema supports only queries.
schema :: Schema IO
schema = Schema
{ query = queryType, mutation = Nothing, subscription = Nothing }
-- GraphQL distinguishes between input and output types. Input types are field
-- argument types and they are defined in Language.GraphQL.Type.In. Output types
-- are result types, they are defined in Language.GraphQL.Type.Out. Root types
-- are always object types.
--
-- Here we define a type "Query". The second argument is an optional
-- description, the third one is the list of interfaces implemented by the
-- object type. The last argument is a field map. Keys are field names, values
-- are field definitions and resolvers. Resolvers are the functions, where the
-- actual logic lives, they return values for the respective fields.
queryType :: Out.ObjectType IO
queryType = Out.ObjectType "Query" (Just "Root Query type.") []
$ HashMap.singleton "cite" citeResolver
where
-- 'ValueResolver' is a 'Resolver' data constructor, it combines a field
-- definition with its resolver function. This function resolves a value for
-- a field (as opposed to the 'EventStreamResolver' used by subscriptions).
-- Our resolver just returns a constant value.
citeResolver = ValueResolver citeField
$ pure "Piscis primum a capite foetat"
-- The first argument is an optional field description. The second one is
-- the field type and the third one is for arguments (we have none in this
-- example).
--
-- GraphQL has named and wrapping types. String is a scalar, named type.
-- Named types are nullable by default. To make our "cite" field
-- non-nullable, we wrap it in the wrapping type, Non-Null.
citeField = Out.Field
(Just "Provides a cite.") (Out.NonNullScalarType string) HashMap.empty
-- Now we can execute a query. Since our schema defines only one field,
-- everything we can do is to ask to resolve it and give back the result.
-- Since subscriptions don't return plain values, the 'graphql' function returns
-- an 'Either'. 'Left' is for subscriptions, 'Right' is for queries and
-- mutations.
main :: IO ()
main = do
Right result <- graphql schema "{ cite }"
ByteString.Lazy.Char8.putStrLn $ Aeson.encode result
```
Executing this query produces the following JSON:
```json
{
"data": {
"cite": "Piscis primum a capite foetat"
}
}
```
## 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.
Should you have questions on usage, please open an issue and ask this helps
to write useful documentation.
Feel free to contact on Slack in [#haskell on
GraphQL](https://graphql.slack.com/messages/haskell/). You can obtain an
invitation [here](https://graphql-slack.herokuapp.com/).

View File

@ -1,152 +0,0 @@
---
title: GraphQL Haskell Tutorial
---
== Getting started ==
Welcome to GraphQL!
We have written a small tutorial to help you (and ourselves) understand the
graphql package.
Since this file is a literate haskell file, we start by importing some
dependencies.
> {-# LANGUAGE OverloadedStrings #-}
> module Main where
>
> import Control.Monad.IO.Class (liftIO)
> import Data.Aeson (encode)
> import Data.ByteString.Lazy.Char8 (putStrLn)
> import qualified Data.HashMap.Strict as HashMap
> import Data.Text (Text)
> import qualified Data.Text as Text
> import Data.Time (getCurrentTime)
>
> import Language.GraphQL
> import Language.GraphQL.Type
> import qualified Language.GraphQL.Type.Out as Out
>
> import Prelude hiding (putStrLn)
=== First example ===
Now, as our first example, we are going to look at the example from
[graphql.js](https://github.com/graphql/graphql-js).
First we build a GraphQL schema.
> schema1 :: Schema IO
> schema1 = Schema
> { query = queryType , mutation = Nothing , subscription = Nothing }
>
> queryType :: ObjectType IO
> queryType = ObjectType "Query" Nothing []
> $ HashMap.singleton "hello"
> $ ValueResolver helloField hello
>
> helloField :: Field IO
> helloField = Field Nothing (Out.NamedScalarType string) mempty
>
> hello :: Resolve IO
> hello = pure $ String "it's me"
This defines a simple schema with one type and one field, that resolves to a
fixed value.
Next we define our query.
> query1 :: Text
> query1 = "{ hello }"
To run the query, we call the `graphql` with the schema and the query.
> main1 :: IO ()
> main1 = graphql schema1 query1
> >>= either (const $ pure ()) (putStrLn . encode)
This runs the query by fetching the one field defined, returning
```{"data" : {"hello":"it's me"}}```
=== Monadic actions ===
For this example, we're going to be using time.
> schema2 :: Schema IO
> schema2 = Schema
> { query = queryType2, mutation = Nothing, subscription = Nothing }
>
> queryType2 :: ObjectType IO
> queryType2 = ObjectType "Query" Nothing []
> $ HashMap.singleton "time"
> $ ValueResolver timeField time
>
> timeField :: Field IO
> timeField = Field Nothing (Out.NamedScalarType string) mempty
>
> time :: Resolve IO
> time = do
> t <- liftIO getCurrentTime
> pure $ String $ Text.pack $ show t
This defines a simple schema with one type and one field, which resolves to the
current time.
Next we define our query.
> query2 :: Text
> query2 = "{ time }"
>
> main2 :: IO ()
> main2 = graphql schema2 query2
> >>= either (const $ pure ()) (putStrLn . encode)
This runs the query, returning the current time
```{"data": {"time":"2016-03-08 23:28:14.546899 UTC"}}```
=== Combining resolvers ===
Now that we have two resolvers, we can define a schema which uses them both.
> schema3 :: Schema IO
> schema3 = Schema
> { query = queryType3, mutation = Nothing, subscription = Nothing }
>
> queryType3 :: ObjectType IO
> queryType3 = ObjectType "Query" Nothing [] $ HashMap.fromList
> [ ("hello", ValueResolver helloField hello)
> , ("time", ValueResolver timeField time)
> ]
>
> query3 :: Text
> query3 = "query timeAndHello { time hello }"
>
> main3 :: IO ()
> main3 = graphql schema3 query3
> >>= either (const $ pure ()) (putStrLn . encode)
This queries for both time and hello, returning
```{ "data": {"hello":"it's me","time":"2016-03-08 23:29:11.62108 UTC"}}```
Notice that we can name our queries, as we did with `timeAndHello`. Since we
have only been using single queries, we can use the shorthand `{ time hello }`,
as we have been doing in the previous examples.
In GraphQL there can only be one operation per query.
== Further examples ==
More examples on queries and a more complex schema can be found in the test
directory, in the [Test.StarWars](../../tests/Test/StarWars) module. This
includes a more complex schema, and more complex queries.
> main :: IO ()
> main = main1 >> main2 >> main3

View File

@ -1,37 +1,30 @@
cabal-version: 1.12 cabal-version: 2.2
-- This file has been generated from package.yaml by hpack version 0.33.0.
--
-- see: https://github.com/sol/hpack
--
-- hash: 3ef060c57424074b84204bae61ee0a63e3470a7a060c45a977ff2bcbe4df8775
name: graphql name: graphql
version: 0.10.0.0 version: 1.0.0.0
synopsis: Haskell GraphQL implementation synopsis: Haskell GraphQL implementation
description: This package provides a rudimentary parser for the <https://graphql.github.io/graphql-spec/June2018/ GraphQL> language. description: Haskell <https://spec.graphql.org/June2018/ GraphQL> implementation.
category: Language category: Language
homepage: https://github.com/caraus-ecms/graphql#readme homepage: https://www.caraus.tech/projects/pub-graphql
bug-reports: https://github.com/caraus-ecms/graphql/issues bug-reports: https://www.caraus.tech/projects/pub-graphql/issues
author: Danny Navarro <j@dannynavarro.net>, author: Danny Navarro <j@dannynavarro.net>,
Matthías Páll Gissurarson <mpg@mpg.is>, Matthías Páll Gissurarson <mpg@mpg.is>,
Sólrún Halla Einarsdóttir <she@mpg.is> Sólrún Halla Einarsdóttir <she@mpg.is>
maintainer: belka@caraus.de maintainer: belka@caraus.de
copyright: (c) 2019-2020 Eugen Wissner, copyright: (c) 2019-2021 Eugen Wissner,
(c) 2015-2017 J. Daniel Navarro (c) 2015-2017 J. Daniel Navarro
license: BSD3 license: MPL-2.0 AND BSD-3-Clause
license-file: LICENSE license-files: LICENSE,
LICENSE.MPL
build-type: Simple build-type: Simple
extra-source-files: extra-source-files:
CHANGELOG.md CHANGELOG.md
CONTRIBUTING.md
LICENSE
README.md README.md
docs/tutorial/tutorial.lhs tested-with: GHC == 8.10.4
source-repository head source-repository head
type: git type: git
location: https://github.com/caraus-ecms/graphql location: git://caraus.tech/pub/graphql.git
library library
exposed-modules: exposed-modules:
@ -45,6 +38,7 @@ library
Language.GraphQL.Error Language.GraphQL.Error
Language.GraphQL.Execute Language.GraphQL.Execute
Language.GraphQL.Execute.Coerce Language.GraphQL.Execute.Coerce
Language.GraphQL.Execute.OrderedMap
Language.GraphQL.Type Language.GraphQL.Type
Language.GraphQL.Type.In Language.GraphQL.Type.In
Language.GraphQL.Type.Out Language.GraphQL.Type.Out
@ -54,6 +48,7 @@ library
Test.Hspec.GraphQL Test.Hspec.GraphQL
other-modules: other-modules:
Language.GraphQL.Execute.Execution Language.GraphQL.Execute.Execution
Language.GraphQL.Execute.Internal
Language.GraphQL.Execute.Subscribe Language.GraphQL.Execute.Subscribe
Language.GraphQL.Execute.Transform Language.GraphQL.Execute.Transform
Language.GraphQL.Type.Definition Language.GraphQL.Type.Definition
@ -61,59 +56,55 @@ library
Language.GraphQL.Validate.Rules Language.GraphQL.Validate.Rules
hs-source-dirs: hs-source-dirs:
src src
ghc-options: -Wall
build-depends: build-depends:
aeson aeson >= 1.5.6 && < 1.6
, base >= 4.7 && < 5 , base >= 4.7 && < 5
, conduit , conduit >= 1.3.4 && < 1.4
, containers , containers >= 0.6.2 && < 0.7
, exceptions , exceptions >= 0.10.4 && < 0.11
, hspec-expectations , hspec-expectations >= 0.8.2 && < 0.9
, megaparsec , megaparsec >= 9.0.1 && < 9.1
, parser-combinators , parser-combinators >= 1.3.0 && < 1.4
, scientific , scientific >= 0.3.7 && < 0.4
, text , text >= 1.2.4 && < 1.3
, transformers , transformers >= 0.5.6 && < 0.6
, unordered-containers , unordered-containers >= 0.2.14 && < 0.3
, vector >= 0.12.3 && < 0.13
default-language: Haskell2010 default-language: Haskell2010
test-suite tasty test-suite graphql-test
type: exitcode-stdio-1.0 type: exitcode-stdio-1.0
main-is: Spec.hs main-is: Spec.hs
other-modules: other-modules:
Language.GraphQL.AST.DocumentSpec
Language.GraphQL.AST.EncoderSpec Language.GraphQL.AST.EncoderSpec
Language.GraphQL.AST.LexerSpec Language.GraphQL.AST.LexerSpec
Language.GraphQL.AST.ParserSpec Language.GraphQL.AST.ParserSpec
Language.GraphQL.ErrorSpec Language.GraphQL.ErrorSpec
Language.GraphQL.Execute.CoerceSpec Language.GraphQL.Execute.CoerceSpec
Language.GraphQL.Execute.OrderedMapSpec
Language.GraphQL.ExecuteSpec Language.GraphQL.ExecuteSpec
Language.GraphQL.Type.OutSpec Language.GraphQL.Type.OutSpec
Language.GraphQL.ValidateSpec Language.GraphQL.Validate.RulesSpec
Test.DirectiveSpec Test.DirectiveSpec
Test.FragmentSpec Test.FragmentSpec
Test.RootOperationSpec Test.RootOperationSpec
Test.StarWars.Data
Test.StarWars.QuerySpec
Test.StarWars.Schema
Paths_graphql
hs-source-dirs: hs-source-dirs:
tests tests
ghc-options: -threaded -rtsopts -with-rtsopts=-N ghc-options: -threaded -rtsopts -with-rtsopts=-N -Wall
build-depends: build-depends:
QuickCheck QuickCheck >= 2.14.1 && < 2.15
, aeson , aeson
, base >= 4.7 && < 5 , base >= 4.7 && < 5
, conduit , conduit
, containers
, exceptions , exceptions
, graphql , graphql
, hspec , hspec >= 2.8.2 && < 2.9
, hspec-expectations , hspec-megaparsec >= 2.2.0 && < 2.3
, hspec-megaparsec
, megaparsec , megaparsec
, parser-combinators , raw-strings-qq >= 1.1 && < 1.2
, raw-strings-qq
, scientific , scientific
, text , text
, transformers
, unordered-containers , unordered-containers
default-language: Haskell2010 default-language: Haskell2010

View File

@ -1,62 +0,0 @@
name: graphql
version: 0.10.0.0
synopsis: Haskell GraphQL implementation
description:
This package provides a rudimentary parser for the
<https://graphql.github.io/graphql-spec/June2018/ GraphQL> language.
maintainer: belka@caraus.de
github: caraus-ecms/graphql
category: Language
copyright:
- (c) 2019-2020 Eugen Wissner
- (c) 2015-2017 J. Daniel Navarro
author:
- Danny Navarro <j@dannynavarro.net>
- Matthías Páll Gissurarson <mpg@mpg.is>
- Sólrún Halla Einarsdóttir <she@mpg.is>
extra-source-files:
- CHANGELOG.md
- CONTRIBUTING.md
- LICENSE
- README.md
- docs/tutorial/tutorial.lhs
dependencies:
- aeson
- base >= 4.7 && < 5
- conduit
- containers
- exceptions
- hspec-expectations
- megaparsec
- parser-combinators
- scientific
- text
- transformers
- unordered-containers
library:
source-dirs: src
other-modules:
- Language.GraphQL.Execute.Execution
- Language.GraphQL.Execute.Subscribe
- Language.GraphQL.Execute.Transform
- Language.GraphQL.Type.Definition
- Language.GraphQL.Type.Internal
- Language.GraphQL.Validate.Rules
tests:
tasty:
main: Spec.hs
source-dirs: tests
ghc-options:
- -threaded
- -rtsopts
- -with-rtsopts=-N
dependencies:
- graphql
- hspec
- hspec-megaparsec
- QuickCheck
- raw-strings-qq

View File

@ -11,13 +11,14 @@ import Control.Monad.Catch (MonadCatch)
import qualified Data.Aeson as Aeson import qualified Data.Aeson as Aeson
import qualified Data.Aeson.Types as Aeson import qualified Data.Aeson.Types as Aeson
import qualified Data.HashMap.Strict as HashMap import qualified Data.HashMap.Strict as HashMap
import Data.Maybe (catMaybes)
import qualified Data.Sequence as Seq import qualified Data.Sequence as Seq
import Data.Text (Text) import Data.Text (Text)
import Language.GraphQL.AST import Language.GraphQL.AST
import Language.GraphQL.Error import Language.GraphQL.Error
import Language.GraphQL.Execute import Language.GraphQL.Execute
import qualified Language.GraphQL.Validate as Validate import qualified Language.GraphQL.Validate as Validate
import Language.GraphQL.Type.Schema import Language.GraphQL.Type.Schema (Schema)
import Text.Megaparsec (parse) import Text.Megaparsec (parse)
-- | If the text parses correctly as a @GraphQL@ query the query is -- | If the text parses correctly as a @GraphQL@ query the query is
@ -55,24 +56,19 @@ graphqlSubs schema operationName variableValues document' =
[ ("data", data'') [ ("data", data'')
, ("errors", Aeson.toJSON $ fromError <$> errors') , ("errors", Aeson.toJSON $ fromError <$> errors')
] ]
fromError Error{ locations = [], ..} = fromError Error{..} = Aeson.object $ catMaybes
Aeson.object [("message", Aeson.toJSON message)] [ Just ("message", Aeson.toJSON message)
fromError Error{..} = Aeson.object , toMaybe fromLocation "locations" locations
, toMaybe fromPath "path" path
]
fromValidationError Validate.Error{..} = Aeson.object
[ ("message", Aeson.toJSON message) [ ("message", Aeson.toJSON message)
, ("locations", Aeson.listValue fromLocation locations) , ("locations", Aeson.listValue fromLocation locations)
] ]
fromValidationError Validate.Error{..} toMaybe _ _ [] = Nothing
| [] <- path = Aeson.object toMaybe f key xs = Just (key, Aeson.listValue f xs)
[ ("message", Aeson.toJSON message) fromPath (Segment segment) = Aeson.String segment
, ("locations", Aeson.listValue fromLocation locations) fromPath (Index index) = Aeson.toJSON index
]
| otherwise = Aeson.object
[ ("message", Aeson.toJSON message)
, ("locations", Aeson.listValue fromLocation locations)
, ("path", Aeson.listValue fromPath path)
]
fromPath (Validate.Segment segment) = Aeson.String segment
fromPath (Validate.Index index) = Aeson.toJSON index
fromLocation Location{..} = Aeson.object fromLocation Location{..} = Aeson.object
[ ("line", Aeson.toJSON line) [ ("line", Aeson.toJSON line)
, ("column", Aeson.toJSON column) , ("column", Aeson.toJSON column)

View File

@ -2,6 +2,8 @@
v. 2.0. If a copy of the MPL was not distributed with this file, You can v. 2.0. If a copy of the MPL was not distributed with this file, You can
obtain one at https://mozilla.org/MPL/2.0/. -} obtain one at https://mozilla.org/MPL/2.0/. -}
{-# LANGUAGE Safe #-}
-- | Various parts of a GraphQL document can be annotated with directives. -- | Various parts of a GraphQL document can be annotated with directives.
-- This module describes locations in a document where directives can appear. -- This module describes locations in a document where directives can appear.
module Language.GraphQL.AST.DirectiveLocation module Language.GraphQL.AST.DirectiveLocation
@ -16,7 +18,13 @@ module Language.GraphQL.AST.DirectiveLocation
data DirectiveLocation data DirectiveLocation
= ExecutableDirectiveLocation ExecutableDirectiveLocation = ExecutableDirectiveLocation ExecutableDirectiveLocation
| TypeSystemDirectiveLocation TypeSystemDirectiveLocation | TypeSystemDirectiveLocation TypeSystemDirectiveLocation
deriving (Eq, Show) deriving Eq
instance Show DirectiveLocation where
show (ExecutableDirectiveLocation directiveLocation) =
show directiveLocation
show (TypeSystemDirectiveLocation directiveLocation) =
show directiveLocation
-- | Where directives can appear in an executable definition, like a query. -- | Where directives can appear in an executable definition, like a query.
data ExecutableDirectiveLocation data ExecutableDirectiveLocation
@ -27,7 +35,16 @@ data ExecutableDirectiveLocation
| FragmentDefinition | FragmentDefinition
| FragmentSpread | FragmentSpread
| InlineFragment | InlineFragment
deriving (Eq, Show) deriving Eq
instance Show ExecutableDirectiveLocation where
show Query = "QUERY"
show Mutation = "MUTATION"
show Subscription = "SUBSCRIPTION"
show Field = "FIELD"
show FragmentDefinition = "FRAGMENT_DEFINITION"
show FragmentSpread = "FRAGMENT_SPREAD"
show InlineFragment = "INLINE_FRAGMENT"
-- | Where directives can appear in a type system definition. -- | Where directives can appear in a type system definition.
data TypeSystemDirectiveLocation data TypeSystemDirectiveLocation
@ -42,4 +59,17 @@ data TypeSystemDirectiveLocation
| EnumValue | EnumValue
| InputObject | InputObject
| InputFieldDefinition | InputFieldDefinition
deriving (Eq, Show) deriving Eq
instance Show TypeSystemDirectiveLocation where
show Schema = "SCHEMA"
show Scalar = "SCALAR"
show Object = "OBJECT"
show FieldDefinition = "FIELD_DEFINITION"
show ArgumentDefinition = "ARGUMENT_DEFINITION"
show Interface = "INTERFACE"
show Union = "UNION"
show Enum = "ENUM"
show EnumValue = "ENUM_VALUE"
show InputObject = "INPUT_OBJECT"
show InputFieldDefinition = "INPUT_FIELD_DEFINITION"

View File

@ -1,12 +1,16 @@
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE ExplicitForAll #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE Safe #-}
-- | This module defines an abstract syntax tree for the @GraphQL@ language. It -- | This module defines an abstract syntax tree for the @GraphQL@ language. It
-- follows closely the structure given in the specification. Please refer to -- follows closely the structure given in the specification. Please refer to
-- <https://facebook.github.io/graphql/ Facebook's GraphQL Specification>. -- <https://facebook.github.io/graphql/ Facebook's GraphQL Specification>.
-- for more information. -- for more information.
module Language.GraphQL.AST.Document module Language.GraphQL.AST.Document
( Alias ( Argument(..)
, Argument(..)
, ArgumentsDefinition(..) , ArgumentsDefinition(..)
, ConstValue(..) , ConstValue(..)
, Definition(..) , Definition(..)
@ -15,13 +19,17 @@ module Language.GraphQL.AST.Document
, Document , Document
, EnumValueDefinition(..) , EnumValueDefinition(..)
, ExecutableDefinition(..) , ExecutableDefinition(..)
, Field(..)
, FieldDefinition(..) , FieldDefinition(..)
, FragmentDefinition(..) , FragmentDefinition(..)
, FragmentSpread(..)
, ImplementsInterfaces(..) , ImplementsInterfaces(..)
, InlineFragment(..)
, InputValueDefinition(..) , InputValueDefinition(..)
, Location(..) , Location(..)
, Name , Name
, NamedType , NamedType
, Node(..)
, NonNullType(..) , NonNullType(..)
, ObjectField(..) , ObjectField(..)
, OperationDefinition(..) , OperationDefinition(..)
@ -40,14 +48,18 @@ module Language.GraphQL.AST.Document
, UnionMemberTypes(..) , UnionMemberTypes(..)
, Value(..) , Value(..)
, VariableDefinition(..) , VariableDefinition(..)
, escape
) where ) where
import Data.Char (ord)
import Data.Foldable (toList) import Data.Foldable (toList)
import Data.Int (Int32) import Data.Int (Int32)
import Data.List (intercalate)
import Data.List.NonEmpty (NonEmpty) import Data.List.NonEmpty (NonEmpty)
import Numeric (showFloat, showHex)
import Data.Text (Text) import Data.Text (Text)
import qualified Data.Text as Text import qualified Data.Text as Text
import Language.GraphQL.AST.DirectiveLocation import Language.GraphQL.AST.DirectiveLocation (DirectiveLocation)
-- * Language -- * Language
@ -68,6 +80,18 @@ instance Ord Location where
| thisLine > thatLine = GT | thisLine > thatLine = GT
| otherwise = compare thisColumn thatColumn | otherwise = compare thisColumn thatColumn
-- | Contains some tree node with a location.
data Node a = Node
{ node :: a
, location :: Location
} deriving Eq
instance Show a => Show (Node a) where
show Node{ node } = show node
instance Functor Node where
fmap f Node{..} = Node (f node) location
-- ** Document -- ** Document
-- | GraphQL document. -- | GraphQL document.
@ -116,10 +140,15 @@ type SelectionSet = NonEmpty Selection
-- | Field selection. -- | Field selection.
type SelectionSetOpt = [Selection] type SelectionSetOpt = [Selection]
-- | Selection is a single entry in a selection set. It can be a single field, -- | Selection is a single entry in a selection set. It can be a single 'Field',
-- fragment spread or inline fragment. -- 'FragmentSpread' or an 'InlineFragment'.
-- data Selection
-- The only required property of a field is its name. Optionally it can also = FieldSelection Field
| FragmentSpreadSelection FragmentSpread
| InlineFragmentSelection InlineFragment
deriving (Eq, Show)
-- | The only required property of a field is its name. Optionally it can also
-- have an alias, arguments, directives and a list of subfields. -- have an alias, arguments, directives and a list of subfields.
-- --
-- In the following query "user" is a field with two subfields, "id" and "name": -- In the following query "user" is a field with two subfields, "id" and "name":
@ -132,8 +161,27 @@ type SelectionSetOpt = [Selection]
-- } -- }
-- } -- }
-- @ -- @
data Field =
Field (Maybe Name) Name [Argument] [Directive] SelectionSetOpt Location
deriving (Eq, Show)
-- | Inline fragments don't have any name and the type condition ("on UserType")
-- is optional.
-- --
-- A fragment spread refers to a fragment defined outside the operation and is -- @
-- {
-- user {
-- ... on UserType {
-- id
-- name
-- }
-- }
-- @
data InlineFragment = InlineFragment
(Maybe TypeCondition) [Directive] SelectionSet Location
deriving (Eq, Show)
-- | A fragment spread refers to a fragment defined outside the operation and is
-- expanded at the execution time. -- expanded at the execution time.
-- --
-- @ -- @
@ -148,23 +196,7 @@ type SelectionSetOpt = [Selection]
-- name -- name
-- } -- }
-- @ -- @
-- data FragmentSpread = FragmentSpread Name [Directive] Location
-- 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) deriving (Eq, Show)
-- ** Arguments -- ** Arguments
@ -180,23 +212,7 @@ data Selection
-- @ -- @
-- --
-- Here "id" is an argument for the field "user" and its value is 4. -- Here "id" is an argument for the field "user" and its value is 4.
data Argument = Argument Name Value deriving (Eq,Show) data Argument = Argument Name (Node Value) Location 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 -- ** Fragments
@ -210,6 +226,34 @@ type TypeCondition = Name
-- ** Input Values -- ** Input Values
-- | Escapes a single character according to the GraphQL escaping rules for
-- double-quoted string values.
--
-- Characters, that should be escaped, are written as escaped characters with a
-- backslash or Unicode with an \"\\u\". Other characters are returned as
-- strings.
escape :: Char -> String
escape char'
| char' == '\\' = "\\\\"
| char' == '\"' = "\\\""
| char' == '\b' = "\\b"
| char' == '\f' = "\\f"
| char' == '\n' = "\\n"
| char' == '\r' = "\\r"
| char' == '\t' = "\\t"
| char' < '\x0010' = unicode "\\u000" char'
| char' < '\x0020' = unicode "\\u00" char'
| otherwise = [char']
where
unicode prefix uchar = prefix <> (showHex $ ord uchar) ""
showList' :: Show a => [a] -> String
showList' list = "[" ++ intercalate ", " (show <$> list) ++ "]"
showObject :: Show a => [ObjectField a] -> String
showObject fields =
"{ " ++ intercalate ", " (show <$> fields) ++ " }"
-- | Input value (literal or variable). -- | Input value (literal or variable).
data Value data Value
= Variable Name = Variable Name
@ -219,9 +263,21 @@ data Value
| Boolean Bool | Boolean Bool
| Null | Null
| Enum Name | Enum Name
| List [Value] | List [Node Value]
| Object [ObjectField Value] | Object [ObjectField Value]
deriving (Eq, Show) deriving Eq
instance Show Value where
showList = mappend . showList'
show (Variable variableName) = '$' : Text.unpack variableName
show (Int integer) = show integer
show (Float float) = show $ ConstFloat float
show (String text) = show $ ConstString text
show (Boolean boolean) = show boolean
show Null = "null"
show (Enum name) = Text.unpack name
show (List list) = show list
show (Object fields) = showObject fields
-- | Constant input value. -- | Constant input value.
data ConstValue data ConstValue
@ -231,15 +287,35 @@ data ConstValue
| ConstBoolean Bool | ConstBoolean Bool
| ConstNull | ConstNull
| ConstEnum Name | ConstEnum Name
| ConstList [ConstValue] | ConstList [Node ConstValue]
| ConstObject [ObjectField ConstValue] | ConstObject [ObjectField ConstValue]
deriving (Eq, Show) deriving Eq
instance Show ConstValue where
showList = mappend . showList'
show (ConstInt integer) = show integer
show (ConstFloat float) = showFloat float mempty
show (ConstString text) = "\"" <> Text.foldr (mappend . escape) "\"" text
show (ConstBoolean boolean) = show boolean
show ConstNull = "null"
show (ConstEnum name) = Text.unpack name
show (ConstList list) = show list
show (ConstObject fields) = showObject fields
-- | Key-value pair. -- | Key-value pair.
-- --
-- A list of 'ObjectField's represents a GraphQL object type. -- A list of 'ObjectField's represents a GraphQL object type.
data ObjectField a = ObjectField Name a data ObjectField a = ObjectField
deriving (Eq, Show) { name :: Name
, value :: Node a
, location :: Location
} deriving Eq
instance Show a => Show (ObjectField a) where
show ObjectField{..} = Text.unpack name ++ ": " ++ show value
instance Functor ObjectField where
fmap f ObjectField{..} = ObjectField name (f <$> value) location
-- ** Variables -- ** Variables
@ -248,18 +324,19 @@ data ObjectField a = ObjectField Name a
-- Each operation can include a list of variables: -- Each operation can include a list of variables:
-- --
-- @ -- @
-- query (protagonist: String = "Zarathustra") { -- query (protagonist: String = \"Zarathustra\") {
-- getAuthor(protagonist: $protagonist) -- getAuthor(protagonist: $protagonist)
-- } -- }
-- @ -- @
-- --
-- This query defines an optional variable @protagonist@ of type @String@, -- This query defines an optional variable @protagonist@ of type @String@,
-- its default value is "Zarathustra". If no default value is defined and no -- its default value is \"Zarathustra\". If no default value is defined and no
-- value is provided, a variable can still be @null@ if its type is nullable. -- value is provided, a variable can still be @null@ if its type is nullable.
-- --
-- Variables are usually passed along with the query, but not in the query -- Variables are usually passed along with the query, but not in the query
-- itself. They make queries reusable. -- itself. They make queries reusable.
data VariableDefinition = VariableDefinition Name Type (Maybe ConstValue) data VariableDefinition =
VariableDefinition Name Type (Maybe (Node ConstValue)) Location
deriving (Eq, Show) deriving (Eq, Show)
-- ** Type References -- ** Type References
@ -269,7 +346,12 @@ data Type
= TypeNamed Name = TypeNamed Name
| TypeList Type | TypeList Type
| TypeNonNull NonNullType | TypeNonNull NonNullType
deriving (Eq, Show) deriving Eq
instance Show Type where
show (TypeNamed typeName) = Text.unpack typeName
show (TypeList listType) = concat ["[", show listType, "]"]
show (TypeNonNull nonNullType) = show nonNullType
-- | Represents type names. -- | Represents type names.
type NamedType = Name type NamedType = Name
@ -278,7 +360,11 @@ type NamedType = Name
data NonNullType data NonNullType
= NonNullTypeNamed Name = NonNullTypeNamed Name
| NonNullTypeList Type | NonNullTypeList Type
deriving (Eq, Show) deriving Eq
instance Show NonNullType where
show (NonNullTypeNamed typeName) = '!' : Text.unpack typeName
show (NonNullTypeList listType) = concat ["![", show listType, "]"]
-- ** Directives -- ** Directives
@ -286,7 +372,7 @@ data NonNullType
-- --
-- Directives begin with "@", can accept arguments, and can be applied to the -- Directives begin with "@", can accept arguments, and can be applied to the
-- most GraphQL elements, providing additional information. -- most GraphQL elements, providing additional information.
data Directive = Directive Name [Argument] deriving (Eq, Show) data Directive = Directive Name [Argument] Location deriving (Eq, Show)
-- * Type System -- * Type System
@ -485,8 +571,8 @@ instance Monoid ArgumentsDefinition where
-- @ -- @
-- --
-- The input type "Point2D" contains two value definitions: "x" and "y". -- The input type "Point2D" contains two value definitions: "x" and "y".
data InputValueDefinition data InputValueDefinition = InputValueDefinition
= InputValueDefinition Description Name Type (Maybe ConstValue) [Directive] Description Name Type (Maybe (Node ConstValue)) [Directive]
deriving (Eq, Show) deriving (Eq, Show)
-- ** Unions -- ** Unions

View File

@ -1,6 +1,8 @@
{-# LANGUAGE ExplicitForAll #-} {-# LANGUAGE ExplicitForAll #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE LambdaCase #-} {-# LANGUAGE LambdaCase #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE Safe #-}
-- | This module defines a minifier and a printer for the @GraphQL@ language. -- | This module defines a minifier and a printer for the @GraphQL@ language.
module Language.GraphQL.AST.Encoder module Language.GraphQL.AST.Encoder
@ -14,7 +16,6 @@ module Language.GraphQL.AST.Encoder
, value , value
) where ) where
import Data.Char (ord)
import Data.Foldable (fold) import Data.Foldable (fold)
import qualified Data.List.NonEmpty as NonEmpty import qualified Data.List.NonEmpty as NonEmpty
import Data.Text (Text) import Data.Text (Text)
@ -23,9 +24,9 @@ import qualified Data.Text.Lazy as Lazy (Text)
import qualified Data.Text.Lazy as Lazy.Text import qualified Data.Text.Lazy as Lazy.Text
import Data.Text.Lazy.Builder (Builder) import Data.Text.Lazy.Builder (Builder)
import qualified Data.Text.Lazy.Builder as Builder import qualified Data.Text.Lazy.Builder as Builder
import Data.Text.Lazy.Builder.Int (decimal, hexadecimal) import Data.Text.Lazy.Builder.Int (decimal)
import Data.Text.Lazy.Builder.RealFloat (realFloat) import Data.Text.Lazy.Builder.RealFloat (realFloat)
import Language.GraphQL.AST.Document import qualified Language.GraphQL.AST.Document as Full
-- | Instructs the encoder whether the GraphQL document should be minified or -- | Instructs the encoder whether the GraphQL document should be minified or
-- pretty printed. -- pretty printed.
@ -44,77 +45,78 @@ minified :: Formatter
minified = Minified minified = Minified
-- | Converts a Document' into a string. -- | Converts a Document' into a string.
document :: Formatter -> Document -> Lazy.Text document :: Formatter -> Full.Document -> Lazy.Text
document formatter defs document formatter defs
| Pretty _ <- formatter = Lazy.Text.intercalate "\n" encodeDocument | Pretty _ <- formatter = Lazy.Text.intercalate "\n" encodeDocument
| Minified <-formatter = Lazy.Text.snoc (mconcat encodeDocument) '\n' | Minified <-formatter = Lazy.Text.snoc (mconcat encodeDocument) '\n'
where where
encodeDocument = foldr executableDefinition [] defs encodeDocument = foldr executableDefinition [] defs
executableDefinition (ExecutableDefinition executableDefinition') acc = executableDefinition (Full.ExecutableDefinition executableDefinition') acc =
definition formatter executableDefinition' : acc definition formatter executableDefinition' : acc
executableDefinition _ acc = acc executableDefinition _ acc = acc
-- | Converts a t'ExecutableDefinition' into a string. -- | Converts a t'Full.ExecutableDefinition' into a string.
definition :: Formatter -> ExecutableDefinition -> Lazy.Text definition :: Formatter -> Full.ExecutableDefinition -> Lazy.Text
definition formatter x definition formatter x
| Pretty _ <- formatter = Lazy.Text.snoc (encodeDefinition x) '\n' | Pretty _ <- formatter = Lazy.Text.snoc (encodeDefinition x) '\n'
| Minified <- formatter = encodeDefinition x | Minified <- formatter = encodeDefinition x
where where
encodeDefinition (DefinitionOperation operation) encodeDefinition (Full.DefinitionOperation operation)
= operationDefinition formatter operation = operationDefinition formatter operation
encodeDefinition (DefinitionFragment fragment) encodeDefinition (Full.DefinitionFragment fragment)
= fragmentDefinition formatter fragment = fragmentDefinition formatter fragment
-- | Converts a 'OperationDefinition into a string. -- | Converts a 'Full.OperationDefinition into a string.
operationDefinition :: Formatter -> OperationDefinition -> Lazy.Text operationDefinition :: Formatter -> Full.OperationDefinition -> Lazy.Text
operationDefinition formatter = \case operationDefinition formatter = \case
SelectionSet sels _ -> selectionSet formatter sels Full.SelectionSet sels _ -> selectionSet formatter sels
OperationDefinition Query name vars dirs sels _ -> Full.OperationDefinition Full.Query name vars dirs sels _ ->
"query " <> node formatter name vars dirs sels "query " <> root name vars dirs sels
OperationDefinition Mutation name vars dirs sels _ -> Full.OperationDefinition Full.Mutation name vars dirs sels _ ->
"mutation " <> node formatter name vars dirs sels "mutation " <> root name vars dirs sels
OperationDefinition Subscription name vars dirs sels _ -> Full.OperationDefinition Full.Subscription name vars dirs sels _ ->
"subscription " <> node formatter name vars dirs sels "subscription " <> root name vars dirs sels
where
-- | Converts a Query or Mutation into a string. -- | Converts a Query or Mutation into a string.
node :: Formatter -> root :: Maybe Full.Name ->
Maybe Name -> [Full.VariableDefinition] ->
[VariableDefinition] -> [Full.Directive] ->
[Directive] -> Full.SelectionSet ->
SelectionSet ->
Lazy.Text Lazy.Text
node formatter name vars dirs sels root name vars dirs sels
= Lazy.Text.fromStrict (fold name) = Lazy.Text.fromStrict (fold name)
<> optempty (variableDefinitions formatter) vars <> optempty (variableDefinitions formatter) vars
<> optempty (directives formatter) dirs <> optempty (directives formatter) dirs
<> eitherFormat formatter " " mempty <> eitherFormat formatter " " mempty
<> selectionSet formatter sels <> selectionSet formatter sels
variableDefinitions :: Formatter -> [VariableDefinition] -> Lazy.Text variableDefinitions :: Formatter -> [Full.VariableDefinition] -> Lazy.Text
variableDefinitions formatter variableDefinitions formatter
= parensCommas formatter $ variableDefinition formatter = parensCommas formatter $ variableDefinition formatter
variableDefinition :: Formatter -> VariableDefinition -> Lazy.Text variableDefinition :: Formatter -> Full.VariableDefinition -> Lazy.Text
variableDefinition formatter (VariableDefinition var ty defaultValue') variableDefinition formatter variableDefinition' =
= variable var let Full.VariableDefinition variableName variableType defaultValue' _ =
variableDefinition'
in variable variableName
<> eitherFormat formatter ": " ":" <> eitherFormat formatter ": " ":"
<> type' ty <> type' variableType
<> maybe mempty (defaultValue formatter) defaultValue' <> maybe mempty (defaultValue formatter) (Full.node <$> defaultValue')
defaultValue :: Formatter -> ConstValue -> Lazy.Text defaultValue :: Formatter -> Full.ConstValue -> Lazy.Text
defaultValue formatter val defaultValue formatter val
= eitherFormat formatter " = " "=" = eitherFormat formatter " = " "="
<> value formatter (fromConstValue val) <> value formatter (fromConstValue val)
variable :: Name -> Lazy.Text variable :: Full.Name -> Lazy.Text
variable var = "$" <> Lazy.Text.fromStrict var variable var = "$" <> Lazy.Text.fromStrict var
selectionSet :: Formatter -> SelectionSet -> Lazy.Text selectionSet :: Formatter -> Full.SelectionSet -> Lazy.Text
selectionSet formatter selectionSet formatter
= bracesList formatter (selection formatter) = bracesList formatter (selection formatter)
. NonEmpty.toList . NonEmpty.toList
selectionSetOpt :: Formatter -> SelectionSetOpt -> Lazy.Text selectionSetOpt :: Formatter -> Full.SelectionSetOpt -> Lazy.Text
selectionSetOpt formatter = bracesList formatter $ selection formatter selectionSetOpt formatter = bracesList formatter $ selection formatter
indentSymbol :: Lazy.Text indentSymbol :: Lazy.Text
@ -123,15 +125,15 @@ indentSymbol = " "
indent :: (Integral a) => a -> Lazy.Text indent :: (Integral a) => a -> Lazy.Text
indent indentation = Lazy.Text.replicate (fromIntegral indentation) indentSymbol indent indentation = Lazy.Text.replicate (fromIntegral indentation) indentSymbol
selection :: Formatter -> Selection -> Lazy.Text selection :: Formatter -> Full.Selection -> Lazy.Text
selection formatter = Lazy.Text.append indent' . encodeSelection selection formatter = Lazy.Text.append indent' . encodeSelection
where where
encodeSelection (Field alias name args directives' selections) = encodeSelection (Full.FieldSelection fieldSelection) =
field incrementIndent alias name args directives' selections field incrementIndent fieldSelection
encodeSelection (InlineFragment typeCondition directives' selections) = encodeSelection (Full.InlineFragmentSelection fragmentSelection) =
inlineFragment incrementIndent typeCondition directives' selections inlineFragment incrementIndent fragmentSelection
encodeSelection (FragmentSpread name directives') = encodeSelection (Full.FragmentSpreadSelection fragmentSelection) =
fragmentSpread incrementIndent name directives' fragmentSpread incrementIndent fragmentSelection
incrementIndent incrementIndent
| Pretty indentation <- formatter = Pretty $ indentation + 1 | Pretty indentation <- formatter = Pretty $ indentation + 1
| otherwise = Minified | otherwise = Minified
@ -142,15 +144,9 @@ selection formatter = Lazy.Text.append indent' . encodeSelection
colon :: Formatter -> Lazy.Text colon :: Formatter -> Lazy.Text
colon formatter = eitherFormat formatter ": " ":" colon formatter = eitherFormat formatter ": " ":"
-- | Converts Field into a string -- | Converts Field into a string.
field :: Formatter -> field :: Formatter -> Full.Field -> Lazy.Text
Maybe Name -> field formatter (Full.Field alias name args dirs set _)
Name ->
[Argument] ->
[Directive] ->
[Selection] ->
Lazy.Text
field formatter alias name args dirs set
= optempty prependAlias (fold alias) = optempty prependAlias (fold alias)
<> Lazy.Text.fromStrict name <> Lazy.Text.fromStrict name
<> optempty (arguments formatter) args <> optempty (arguments formatter) args
@ -161,36 +157,32 @@ field formatter alias name args dirs set
selectionSetOpt' = (eitherFormat formatter " " "" <>) selectionSetOpt' = (eitherFormat formatter " " "" <>)
. selectionSetOpt formatter . selectionSetOpt formatter
arguments :: Formatter -> [Argument] -> Lazy.Text arguments :: Formatter -> [Full.Argument] -> Lazy.Text
arguments formatter = parensCommas formatter $ argument formatter arguments formatter = parensCommas formatter $ argument formatter
argument :: Formatter -> Argument -> Lazy.Text argument :: Formatter -> Full.Argument -> Lazy.Text
argument formatter (Argument name value') argument formatter (Full.Argument name value' _)
= Lazy.Text.fromStrict name = Lazy.Text.fromStrict name
<> colon formatter <> colon formatter
<> value formatter value' <> value formatter (Full.node value')
-- * Fragments -- * Fragments
fragmentSpread :: Formatter -> Name -> [Directive] -> Lazy.Text fragmentSpread :: Formatter -> Full.FragmentSpread -> Lazy.Text
fragmentSpread formatter name directives' fragmentSpread formatter (Full.FragmentSpread name directives' _)
= "..." <> Lazy.Text.fromStrict name = "..." <> Lazy.Text.fromStrict name
<> optempty (directives formatter) directives' <> optempty (directives formatter) directives'
inlineFragment :: inlineFragment :: Formatter -> Full.InlineFragment -> Lazy.Text
Formatter -> inlineFragment formatter (Full.InlineFragment typeCondition directives' selections _)
Maybe TypeCondition -> = "... on "
[Directive] -> <> Lazy.Text.fromStrict (fold typeCondition)
SelectionSet -> <> directives formatter directives'
Lazy.Text
inlineFragment formatter tc dirs sels = "... on "
<> Lazy.Text.fromStrict (fold tc)
<> directives formatter dirs
<> eitherFormat formatter " " mempty <> eitherFormat formatter " " mempty
<> selectionSet formatter sels <> selectionSet formatter selections
fragmentDefinition :: Formatter -> FragmentDefinition -> Lazy.Text fragmentDefinition :: Formatter -> Full.FragmentDefinition -> Lazy.Text
fragmentDefinition formatter (FragmentDefinition name tc dirs sels _) fragmentDefinition formatter (Full.FragmentDefinition name tc dirs sels _)
= "fragment " <> Lazy.Text.fromStrict name = "fragment " <> Lazy.Text.fromStrict name
<> " on " <> Lazy.Text.fromStrict tc <> " on " <> Lazy.Text.fromStrict tc
<> optempty (directives formatter) dirs <> optempty (directives formatter) dirs
@ -199,39 +191,39 @@ fragmentDefinition formatter (FragmentDefinition name tc dirs sels _)
-- * Miscellaneous -- * Miscellaneous
-- | Converts a 'Directive' into a string. -- | Converts a 'Full.Directive' into a string.
directive :: Formatter -> Directive -> Lazy.Text directive :: Formatter -> Full.Directive -> Lazy.Text
directive formatter (Directive name args) directive formatter (Full.Directive name args _)
= "@" <> Lazy.Text.fromStrict name <> optempty (arguments formatter) args = "@" <> Lazy.Text.fromStrict name <> optempty (arguments formatter) args
directives :: Formatter -> [Directive] -> Lazy.Text directives :: Formatter -> [Full.Directive] -> Lazy.Text
directives Minified = spaces (directive Minified) directives Minified = spaces (directive Minified)
directives formatter = Lazy.Text.cons ' ' . spaces (directive formatter) directives formatter = Lazy.Text.cons ' ' . spaces (directive formatter)
-- | Converts a 'Value' into a string. -- | Converts a 'Full.Value' into a string.
value :: Formatter -> Value -> Lazy.Text value :: Formatter -> Full.Value -> Lazy.Text
value _ (Variable x) = variable x value _ (Full.Variable x) = variable x
value _ (Int x) = Builder.toLazyText $ decimal x value _ (Full.Int x) = Builder.toLazyText $ decimal x
value _ (Float x) = Builder.toLazyText $ realFloat x value _ (Full.Float x) = Builder.toLazyText $ realFloat x
value _ (Boolean x) = booleanValue x value _ (Full.Boolean x) = booleanValue x
value _ Null = "null" value _ Full.Null = "null"
value formatter (String string) = stringValue formatter string value formatter (Full.String string) = stringValue formatter string
value _ (Enum x) = Lazy.Text.fromStrict x value _ (Full.Enum x) = Lazy.Text.fromStrict x
value formatter (List x) = listValue formatter x value formatter (Full.List x) = listValue formatter x
value formatter (Object x) = objectValue formatter x value formatter (Full.Object x) = objectValue formatter x
fromConstValue :: ConstValue -> Value fromConstValue :: Full.ConstValue -> Full.Value
fromConstValue (ConstInt x) = Int x fromConstValue (Full.ConstInt x) = Full.Int x
fromConstValue (ConstFloat x) = Float x fromConstValue (Full.ConstFloat x) = Full.Float x
fromConstValue (ConstBoolean x) = Boolean x fromConstValue (Full.ConstBoolean x) = Full.Boolean x
fromConstValue ConstNull = Null fromConstValue Full.ConstNull = Full.Null
fromConstValue (ConstString string) = String string fromConstValue (Full.ConstString string) = Full.String string
fromConstValue (ConstEnum x) = Enum x fromConstValue (Full.ConstEnum x) = Full.Enum x
fromConstValue (ConstList x) = List $ fromConstValue <$> x fromConstValue (Full.ConstList x) = Full.List $ fmap fromConstValue <$> x
fromConstValue (ConstObject x) = Object $ fromConstObjectField <$> x fromConstValue (Full.ConstObject x) = Full.Object $ fromConstObjectField <$> x
where where
fromConstObjectField (ObjectField key value') = fromConstObjectField Full.ObjectField{value = value', ..} =
ObjectField key $ fromConstValue value' Full.ObjectField name (fromConstValue <$> value') location
booleanValue :: Bool -> Lazy.Text booleanValue :: Bool -> Lazy.Text
booleanValue True = "true" booleanValue True = "true"
@ -241,11 +233,12 @@ quote :: Builder.Builder
quote = Builder.singleton '\"' quote = Builder.singleton '\"'
oneLine :: Text -> Builder oneLine :: Text -> Builder
oneLine string = quote <> Text.foldr (mappend . escape) quote string oneLine string = quote <> Text.foldr merge quote string
where
merge = mappend . Builder.fromString . Full.escape
stringValue :: Formatter -> Text -> Lazy.Text stringValue :: Formatter -> Text -> Lazy.Text
stringValue Minified string = Builder.toLazyText stringValue Minified string = Builder.toLazyText $ oneLine string
$ quote <> Text.foldr (mappend . escape) quote string
stringValue (Pretty indentation) string = stringValue (Pretty indentation) string =
if hasEscaped string if hasEscaped string
then stringValue Minified string then stringValue Minified string
@ -273,25 +266,10 @@ stringValue (Pretty indentation) string =
= Builder.fromLazyText (indent (indentation + 1)) = Builder.fromLazyText (indent (indentation + 1))
<> line' <> newline <> acc <> line' <> newline <> acc
escape :: Char -> Builder listValue :: Formatter -> [Full.Node Full.Value] -> Lazy.Text
escape char' listValue formatter = bracketsCommas formatter $ value formatter . Full.node
| 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 -> [Value] -> Lazy.Text objectValue :: Formatter -> [Full.ObjectField Full.Value] -> Lazy.Text
listValue formatter = bracketsCommas formatter $ value formatter
objectValue :: Formatter -> [ObjectField Value] -> Lazy.Text
objectValue formatter = intercalate $ objectField formatter objectValue formatter = intercalate $ objectField formatter
where where
intercalate f intercalate f
@ -299,22 +277,22 @@ objectValue formatter = intercalate $ objectField formatter
. Lazy.Text.intercalate (eitherFormat formatter ", " ",") . Lazy.Text.intercalate (eitherFormat formatter ", " ",")
. fmap f . fmap f
objectField :: Formatter -> ObjectField Value -> Lazy.Text objectField :: Formatter -> Full.ObjectField Full.Value -> Lazy.Text
objectField formatter (ObjectField name value') = objectField formatter (Full.ObjectField name (Full.Node value' _) _) =
Lazy.Text.fromStrict name <> colon formatter <> value formatter value' Lazy.Text.fromStrict name <> colon formatter <> value formatter value'
-- | Converts a 'Type' a type into a string. -- | Converts a 'Full.Type' a type into a string.
type' :: Type -> Lazy.Text type' :: Full.Type -> Lazy.Text
type' (TypeNamed x) = Lazy.Text.fromStrict x type' (Full.TypeNamed x) = Lazy.Text.fromStrict x
type' (TypeList x) = listType x type' (Full.TypeList x) = listType x
type' (TypeNonNull x) = nonNullType x type' (Full.TypeNonNull x) = nonNullType x
listType :: Type -> Lazy.Text listType :: Full.Type -> Lazy.Text
listType x = brackets (type' x) listType x = brackets (type' x)
nonNullType :: NonNullType -> Lazy.Text nonNullType :: Full.NonNullType -> Lazy.Text
nonNullType (NonNullTypeNamed x) = Lazy.Text.fromStrict x <> "!" nonNullType (Full.NonNullTypeNamed x) = Lazy.Text.fromStrict x <> "!"
nonNullType (NonNullTypeList x) = listType x <> "!" nonNullType (Full.NonNullTypeList x) = listType x <> "!"
-- * Internal -- * Internal

View File

@ -92,16 +92,16 @@ dollar :: Parser T.Text
dollar = symbol "$" dollar = symbol "$"
-- | Parser for "@". -- | Parser for "@".
at :: Parser Text at :: Parser ()
at = symbol "@" at = symbol "@" >> pure ()
-- | Parser for "&". -- | Parser for "&".
amp :: Parser T.Text amp :: Parser T.Text
amp = symbol "&" amp = symbol "&"
-- | Parser for ":". -- | Parser for ":".
colon :: Parser T.Text colon :: Parser ()
colon = symbol ":" colon = symbol ":" >> pure ()
-- | Parser for "=". -- | Parser for "=".
equals :: Parser T.Text equals :: Parser T.Text

View File

@ -1,3 +1,4 @@
{-# LANGUAGE ExplicitForAll #-}
{-# LANGUAGE LambdaCase #-} {-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE RecordWildCards #-}
@ -13,12 +14,8 @@ import qualified Control.Applicative.Combinators.NonEmpty as NonEmpty
import Data.List.NonEmpty (NonEmpty(..)) import Data.List.NonEmpty (NonEmpty(..))
import Data.Text (Text) import Data.Text (Text)
import qualified Language.GraphQL.AST.DirectiveLocation as Directive import qualified Language.GraphQL.AST.DirectiveLocation as Directive
import Language.GraphQL.AST.DirectiveLocation import Language.GraphQL.AST.DirectiveLocation (DirectiveLocation)
( DirectiveLocation import qualified Language.GraphQL.AST.Document as Full
, ExecutableDirectiveLocation
, TypeSystemDirectiveLocation
)
import Language.GraphQL.AST.Document
import Language.GraphQL.AST.Lexer import Language.GraphQL.AST.Lexer
import Text.Megaparsec import Text.Megaparsec
( MonadParsec(..) ( MonadParsec(..)
@ -32,13 +29,13 @@ import Text.Megaparsec
) )
-- | Parser for the GraphQL documents. -- | Parser for the GraphQL documents.
document :: Parser Document document :: Parser Full.Document
document = unicodeBOM document = unicodeBOM
*> spaceConsumer *> spaceConsumer
*> lexeme (NonEmpty.some definition) *> lexeme (NonEmpty.some definition)
definition :: Parser Definition definition :: Parser Full.Definition
definition = ExecutableDefinition <$> executableDefinition definition = Full.ExecutableDefinition <$> executableDefinition
<|> typeSystemDefinition' <|> typeSystemDefinition'
<|> typeSystemExtension' <|> typeSystemExtension'
<?> "Definition" <?> "Definition"
@ -46,41 +43,41 @@ definition = ExecutableDefinition <$> executableDefinition
typeSystemDefinition' = do typeSystemDefinition' = do
location <- getLocation location <- getLocation
definition' <- typeSystemDefinition definition' <- typeSystemDefinition
pure $ TypeSystemDefinition definition' location pure $ Full.TypeSystemDefinition definition' location
typeSystemExtension' = do typeSystemExtension' = do
location <- getLocation location <- getLocation
definition' <- typeSystemExtension definition' <- typeSystemExtension
pure $ TypeSystemExtension definition' location pure $ Full.TypeSystemExtension definition' location
getLocation :: Parser Location getLocation :: Parser Full.Location
getLocation = fromSourcePosition <$> getSourcePos getLocation = fromSourcePosition <$> getSourcePos
where where
fromSourcePosition SourcePos{..} = fromSourcePosition SourcePos{..} =
Location (wordFromPosition sourceLine) (wordFromPosition sourceColumn) Full.Location (wordFromPosition sourceLine) (wordFromPosition sourceColumn)
wordFromPosition = fromIntegral . unPos wordFromPosition = fromIntegral . unPos
executableDefinition :: Parser ExecutableDefinition executableDefinition :: Parser Full.ExecutableDefinition
executableDefinition = DefinitionOperation <$> operationDefinition executableDefinition = Full.DefinitionOperation <$> operationDefinition
<|> DefinitionFragment <$> fragmentDefinition <|> Full.DefinitionFragment <$> fragmentDefinition
<?> "ExecutableDefinition" <?> "ExecutableDefinition"
typeSystemDefinition :: Parser TypeSystemDefinition typeSystemDefinition :: Parser Full.TypeSystemDefinition
typeSystemDefinition = schemaDefinition typeSystemDefinition = schemaDefinition
<|> typeSystemDefinitionWithDescription <|> typeSystemDefinitionWithDescription
<?> "TypeSystemDefinition" <?> "TypeSystemDefinition"
where where
typeSystemDefinitionWithDescription = description typeSystemDefinitionWithDescription = description
>>= liftA2 (<|>) typeDefinition' directiveDefinition >>= liftA2 (<|>) typeDefinition' directiveDefinition
typeDefinition' description' = TypeDefinition typeDefinition' description' = Full.TypeDefinition
<$> typeDefinition description' <$> typeDefinition description'
typeSystemExtension :: Parser TypeSystemExtension typeSystemExtension :: Parser Full.TypeSystemExtension
typeSystemExtension = SchemaExtension <$> schemaExtension typeSystemExtension = Full.SchemaExtension <$> schemaExtension
<|> TypeExtension <$> typeExtension <|> Full.TypeExtension <$> typeExtension
<?> "TypeSystemExtension" <?> "TypeSystemExtension"
directiveDefinition :: Description -> Parser TypeSystemDefinition directiveDefinition :: Full.Description -> Parser Full.TypeSystemDefinition
directiveDefinition description' = DirectiveDefinition description' directiveDefinition description' = Full.DirectiveDefinition description'
<$ symbol "directive" <$ symbol "directive"
<* at <* at
<*> name <*> name
@ -95,36 +92,30 @@ directiveLocations = optional pipe
<?> "DirectiveLocations" <?> "DirectiveLocations"
directiveLocation :: Parser DirectiveLocation directiveLocation :: Parser DirectiveLocation
directiveLocation directiveLocation = e (Directive.Query <$ symbol "QUERY")
= Directive.ExecutableDirectiveLocation <$> executableDirectiveLocation <|> e (Directive.Mutation <$ symbol "MUTATION")
<|> Directive.TypeSystemDirectiveLocation <$> typeSystemDirectiveLocation <|> e (Directive.Subscription <$ symbol "SUBSCRIPTION")
<|> t (Directive.FieldDefinition <$ symbol "FIELD_DEFINITION")
<|> e (Directive.Field <$ symbol "FIELD")
<|> e (Directive.FragmentDefinition <$ "FRAGMENT_DEFINITION")
<|> e (Directive.FragmentSpread <$ "FRAGMENT_SPREAD")
<|> e (Directive.InlineFragment <$ "INLINE_FRAGMENT")
<|> t (Directive.Schema <$ symbol "SCHEMA")
<|> t (Directive.Scalar <$ symbol "SCALAR")
<|> t (Directive.Object <$ symbol "OBJECT")
<|> t (Directive.ArgumentDefinition <$ symbol "ARGUMENT_DEFINITION")
<|> t (Directive.Interface <$ symbol "INTERFACE")
<|> t (Directive.Union <$ symbol "UNION")
<|> t (Directive.EnumValue <$ symbol "ENUM_VALUE")
<|> t (Directive.Enum <$ symbol "ENUM")
<|> t (Directive.InputObject <$ symbol "INPUT_OBJECT")
<|> t (Directive.InputFieldDefinition <$ symbol "INPUT_FIELD_DEFINITION")
<?> "DirectiveLocation" <?> "DirectiveLocation"
where
e = fmap Directive.ExecutableDirectiveLocation
t = fmap Directive.TypeSystemDirectiveLocation
executableDirectiveLocation :: Parser ExecutableDirectiveLocation typeDefinition :: Full.Description -> Parser Full.TypeDefinition
executableDirectiveLocation = Directive.Query <$ symbol "QUERY"
<|> Directive.Mutation <$ symbol "MUTATION"
<|> Directive.Subscription <$ symbol "SUBSCRIPTION"
<|> Directive.Field <$ symbol "FIELD"
<|> Directive.FragmentDefinition <$ "FRAGMENT_DEFINITION"
<|> Directive.FragmentSpread <$ "FRAGMENT_SPREAD"
<|> Directive.InlineFragment <$ "INLINE_FRAGMENT"
<?> "ExecutableDirectiveLocation"
typeSystemDirectiveLocation :: Parser TypeSystemDirectiveLocation
typeSystemDirectiveLocation = Directive.Schema <$ symbol "SCHEMA"
<|> Directive.Scalar <$ symbol "SCALAR"
<|> Directive.Object <$ symbol "OBJECT"
<|> Directive.FieldDefinition <$ symbol "FIELD_DEFINITION"
<|> Directive.ArgumentDefinition <$ symbol "ARGUMENT_DEFINITION"
<|> Directive.Interface <$ symbol "INTERFACE"
<|> Directive.Union <$ symbol "UNION"
<|> Directive.Enum <$ symbol "ENUM"
<|> Directive.EnumValue <$ symbol "ENUM_VALUE"
<|> Directive.InputObject <$ symbol "INPUT_OBJECT"
<|> Directive.InputFieldDefinition <$ symbol "INPUT_FIELD_DEFINITION"
<?> "TypeSystemDirectiveLocation"
typeDefinition :: Description -> Parser TypeDefinition
typeDefinition description' = scalarTypeDefinition description' typeDefinition description' = scalarTypeDefinition description'
<|> objectTypeDefinition description' <|> objectTypeDefinition description'
<|> interfaceTypeDefinition description' <|> interfaceTypeDefinition description'
@ -133,7 +124,7 @@ typeDefinition description' = scalarTypeDefinition description'
<|> inputObjectTypeDefinition description' <|> inputObjectTypeDefinition description'
<?> "TypeDefinition" <?> "TypeDefinition"
typeExtension :: Parser TypeExtension typeExtension :: Parser Full.TypeExtension
typeExtension = scalarTypeExtension typeExtension = scalarTypeExtension
<|> objectTypeExtension <|> objectTypeExtension
<|> interfaceTypeExtension <|> interfaceTypeExtension
@ -142,143 +133,143 @@ typeExtension = scalarTypeExtension
<|> inputObjectTypeExtension <|> inputObjectTypeExtension
<?> "TypeExtension" <?> "TypeExtension"
scalarTypeDefinition :: Description -> Parser TypeDefinition scalarTypeDefinition :: Full.Description -> Parser Full.TypeDefinition
scalarTypeDefinition description' = ScalarTypeDefinition description' scalarTypeDefinition description' = Full.ScalarTypeDefinition description'
<$ symbol "scalar" <$ symbol "scalar"
<*> name <*> name
<*> directives <*> directives
<?> "ScalarTypeDefinition" <?> "ScalarTypeDefinition"
scalarTypeExtension :: Parser TypeExtension scalarTypeExtension :: Parser Full.TypeExtension
scalarTypeExtension = extend "scalar" "ScalarTypeExtension" scalarTypeExtension = extend "scalar" "ScalarTypeExtension"
$ (ScalarTypeExtension <$> name <*> NonEmpty.some directive) :| [] $ (Full.ScalarTypeExtension <$> name <*> NonEmpty.some directive) :| []
objectTypeDefinition :: Description -> Parser TypeDefinition objectTypeDefinition :: Full.Description -> Parser Full.TypeDefinition
objectTypeDefinition description' = ObjectTypeDefinition description' objectTypeDefinition description' = Full.ObjectTypeDefinition description'
<$ symbol "type" <$ symbol "type"
<*> name <*> name
<*> option (ImplementsInterfaces []) (implementsInterfaces sepBy1) <*> option (Full.ImplementsInterfaces []) (implementsInterfaces sepBy1)
<*> directives <*> directives
<*> braces (many fieldDefinition) <*> braces (many fieldDefinition)
<?> "ObjectTypeDefinition" <?> "ObjectTypeDefinition"
objectTypeExtension :: Parser TypeExtension objectTypeExtension :: Parser Full.TypeExtension
objectTypeExtension = extend "type" "ObjectTypeExtension" objectTypeExtension = extend "type" "ObjectTypeExtension"
$ fieldsDefinitionExtension :| $ fieldsDefinitionExtension :|
[ directivesExtension [ directivesExtension
, implementsInterfacesExtension , implementsInterfacesExtension
] ]
where where
fieldsDefinitionExtension = ObjectTypeFieldsDefinitionExtension fieldsDefinitionExtension = Full.ObjectTypeFieldsDefinitionExtension
<$> name <$> name
<*> option (ImplementsInterfaces []) (implementsInterfaces sepBy1) <*> option (Full.ImplementsInterfaces []) (implementsInterfaces sepBy1)
<*> directives <*> directives
<*> braces (NonEmpty.some fieldDefinition) <*> braces (NonEmpty.some fieldDefinition)
directivesExtension = ObjectTypeDirectivesExtension directivesExtension = Full.ObjectTypeDirectivesExtension
<$> name <$> name
<*> option (ImplementsInterfaces []) (implementsInterfaces sepBy1) <*> option (Full.ImplementsInterfaces []) (implementsInterfaces sepBy1)
<*> NonEmpty.some directive <*> NonEmpty.some directive
implementsInterfacesExtension = ObjectTypeImplementsInterfacesExtension implementsInterfacesExtension = Full.ObjectTypeImplementsInterfacesExtension
<$> name <$> name
<*> implementsInterfaces NonEmpty.sepBy1 <*> implementsInterfaces NonEmpty.sepBy1
description :: Parser Description description :: Parser Full.Description
description = Description description = Full.Description
<$> optional stringValue <$> optional stringValue
<?> "Description" <?> "Description"
unionTypeDefinition :: Description -> Parser TypeDefinition unionTypeDefinition :: Full.Description -> Parser Full.TypeDefinition
unionTypeDefinition description' = UnionTypeDefinition description' unionTypeDefinition description' = Full.UnionTypeDefinition description'
<$ symbol "union" <$ symbol "union"
<*> name <*> name
<*> directives <*> directives
<*> option (UnionMemberTypes []) (unionMemberTypes sepBy1) <*> option (Full.UnionMemberTypes []) (unionMemberTypes sepBy1)
<?> "UnionTypeDefinition" <?> "UnionTypeDefinition"
unionTypeExtension :: Parser TypeExtension unionTypeExtension :: Parser Full.TypeExtension
unionTypeExtension = extend "union" "UnionTypeExtension" unionTypeExtension = extend "union" "UnionTypeExtension"
$ unionMemberTypesExtension :| [directivesExtension] $ unionMemberTypesExtension :| [directivesExtension]
where where
unionMemberTypesExtension = UnionTypeUnionMemberTypesExtension unionMemberTypesExtension = Full.UnionTypeUnionMemberTypesExtension
<$> name <$> name
<*> directives <*> directives
<*> unionMemberTypes NonEmpty.sepBy1 <*> unionMemberTypes NonEmpty.sepBy1
directivesExtension = UnionTypeDirectivesExtension directivesExtension = Full.UnionTypeDirectivesExtension
<$> name <$> name
<*> NonEmpty.some directive <*> NonEmpty.some directive
unionMemberTypes :: unionMemberTypes ::
Foldable t => Foldable t =>
(Parser Text -> Parser Text -> Parser (t NamedType)) -> (Parser Text -> Parser Text -> Parser (t Full.NamedType)) ->
Parser (UnionMemberTypes t) Parser (Full.UnionMemberTypes t)
unionMemberTypes sepBy' = UnionMemberTypes unionMemberTypes sepBy' = Full.UnionMemberTypes
<$ equals <$ equals
<* optional pipe <* optional pipe
<*> name `sepBy'` pipe <*> name `sepBy'` pipe
<?> "UnionMemberTypes" <?> "UnionMemberTypes"
interfaceTypeDefinition :: Description -> Parser TypeDefinition interfaceTypeDefinition :: Full.Description -> Parser Full.TypeDefinition
interfaceTypeDefinition description' = InterfaceTypeDefinition description' interfaceTypeDefinition description' = Full.InterfaceTypeDefinition description'
<$ symbol "interface" <$ symbol "interface"
<*> name <*> name
<*> directives <*> directives
<*> braces (many fieldDefinition) <*> braces (many fieldDefinition)
<?> "InterfaceTypeDefinition" <?> "InterfaceTypeDefinition"
interfaceTypeExtension :: Parser TypeExtension interfaceTypeExtension :: Parser Full.TypeExtension
interfaceTypeExtension = extend "interface" "InterfaceTypeExtension" interfaceTypeExtension = extend "interface" "InterfaceTypeExtension"
$ fieldsDefinitionExtension :| [directivesExtension] $ fieldsDefinitionExtension :| [directivesExtension]
where where
fieldsDefinitionExtension = InterfaceTypeFieldsDefinitionExtension fieldsDefinitionExtension = Full.InterfaceTypeFieldsDefinitionExtension
<$> name <$> name
<*> directives <*> directives
<*> braces (NonEmpty.some fieldDefinition) <*> braces (NonEmpty.some fieldDefinition)
directivesExtension = InterfaceTypeDirectivesExtension directivesExtension = Full.InterfaceTypeDirectivesExtension
<$> name <$> name
<*> NonEmpty.some directive <*> NonEmpty.some directive
enumTypeDefinition :: Description -> Parser TypeDefinition enumTypeDefinition :: Full.Description -> Parser Full.TypeDefinition
enumTypeDefinition description' = EnumTypeDefinition description' enumTypeDefinition description' = Full.EnumTypeDefinition description'
<$ symbol "enum" <$ symbol "enum"
<*> name <*> name
<*> directives <*> directives
<*> listOptIn braces enumValueDefinition <*> listOptIn braces enumValueDefinition
<?> "EnumTypeDefinition" <?> "EnumTypeDefinition"
enumTypeExtension :: Parser TypeExtension enumTypeExtension :: Parser Full.TypeExtension
enumTypeExtension = extend "enum" "EnumTypeExtension" enumTypeExtension = extend "enum" "EnumTypeExtension"
$ enumValuesDefinitionExtension :| [directivesExtension] $ enumValuesDefinitionExtension :| [directivesExtension]
where where
enumValuesDefinitionExtension = EnumTypeEnumValuesDefinitionExtension enumValuesDefinitionExtension = Full.EnumTypeEnumValuesDefinitionExtension
<$> name <$> name
<*> directives <*> directives
<*> braces (NonEmpty.some enumValueDefinition) <*> braces (NonEmpty.some enumValueDefinition)
directivesExtension = EnumTypeDirectivesExtension directivesExtension = Full.EnumTypeDirectivesExtension
<$> name <$> name
<*> NonEmpty.some directive <*> NonEmpty.some directive
inputObjectTypeDefinition :: Description -> Parser TypeDefinition inputObjectTypeDefinition :: Full.Description -> Parser Full.TypeDefinition
inputObjectTypeDefinition description' = InputObjectTypeDefinition description' inputObjectTypeDefinition description' = Full.InputObjectTypeDefinition description'
<$ symbol "input" <$ symbol "input"
<*> name <*> name
<*> directives <*> directives
<*> listOptIn braces inputValueDefinition <*> listOptIn braces inputValueDefinition
<?> "InputObjectTypeDefinition" <?> "InputObjectTypeDefinition"
inputObjectTypeExtension :: Parser TypeExtension inputObjectTypeExtension :: Parser Full.TypeExtension
inputObjectTypeExtension = extend "input" "InputObjectTypeExtension" inputObjectTypeExtension = extend "input" "InputObjectTypeExtension"
$ inputFieldsDefinitionExtension :| [directivesExtension] $ inputFieldsDefinitionExtension :| [directivesExtension]
where where
inputFieldsDefinitionExtension = InputObjectTypeInputFieldsDefinitionExtension inputFieldsDefinitionExtension = Full.InputObjectTypeInputFieldsDefinitionExtension
<$> name <$> name
<*> directives <*> directives
<*> braces (NonEmpty.some inputValueDefinition) <*> braces (NonEmpty.some inputValueDefinition)
directivesExtension = InputObjectTypeDirectivesExtension directivesExtension = Full.InputObjectTypeDirectivesExtension
<$> name <$> name
<*> NonEmpty.some directive <*> NonEmpty.some directive
enumValueDefinition :: Parser EnumValueDefinition enumValueDefinition :: Parser Full.EnumValueDefinition
enumValueDefinition = EnumValueDefinition enumValueDefinition = Full.EnumValueDefinition
<$> description <$> description
<*> enumValue <*> enumValue
<*> directives <*> directives
@ -286,16 +277,16 @@ enumValueDefinition = EnumValueDefinition
implementsInterfaces :: implementsInterfaces ::
Foldable t => Foldable t =>
(Parser Text -> Parser Text -> Parser (t NamedType)) -> (Parser Text -> Parser Text -> Parser (t Full.NamedType)) ->
Parser (ImplementsInterfaces t) Parser (Full.ImplementsInterfaces t)
implementsInterfaces sepBy' = ImplementsInterfaces implementsInterfaces sepBy' = Full.ImplementsInterfaces
<$ symbol "implements" <$ symbol "implements"
<* optional amp <* optional amp
<*> name `sepBy'` amp <*> name `sepBy'` amp
<?> "ImplementsInterfaces" <?> "ImplementsInterfaces"
inputValueDefinition :: Parser InputValueDefinition inputValueDefinition :: Parser Full.InputValueDefinition
inputValueDefinition = InputValueDefinition inputValueDefinition = Full.InputValueDefinition
<$> description <$> description
<*> name <*> name
<* colon <* colon
@ -304,13 +295,13 @@ inputValueDefinition = InputValueDefinition
<*> directives <*> directives
<?> "InputValueDefinition" <?> "InputValueDefinition"
argumentsDefinition :: Parser ArgumentsDefinition argumentsDefinition :: Parser Full.ArgumentsDefinition
argumentsDefinition = ArgumentsDefinition argumentsDefinition = Full.ArgumentsDefinition
<$> listOptIn parens inputValueDefinition <$> listOptIn parens inputValueDefinition
<?> "ArgumentsDefinition" <?> "ArgumentsDefinition"
fieldDefinition :: Parser FieldDefinition fieldDefinition :: Parser Full.FieldDefinition
fieldDefinition = FieldDefinition fieldDefinition = Full.FieldDefinition
<$> description <$> description
<*> name <*> name
<*> argumentsDefinition <*> argumentsDefinition
@ -319,33 +310,33 @@ fieldDefinition = FieldDefinition
<*> directives <*> directives
<?> "FieldDefinition" <?> "FieldDefinition"
schemaDefinition :: Parser TypeSystemDefinition schemaDefinition :: Parser Full.TypeSystemDefinition
schemaDefinition = SchemaDefinition schemaDefinition = Full.SchemaDefinition
<$ symbol "schema" <$ symbol "schema"
<*> directives <*> directives
<*> operationTypeDefinitions <*> operationTypeDefinitions
<?> "SchemaDefinition" <?> "SchemaDefinition"
operationTypeDefinitions :: Parser (NonEmpty OperationTypeDefinition) operationTypeDefinitions :: Parser (NonEmpty Full.OperationTypeDefinition)
operationTypeDefinitions = braces $ NonEmpty.some operationTypeDefinition operationTypeDefinitions = braces $ NonEmpty.some operationTypeDefinition
schemaExtension :: Parser SchemaExtension schemaExtension :: Parser Full.SchemaExtension
schemaExtension = extend "schema" "SchemaExtension" schemaExtension = extend "schema" "SchemaExtension"
$ schemaOperationExtension :| [directivesExtension] $ schemaOperationExtension :| [directivesExtension]
where where
directivesExtension = SchemaDirectivesExtension directivesExtension = Full.SchemaDirectivesExtension
<$> NonEmpty.some directive <$> NonEmpty.some directive
schemaOperationExtension = SchemaOperationExtension schemaOperationExtension = Full.SchemaOperationExtension
<$> directives <$> directives
<*> operationTypeDefinitions <*> operationTypeDefinitions
operationTypeDefinition :: Parser OperationTypeDefinition operationTypeDefinition :: Parser Full.OperationTypeDefinition
operationTypeDefinition = OperationTypeDefinition operationTypeDefinition = Full.OperationTypeDefinition
<$> operationType <* colon <$> operationType <* colon
<*> name <*> name
<?> "OperationTypeDefinition" <?> "OperationTypeDefinition"
operationDefinition :: Parser OperationDefinition operationDefinition :: Parser Full.OperationDefinition
operationDefinition = shorthand operationDefinition = shorthand
<|> operationDefinition' <|> operationDefinition'
<?> "OperationDefinition" <?> "OperationDefinition"
@ -353,7 +344,7 @@ operationDefinition = shorthand
shorthand = do shorthand = do
location <- getLocation location <- getLocation
selectionSet' <- selectionSet selectionSet' <- selectionSet
pure $ SelectionSet selectionSet' location pure $ Full.SelectionSet selectionSet' location
operationDefinition' = do operationDefinition' = do
location <- getLocation location <- getLocation
operationType' <- operationType operationType' <- operationType
@ -361,60 +352,74 @@ operationDefinition = shorthand
variableDefinitions' <- variableDefinitions variableDefinitions' <- variableDefinitions
directives' <- directives directives' <- directives
selectionSet' <- selectionSet selectionSet' <- selectionSet
pure $ OperationDefinition operationType' operationName variableDefinitions' directives' selectionSet' location pure $ Full.OperationDefinition
operationType'
operationName
variableDefinitions'
directives'
selectionSet'
location
operationType :: Parser OperationType operationType :: Parser Full.OperationType
operationType = Query <$ symbol "query" operationType = Full.Query <$ symbol "query"
<|> Mutation <$ symbol "mutation" <|> Full.Mutation <$ symbol "mutation"
<|> Subscription <$ symbol "subscription" <|> Full.Subscription <$ symbol "subscription"
<?> "OperationType" <?> "OperationType"
selectionSet :: Parser SelectionSet selectionSet :: Parser Full.SelectionSet
selectionSet = braces (NonEmpty.some selection) <?> "SelectionSet" selectionSet = braces (NonEmpty.some selection) <?> "SelectionSet"
selectionSetOpt :: Parser SelectionSetOpt selectionSetOpt :: Parser Full.SelectionSetOpt
selectionSetOpt = listOptIn braces selection <?> "SelectionSet" selectionSetOpt = listOptIn braces selection <?> "SelectionSet"
selection :: Parser Selection selection :: Parser Full.Selection
selection = field selection = Full.FieldSelection <$> field
<|> try fragmentSpread <|> Full.FragmentSpreadSelection <$> try fragmentSpread
<|> inlineFragment <|> Full.InlineFragmentSelection <$> inlineFragment
<?> "Selection" <?> "Selection"
field :: Parser Selection field :: Parser Full.Field
field = Field field = label "Field" $ do
<$> optional alias location <- getLocation
<*> name alias' <- optional alias
<*> arguments name' <- name
<*> directives arguments' <- arguments
<*> selectionSetOpt directives' <- directives
<?> "Field" selectionSetOpt' <- selectionSetOpt
pure $ Full.Field alias' name' arguments' directives' selectionSetOpt' location
alias :: Parser Alias alias :: Parser Full.Name
alias = try (name <* colon) <?> "Alias" alias = try (name <* colon) <?> "Alias"
arguments :: Parser [Argument] arguments :: Parser [Full.Argument]
arguments = listOptIn parens argument <?> "Arguments" arguments = listOptIn parens argument <?> "Arguments"
argument :: Parser Argument argument :: Parser Full.Argument
argument = Argument <$> name <* colon <*> value <?> "Argument" argument = label "Argument" $ do
location <- getLocation
name' <- name
colon
value' <- valueNode value
pure $ Full.Argument name' value' location
fragmentSpread :: Parser Selection fragmentSpread :: Parser Full.FragmentSpread
fragmentSpread = FragmentSpread fragmentSpread = label "FragmentSpread" $ do
<$ spread location <- getLocation
<*> fragmentName _ <- spread
<*> directives fragmentName' <- fragmentName
<?> "FragmentSpread" directives' <- directives
pure $ Full.FragmentSpread fragmentName' directives' location
inlineFragment :: Parser Selection inlineFragment :: Parser Full.InlineFragment
inlineFragment = InlineFragment inlineFragment = label "InlineFragment" $ do
<$ spread location <- getLocation
<*> optional typeCondition _ <- spread
<*> directives typeCondition' <- optional typeCondition
<*> selectionSet directives' <- directives
<?> "InlineFragment" selectionSet' <- selectionSet
pure $ Full.InlineFragment typeCondition' directives' selectionSet' location
fragmentDefinition :: Parser FragmentDefinition fragmentDefinition :: Parser Full.FragmentDefinition
fragmentDefinition = label "FragmentDefinition" $ do fragmentDefinition = label "FragmentDefinition" $ do
location <- getLocation location <- getLocation
_ <- symbol "fragment" _ <- symbol "fragment"
@ -422,36 +427,42 @@ fragmentDefinition = label "FragmentDefinition" $ do
typeCondition' <- typeCondition typeCondition' <- typeCondition
directives' <- directives directives' <- directives
selectionSet' <- selectionSet selectionSet' <- selectionSet
pure $ FragmentDefinition pure $ Full.FragmentDefinition
fragmentName' typeCondition' directives' selectionSet' location fragmentName' typeCondition' directives' selectionSet' location
fragmentName :: Parser Name fragmentName :: Parser Full.Name
fragmentName = but (symbol "on") *> name <?> "FragmentName" fragmentName = but (symbol "on") *> name <?> "FragmentName"
typeCondition :: Parser TypeCondition typeCondition :: Parser Full.TypeCondition
typeCondition = symbol "on" *> name <?> "TypeCondition" typeCondition = symbol "on" *> name <?> "TypeCondition"
value :: Parser Value valueNode :: forall a. Parser a -> Parser (Full.Node a)
value = Variable <$> variable valueNode valueParser = do
<|> Float <$> try float location <- getLocation
<|> Int <$> integer value' <- valueParser
<|> Boolean <$> booleanValue pure $ Full.Node value' location
<|> Null <$ nullValue
<|> String <$> stringValue value :: Parser Full.Value
<|> Enum <$> try enumValue value = Full.Variable <$> variable
<|> List <$> brackets (some value) <|> Full.Float <$> try float
<|> Object <$> braces (some $ objectField value) <|> Full.Int <$> integer
<|> Full.Boolean <$> booleanValue
<|> Full.Null <$ nullValue
<|> Full.String <$> stringValue
<|> Full.Enum <$> try enumValue
<|> Full.List <$> brackets (some $ valueNode value)
<|> Full.Object <$> braces (some $ objectField $ valueNode value)
<?> "Value" <?> "Value"
constValue :: Parser ConstValue constValue :: Parser Full.ConstValue
constValue = ConstFloat <$> try float constValue = Full.ConstFloat <$> try float
<|> ConstInt <$> integer <|> Full.ConstInt <$> integer
<|> ConstBoolean <$> booleanValue <|> Full.ConstBoolean <$> booleanValue
<|> ConstNull <$ nullValue <|> Full.ConstNull <$ nullValue
<|> ConstString <$> stringValue <|> Full.ConstString <$> stringValue
<|> ConstEnum <$> try enumValue <|> Full.ConstEnum <$> try enumValue
<|> ConstList <$> brackets (some constValue) <|> Full.ConstList <$> brackets (many $ valueNode constValue)
<|> ConstObject <$> braces (some $ objectField constValue) <|> Full.ConstObject <$> braces (many $ objectField $ valueNode constValue)
<?> "Value" <?> "Value"
booleanValue :: Parser Bool booleanValue :: Parser Bool
@ -459,7 +470,7 @@ booleanValue = True <$ symbol "true"
<|> False <$ symbol "false" <|> False <$ symbol "false"
<?> "BooleanValue" <?> "BooleanValue"
enumValue :: Parser Name enumValue :: Parser Full.Name
enumValue = but (symbol "true") enumValue = but (symbol "true")
*> but (symbol "false") *> but (symbol "false")
*> but (symbol "null") *> but (symbol "null")
@ -472,51 +483,54 @@ stringValue = blockString <|> string <?> "StringValue"
nullValue :: Parser Text nullValue :: Parser Text
nullValue = symbol "null" <?> "NullValue" nullValue = symbol "null" <?> "NullValue"
objectField :: Parser a -> Parser (ObjectField a) objectField :: forall a. Parser (Full.Node a) -> Parser (Full.ObjectField a)
objectField valueParser = ObjectField objectField valueParser = label "ObjectField" $ do
<$> name location <- getLocation
<* colon fieldName <- name
<*> valueParser colon
<?> "ObjectField" fieldValue <- valueParser
pure $ Full.ObjectField fieldName fieldValue location
variableDefinitions :: Parser [VariableDefinition] variableDefinitions :: Parser [Full.VariableDefinition]
variableDefinitions = listOptIn parens variableDefinition variableDefinitions = listOptIn parens variableDefinition
<?> "VariableDefinitions" <?> "VariableDefinitions"
variableDefinition :: Parser VariableDefinition variableDefinition :: Parser Full.VariableDefinition
variableDefinition = VariableDefinition variableDefinition = label "VariableDefinition" $ do
<$> variable location <- getLocation
<* colon variableName <- variable
<*> type' colon
<*> defaultValue variableType <- type'
<?> "VariableDefinition" variableValue <- defaultValue
pure $ Full.VariableDefinition variableName variableType variableValue location
variable :: Parser Name variable :: Parser Full.Name
variable = dollar *> name <?> "Variable" variable = dollar *> name <?> "Variable"
defaultValue :: Parser (Maybe ConstValue) defaultValue :: Parser (Maybe (Full.Node Full.ConstValue))
defaultValue = optional (equals *> constValue) <?> "DefaultValue" defaultValue = optional (equals *> valueNode constValue) <?> "DefaultValue"
type' :: Parser Type type' :: Parser Full.Type
type' = try (TypeNonNull <$> nonNullType) type' = try (Full.TypeNonNull <$> nonNullType)
<|> TypeList <$> brackets type' <|> Full.TypeList <$> brackets type'
<|> TypeNamed <$> name <|> Full.TypeNamed <$> name
<?> "Type" <?> "Type"
nonNullType :: Parser NonNullType nonNullType :: Parser Full.NonNullType
nonNullType = NonNullTypeNamed <$> name <* bang nonNullType = Full.NonNullTypeNamed <$> name <* bang
<|> NonNullTypeList <$> brackets type' <* bang <|> Full.NonNullTypeList <$> brackets type' <* bang
<?> "NonNullType" <?> "NonNullType"
directives :: Parser [Directive] directives :: Parser [Full.Directive]
directives = many directive <?> "Directives" directives = many directive <?> "Directives"
directive :: Parser Directive directive :: Parser Full.Directive
directive = Directive directive = label "Directive" $ do
<$ at location <- getLocation
<*> name at
<*> arguments directiveName <- name
<?> "Directive" directiveArguments <- arguments
pure $ Full.Directive directiveName directiveArguments location
listOptIn :: (Parser [a] -> Parser [a]) -> Parser a -> Parser [a] listOptIn :: (Parser [a] -> Parser [a]) -> Parser a -> Parser [a]
listOptIn surround = option [] . surround . some listOptIn surround = option [] . surround . some

View File

@ -1,19 +1,19 @@
{-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE ExistentialQuantification #-} {-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE RecordWildCards #-}
-- | Error handling. -- | Error handling.
module Language.GraphQL.Error module Language.GraphQL.Error
( parseError ( CollectErrsT
, CollectErrsT
, Error(..) , Error(..)
, Path(..)
, Resolution(..) , Resolution(..)
, ResolverException(..) , ResolverException(..)
, Response(..) , Response(..)
, ResponseEventStream , ResponseEventStream
, addErr , addErr
, addErrMsg , addErrMsg
, parseError
, runCollectErrs , runCollectErrs
, singleError , singleError
) where ) where
@ -28,7 +28,7 @@ import Data.Text (Text)
import qualified Data.Text as Text import qualified Data.Text as Text
import Language.GraphQL.AST (Location(..), Name) import Language.GraphQL.AST (Location(..), Name)
import Language.GraphQL.Execute.Coerce import Language.GraphQL.Execute.Coerce
import Language.GraphQL.Type.Schema import qualified Language.GraphQL.Type.Schema as Schema
import Prelude hiding (null) import Prelude hiding (null)
import Text.Megaparsec import Text.Megaparsec
( ParseErrorBundle(..) ( ParseErrorBundle(..)
@ -43,7 +43,7 @@ import Text.Megaparsec
-- | Executor context. -- | Executor context.
data Resolution m = Resolution data Resolution m = Resolution
{ errors :: Seq Error { errors :: Seq Error
, types :: HashMap Name (Type m) , types :: HashMap Name (Schema.Type m)
} }
-- | Wraps a parse error into a list of errors. -- | Wraps a parse error into a list of errors.
@ -57,6 +57,7 @@ parseError ParseErrorBundle{..} =
errorObject s SourcePos{..} = Error errorObject s SourcePos{..} = Error
{ message = Text.pack $ init $ parseErrorTextPretty s { message = Text.pack $ init $ parseErrorTextPretty s
, locations = [Location (unPos' sourceLine) (unPos' sourceColumn)] , locations = [Location (unPos' sourceLine) (unPos' sourceColumn)]
, path = []
} }
unPos' = fromIntegral . unPos unPos' = fromIntegral . unPos
go (result, state) x = go (result, state) x =
@ -68,28 +69,42 @@ parseError ParseErrorBundle{..} =
type CollectErrsT m = StateT (Resolution m) m type CollectErrsT m = StateT (Resolution m) m
-- | Adds an error to the list of errors. -- | Adds an error to the list of errors.
{-# DEPRECATED #-}
addErr :: Monad m => Error -> CollectErrsT m () addErr :: Monad m => Error -> CollectErrsT m ()
addErr v = modify appender addErr v = modify appender
where where
appender :: Monad m => Resolution m -> Resolution m appender :: Monad m => Resolution m -> Resolution m
appender resolution@Resolution{..} = resolution{ errors = errors |> v } appender resolution@Resolution{..} = resolution{ errors = errors |> v }
{-# DEPRECATED #-}
makeErrorMessage :: Text -> Error makeErrorMessage :: Text -> Error
makeErrorMessage s = Error s [] makeErrorMessage s = Error s [] []
-- | Constructs a response object containing only the error with the given -- | Constructs a response object containing only the error with the given
-- message. -- message.
{-# DEPRECATED #-}
singleError :: Serialize a => Text -> Response a singleError :: Serialize a => Text -> Response a
singleError message = Response null $ Seq.singleton $ makeErrorMessage message singleError message = Response null $ Seq.singleton $ Error message [] []
-- | Convenience function for just wrapping an error message. -- | Convenience function for just wrapping an error message.
{-# DEPRECATED #-}
addErrMsg :: (Monad m, Serialize a) => Text -> CollectErrsT m a addErrMsg :: (Monad m, Serialize a) => Text -> CollectErrsT m a
addErrMsg errorMessage = (addErr . makeErrorMessage) errorMessage >> pure null addErrMsg errorMessage = (addErr . makeErrorMessage) errorMessage >> pure null
-- | If an error can be associated to a particular field in the GraphQL result,
-- it must contain an entry with the key path that details the path of the
-- response field which experienced the error. This allows clients to identify
-- whether a null result is intentional or caused by a runtime error.
data Path
= Segment Text -- ^ Field name.
| Index Int -- ^ List index if a field returned a list.
deriving (Eq, Show)
-- | @GraphQL@ error. -- | @GraphQL@ error.
data Error = Error data Error = Error
{ message :: Text { message :: Text
, locations :: [Location] , locations :: [Location]
, path :: [Path]
} deriving (Eq, Show) } deriving (Eq, Show)
-- | The server\'s response describes the result of executing the requested -- | The server\'s response describes the result of executing the requested
@ -117,7 +132,7 @@ instance Exception ResolverException
-- | Runs the given query computation, but collects the errors into an error -- | Runs the given query computation, but collects the errors into an error
-- list, which is then sent back with the data. -- list, which is then sent back with the data.
runCollectErrs :: (Monad m, Serialize a) runCollectErrs :: (Monad m, Serialize a)
=> HashMap Name (Type m) => HashMap Name (Schema.Type m)
-> CollectErrsT m a -> CollectErrsT m a
-> m (Response a) -> m (Response a)
runCollectErrs types' res = do runCollectErrs types' res = do

View File

@ -1,4 +1,4 @@
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ExplicitForAll #-}
-- | This module provides functions to execute a @GraphQL@ request. -- | This module provides functions to execute a @GraphQL@ request.
module Language.GraphQL.Execute module Language.GraphQL.Execute
@ -10,15 +10,22 @@ import Control.Monad.Catch (MonadCatch)
import Data.HashMap.Strict (HashMap) import Data.HashMap.Strict (HashMap)
import Data.Sequence (Seq(..)) import Data.Sequence (Seq(..))
import Data.Text (Text) import Data.Text (Text)
import Language.GraphQL.AST.Document (Document, Name) import qualified Language.GraphQL.AST.Document as Full
import Language.GraphQL.Execute.Coerce import Language.GraphQL.Execute.Coerce
import Language.GraphQL.Execute.Execution import Language.GraphQL.Execute.Execution
import Language.GraphQL.Execute.Internal
import qualified Language.GraphQL.Execute.Transform as Transform import qualified Language.GraphQL.Execute.Transform as Transform
import qualified Language.GraphQL.Execute.Subscribe as Subscribe import qualified Language.GraphQL.Execute.Subscribe as Subscribe
import Language.GraphQL.Error import Language.GraphQL.Error
( Error
, ResponseEventStream
, Response(..)
, runCollectErrs
)
import qualified Language.GraphQL.Type.Definition as Definition import qualified Language.GraphQL.Type.Definition as Definition
import qualified Language.GraphQL.Type.Out as Out import qualified Language.GraphQL.Type.Out as Out
import Language.GraphQL.Type.Schema import Language.GraphQL.Type.Schema
import Prelude hiding (null)
-- | The substitution is applied to the document, and the resolvers are applied -- | The substitution is applied to the document, and the resolvers are applied
-- to the resulting fields. The operation name can be used if the document -- to the resulting fields. The operation name can be used if the document
@ -29,35 +36,36 @@ import Language.GraphQL.Type.Schema
execute :: (MonadCatch m, VariableValue a, Serialize b) execute :: (MonadCatch m, VariableValue a, Serialize b)
=> Schema m -- ^ Resolvers. => Schema m -- ^ Resolvers.
-> Maybe Text -- ^ Operation name. -> Maybe Text -- ^ Operation name.
-> HashMap Name a -- ^ Variable substitution function. -> HashMap Full.Name a -- ^ Variable substitution function.
-> Document -- @GraphQL@ document. -> Full.Document -- @GraphQL@ document.
-> m (Either (ResponseEventStream m b) (Response b)) -> m (Either (ResponseEventStream m b) (Response b))
execute schema operationName subs document = execute schema' operationName subs document
case Transform.document schema operationName subs document of = either (pure . rightErrorResponse . singleError [] . show) executeRequest
Left queryError -> pure $ Transform.document schema' operationName subs document
$ Right
$ singleError
$ Transform.queryError queryError
Right transformed -> executeRequest transformed
executeRequest :: (MonadCatch m, Serialize a) executeRequest :: (MonadCatch m, Serialize a)
=> Transform.Document m => Transform.Document m
-> m (Either (ResponseEventStream m a) (Response a)) -> m (Either (ResponseEventStream m a) (Response a))
executeRequest (Transform.Document types' rootObjectType operation) executeRequest (Transform.Document types' rootObjectType operation)
| (Transform.Query _ fields) <- operation = | (Transform.Query _ fields objectLocation) <- operation =
Right <$> executeOperation types' rootObjectType fields Right <$> executeOperation types' rootObjectType objectLocation fields
| (Transform.Mutation _ fields) <- operation = | (Transform.Mutation _ fields objectLocation) <- operation =
Right <$> executeOperation types' rootObjectType fields Right <$> executeOperation types' rootObjectType objectLocation fields
| (Transform.Subscription _ fields) <- operation | (Transform.Subscription _ fields objectLocation) <- operation
= either (Right . singleError) Left = either rightErrorResponse Left
<$> Subscribe.subscribe types' rootObjectType fields <$> Subscribe.subscribe types' rootObjectType objectLocation fields
-- This is actually executeMutation, but we don't distinguish between queries -- This is actually executeMutation, but we don't distinguish between queries
-- and mutations yet. -- and mutations yet.
executeOperation :: (MonadCatch m, Serialize a) executeOperation :: (MonadCatch m, Serialize a)
=> HashMap Name (Type m) => HashMap Full.Name (Type m)
-> Out.ObjectType m -> Out.ObjectType m
-> Full.Location
-> Seq (Transform.Selection m) -> Seq (Transform.Selection m)
-> m (Response a) -> m (Response a)
executeOperation types' objectType fields = executeOperation types' objectType objectLocation fields
runCollectErrs types' $ executeSelectionSet Definition.Null objectType fields = runCollectErrs types'
$ executeSelectionSet Definition.Null objectType objectLocation fields
rightErrorResponse :: Serialize b => forall a. Error -> Either a (Response b)
rightErrorResponse = Right . Response null . pure

View File

@ -19,7 +19,6 @@ import qualified Data.Aeson as Aeson
import Data.Int (Int32) import Data.Int (Int32)
import Data.HashMap.Strict (HashMap) import Data.HashMap.Strict (HashMap)
import qualified Data.HashMap.Strict as HashMap import qualified Data.HashMap.Strict as HashMap
import Data.Map.Strict (Map)
import Data.String (IsString(..)) import Data.String (IsString(..))
import Data.Text (Text) import Data.Text (Text)
import qualified Data.Text.Lazy as Text.Lazy import qualified Data.Text.Lazy as Text.Lazy
@ -27,6 +26,8 @@ import qualified Data.Text.Lazy.Builder as Text.Builder
import qualified Data.Text.Lazy.Builder.Int as Text.Builder import qualified Data.Text.Lazy.Builder.Int as Text.Builder
import Data.Scientific (toBoundedInteger, toRealFloat) import Data.Scientific (toBoundedInteger, toRealFloat)
import Language.GraphQL.AST (Name) import Language.GraphQL.AST (Name)
import Language.GraphQL.Execute.OrderedMap (OrderedMap)
import qualified Language.GraphQL.Execute.OrderedMap as OrderedMap
import qualified Language.GraphQL.Type as Type import qualified Language.GraphQL.Type as Type
import qualified Language.GraphQL.Type.In as In import qualified Language.GraphQL.Type.In as In
import qualified Language.GraphQL.Type.Out as Out import qualified Language.GraphQL.Type.Out as Out
@ -209,7 +210,7 @@ data Output a
| Boolean Bool | Boolean Bool
| Enum Name | Enum Name
| List [a] | List [a]
| Object (Map Name a) | Object (OrderedMap a)
deriving (Eq, Show) deriving (Eq, Show)
instance forall a. IsString (Output a) where instance forall a. IsString (Output a) where
@ -229,6 +230,9 @@ instance Serialize Aeson.Value where
, Boolean boolean <- value = Just $ Aeson.Bool boolean , Boolean boolean <- value = Just $ Aeson.Bool boolean
serialize _ (Enum enum) = Just $ Aeson.String enum serialize _ (Enum enum) = Just $ Aeson.String enum
serialize _ (List list) = Just $ Aeson.toJSON list serialize _ (List list) = Just $ Aeson.toJSON list
serialize _ (Object object) = Just $ Aeson.toJSON object serialize _ (Object object) = Just
$ Aeson.object
$ OrderedMap.toList
$ Aeson.toJSON <$> object
serialize _ _ = Nothing serialize _ _ = Nothing
null = Aeson.Null null = Aeson.Null

View File

@ -1,4 +1,5 @@
{-# LANGUAGE ExplicitForAll #-} {-# LANGUAGE ExplicitForAll #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ViewPatterns #-} {-# LANGUAGE ViewPatterns #-}
@ -13,37 +14,40 @@ import Control.Monad.Trans.Class (lift)
import Control.Monad.Trans.Reader (runReaderT) import Control.Monad.Trans.Reader (runReaderT)
import Control.Monad.Trans.State (gets) import Control.Monad.Trans.State (gets)
import Data.List.NonEmpty (NonEmpty(..)) import Data.List.NonEmpty (NonEmpty(..))
import Data.Map.Strict (Map) import qualified Data.List.NonEmpty as NonEmpty
import Data.HashMap.Strict (HashMap) import Data.HashMap.Strict (HashMap)
import qualified Data.HashMap.Strict as HashMap import qualified Data.HashMap.Strict as HashMap
import qualified Data.Map.Strict as Map
import Data.Maybe (fromMaybe) import Data.Maybe (fromMaybe)
import Data.Sequence (Seq(..)) import Data.Sequence (Seq(..))
import qualified Data.Text as Text import qualified Data.Text as Text
import Language.GraphQL.AST (Name) import qualified Language.GraphQL.AST as Full
import Language.GraphQL.Error import Language.GraphQL.Error
import Language.GraphQL.Execute.Coerce import Language.GraphQL.Execute.Coerce
import Language.GraphQL.Execute.Internal
import Language.GraphQL.Execute.OrderedMap (OrderedMap)
import qualified Language.GraphQL.Execute.OrderedMap as OrderedMap
import qualified Language.GraphQL.Execute.Transform as Transform import qualified Language.GraphQL.Execute.Transform as Transform
import qualified Language.GraphQL.Type as Type import qualified Language.GraphQL.Type as Type
import qualified Language.GraphQL.Type.In as In import qualified Language.GraphQL.Type.In as In
import qualified Language.GraphQL.Type.Out as Out import qualified Language.GraphQL.Type.Out as Out
import Language.GraphQL.Type.Internal import qualified Language.GraphQL.Type.Internal as Internal
import Language.GraphQL.Type.Schema
import Prelude hiding (null) import Prelude hiding (null)
resolveFieldValue :: MonadCatch m resolveFieldValue :: MonadCatch m
=> Type.Value => Type.Value
-> Type.Subs -> Type.Subs
-> Type.Resolve m -> Type.Resolve m
-> Full.Location
-> CollectErrsT m Type.Value -> CollectErrsT m Type.Value
resolveFieldValue result args resolver = resolveFieldValue result args resolver location' =
catch (lift $ runReaderT resolver context) handleFieldError catch (lift $ runReaderT resolver context) handleFieldError
where where
handleFieldError :: MonadCatch m handleFieldError :: MonadCatch m
=> ResolverException => ResolverException
-> CollectErrsT m Type.Value -> CollectErrsT m Type.Value
handleFieldError e = handleFieldError e
addErr (Error (Text.pack $ displayException e) []) >> pure Type.Null = addError Type.Null
$ Error (Text.pack $ displayException e) [location'] []
context = Type.Context context = Type.Context
{ Type.arguments = Type.Arguments args { Type.arguments = Type.Arguments args
, Type.values = result , Type.values = result
@ -52,32 +56,32 @@ resolveFieldValue result args resolver =
collectFields :: Monad m collectFields :: Monad m
=> Out.ObjectType m => Out.ObjectType m
-> Seq (Transform.Selection m) -> Seq (Transform.Selection m)
-> Map Name (NonEmpty (Transform.Field m)) -> OrderedMap (NonEmpty (Transform.Field m))
collectFields objectType = foldl forEach Map.empty collectFields objectType = foldl forEach OrderedMap.empty
where where
forEach groupedFields (Transform.SelectionField field) = forEach groupedFields (Transform.SelectionField field) =
let responseKey = aliasOrName field let responseKey = aliasOrName field
in Map.insertWith (<>) responseKey (field :| []) groupedFields in OrderedMap.insert responseKey (field :| []) groupedFields
forEach groupedFields (Transform.SelectionFragment selectionFragment) forEach groupedFields (Transform.SelectionFragment selectionFragment)
| Transform.Fragment fragmentType fragmentSelectionSet <- selectionFragment | Transform.Fragment fragmentType fragmentSelectionSet <- selectionFragment
, doesFragmentTypeApply fragmentType objectType = , Internal.doesFragmentTypeApply fragmentType objectType =
let fragmentGroupedFieldSet = collectFields objectType fragmentSelectionSet let fragmentGroupedFieldSet = collectFields objectType fragmentSelectionSet
in Map.unionWith (<>) groupedFields fragmentGroupedFieldSet in groupedFields <> fragmentGroupedFieldSet
| otherwise = groupedFields | otherwise = groupedFields
aliasOrName :: forall m. Transform.Field m -> Name aliasOrName :: forall m. Transform.Field m -> Full.Name
aliasOrName (Transform.Field alias name _ _) = fromMaybe name alias aliasOrName (Transform.Field alias name _ _ _) = fromMaybe name alias
resolveAbstractType :: Monad m resolveAbstractType :: Monad m
=> AbstractType m => Internal.AbstractType m
-> Type.Subs -> Type.Subs
-> CollectErrsT m (Maybe (Out.ObjectType m)) -> CollectErrsT m (Maybe (Out.ObjectType m))
resolveAbstractType abstractType values' resolveAbstractType abstractType values'
| Just (Type.String typeName) <- HashMap.lookup "__typename" values' = do | Just (Type.String typeName) <- HashMap.lookup "__typename" values' = do
types' <- gets types types' <- gets types
case HashMap.lookup typeName types' of case HashMap.lookup typeName types' of
Just (ObjectType objectType) -> Just (Internal.ObjectType objectType) ->
if instanceOf objectType abstractType if Internal.instanceOf objectType abstractType
then pure $ Just objectType then pure $ Just objectType
else pure Nothing else pure Nothing
_ -> pure Nothing _ -> pure Nothing
@ -96,11 +100,15 @@ executeField fieldResolver prev fields
where where
executeField' fieldDefinition resolver = do executeField' fieldDefinition resolver = do
let Out.Field _ fieldType argumentDefinitions = fieldDefinition let Out.Field _ fieldType argumentDefinitions = fieldDefinition
let (Transform.Field _ _ arguments' _ :| []) = fields let Transform.Field _ _ arguments' _ location' = NonEmpty.head fields
case coerceArgumentValues argumentDefinitions arguments' of case coerceArgumentValues argumentDefinitions arguments' of
Nothing -> addErrMsg "Argument coercing failed." Left [] ->
Just argumentValues -> do let errorMessage = "Not all required arguments are specified."
answer <- resolveFieldValue prev argumentValues resolver in addError null $ Error errorMessage [location'] []
Left errorLocations -> addError null
$ Error "Argument coercing failed." errorLocations []
Right argumentValues -> do
answer <- resolveFieldValue prev argumentValues resolver location'
completeValue fieldType fields answer completeValue fieldType fields answer
completeValue :: (MonadCatch m, Serialize a) completeValue :: (MonadCatch m, Serialize a)
@ -111,55 +119,67 @@ completeValue :: (MonadCatch m, Serialize a)
completeValue (Out.isNonNullType -> False) _ Type.Null = pure null completeValue (Out.isNonNullType -> False) _ Type.Null = pure null
completeValue outputType@(Out.ListBaseType listType) fields (Type.List list) completeValue outputType@(Out.ListBaseType listType) fields (Type.List list)
= traverse (completeValue listType fields) list = traverse (completeValue listType fields) list
>>= coerceResult outputType . List >>= coerceResult outputType (firstFieldLocation fields) . List
completeValue outputType@(Out.ScalarBaseType _) _ (Type.Int int) = completeValue outputType@(Out.ScalarBaseType _) fields (Type.Int int) =
coerceResult outputType $ Int int coerceResult outputType (firstFieldLocation fields) $ Int int
completeValue outputType@(Out.ScalarBaseType _) _ (Type.Boolean boolean) = completeValue outputType@(Out.ScalarBaseType _) fields (Type.Boolean boolean) =
coerceResult outputType $ Boolean boolean coerceResult outputType (firstFieldLocation fields) $ Boolean boolean
completeValue outputType@(Out.ScalarBaseType _) _ (Type.Float float) = completeValue outputType@(Out.ScalarBaseType _) fields (Type.Float float) =
coerceResult outputType $ Float float coerceResult outputType (firstFieldLocation fields) $ Float float
completeValue outputType@(Out.ScalarBaseType _) _ (Type.String string) = completeValue outputType@(Out.ScalarBaseType _) fields (Type.String string) =
coerceResult outputType $ String string coerceResult outputType (firstFieldLocation fields) $ String string
completeValue outputType@(Out.EnumBaseType enumType) _ (Type.Enum enum) = completeValue outputType@(Out.EnumBaseType enumType) fields (Type.Enum enum) =
let Type.EnumType _ _ enumMembers = enumType let Type.EnumType _ _ enumMembers = enumType
location = firstFieldLocation fields
in if HashMap.member enum enumMembers in if HashMap.member enum enumMembers
then coerceResult outputType $ Enum enum then coerceResult outputType location $ Enum enum
else addErrMsg "Value completion failed." else addError null $ Error "Enum value completion failed." [location] []
completeValue (Out.ObjectBaseType objectType) fields result = completeValue (Out.ObjectBaseType objectType) fields result
executeSelectionSet result objectType $ mergeSelectionSets fields = executeSelectionSet result objectType (firstFieldLocation fields)
$ mergeSelectionSets fields
completeValue (Out.InterfaceBaseType interfaceType) fields result completeValue (Out.InterfaceBaseType interfaceType) fields result
| Type.Object objectMap <- result = do | Type.Object objectMap <- result = do
let abstractType = AbstractInterfaceType interfaceType let abstractType = Internal.AbstractInterfaceType interfaceType
let location = firstFieldLocation fields
concreteType <- resolveAbstractType abstractType objectMap concreteType <- resolveAbstractType abstractType objectMap
case concreteType of case concreteType of
Just objectType -> executeSelectionSet result objectType Just objectType -> executeSelectionSet result objectType location
$ mergeSelectionSets fields $ mergeSelectionSets fields
Nothing -> addErrMsg "Value completion failed." Nothing -> addError null
$ Error "Interface value completion failed." [location] []
completeValue (Out.UnionBaseType unionType) fields result completeValue (Out.UnionBaseType unionType) fields result
| Type.Object objectMap <- result = do | Type.Object objectMap <- result = do
let abstractType = AbstractUnionType unionType let abstractType = Internal.AbstractUnionType unionType
let location = firstFieldLocation fields
concreteType <- resolveAbstractType abstractType objectMap concreteType <- resolveAbstractType abstractType objectMap
case concreteType of case concreteType of
Just objectType -> executeSelectionSet result objectType Just objectType -> executeSelectionSet result objectType
$ mergeSelectionSets fields location $ mergeSelectionSets fields
Nothing -> addErrMsg "Value completion failed." Nothing -> addError null
completeValue _ _ _ = addErrMsg "Value completion failed." $ Error "Union value completion failed." [location] []
completeValue _ (Transform.Field _ _ _ _ location :| _) _ =
addError null $ Error "Value completion failed." [location] []
mergeSelectionSets :: MonadCatch m mergeSelectionSets :: MonadCatch m
=> NonEmpty (Transform.Field m) => NonEmpty (Transform.Field m)
-> Seq (Transform.Selection m) -> Seq (Transform.Selection m)
mergeSelectionSets = foldr forEach mempty mergeSelectionSets = foldr forEach mempty
where where
forEach (Transform.Field _ _ _ fieldSelectionSet) selectionSet = forEach (Transform.Field _ _ _ fieldSelectionSet _) selectionSet =
selectionSet <> fieldSelectionSet selectionSet <> fieldSelectionSet
firstFieldLocation :: MonadCatch m => NonEmpty (Transform.Field m) -> Full.Location
firstFieldLocation (Transform.Field _ _ _ _ fieldLocation :| _) = fieldLocation
coerceResult :: (MonadCatch m, Serialize a) coerceResult :: (MonadCatch m, Serialize a)
=> Out.Type m => Out.Type m
-> Full.Location
-> Output a -> Output a
-> CollectErrsT m a -> CollectErrsT m a
coerceResult outputType result coerceResult outputType parentLocation result
| Just serialized <- serialize outputType result = pure serialized | Just serialized <- serialize outputType result = pure serialized
| otherwise = addErrMsg "Result coercion failed." | otherwise = addError null
$ Error "Result coercion failed." [parentLocation] []
-- | Takes an 'Out.ObjectType' and a list of 'Transform.Selection's and applies -- | Takes an 'Out.ObjectType' and a list of 'Transform.Selection's and applies
-- each field to each 'Transform.Selection'. Resolves into a value containing -- each field to each 'Transform.Selection'. Resolves into a value containing
@ -167,29 +187,45 @@ coerceResult outputType result
executeSelectionSet :: (MonadCatch m, Serialize a) executeSelectionSet :: (MonadCatch m, Serialize a)
=> Type.Value => Type.Value
-> Out.ObjectType m -> Out.ObjectType m
-> Full.Location
-> Seq (Transform.Selection m) -> Seq (Transform.Selection m)
-> CollectErrsT m a -> CollectErrsT m a
executeSelectionSet result objectType@(Out.ObjectType _ _ _ resolvers) selectionSet = do executeSelectionSet result objectType@(Out.ObjectType _ _ _ resolvers) objectLocation selectionSet = do
let fields = collectFields objectType selectionSet let fields = collectFields objectType selectionSet
resolvedValues <- Map.traverseMaybeWithKey forEach fields resolvedValues <- OrderedMap.traverseMaybe forEach fields
coerceResult (Out.NonNullObjectType objectType) $ Object resolvedValues coerceResult (Out.NonNullObjectType objectType) objectLocation
$ Object resolvedValues
where where
forEach _ fields@(field :| _) = forEach fields@(field :| _) =
let Transform.Field _ name _ _ = field let Transform.Field _ name _ _ _ = field
in traverse (tryResolver fields) $ lookupResolver name in traverse (tryResolver fields) $ lookupResolver name
lookupResolver = flip HashMap.lookup resolvers lookupResolver = flip HashMap.lookup resolvers
tryResolver fields resolver = tryResolver fields resolver =
executeField resolver result fields >>= lift . pure executeField resolver result fields >>= lift . pure
coerceArgumentValues coerceArgumentValues
:: HashMap Name In.Argument :: HashMap Full.Name In.Argument
-> HashMap Name Transform.Input -> HashMap Full.Name (Full.Node Transform.Input)
-> Maybe Type.Subs -> Either [Full.Location] Type.Subs
coerceArgumentValues argumentDefinitions argumentValues = coerceArgumentValues argumentDefinitions argumentNodes =
HashMap.foldrWithKey forEach (pure mempty) argumentDefinitions HashMap.foldrWithKey forEach (pure mempty) argumentDefinitions
where where
forEach variableName (In.Argument _ variableType defaultValue) = forEach argumentName (In.Argument _ variableType defaultValue) = \case
matchFieldValues coerceArgumentValue argumentValues variableName variableType defaultValue Right resultMap
| Just matchedValues
<- matchFieldValues' argumentName variableType defaultValue $ Just resultMap
-> Right matchedValues
| otherwise -> Left $ generateError argumentName []
Left errorLocations
| Just _
<- matchFieldValues' argumentName variableType defaultValue $ pure mempty
-> Left errorLocations
| otherwise -> Left $ generateError argumentName errorLocations
generateError argumentName errorLocations =
case HashMap.lookup argumentName argumentNodes of
Just (Full.Node _ errorLocation) -> [errorLocation]
Nothing -> errorLocations
matchFieldValues' = matchFieldValues coerceArgumentValue (Full.node <$> argumentNodes)
coerceArgumentValue inputType (Transform.Int integer) = coerceArgumentValue inputType (Transform.Int integer) =
coerceInputLiteral inputType (Type.Int integer) coerceInputLiteral inputType (Type.Int integer)
coerceArgumentValue inputType (Transform.Boolean boolean) = coerceArgumentValue inputType (Transform.Boolean boolean) =

View File

@ -0,0 +1,31 @@
{- This Source Code Form is subject to the terms of the Mozilla Public License,
v. 2.0. If a copy of the MPL was not distributed with this file, You can
obtain one at https://mozilla.org/MPL/2.0/. -}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE ExplicitForAll #-}
{-# LANGUAGE NamedFieldPuns #-}
module Language.GraphQL.Execute.Internal
( addError
, singleError
) where
import Control.Monad.Trans.State (modify)
import Control.Monad.Catch (MonadCatch)
import Data.Sequence ((|>))
import qualified Data.Text as Text
import qualified Language.GraphQL.AST as Full
import Language.GraphQL.Error (CollectErrsT, Error(..), Resolution(..))
import Prelude hiding (null)
addError :: MonadCatch m => forall a. a -> Error -> CollectErrsT m a
addError returnValue error' = modify appender >> pure returnValue
where
appender :: Resolution m -> Resolution m
appender resolution@Resolution{ errors } = resolution
{ errors = errors |> error'
}
singleError :: [Full.Location] -> String -> Error
singleError errorLocations message = Error (Text.pack message) errorLocations []

View File

@ -0,0 +1,148 @@
{- This Source Code Form is subject to the terms of the Mozilla Public License,
v. 2.0. If a copy of the MPL was not distributed with this file, You can
obtain one at https://mozilla.org/MPL/2.0/. -}
{-# LANGUAGE ExplicitForAll #-}
-- | This module contains a map data structure, that preserves insertion order.
-- Some definitions conflict with functions from prelude, so this module should
-- probably be imported qualified.
module Language.GraphQL.Execute.OrderedMap
( OrderedMap
, elems
, empty
, insert
, foldlWithKey'
, keys
, lookup
, replace
, singleton
, size
, toList
, traverseMaybe
) where
import qualified Data.Foldable as Foldable
import Data.HashMap.Strict (HashMap, (!))
import qualified Data.HashMap.Strict as HashMap
import Data.Text (Text)
import Data.Vector (Vector)
import qualified Data.Vector as Vector
import Prelude hiding (filter, lookup)
-- | This map associates values with the given text keys. Insertion order is
-- preserved. When inserting a value with a key, that is already available in
-- the map, the existing value isn't overridden, but combined with the new value
-- using its 'Semigroup' instance.
--
-- Internally this map uses an array with keys to preserve the order and an
-- unorded map with key-value pairs.
data OrderedMap v = OrderedMap (Vector Text) (HashMap Text v)
deriving (Eq)
instance Functor OrderedMap where
fmap f (OrderedMap vector hashMap) = OrderedMap vector $ fmap f hashMap
instance Foldable OrderedMap where
foldr f = foldrWithKey $ const f
null (OrderedMap vector _) = Vector.null vector
instance Semigroup v => Semigroup (OrderedMap v) where
(<>) = foldlWithKey'
$ \accumulator key value -> insert key value accumulator
instance Semigroup v => Monoid (OrderedMap v) where
mempty = empty
instance Traversable OrderedMap where
traverse f (OrderedMap vector hashMap) = OrderedMap vector
<$> traverse f hashMap
instance Show v => Show (OrderedMap v) where
showsPrec precedence map' = showParen (precedence > 10)
$ showString "fromList " . shows (toList map')
-- * Construction
-- | Constructs a map with a single element.
singleton :: forall v. Text -> v -> OrderedMap v
singleton key value = OrderedMap (Vector.singleton key)
$ HashMap.singleton key value
-- | Constructs an empty map.
empty :: forall v. OrderedMap v
empty = OrderedMap mempty mempty
-- * Traversal
-- | Reduces this map by applying a binary operator from right to left to all
-- elements, using the given starting value.
foldrWithKey :: forall v a. (Text -> v -> a -> a) -> a -> OrderedMap v -> a
foldrWithKey f initial (OrderedMap vector hashMap) = foldr go initial vector
where
go key = f key (hashMap ! key)
-- | Reduces this map by applying a binary operator from left to right to all
-- elements, using the given starting value.
foldlWithKey' :: forall v a. (a -> Text -> v -> a) -> a -> OrderedMap v -> a
foldlWithKey' f initial (OrderedMap vector hashMap) =
Vector.foldl' go initial vector
where
go accumulator key = f accumulator key (hashMap ! key)
-- | Traverse over the elements and collect the 'Just' results.
traverseMaybe
:: Applicative f
=> forall a
. (a -> f (Maybe b))
-> OrderedMap a
-> f (OrderedMap b)
traverseMaybe f orderedMap = foldlWithKey' filter empty
<$> traverse f orderedMap
where
filter accumulator key (Just value) = replace key value accumulator
filter accumulator _ Nothing = accumulator
-- * Lists
-- | Converts this map to the list of key-value pairs.
toList :: forall v. OrderedMap v -> [(Text, v)]
toList = foldrWithKey ((.) (:) . (,)) []
-- | Returns a list with all keys in this map.
keys :: forall v. OrderedMap v -> [Text]
keys (OrderedMap vector _) = Foldable.toList vector
-- | Returns a list with all elements in this map.
elems :: forall v. OrderedMap v -> [v]
elems = fmap snd . toList
-- * Basic interface
-- | Associates the specified value with the specified key in this map. If this
-- map previously contained a mapping for the key, the existing and new values
-- are combined.
insert :: Semigroup v => Text -> v -> OrderedMap v -> OrderedMap v
insert key value (OrderedMap vector hashMap)
| Just available <- HashMap.lookup key hashMap = OrderedMap vector
$ HashMap.insert key (available <> value) hashMap
| otherwise = OrderedMap (Vector.snoc vector key)
$ HashMap.insert key value hashMap
-- | Associates the specified value with the specified key in this map. If this
-- map previously contained a mapping for the key, the existing value is
-- replaced by the new one.
replace :: Text -> v -> OrderedMap v -> OrderedMap v
replace key value (OrderedMap vector hashMap)
| HashMap.member key hashMap = OrderedMap vector
$ HashMap.insert key value hashMap
| otherwise = OrderedMap (Vector.snoc vector key)
$ HashMap.insert key value hashMap
-- | Gives the size of this map, i.e. number of elements in it.
size :: forall v. OrderedMap v -> Int
size (OrderedMap vector _) = Vector.length vector
-- | Looks up a value in this map by key.
lookup :: forall v. Text -> OrderedMap v -> Maybe v
lookup key (OrderedMap _ hashMap) = HashMap.lookup key hashMap

View File

@ -9,62 +9,78 @@ module Language.GraphQL.Execute.Subscribe
) where ) where
import Conduit import Conduit
import Control.Arrow (left)
import Control.Monad.Catch (Exception(..), MonadCatch(..)) import Control.Monad.Catch (Exception(..), MonadCatch(..))
import Control.Monad.Trans.Reader (ReaderT(..), runReaderT) import Control.Monad.Trans.Reader (ReaderT(..), runReaderT)
import Data.HashMap.Strict (HashMap) import Data.HashMap.Strict (HashMap)
import qualified Data.HashMap.Strict as HashMap import qualified Data.HashMap.Strict as HashMap
import qualified Data.Map.Strict as Map
import qualified Data.List.NonEmpty as NonEmpty import qualified Data.List.NonEmpty as NonEmpty
import Data.Sequence (Seq(..)) import Data.Sequence (Seq(..))
import Data.Text (Text) import qualified Language.GraphQL.AST as Full
import qualified Data.Text as Text
import Language.GraphQL.AST (Name)
import Language.GraphQL.Execute.Coerce import Language.GraphQL.Execute.Coerce
import Language.GraphQL.Execute.Execution import Language.GraphQL.Execute.Execution
import Language.GraphQL.Execute.Internal
import qualified Language.GraphQL.Execute.OrderedMap as OrderedMap
import qualified Language.GraphQL.Execute.Transform as Transform import qualified Language.GraphQL.Execute.Transform as Transform
import Language.GraphQL.Error import Language.GraphQL.Error
( Error(..)
, ResolverException
, Response
, ResponseEventStream
, runCollectErrs
)
import qualified Language.GraphQL.Type.Definition as Definition import qualified Language.GraphQL.Type.Definition as Definition
import qualified Language.GraphQL.Type as Type import qualified Language.GraphQL.Type as Type
import qualified Language.GraphQL.Type.Out as Out import qualified Language.GraphQL.Type.Out as Out
import Language.GraphQL.Type.Schema import Language.GraphQL.Type.Schema
-- This is actually executeMutation, but we don't distinguish between queries
-- and mutations yet.
subscribe :: (MonadCatch m, Serialize a) subscribe :: (MonadCatch m, Serialize a)
=> HashMap Name (Type m) => HashMap Full.Name (Type m)
-> Out.ObjectType m -> Out.ObjectType m
-> Full.Location
-> Seq (Transform.Selection m) -> Seq (Transform.Selection m)
-> m (Either Text (ResponseEventStream m a)) -> m (Either Error (ResponseEventStream m a))
subscribe types' objectType fields = do subscribe types' objectType objectLocation fields = do
sourceStream <- createSourceEventStream types' objectType fields sourceStream <-
traverse (mapSourceToResponseEvent types' objectType fields) sourceStream createSourceEventStream types' objectType objectLocation fields
let traverser =
mapSourceToResponseEvent types' objectType objectLocation fields
traverse traverser sourceStream
mapSourceToResponseEvent :: (MonadCatch m, Serialize a) mapSourceToResponseEvent :: (MonadCatch m, Serialize a)
=> HashMap Name (Type m) => HashMap Full.Name (Type m)
-> Out.ObjectType m -> Out.ObjectType m
-> Full.Location
-> Seq (Transform.Selection m) -> Seq (Transform.Selection m)
-> Out.SourceEventStream m -> Out.SourceEventStream m
-> m (ResponseEventStream m a) -> m (ResponseEventStream m a)
mapSourceToResponseEvent types' subscriptionType fields sourceStream = pure mapSourceToResponseEvent types' subscriptionType objectLocation fields sourceStream
= pure
$ sourceStream $ sourceStream
.| mapMC (executeSubscriptionEvent types' subscriptionType fields) .| mapMC (executeSubscriptionEvent types' subscriptionType objectLocation fields)
createSourceEventStream :: MonadCatch m createSourceEventStream :: MonadCatch m
=> HashMap Name (Type m) => HashMap Full.Name (Type m)
-> Out.ObjectType m -> Out.ObjectType m
-> Full.Location
-> Seq (Transform.Selection m) -> Seq (Transform.Selection m)
-> m (Either Text (Out.SourceEventStream m)) -> m (Either Error (Out.SourceEventStream m))
createSourceEventStream _types subscriptionType@(Out.ObjectType _ _ _ fieldTypes) fields createSourceEventStream _types subscriptionType objectLocation fields
| [fieldGroup] <- Map.elems groupedFieldSet | [fieldGroup] <- OrderedMap.elems groupedFieldSet
, Transform.Field _ fieldName arguments' _ <- NonEmpty.head fieldGroup , Transform.Field _ fieldName arguments' _ errorLocation <- NonEmpty.head fieldGroup
, Out.ObjectType _ _ _ fieldTypes <- subscriptionType
, resolverT <- fieldTypes HashMap.! fieldName , resolverT <- fieldTypes HashMap.! fieldName
, Out.EventStreamResolver fieldDefinition _ resolver <- resolverT , Out.EventStreamResolver fieldDefinition _ resolver <- resolverT
, Out.Field _ _fieldType argumentDefinitions <- fieldDefinition = , Out.Field _ _fieldType argumentDefinitions <- fieldDefinition =
case coerceArgumentValues argumentDefinitions arguments' of case coerceArgumentValues argumentDefinitions arguments' of
Nothing -> pure $ Left "Argument coercion failed." Left _ -> pure
Just argumentValues -> $ Left
resolveFieldEventStream Type.Null argumentValues resolver $ Error "Argument coercion failed." [errorLocation] []
| otherwise = pure $ Left "Subscription contains more than one field." Right argumentValues -> left (singleError [errorLocation])
<$> resolveFieldEventStream Type.Null argumentValues resolver
| otherwise = pure
$ Left
$ Error "Subscription contains more than one field." [objectLocation] []
where where
groupedFieldSet = collectFields subscriptionType fields groupedFieldSet = collectFields subscriptionType fields
@ -72,26 +88,26 @@ resolveFieldEventStream :: MonadCatch m
=> Type.Value => Type.Value
-> Type.Subs -> Type.Subs
-> Out.Subscribe m -> Out.Subscribe m
-> m (Either Text (Out.SourceEventStream m)) -> m (Either String (Out.SourceEventStream m))
resolveFieldEventStream result args resolver = resolveFieldEventStream result args resolver =
catch (Right <$> runReaderT resolver context) handleEventStreamError catch (Right <$> runReaderT resolver context) handleEventStreamError
where where
handleEventStreamError :: MonadCatch m handleEventStreamError :: MonadCatch m
=> ResolverException => ResolverException
-> m (Either Text (Out.SourceEventStream m)) -> m (Either String (Out.SourceEventStream m))
handleEventStreamError = pure . Left . Text.pack . displayException handleEventStreamError = pure . Left . displayException
context = Type.Context context = Type.Context
{ Type.arguments = Type.Arguments args { Type.arguments = Type.Arguments args
, Type.values = result , Type.values = result
} }
-- This is actually executeMutation, but we don't distinguish between queries
-- and mutations yet.
executeSubscriptionEvent :: (MonadCatch m, Serialize a) executeSubscriptionEvent :: (MonadCatch m, Serialize a)
=> HashMap Name (Type m) => HashMap Full.Name (Type m)
-> Out.ObjectType m -> Out.ObjectType m
-> Full.Location
-> Seq (Transform.Selection m) -> Seq (Transform.Selection m)
-> Definition.Value -> Definition.Value
-> m (Response a) -> m (Response a)
executeSubscriptionEvent types' objectType fields initialValue = executeSubscriptionEvent types' objectType objectLocation fields initialValue
runCollectErrs types' $ executeSelectionSet initialValue objectType fields = runCollectErrs types'
$ executeSelectionSet initialValue objectType objectLocation fields

View File

@ -1,3 +1,7 @@
{- This Source Code Form is subject to the terms of the Mozilla Public License,
v. 2.0. If a copy of the MPL was not distributed with this file, You can
obtain one at https://mozilla.org/MPL/2.0/. -}
{-# LANGUAGE ExplicitForAll #-} {-# LANGUAGE ExplicitForAll #-}
{-# LANGUAGE LambdaCase #-} {-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
@ -25,7 +29,6 @@ module Language.GraphQL.Execute.Transform
, QueryError(..) , QueryError(..)
, Selection(..) , Selection(..)
, document , document
, queryError
) where ) where
import Control.Monad (foldM, unless) import Control.Monad (foldM, unless)
@ -47,24 +50,23 @@ import Language.GraphQL.AST (Name)
import qualified Language.GraphQL.Execute.Coerce as Coerce import qualified Language.GraphQL.Execute.Coerce as Coerce
import qualified Language.GraphQL.Type.Definition as Definition import qualified Language.GraphQL.Type.Definition as Definition
import qualified Language.GraphQL.Type as Type import qualified Language.GraphQL.Type as Type
import qualified Language.GraphQL.Type.In as In import qualified Language.GraphQL.Type.Internal as Type
import Language.GraphQL.Type.Internal
import qualified Language.GraphQL.Type.Out as Out import qualified Language.GraphQL.Type.Out as Out
import Language.GraphQL.Type.Schema import qualified Language.GraphQL.Type.Schema as Schema
-- | Associates a fragment name with a list of 'Field's. -- | Associates a fragment name with a list of 'Field's.
data Replacement m = Replacement data Replacement m = Replacement
{ fragments :: HashMap Full.Name (Fragment m) { fragments :: HashMap Full.Name (Fragment m)
, fragmentDefinitions :: FragmentDefinitions , fragmentDefinitions :: FragmentDefinitions
, variableValues :: Type.Subs , variableValues :: Type.Subs
, types :: HashMap Full.Name (Type m) , types :: HashMap Full.Name (Schema.Type m)
} }
type FragmentDefinitions = HashMap Full.Name Full.FragmentDefinition type FragmentDefinitions = HashMap Full.Name Full.FragmentDefinition
-- | Represents fragments and inline fragments. -- | Represents fragments and inline fragments.
data Fragment m data Fragment m
= Fragment (CompositeType m) (Seq (Selection m)) = Fragment (Type.CompositeType m) (Seq (Selection m))
-- | Single selection element. -- | Single selection element.
data Selection m data Selection m
@ -72,20 +74,22 @@ data Selection m
| SelectionField (Field m) | SelectionField (Field m)
-- | GraphQL has 3 operation types: queries, mutations and subscribtions. -- | GraphQL has 3 operation types: queries, mutations and subscribtions.
--
-- Currently only queries and mutations are supported.
data Operation m data Operation m
= Query (Maybe Text) (Seq (Selection m)) = Query (Maybe Text) (Seq (Selection m)) Full.Location
| Mutation (Maybe Text) (Seq (Selection m)) | Mutation (Maybe Text) (Seq (Selection m)) Full.Location
| Subscription (Maybe Text) (Seq (Selection m)) | Subscription (Maybe Text) (Seq (Selection m)) Full.Location
-- | Single GraphQL field. -- | Single GraphQL field.
data Field m = Field data Field m = Field
(Maybe Full.Name) Full.Name (HashMap Full.Name Input) (Seq (Selection m)) (Maybe Full.Name)
Full.Name
(HashMap Full.Name (Full.Node Input))
(Seq (Selection m))
Full.Location
-- | Contains the operation to be executed along with its root type. -- | Contains the operation to be executed along with its root type.
data Document m = Document data Document m = Document
(HashMap Full.Name (Type m)) (Out.ObjectType m) (Operation m) (HashMap Full.Name (Schema.Type m)) (Out.ObjectType m) (Operation m)
data OperationDefinition = OperationDefinition data OperationDefinition = OperationDefinition
Full.OperationType Full.OperationType
@ -93,16 +97,26 @@ data OperationDefinition = OperationDefinition
[Full.VariableDefinition] [Full.VariableDefinition]
[Full.Directive] [Full.Directive]
Full.SelectionSet Full.SelectionSet
Full.Location
-- | Query error types. -- | Query error types.
data QueryError data QueryError
= OperationNotFound Text = OperationNotFound Text
| OperationNameRequired | OperationNameRequired
| CoercionError | CoercionError
| TransformationError
| EmptyDocument | EmptyDocument
| UnsupportedRootOperation | UnsupportedRootOperation
instance Show QueryError where
show (OperationNotFound operationName) = unwords
["Operation", Text.unpack operationName, "couldn't be found in the document."]
show OperationNameRequired = "Missing operation name."
show CoercionError = "Coercion error."
show EmptyDocument =
"The document doesn't contain any executable operations."
show UnsupportedRootOperation =
"Root operation type couldn't be found in the schema."
data Input data Input
= Int Int32 = Int Int32
| Float Double | Float Double
@ -115,17 +129,6 @@ data Input
| Variable Type.Value | Variable Type.Value
deriving (Eq, Show) deriving (Eq, Show)
queryError :: QueryError -> Text
queryError (OperationNotFound operationName) = Text.unwords
["Operation", operationName, "couldn't be found in the document."]
queryError OperationNameRequired = "Missing operation name."
queryError CoercionError = "Coercion error."
queryError TransformationError = "Schema transformation error."
queryError EmptyDocument =
"The document doesn't contain any executable operations."
queryError UnsupportedRootOperation =
"Root operation type couldn't be found in the schema."
getOperation getOperation
:: Maybe Full.Name :: Maybe Full.Name
-> NonEmpty OperationDefinition -> NonEmpty OperationDefinition
@ -136,54 +139,25 @@ getOperation (Just operationName) operations
| Just operation' <- find matchingName operations = pure operation' | Just operation' <- find matchingName operations = pure operation'
| otherwise = Left $ OperationNotFound operationName | otherwise = Left $ OperationNotFound operationName
where where
matchingName (OperationDefinition _ name _ _ _) = matchingName (OperationDefinition _ name _ _ _ _) =
name == Just operationName name == Just operationName
lookupInputType
:: Full.Type
-> HashMap.HashMap Full.Name (Type m)
-> Maybe In.Type
lookupInputType (Full.TypeNamed name) types =
case HashMap.lookup name types of
Just (ScalarType scalarType) ->
Just $ In.NamedScalarType scalarType
Just (EnumType enumType) ->
Just $ In.NamedEnumType enumType
Just (InputObjectType objectType) ->
Just $ In.NamedInputObjectType objectType
_ -> Nothing
lookupInputType (Full.TypeList list) types
= In.ListType
<$> lookupInputType list types
lookupInputType (Full.TypeNonNull (Full.NonNullTypeNamed nonNull)) types =
case HashMap.lookup nonNull types of
Just (ScalarType scalarType) ->
Just $ In.NonNullScalarType scalarType
Just (EnumType enumType) ->
Just $ In.NonNullEnumType enumType
Just (InputObjectType objectType) ->
Just $ In.NonNullInputObjectType objectType
_ -> Nothing
lookupInputType (Full.TypeNonNull (Full.NonNullTypeList nonNull)) types
= In.NonNullListType
<$> lookupInputType nonNull types
coerceVariableValues :: Coerce.VariableValue a coerceVariableValues :: Coerce.VariableValue a
=> forall m => forall m
. HashMap Full.Name (Type m) . HashMap Full.Name (Schema.Type m)
-> OperationDefinition -> OperationDefinition
-> HashMap.HashMap Full.Name a -> HashMap.HashMap Full.Name a
-> Either QueryError Type.Subs -> Either QueryError Type.Subs
coerceVariableValues types operationDefinition variableValues = coerceVariableValues types operationDefinition variableValues =
let OperationDefinition _ _ variableDefinitions _ _ = operationDefinition let OperationDefinition _ _ variableDefinitions _ _ _ = operationDefinition
in maybe (Left CoercionError) Right in maybe (Left CoercionError) Right
$ foldr forEach (Just HashMap.empty) variableDefinitions $ foldr forEach (Just HashMap.empty) variableDefinitions
where where
forEach variableDefinition coercedValues = do forEach variableDefinition coercedValues = do
let Full.VariableDefinition variableName variableTypeName defaultValue = let Full.VariableDefinition variableName variableTypeName defaultValue _ =
variableDefinition variableDefinition
let defaultValue' = constValue <$> defaultValue let defaultValue' = constValue . Full.node <$> defaultValue
variableType <- lookupInputType variableTypeName types variableType <- Type.lookupInputType variableTypeName types
Coerce.matchFieldValues Coerce.matchFieldValues
coerceVariableValue' coerceVariableValue'
@ -203,23 +177,24 @@ constValue (Full.ConstString x) = Type.String x
constValue (Full.ConstBoolean b) = Type.Boolean b constValue (Full.ConstBoolean b) = Type.Boolean b
constValue Full.ConstNull = Type.Null constValue Full.ConstNull = Type.Null
constValue (Full.ConstEnum e) = Type.Enum e constValue (Full.ConstEnum e) = Type.Enum e
constValue (Full.ConstList l) = Type.List $ constValue <$> l constValue (Full.ConstList list) = Type.List $ constValue . Full.node <$> list
constValue (Full.ConstObject o) = constValue (Full.ConstObject o) =
Type.Object $ HashMap.fromList $ constObjectField <$> o Type.Object $ HashMap.fromList $ constObjectField <$> o
where where
constObjectField (Full.ObjectField key value') = (key, constValue value') constObjectField Full.ObjectField{value = value', ..} =
(name, constValue $ Full.node value')
-- | Rewrites the original syntax tree into an intermediate representation used -- | Rewrites the original syntax tree into an intermediate representation used
-- for query execution. -- for query execution.
document :: Coerce.VariableValue a document :: Coerce.VariableValue a
=> forall m => forall m
. Schema m . Type.Schema m
-> Maybe Full.Name -> Maybe Full.Name
-> HashMap Full.Name a -> HashMap Full.Name a
-> Full.Document -> Full.Document
-> Either QueryError (Document m) -> Either QueryError (Document m)
document schema operationName subs ast = do document schema operationName subs ast = do
let referencedTypes = collectReferencedTypes schema let referencedTypes = Schema.types schema
(operations, fragmentTable) <- defragment ast (operations, fragmentTable) <- defragment ast
chosenOperation <- getOperation operationName operations chosenOperation <- getOperation operationName operations
@ -232,15 +207,15 @@ document schema operationName subs ast = do
, types = referencedTypes , types = referencedTypes
} }
case chosenOperation of case chosenOperation of
OperationDefinition Full.Query _ _ _ _ -> OperationDefinition Full.Query _ _ _ _ _ ->
pure $ Document referencedTypes (query schema) pure $ Document referencedTypes (Schema.query schema)
$ operation chosenOperation replacement $ operation chosenOperation replacement
OperationDefinition Full.Mutation _ _ _ _ OperationDefinition Full.Mutation _ _ _ _ _
| Just mutationType <- mutation schema -> | Just mutationType <- Schema.mutation schema ->
pure $ Document referencedTypes mutationType pure $ Document referencedTypes mutationType
$ operation chosenOperation replacement $ operation chosenOperation replacement
OperationDefinition Full.Subscription _ _ _ _ OperationDefinition Full.Subscription _ _ _ _ _
| Just subscriptionType <- subscription schema -> | Just subscriptionType <- Schema.subscription schema ->
pure $ Document referencedTypes subscriptionType pure $ Document referencedTypes subscriptionType
$ operation chosenOperation replacement $ operation chosenOperation replacement
_ -> Left UnsupportedRootOperation _ -> Left UnsupportedRootOperation
@ -264,10 +239,10 @@ defragment ast =
(operations, HashMap.insert name fragment fragments') (operations, HashMap.insert name fragment fragments')
defragment' _ acc = acc defragment' _ acc = acc
transform = \case transform = \case
Full.OperationDefinition type' name variables directives' selections _ -> Full.OperationDefinition type' name variables directives' selections location ->
OperationDefinition type' name variables directives' selections OperationDefinition type' name variables directives' selections location
Full.SelectionSet selectionSet _ -> Full.SelectionSet selectionSet location ->
OperationDefinition Full.Query Nothing mempty mempty selectionSet OperationDefinition Full.Query Nothing mempty mempty selectionSet location
-- * Operation -- * Operation
@ -276,31 +251,46 @@ operation operationDefinition replacement
= runIdentity = runIdentity
$ evalStateT (collectFragments >> transform operationDefinition) replacement $ evalStateT (collectFragments >> transform operationDefinition) replacement
where where
transform (OperationDefinition Full.Query name _ _ sels) = transform (OperationDefinition Full.Query name _ _ sels location) =
Query name <$> appendSelection sels flip (Query name) location <$> appendSelection sels
transform (OperationDefinition Full.Mutation name _ _ sels) = transform (OperationDefinition Full.Mutation name _ _ sels location) =
Mutation name <$> appendSelection sels flip (Mutation name) location <$> appendSelection sels
transform (OperationDefinition Full.Subscription name _ _ sels) = transform (OperationDefinition Full.Subscription name _ _ sels location) =
Subscription name <$> appendSelection sels flip (Subscription name) location <$> appendSelection sels
-- * Selection -- * Selection
selection selection
:: Full.Selection :: Full.Selection
-> State (Replacement m) (Either (Seq (Selection m)) (Selection m)) -> State (Replacement m) (Either (Seq (Selection m)) (Selection m))
selection (Full.Field alias name arguments' directives' selections) = selection (Full.FieldSelection fieldSelection) =
maybe (Left mempty) (Right . SelectionField) <$> do maybe (Left mempty) (Right . SelectionField) <$> field fieldSelection
selection (Full.FragmentSpreadSelection fragmentSelection)
= maybe (Left mempty) (Right . SelectionFragment)
<$> fragmentSpread fragmentSelection
selection (Full.InlineFragmentSelection fragmentSelection) =
inlineFragment fragmentSelection
field :: Full.Field -> State (Replacement m) (Maybe (Field m))
field (Full.Field alias name arguments' directives' selections location) = do
fieldArguments <- foldM go HashMap.empty arguments' fieldArguments <- foldM go HashMap.empty arguments'
fieldSelections <- appendSelection selections fieldSelections <- appendSelection selections
fieldDirectives <- Definition.selection <$> directives directives' fieldDirectives <- Definition.selection <$> directives directives'
let field' = Field alias name fieldArguments fieldSelections let field' = Field alias name fieldArguments fieldSelections location
pure $ field' <$ fieldDirectives pure $ field' <$ fieldDirectives
where where
go arguments (Full.Argument name' value') = go arguments (Full.Argument name' (Full.Node value' _) location') = do
inputField arguments name' value' objectFieldValue <- input value'
case objectFieldValue of
Just fieldValue ->
let argumentNode = Full.Node fieldValue location'
in pure $ HashMap.insert name' argumentNode arguments
Nothing -> pure arguments
selection (Full.FragmentSpread name directives') = fragmentSpread
maybe (Left mempty) (Right . SelectionFragment) <$> do :: Full.FragmentSpread
-> State (Replacement m) (Maybe (Fragment m))
fragmentSpread (Full.FragmentSpread name directives' _) = do
spreadDirectives <- Definition.selection <$> directives directives' spreadDirectives <- Definition.selection <$> directives directives'
fragments' <- gets fragments fragments' <- gets fragments
@ -314,7 +304,11 @@ selection (Full.FragmentSpread name directives') =
Just fragment -> lift $ pure $ fragment <$ spreadDirectives Just fragment -> lift $ pure $ fragment <$ spreadDirectives
_ -> lift $ pure Nothing _ -> lift $ pure Nothing
| otherwise -> lift $ pure Nothing | otherwise -> lift $ pure Nothing
selection (Full.InlineFragment type' directives' selections) = do
inlineFragment
:: Full.InlineFragment
-> State (Replacement m) (Either (Seq (Selection m)) (Selection m))
inlineFragment (Full.InlineFragment type' directives' selections _) = do
fragmentDirectives <- Definition.selection <$> directives directives' fragmentDirectives <- Definition.selection <$> directives directives'
case fragmentDirectives of case fragmentDirectives of
Nothing -> pure $ Left mempty Nothing -> pure $ Left mempty
@ -325,7 +319,7 @@ selection (Full.InlineFragment type' directives' selections) = do
Nothing -> pure $ Left fragmentSelectionSet Nothing -> pure $ Left fragmentSelectionSet
Just typeName -> do Just typeName -> do
types' <- gets types types' <- gets types
case lookupTypeCondition typeName types' of case Type.lookupTypeCondition typeName types' of
Just typeCondition -> pure $ Just typeCondition -> pure $
selectionFragment typeCondition fragmentSelectionSet selectionFragment typeCondition fragmentSelectionSet
Nothing -> pure $ Left mempty Nothing -> pure $ Left mempty
@ -346,10 +340,10 @@ appendSelection = foldM go mempty
directives :: [Full.Directive] -> State (Replacement m) [Definition.Directive] directives :: [Full.Directive] -> State (Replacement m) [Definition.Directive]
directives = traverse directive directives = traverse directive
where where
directive (Full.Directive directiveName directiveArguments) directive (Full.Directive directiveName directiveArguments _)
= Definition.Directive directiveName . Type.Arguments = Definition.Directive directiveName . Type.Arguments
<$> foldM go HashMap.empty directiveArguments <$> foldM go HashMap.empty directiveArguments
go arguments (Full.Argument name value') = do go arguments (Full.Argument name (Full.Node value' _) _) = do
substitutedValue <- value value' substitutedValue <- value value'
return $ HashMap.insert name substitutedValue arguments return $ HashMap.insert name substitutedValue arguments
@ -372,7 +366,7 @@ fragmentDefinition (Full.FragmentDefinition name type' _ selections _) = do
fragmentSelection <- appendSelection selections fragmentSelection <- appendSelection selections
types' <- gets types types' <- gets types
case lookupTypeCondition type' types' of case Type.lookupTypeCondition type' types' of
Just compositeType -> do Just compositeType -> do
let newValue = Fragment compositeType fragmentSelection let newValue = Fragment compositeType fragmentSelection
modify $ insertFragment newValue modify $ insertFragment newValue
@ -395,11 +389,12 @@ value (Full.String string) = pure $ Type.String string
value (Full.Boolean boolean) = pure $ Type.Boolean boolean value (Full.Boolean boolean) = pure $ Type.Boolean boolean
value Full.Null = pure Type.Null value Full.Null = pure Type.Null
value (Full.Enum enum) = pure $ Type.Enum enum value (Full.Enum enum) = pure $ Type.Enum enum
value (Full.List list) = Type.List <$> traverse value list value (Full.List list) = Type.List <$> traverse (value . Full.node) list
value (Full.Object object) = value (Full.Object object) =
Type.Object . HashMap.fromList <$> traverse objectField object Type.Object . HashMap.fromList <$> traverse objectField object
where where
objectField (Full.ObjectField name value') = (name,) <$> value value' objectField Full.ObjectField{value = value', ..} =
(name,) <$> value (Full.node value')
input :: forall m. Full.Value -> State (Replacement m) (Maybe Input) input :: forall m. Full.Value -> State (Replacement m) (Maybe Input)
input (Full.Variable name) = input (Full.Variable name) =
@ -410,13 +405,13 @@ input (Full.String string) = pure $ pure $ String string
input (Full.Boolean boolean) = pure $ pure $ Boolean boolean input (Full.Boolean boolean) = pure $ pure $ Boolean boolean
input Full.Null = pure $ pure Null input Full.Null = pure $ pure Null
input (Full.Enum enum) = pure $ pure $ Enum enum input (Full.Enum enum) = pure $ pure $ Enum enum
input (Full.List list) = pure . List <$> traverse value list input (Full.List list) = pure . List <$> traverse (value . Full.node) list
input (Full.Object object) = do input (Full.Object object) = do
objectFields <- foldM objectField HashMap.empty object objectFields <- foldM objectField HashMap.empty object
pure $ pure $ Object objectFields pure $ pure $ Object objectFields
where where
objectField resultMap (Full.ObjectField name value') = objectField resultMap Full.ObjectField{value = value', ..} =
inputField resultMap name value' inputField resultMap name $ Full.node value'
inputField :: forall m inputField :: forall m
. HashMap Full.Name Input . HashMap Full.Name Input

View File

@ -21,6 +21,6 @@ module Language.GraphQL.Type
) where ) where
import Language.GraphQL.Type.Definition import Language.GraphQL.Type.Definition
import Language.GraphQL.Type.Schema (Schema(..)) import Language.GraphQL.Type.Schema (Schema, schema, schemaWithTypes)
import qualified Language.GraphQL.Type.In as In import qualified Language.GraphQL.Type.In as In
import qualified Language.GraphQL.Type.Out as Out import qualified Language.GraphQL.Type.Out as Out

View File

@ -22,6 +22,7 @@ import Data.HashMap.Strict (HashMap)
import qualified Data.HashMap.Strict as HashMap import qualified Data.HashMap.Strict as HashMap
import Data.String (IsString(..)) import Data.String (IsString(..))
import Data.Text (Text) import Data.Text (Text)
import qualified Data.Text as Text
import Language.GraphQL.AST (Name) import Language.GraphQL.AST (Name)
import Prelude hiding (id) import Prelude hiding (id)
@ -63,6 +64,9 @@ data ScalarType = ScalarType Name (Maybe Text)
instance Eq ScalarType where instance Eq ScalarType where
(ScalarType this _) == (ScalarType that _) = this == that (ScalarType this _) == (ScalarType that _) = this == that
instance Show ScalarType where
show (ScalarType typeName _) = Text.unpack typeName
-- | Enum type definition. -- | Enum type definition.
-- --
-- Some leaf values of requests and input values are Enums. GraphQL serializes -- Some leaf values of requests and input values are Enums. GraphQL serializes
@ -73,6 +77,9 @@ data EnumType = EnumType Name (Maybe Text) (HashMap Name EnumValue)
instance Eq EnumType where instance Eq EnumType where
(EnumType this _ _) == (EnumType that _ _) = this == that (EnumType this _ _) == (EnumType that _ _) = this == that
instance Show EnumType where
show (EnumType typeName _ _) = Text.unpack typeName
-- | Enum value is a single member of an 'EnumType'. -- | Enum value is a single member of an 'EnumType'.
newtype EnumValue = EnumValue (Maybe Text) newtype EnumValue = EnumValue (Maybe Text)

View File

@ -11,6 +11,7 @@
-- with 'Language.GraphQL.Type.Out'. -- with 'Language.GraphQL.Type.Out'.
module Language.GraphQL.Type.In module Language.GraphQL.Type.In
( Argument(..) ( Argument(..)
, Arguments
, InputField(..) , InputField(..)
, InputObjectType(..) , InputObjectType(..)
, Type(..) , Type(..)
@ -23,11 +24,12 @@ module Language.GraphQL.Type.In
import Data.HashMap.Strict (HashMap) import Data.HashMap.Strict (HashMap)
import Data.Text (Text) import Data.Text (Text)
import qualified Data.Text as Text
import Language.GraphQL.AST.Document (Name) import Language.GraphQL.AST.Document (Name)
import Language.GraphQL.Type.Definition import qualified Language.GraphQL.Type.Definition as Definition
-- | Single field of an 'InputObjectType'. -- | Single field of an 'InputObjectType'.
data InputField = InputField (Maybe Text) Type (Maybe Value) data InputField = InputField (Maybe Text) Type (Maybe Definition.Value)
-- | Input object type definition. -- | Input object type definition.
-- --
@ -39,31 +41,47 @@ data InputObjectType = InputObjectType
instance Eq InputObjectType where instance Eq InputObjectType where
(InputObjectType this _ _) == (InputObjectType that _ _) = this == that (InputObjectType this _ _) == (InputObjectType that _ _) = this == that
instance Show InputObjectType where
show (InputObjectType typeName _ _) = Text.unpack typeName
-- | These types may be used as input types for arguments and directives. -- | These types may be used as input types for arguments and directives.
-- --
-- GraphQL distinguishes between "wrapping" and "named" types. Each wrapping -- GraphQL distinguishes between "wrapping" and "named" types. Each wrapping
-- type can wrap other wrapping or named types. Wrapping types are lists and -- type can wrap other wrapping or named types. Wrapping types are lists and
-- Non-Null types (named types are nullable by default). -- Non-Null types (named types are nullable by default).
data Type data Type
= NamedScalarType ScalarType = NamedScalarType Definition.ScalarType
| NamedEnumType EnumType | NamedEnumType Definition.EnumType
| NamedInputObjectType InputObjectType | NamedInputObjectType InputObjectType
| ListType Type | ListType Type
| NonNullScalarType ScalarType | NonNullScalarType Definition.ScalarType
| NonNullEnumType EnumType | NonNullEnumType Definition.EnumType
| NonNullInputObjectType InputObjectType | NonNullInputObjectType InputObjectType
| NonNullListType Type | NonNullListType Type
deriving Eq deriving Eq
instance Show Type where
show (NamedScalarType scalarType) = show scalarType
show (NamedEnumType enumType) = show enumType
show (NamedInputObjectType inputObjectType) = show inputObjectType
show (ListType baseType) = concat ["[", show baseType, "]"]
show (NonNullScalarType scalarType) = '!' : show scalarType
show (NonNullEnumType enumType) = '!' : show enumType
show (NonNullInputObjectType inputObjectType) = '!' : show inputObjectType
show (NonNullListType baseType) = concat ["![", show baseType, "]"]
-- | Field argument definition. -- | Field argument definition.
data Argument = Argument (Maybe Text) Type (Maybe Value) data Argument = Argument (Maybe Text) Type (Maybe Definition.Value)
-- | Field argument definitions.
type Arguments = HashMap Name Argument
-- | Matches either 'NamedScalarType' or 'NonNullScalarType'. -- | Matches either 'NamedScalarType' or 'NonNullScalarType'.
pattern ScalarBaseType :: ScalarType -> Type pattern ScalarBaseType :: Definition.ScalarType -> Type
pattern ScalarBaseType scalarType <- (isScalarType -> Just scalarType) pattern ScalarBaseType scalarType <- (isScalarType -> Just scalarType)
-- | Matches either 'NamedEnumType' or 'NonNullEnumType'. -- | Matches either 'NamedEnumType' or 'NonNullEnumType'.
pattern EnumBaseType :: EnumType -> Type pattern EnumBaseType :: Definition.EnumType -> Type
pattern EnumBaseType enumType <- (isEnumType -> Just enumType) pattern EnumBaseType enumType <- (isEnumType -> Just enumType)
-- | Matches either 'NamedInputObjectType' or 'NonNullInputObjectType'. -- | Matches either 'NamedInputObjectType' or 'NonNullInputObjectType'.
@ -76,7 +94,7 @@ pattern ListBaseType listType <- (isListType -> Just listType)
{-# COMPLETE EnumBaseType, ListBaseType, InputObjectBaseType, ScalarBaseType #-} {-# COMPLETE EnumBaseType, ListBaseType, InputObjectBaseType, ScalarBaseType #-}
isScalarType :: Type -> Maybe ScalarType isScalarType :: Type -> Maybe Definition.ScalarType
isScalarType (NamedScalarType inputType) = Just inputType isScalarType (NamedScalarType inputType) = Just inputType
isScalarType (NonNullScalarType inputType) = Just inputType isScalarType (NonNullScalarType inputType) = Just inputType
isScalarType _ = Nothing isScalarType _ = Nothing
@ -86,7 +104,7 @@ isInputObjectType (NamedInputObjectType inputType) = Just inputType
isInputObjectType (NonNullInputObjectType inputType) = Just inputType isInputObjectType (NonNullInputObjectType inputType) = Just inputType
isInputObjectType _ = Nothing isInputObjectType _ = Nothing
isEnumType :: Type -> Maybe EnumType isEnumType :: Type -> Maybe Definition.EnumType
isEnumType (NamedEnumType inputType) = Just inputType isEnumType (NamedEnumType inputType) = Just inputType
isEnumType (NonNullEnumType inputType) = Just inputType isEnumType (NonNullEnumType inputType) = Just inputType
isEnumType _ = Nothing isEnumType _ = Nothing

View File

@ -3,23 +3,96 @@
obtain one at https://mozilla.org/MPL/2.0/. -} obtain one at https://mozilla.org/MPL/2.0/. -}
{-# LANGUAGE ExplicitForAll #-} {-# LANGUAGE ExplicitForAll #-}
{-# LANGUAGE LambdaCase #-}
module Language.GraphQL.Type.Internal module Language.GraphQL.Type.Internal
( AbstractType(..) ( AbstractType(..)
, CompositeType(..) , CompositeType(..)
, collectReferencedTypes , Directive(..)
, Directives
, Schema(..)
, Type(..)
, description
, directives
, doesFragmentTypeApply , doesFragmentTypeApply
, implementations
, instanceOf , instanceOf
, lookupCompositeField
, lookupInputType
, lookupTypeCondition , lookupTypeCondition
, lookupTypeField
, mutation
, outToComposite
, subscription
, query
, types
) where ) where
import Data.HashMap.Strict (HashMap) import Data.HashMap.Strict (HashMap)
import qualified Data.HashMap.Strict as HashMap import qualified Data.HashMap.Strict as HashMap
import Language.GraphQL.AST (Name) import Data.Text (Text)
import qualified Language.GraphQL.AST as Full
import Language.GraphQL.AST.DirectiveLocation (DirectiveLocation)
import qualified Language.GraphQL.Type.Definition as Definition import qualified Language.GraphQL.Type.Definition as Definition
import qualified Language.GraphQL.Type.In as In import qualified Language.GraphQL.Type.In as In
import qualified Language.GraphQL.Type.Out as Out import qualified Language.GraphQL.Type.Out as Out
import Language.GraphQL.Type.Schema
-- | These are all of the possible kinds of types.
data Type m
= ScalarType Definition.ScalarType
| EnumType Definition.EnumType
| ObjectType (Out.ObjectType m)
| InputObjectType In.InputObjectType
| InterfaceType (Out.InterfaceType m)
| UnionType (Out.UnionType m)
deriving Eq
-- | Directive definition.
data Directive = Directive (Maybe Text) [DirectiveLocation] In.Arguments
-- | Directive definitions.
type Directives = HashMap Full.Name Directive
-- | A Schema is created by supplying the root types of each type of operation,
-- query and mutation (optional). A schema definition is then supplied to the
-- validator and executor.
data Schema m = Schema
(Maybe Text) -- ^ Description.
(Out.ObjectType m) -- ^ Query.
(Maybe (Out.ObjectType m)) -- ^ Mutation.
(Maybe (Out.ObjectType m)) -- ^ Subscription.
Directives -- ^ Directives
(HashMap Full.Name (Type m)) -- ^ Types.
-- Interface implementations (used only for faster access).
(HashMap Full.Name [Type m])
-- | Schema description.
description :: forall m. Schema m -> Maybe Text
description (Schema description' _ _ _ _ _ _) = description'
-- | Schema query type.
query :: forall m. Schema m -> Out.ObjectType m
query (Schema _ query' _ _ _ _ _) = query'
-- | Schema mutation type.
mutation :: forall m. Schema m -> Maybe (Out.ObjectType m)
mutation (Schema _ _ mutation' _ _ _ _) = mutation'
-- | Schema subscription type.
subscription :: forall m. Schema m -> Maybe (Out.ObjectType m)
subscription (Schema _ _ _ subscription' _ _ _) = subscription'
-- | Schema directive definitions.
directives :: forall m. Schema m -> Directives
directives (Schema _ _ _ _ directives' _ _) = directives'
-- | Types referenced by the schema.
types :: forall m. Schema m -> HashMap Full.Name (Type m)
types (Schema _ _ _ _ _ types' _) = types'
-- | Interface implementations.
implementations :: forall m. Schema m -> HashMap Full.Name [Type m]
implementations (Schema _ _ _ _ _ _ implementations') = implementations'
-- | These types may describe the parent context of a selection set. -- | These types may describe the parent context of a selection set.
data CompositeType m data CompositeType m
@ -34,65 +107,6 @@ data AbstractType m
| AbstractInterfaceType (Out.InterfaceType m) | AbstractInterfaceType (Out.InterfaceType m)
deriving Eq deriving Eq
-- | Traverses the schema and finds all referenced types.
collectReferencedTypes :: forall m. Schema m -> HashMap Name (Type m)
collectReferencedTypes schema =
let queryTypes = traverseObjectType (query schema) HashMap.empty
in maybe queryTypes (`traverseObjectType` queryTypes) $ mutation schema
where
collect traverser typeName element foundTypes
| HashMap.member typeName foundTypes = foundTypes
| otherwise = traverser $ HashMap.insert typeName element foundTypes
visitFields (Out.Field _ outputType arguments) foundTypes
= traverseOutputType outputType
$ foldr visitArguments foundTypes arguments
visitArguments (In.Argument _ inputType _) = traverseInputType inputType
visitInputFields (In.InputField _ inputType _) = traverseInputType inputType
getField (Out.ValueResolver field _) = field
getField (Out.EventStreamResolver field _ _) = field
traverseInputType (In.InputObjectBaseType objectType) =
let (In.InputObjectType typeName _ inputFields) = objectType
element = InputObjectType objectType
traverser = flip (foldr visitInputFields) inputFields
in collect traverser typeName element
traverseInputType (In.ListBaseType listType) =
traverseInputType listType
traverseInputType (In.ScalarBaseType scalarType) =
let (Definition.ScalarType typeName _) = scalarType
in collect Prelude.id typeName (ScalarType scalarType)
traverseInputType (In.EnumBaseType enumType) =
let (Definition.EnumType typeName _ _) = enumType
in collect Prelude.id typeName (EnumType enumType)
traverseOutputType (Out.ObjectBaseType objectType) =
traverseObjectType objectType
traverseOutputType (Out.InterfaceBaseType interfaceType) =
traverseInterfaceType interfaceType
traverseOutputType (Out.UnionBaseType unionType) =
let (Out.UnionType typeName _ types) = unionType
traverser = flip (foldr traverseObjectType) types
in collect traverser typeName (UnionType unionType)
traverseOutputType (Out.ListBaseType listType) =
traverseOutputType listType
traverseOutputType (Out.ScalarBaseType scalarType) =
let (Definition.ScalarType typeName _) = scalarType
in collect Prelude.id typeName (ScalarType scalarType)
traverseOutputType (Out.EnumBaseType enumType) =
let (Definition.EnumType typeName _ _) = enumType
in collect Prelude.id typeName (EnumType enumType)
traverseObjectType objectType foundTypes =
let (Out.ObjectType typeName _ interfaces fields) = objectType
element = ObjectType objectType
traverser = polymorphicTraverser interfaces (getField <$> fields)
in collect traverser typeName element foundTypes
traverseInterfaceType interfaceType foundTypes =
let (Out.InterfaceType typeName _ interfaces fields) = interfaceType
element = InterfaceType interfaceType
traverser = polymorphicTraverser interfaces fields
in collect traverser typeName element foundTypes
polymorphicTraverser interfaces fields
= flip (foldr visitFields) fields
. flip (foldr traverseInterfaceType) interfaces
doesFragmentTypeApply :: forall m doesFragmentTypeApply :: forall m
. CompositeType m . CompositeType m
-> Out.ObjectType m -> Out.ObjectType m
@ -118,13 +132,69 @@ instanceOf objectType (AbstractUnionType unionType) =
go unionMemberType acc = acc || objectType == unionMemberType go unionMemberType acc = acc || objectType == unionMemberType
lookupTypeCondition :: forall m lookupTypeCondition :: forall m
. Name . Full.Name
-> HashMap Name (Type m) -> HashMap Full.Name (Type m)
-> Maybe (CompositeType m) -> Maybe (CompositeType m)
lookupTypeCondition type' types' = lookupTypeCondition type' types' =
case HashMap.lookup type' types' of case HashMap.lookup type' types' of
Just (ObjectType objectType) -> Just $ CompositeObjectType objectType Just (ObjectType objectType) ->
Just $ CompositeObjectType objectType
Just (UnionType unionType) -> Just $ CompositeUnionType unionType Just (UnionType unionType) -> Just $ CompositeUnionType unionType
Just (InterfaceType interfaceType) -> Just (InterfaceType interfaceType) ->
Just $ CompositeInterfaceType interfaceType Just $ CompositeInterfaceType interfaceType
_ -> Nothing _ -> Nothing
lookupInputType :: Full.Type -> HashMap Full.Name (Type m) -> Maybe In.Type
lookupInputType (Full.TypeNamed name) types' =
case HashMap.lookup name types' of
Just (ScalarType scalarType) ->
Just $ In.NamedScalarType scalarType
Just (EnumType enumType) ->
Just $ In.NamedEnumType enumType
Just (InputObjectType objectType) ->
Just $ In.NamedInputObjectType objectType
_ -> Nothing
lookupInputType (Full.TypeList list) types'
= In.ListType
<$> lookupInputType list types'
lookupInputType (Full.TypeNonNull (Full.NonNullTypeNamed nonNull)) types' =
case HashMap.lookup nonNull types' of
Just (ScalarType scalarType) ->
Just $ In.NonNullScalarType scalarType
Just (EnumType enumType) ->
Just $ In.NonNullEnumType enumType
Just (InputObjectType objectType) ->
Just $ In.NonNullInputObjectType objectType
_ -> Nothing
lookupInputType (Full.TypeNonNull (Full.NonNullTypeList nonNull)) types'
= In.NonNullListType
<$> lookupInputType nonNull types'
lookupTypeField :: forall a. Full.Name -> Out.Type a -> Maybe (Out.Field a)
lookupTypeField fieldName outputType =
outToComposite outputType >>= lookupCompositeField fieldName
lookupCompositeField :: forall a
. Full.Name
-> CompositeType a
-> Maybe (Out.Field a)
lookupCompositeField fieldName = \case
CompositeObjectType objectType -> objectChild objectType
CompositeInterfaceType interfaceType -> interfaceChild interfaceType
_ -> Nothing
where
objectChild (Out.ObjectType _ _ _ resolvers) =
resolverType <$> HashMap.lookup fieldName resolvers
interfaceChild (Out.InterfaceType _ _ _ fields) =
HashMap.lookup fieldName fields
resolverType (Out.ValueResolver objectField _) = objectField
resolverType (Out.EventStreamResolver objectField _ _) = objectField
outToComposite :: forall a. Out.Type a -> Maybe (CompositeType a)
outToComposite = \case
Out.ObjectBaseType objectType -> Just $ CompositeObjectType objectType
Out.InterfaceBaseType interfaceType ->
Just $ CompositeInterfaceType interfaceType
Out.UnionBaseType unionType -> Just $ CompositeUnionType unionType
Out.ListBaseType listType -> outToComposite listType
_ -> Nothing

View File

@ -38,6 +38,7 @@ import Data.HashMap.Strict (HashMap)
import qualified Data.HashMap.Strict as HashMap import qualified Data.HashMap.Strict as HashMap
import Data.Maybe (fromMaybe) import Data.Maybe (fromMaybe)
import Data.Text (Text) import Data.Text (Text)
import qualified Data.Text as Text
import Language.GraphQL.AST (Name) import Language.GraphQL.AST (Name)
import Language.GraphQL.Type.Definition import Language.GraphQL.Type.Definition
import qualified Language.GraphQL.Type.In as In import qualified Language.GraphQL.Type.In as In
@ -52,6 +53,9 @@ data ObjectType m = ObjectType
instance forall a. Eq (ObjectType a) where instance forall a. Eq (ObjectType a) where
(ObjectType this _ _ _) == (ObjectType that _ _ _) = this == that (ObjectType this _ _ _) == (ObjectType that _ _ _) = this == that
instance forall a. Show (ObjectType a) where
show (ObjectType typeName _ _ _) = Text.unpack typeName
-- | Interface Type Definition. -- | Interface Type Definition.
-- --
-- When a field can return one of a heterogeneous set of types, a Interface type -- When a field can return one of a heterogeneous set of types, a Interface type
@ -63,6 +67,9 @@ data InterfaceType m = InterfaceType
instance forall a. Eq (InterfaceType a) where instance forall a. Eq (InterfaceType a) where
(InterfaceType this _ _ _) == (InterfaceType that _ _ _) = this == that (InterfaceType this _ _ _) == (InterfaceType that _ _ _) = this == that
instance forall a. Show (InterfaceType a) where
show (InterfaceType typeName _ _ _) = Text.unpack typeName
-- | Union Type Definition. -- | Union Type Definition.
-- --
-- When a field can return one of a heterogeneous set of types, a Union type is -- When a field can return one of a heterogeneous set of types, a Union type is
@ -72,11 +79,14 @@ data UnionType m = UnionType Name (Maybe Text) [ObjectType m]
instance forall a. Eq (UnionType a) where instance forall a. Eq (UnionType a) where
(UnionType this _ _) == (UnionType that _ _) = this == that (UnionType this _ _) == (UnionType that _ _) = this == that
instance forall a. Show (UnionType a) where
show (UnionType typeName _ _) = Text.unpack typeName
-- | Output object field definition. -- | Output object field definition.
data Field m = Field data Field m = Field
(Maybe Text) -- ^ Description. (Maybe Text) -- ^ Description.
(Type m) -- ^ Field type. (Type m) -- ^ Field type.
(HashMap Name In.Argument) -- ^ Arguments. In.Arguments -- ^ Arguments.
-- | These types may be used as output types as the result of fields. -- | These types may be used as output types as the result of fields.
-- --
@ -98,6 +108,20 @@ data Type m
| NonNullListType (Type m) | NonNullListType (Type m)
deriving Eq deriving Eq
instance forall a. Show (Type a) where
show (NamedScalarType scalarType) = show scalarType
show (NamedEnumType enumType) = show enumType
show (NamedObjectType inputObjectType) = show inputObjectType
show (NamedInterfaceType interfaceType) = show interfaceType
show (NamedUnionType unionType) = show unionType
show (ListType baseType) = concat ["[", show baseType, "]"]
show (NonNullScalarType scalarType) = '!' : show scalarType
show (NonNullEnumType enumType) = '!' : show enumType
show (NonNullObjectType inputObjectType) = '!' : show inputObjectType
show (NonNullInterfaceType interfaceType) = '!' : show interfaceType
show (NonNullUnionType unionType) = '!' : show unionType
show (NonNullListType baseType) = concat ["![", show baseType, "]"]
-- | Matches either 'NamedScalarType' or 'NonNullScalarType'. -- | Matches either 'NamedScalarType' or 'NonNullScalarType'.
pattern ScalarBaseType :: forall m. ScalarType -> Type m pattern ScalarBaseType :: forall m. ScalarType -> Type m
pattern ScalarBaseType scalarType <- (isScalarType -> Just scalarType) pattern ScalarBaseType scalarType <- (isScalarType -> Just scalarType)

View File

@ -2,36 +2,208 @@
v. 2.0. If a copy of the MPL was not distributed with this file, You can v. 2.0. If a copy of the MPL was not distributed with this file, You can
obtain one at https://mozilla.org/MPL/2.0/. -} obtain one at https://mozilla.org/MPL/2.0/. -}
{-# LANGUAGE ExplicitForAll #-}
{-# LANGUAGE OverloadedStrings #-}
-- | This module provides a representation of a @GraphQL@ Schema in addition to -- | This module provides a representation of a @GraphQL@ Schema in addition to
-- functions for defining and manipulating schemas. -- functions for defining and manipulating schemas.
module Language.GraphQL.Type.Schema module Language.GraphQL.Type.Schema
( Schema(..) ( schema
, Type(..) , schemaWithTypes
, module Language.GraphQL.Type.Internal
) where ) where
import Data.HashMap.Strict (HashMap)
import qualified Data.HashMap.Strict as HashMap
import Data.Text (Text)
import Language.GraphQL.AST.DirectiveLocation (DirectiveLocation(..))
import qualified Language.GraphQL.AST.DirectiveLocation as DirectiveLocation
import qualified Language.GraphQL.AST as Full
import Language.GraphQL.Type.Internal
( Directive(..)
, Directives
, Schema
, Type(..)
, description
, directives
, implementations
, mutation
, subscription
, query
, types
)
import qualified Language.GraphQL.Type.Definition as Definition import qualified Language.GraphQL.Type.Definition as Definition
import qualified Language.GraphQL.Type.Internal as Internal
import qualified Language.GraphQL.Type.In as In import qualified Language.GraphQL.Type.In as In
import qualified Language.GraphQL.Type.Out as Out import qualified Language.GraphQL.Type.Out as Out
-- | These are all of the possible kinds of types. -- | Schema constructor.
data Type m
= ScalarType Definition.ScalarType
| EnumType Definition.EnumType
| ObjectType (Out.ObjectType m)
| InputObjectType In.InputObjectType
| InterfaceType (Out.InterfaceType m)
| UnionType (Out.UnionType m)
deriving Eq
-- | A Schema is created by supplying the root types of each type of operation,
-- query and mutation (optional). A schema definition is then supplied to the
-- validator and executor.
-- --
-- __Note:__ When the schema is constructed, by default only the types that -- __Note:__ When the schema is constructed, by default only the types that
-- are reachable by traversing the root types are included, other types must -- are reachable by traversing the root types are included, other types must
-- be explicitly referenced. -- be explicitly referenced using 'schemaWithTypes' instead.
data Schema m = Schema schema :: forall m
{ query :: Out.ObjectType m . Out.ObjectType m -- ^ Query type.
, mutation :: Maybe (Out.ObjectType m) -> Maybe (Out.ObjectType m) -- ^ Mutation type.
, subscription :: Maybe (Out.ObjectType m) -> Maybe (Out.ObjectType m) -- ^ Subscription type.
} -> Directives -- ^ Directive definitions.
-> Schema m -- ^ Schema.
schema queryRoot mutationRoot subscriptionRoot =
schemaWithTypes Nothing queryRoot mutationRoot subscriptionRoot mempty
-- | Constructs a complete schema, including user-defined types not referenced
-- in the schema directly (for example interface implementations).
schemaWithTypes :: forall m
. Maybe Text -- ^ Schema description
-> Out.ObjectType m -- ^ Query type.
-> Maybe (Out.ObjectType m) -- ^ Mutation type.
-> Maybe (Out.ObjectType m) -- ^ Subscription type.
-> [Type m] -- ^ Additional types.
-> Directives -- ^ Directive definitions.
-> Schema m -- ^ Schema.
schemaWithTypes description' queryRoot mutationRoot subscriptionRoot types' directiveDefinitions =
Internal.Schema description' queryRoot mutationRoot subscriptionRoot
allDirectives collectedTypes collectedImplementations
where
allTypes = foldr addTypeDefinition HashMap.empty types'
addTypeDefinition type'@(ScalarType (Definition.ScalarType typeName _)) accumulator =
HashMap.insert typeName type' accumulator
addTypeDefinition type'@(EnumType (Definition.EnumType typeName _ _)) accumulator =
HashMap.insert typeName type' accumulator
addTypeDefinition type'@(ObjectType (Out.ObjectType typeName _ _ _)) accumulator =
HashMap.insert typeName type' accumulator
addTypeDefinition type'@(InputObjectType (In.InputObjectType typeName _ _)) accumulator =
HashMap.insert typeName type' accumulator
addTypeDefinition type'@(InterfaceType (Out.InterfaceType typeName _ _ _)) accumulator =
HashMap.insert typeName type' accumulator
addTypeDefinition type'@(UnionType (Out.UnionType typeName _ _)) accumulator =
HashMap.insert typeName type' accumulator
collectedTypes = collectReferencedTypes queryRoot mutationRoot subscriptionRoot allTypes
collectedImplementations = collectImplementations collectedTypes
allDirectives = HashMap.union directiveDefinitions defaultDirectives
defaultDirectives = HashMap.fromList
[ ("skip", skipDirective)
, ("include", includeDirective)
, ("deprecated", deprecatedDirective)
]
includeDirective =
Directive includeDescription skipIncludeLocations includeArguments
includeArguments = HashMap.singleton "if"
$ In.Argument (Just "Included when true.") ifType Nothing
includeDescription = Just
"Directs the executor to include this field or fragment only when the \
\`if` argument is true."
skipDirective = Directive skipDescription skipIncludeLocations skipArguments
skipArguments = HashMap.singleton "if"
$ In.Argument (Just "skipped when true.") ifType Nothing
ifType = In.NonNullScalarType Definition.boolean
skipDescription = Just
"Directs the executor to skip this field or fragment when the `if` \
\argument is true."
skipIncludeLocations =
[ ExecutableDirectiveLocation DirectiveLocation.Field
, ExecutableDirectiveLocation DirectiveLocation.FragmentSpread
, ExecutableDirectiveLocation DirectiveLocation.InlineFragment
]
deprecatedDirective =
Directive deprecatedDescription deprecatedLocations deprecatedArguments
reasonDescription = Just
"Explains why this element was deprecated, usually also including a \
\suggestion for how to access supported similar data. Formatted using \
\the Markdown syntax, as specified by \
\[CommonMark](https://commonmark.org/).'"
deprecatedArguments = HashMap.singleton "reason"
$ In.Argument reasonDescription reasonType
$ Just "No longer supported"
reasonType = In.NamedScalarType Definition.string
deprecatedDescription = Just
"Marks an element of a GraphQL schema as no longer supported."
deprecatedLocations =
[ TypeSystemDirectiveLocation DirectiveLocation.FieldDefinition
, TypeSystemDirectiveLocation DirectiveLocation.ArgumentDefinition
, TypeSystemDirectiveLocation DirectiveLocation.InputFieldDefinition
, TypeSystemDirectiveLocation DirectiveLocation.EnumValue
]
-- | Traverses the schema and finds all referenced types.
collectReferencedTypes :: forall m
. Out.ObjectType m
-> Maybe (Out.ObjectType m)
-> Maybe (Out.ObjectType m)
-> HashMap Full.Name (Type m)
-> HashMap Full.Name (Type m)
collectReferencedTypes queryRoot mutationRoot subscriptionRoot extraTypes =
let queryTypes = traverseObjectType queryRoot extraTypes
mutationTypes = maybe queryTypes (`traverseObjectType` queryTypes)
mutationRoot
in maybe mutationTypes (`traverseObjectType` mutationTypes) subscriptionRoot
where
collect traverser typeName element foundTypes
| HashMap.member typeName foundTypes = foundTypes
| otherwise = traverser $ HashMap.insert typeName element foundTypes
visitFields (Out.Field _ outputType arguments) foundTypes
= traverseOutputType outputType
$ foldr visitArguments foundTypes arguments
visitArguments (In.Argument _ inputType _) = traverseInputType inputType
visitInputFields (In.InputField _ inputType _) = traverseInputType inputType
getField (Out.ValueResolver field _) = field
getField (Out.EventStreamResolver field _ _) = field
traverseInputType (In.InputObjectBaseType objectType) =
let In.InputObjectType typeName _ inputFields = objectType
element = InputObjectType objectType
traverser = flip (foldr visitInputFields) inputFields
in collect traverser typeName element
traverseInputType (In.ListBaseType listType) =
traverseInputType listType
traverseInputType (In.ScalarBaseType scalarType) =
let Definition.ScalarType typeName _ = scalarType
in collect Prelude.id typeName (ScalarType scalarType)
traverseInputType (In.EnumBaseType enumType) =
let Definition.EnumType typeName _ _ = enumType
in collect Prelude.id typeName (EnumType enumType)
traverseOutputType (Out.ObjectBaseType objectType) =
traverseObjectType objectType
traverseOutputType (Out.InterfaceBaseType interfaceType) =
traverseInterfaceType interfaceType
traverseOutputType (Out.UnionBaseType unionType) =
let Out.UnionType typeName _ types' = unionType
traverser = flip (foldr traverseObjectType) types'
in collect traverser typeName (UnionType unionType)
traverseOutputType (Out.ListBaseType listType) =
traverseOutputType listType
traverseOutputType (Out.ScalarBaseType scalarType) =
let Definition.ScalarType typeName _ = scalarType
in collect Prelude.id typeName (ScalarType scalarType)
traverseOutputType (Out.EnumBaseType enumType) =
let Definition.EnumType typeName _ _ = enumType
in collect Prelude.id typeName (EnumType enumType)
traverseObjectType objectType foundTypes =
let Out.ObjectType typeName _ interfaces fields = objectType
element = ObjectType objectType
traverser = polymorphicTraverser interfaces (getField <$> fields)
in collect traverser typeName element foundTypes
traverseInterfaceType interfaceType foundTypes =
let Out.InterfaceType typeName _ interfaces fields = interfaceType
element = InterfaceType interfaceType
traverser = polymorphicTraverser interfaces fields
in collect traverser typeName element foundTypes
polymorphicTraverser interfaces fields
= flip (foldr visitFields) fields
. flip (foldr traverseInterfaceType) interfaces
-- | Looks for objects and interfaces under the schema types and collects the
-- interfaces they implement.
collectImplementations :: forall m
. HashMap Full.Name (Type m)
-> HashMap Full.Name [Type m]
collectImplementations = HashMap.foldr go HashMap.empty
where
go implementation@(InterfaceType interfaceType) accumulator =
let Out.InterfaceType _ _ interfaces _ = interfaceType
in foldr (add implementation) accumulator interfaces
go implementation@(ObjectType objectType) accumulator =
let Out.ObjectType _ _ interfaces _ = objectType
in foldr (add implementation) accumulator interfaces
go _ accumulator = accumulator
add implementation (Out.InterfaceType typeName _ _ _) accumulator =
HashMap.insertWith (++) typeName [implementation] accumulator

View File

@ -2,80 +2,484 @@
v. 2.0. If a copy of the MPL was not distributed with this file, You can v. 2.0. If a copy of the MPL was not distributed with this file, You can
obtain one at https://mozilla.org/MPL/2.0/. -} obtain one at https://mozilla.org/MPL/2.0/. -}
{-# LANGUAGE ExplicitForAll #-}
{-# LANGUAGE LambdaCase #-} {-# LANGUAGE LambdaCase #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
-- | GraphQL validator. -- | GraphQL validator.
module Language.GraphQL.Validate module Language.GraphQL.Validate
( Error(..) ( Validation.Error(..)
, Path(..)
, document , document
, module Language.GraphQL.Validate.Rules , module Language.GraphQL.Validate.Rules
) where ) where
import Control.Monad (foldM) import Control.Monad (join)
import Control.Monad.Trans.Reader (Reader, asks, mapReaderT, runReader) import Control.Monad.Trans.Class (MonadTrans(..))
import Data.Foldable (foldrM) import Control.Monad.Trans.Reader (runReaderT)
import Data.Foldable (toList)
import Data.HashMap.Strict (HashMap)
import qualified Data.HashMap.Strict as HashMap
import Data.Sequence (Seq(..), (><), (|>)) import Data.Sequence (Seq(..), (><), (|>))
import qualified Data.Sequence as Seq import qualified Data.Sequence as Seq
import Language.GraphQL.AST.Document import Language.GraphQL.AST.DirectiveLocation (DirectiveLocation(..))
import Language.GraphQL.Type.Internal import qualified Language.GraphQL.AST.DirectiveLocation as DirectiveLocation
import Language.GraphQL.Type.Schema (Schema(..)) import qualified Language.GraphQL.AST.Document as Full
import qualified Language.GraphQL.Type.Internal as Type
import qualified Language.GraphQL.Type.In as In
import qualified Language.GraphQL.Type.Out as Out
import Language.GraphQL.Type.Schema (Schema)
import qualified Language.GraphQL.Type.Schema as Schema
import Language.GraphQL.Validate.Rules import Language.GraphQL.Validate.Rules
import Language.GraphQL.Validate.Validation import Language.GraphQL.Validate.Validation (Validation(Validation))
import qualified Language.GraphQL.Validate.Validation as Validation
type ValidateT m = Reader (Validation m) (Seq Error) type ApplySelectionRule m a
= HashMap Full.Name (Schema.Type m)
-> Validation.Rule m
-> Maybe (Out.Type m)
-> a
-> Seq (Validation.RuleT m)
type ApplyRule m a = Validation.Rule m -> a -> Seq (Validation.RuleT m)
-- | Validates a document and returns a list of found errors. If the returned -- | Validates a document and returns a list of found errors. If the returned
-- list is empty, the document is valid. -- list is empty, the document is valid.
document :: forall m. Schema m -> [Rule m] -> Document -> Seq Error document :: forall m
. Schema m
-> [Validation.Rule m]
-> Full.Document
-> Seq Validation.Error
document schema' rules' document' = document schema' rules' document' =
runReader (foldrM go Seq.empty document') context runReaderT reader context
where where
context = Validation context = Validation
{ ast = document' { Validation.ast = document'
, schema = schema' , Validation.schema = schema'
, types = collectReferencedTypes schema'
, rules = rules'
} }
go definition' accumulator = (accumulator ><) <$> definition definition' reader = do
rule' <- lift $ Seq.fromList rules'
join $ lift $ foldr (definition rule' context) Seq.empty document'
definition :: forall m. Definition -> ValidateT m definition :: Validation.Rule m
definition = \case -> Validation m
definition'@(ExecutableDefinition executableDefinition') -> do -> Full.Definition
applied <- applyRules definition' -> Seq (Validation.RuleT m)
children <- executableDefinition executableDefinition' -> Seq (Validation.RuleT m)
pure $ children >< applied definition (Validation.DefinitionRule rule) _ definition' accumulator =
definition' -> applyRules definition' accumulator |> rule definition'
definition rule context (Full.ExecutableDefinition definition') accumulator =
accumulator >< executableDefinition rule context definition'
definition rule context (Full.TypeSystemDefinition typeSystemDefinition' _) accumulator =
accumulator >< typeSystemDefinition context rule typeSystemDefinition'
definition rule context (Full.TypeSystemExtension extension _) accumulator =
accumulator >< typeSystemExtension context rule extension
typeSystemExtension :: forall m
. Validation m
-> ApplyRule m Full.TypeSystemExtension
typeSystemExtension context rule = \case
Full.SchemaExtension extension -> schemaExtension context rule extension
Full.TypeExtension extension -> typeExtension context rule extension
typeExtension :: forall m. Validation m -> ApplyRule m Full.TypeExtension
typeExtension context rule = \case
Full.ScalarTypeExtension _ directives' ->
directives context rule scalarLocation directives'
Full.ObjectTypeFieldsDefinitionExtension _ _ directives' fields
-> directives context rule objectLocation directives'
>< foldMap (fieldDefinition context rule) fields
Full.ObjectTypeDirectivesExtension _ _ directives' ->
directives context rule objectLocation directives'
Full.ObjectTypeImplementsInterfacesExtension _ _ -> mempty
Full.InterfaceTypeFieldsDefinitionExtension _ directives' fields
-> directives context rule interfaceLocation directives'
>< foldMap (fieldDefinition context rule) fields
Full.InterfaceTypeDirectivesExtension _ directives' ->
directives context rule interfaceLocation directives'
Full.UnionTypeUnionMemberTypesExtension _ directives' _ ->
directives context rule unionLocation directives'
Full.UnionTypeDirectivesExtension _ directives' ->
directives context rule unionLocation directives'
Full.EnumTypeEnumValuesDefinitionExtension _ directives' values
-> directives context rule enumLocation directives'
>< foldMap (enumValueDefinition context rule) values
Full.EnumTypeDirectivesExtension _ directives' ->
directives context rule enumLocation directives'
Full.InputObjectTypeInputFieldsDefinitionExtension _ directives' fields
-> directives context rule inputObjectLocation directives'
>< foldMap forEachInputFieldDefinition fields
Full.InputObjectTypeDirectivesExtension _ directives' ->
directives context rule inputObjectLocation directives'
where where
applyRules definition' = forEachInputFieldDefinition =
asks rules >>= foldM (ruleFilter definition') Seq.empty inputValueDefinition context rule inputFieldDefinitionLocation
ruleFilter definition' accumulator (DefinitionRule rule) =
mapReaderT (runRule accumulator) $ rule definition'
ruleFilter _ accumulator _ = pure accumulator
runRule :: Applicative f => Seq Error -> Maybe Error -> f (Seq Error) schemaExtension :: forall m. Validation m -> ApplyRule m Full.SchemaExtension
runRule accumulator (Just error') = pure $ accumulator |> error' schemaExtension context rule = \case
runRule accumulator Nothing = pure accumulator Full.SchemaOperationExtension directives' _ ->
directives context rule schemaLocation directives'
Full.SchemaDirectivesExtension directives' ->
directives context rule schemaLocation directives'
executableDefinition :: forall m. ExecutableDefinition -> ValidateT m schemaLocation :: DirectiveLocation
executableDefinition (DefinitionOperation definition') = schemaLocation = TypeSystemDirectiveLocation DirectiveLocation.Schema
operationDefinition definition'
executableDefinition (DefinitionFragment definition') =
fragmentDefinition definition'
operationDefinition :: forall m. OperationDefinition -> ValidateT m interfaceLocation :: DirectiveLocation
operationDefinition operation = interfaceLocation = TypeSystemDirectiveLocation DirectiveLocation.Interface
asks rules >>= foldM ruleFilter Seq.empty
objectLocation :: DirectiveLocation
objectLocation = TypeSystemDirectiveLocation DirectiveLocation.Object
unionLocation :: DirectiveLocation
unionLocation = TypeSystemDirectiveLocation DirectiveLocation.Union
enumLocation :: DirectiveLocation
enumLocation = TypeSystemDirectiveLocation DirectiveLocation.Enum
inputObjectLocation :: DirectiveLocation
inputObjectLocation = TypeSystemDirectiveLocation DirectiveLocation.InputObject
scalarLocation :: DirectiveLocation
scalarLocation = TypeSystemDirectiveLocation DirectiveLocation.Scalar
enumValueLocation :: DirectiveLocation
enumValueLocation = TypeSystemDirectiveLocation DirectiveLocation.EnumValue
fieldDefinitionLocation :: DirectiveLocation
fieldDefinitionLocation =
TypeSystemDirectiveLocation DirectiveLocation.FieldDefinition
inputFieldDefinitionLocation :: DirectiveLocation
inputFieldDefinitionLocation =
TypeSystemDirectiveLocation DirectiveLocation.InputFieldDefinition
argumentDefinitionLocation :: DirectiveLocation
argumentDefinitionLocation =
TypeSystemDirectiveLocation DirectiveLocation.ArgumentDefinition
queryLocation :: DirectiveLocation
queryLocation = ExecutableDirectiveLocation DirectiveLocation.Query
mutationLocation :: DirectiveLocation
mutationLocation = ExecutableDirectiveLocation DirectiveLocation.Mutation
subscriptionLocation :: DirectiveLocation
subscriptionLocation =
ExecutableDirectiveLocation DirectiveLocation.Subscription
fieldLocation :: DirectiveLocation
fieldLocation = ExecutableDirectiveLocation DirectiveLocation.Field
fragmentDefinitionLocation :: DirectiveLocation
fragmentDefinitionLocation =
ExecutableDirectiveLocation DirectiveLocation.FragmentDefinition
fragmentSpreadLocation :: DirectiveLocation
fragmentSpreadLocation =
ExecutableDirectiveLocation DirectiveLocation.FragmentSpread
inlineFragmentLocation :: DirectiveLocation
inlineFragmentLocation =
ExecutableDirectiveLocation DirectiveLocation.InlineFragment
executableDefinition :: forall m
. Validation.Rule m
-> Validation m
-> Full.ExecutableDefinition
-> Seq (Validation.RuleT m)
executableDefinition rule context (Full.DefinitionOperation operation) =
operationDefinition rule context operation
executableDefinition rule context (Full.DefinitionFragment fragment) =
fragmentDefinition rule context fragment
typeSystemDefinition :: forall m
. Validation m
-> ApplyRule m Full.TypeSystemDefinition
typeSystemDefinition context rule = \case
Full.SchemaDefinition directives' _ ->
directives context rule schemaLocation directives'
Full.TypeDefinition typeDefinition' ->
typeDefinition context rule typeDefinition'
Full.DirectiveDefinition _ _ arguments' _ ->
argumentsDefinition context rule arguments'
typeDefinition :: forall m. Validation m -> ApplyRule m Full.TypeDefinition
typeDefinition context rule = \case
Full.ScalarTypeDefinition _ _ directives' ->
directives context rule scalarLocation directives'
Full.ObjectTypeDefinition _ _ _ directives' fields
-> directives context rule objectLocation directives'
>< foldMap (fieldDefinition context rule) fields
Full.InterfaceTypeDefinition _ _ directives' fields
-> directives context rule interfaceLocation directives'
>< foldMap (fieldDefinition context rule) fields
Full.UnionTypeDefinition _ _ directives' _ ->
directives context rule unionLocation directives'
Full.EnumTypeDefinition _ _ directives' values
-> directives context rule enumLocation directives'
>< foldMap (enumValueDefinition context rule) values
Full.InputObjectTypeDefinition _ _ directives' fields
-> directives context rule inputObjectLocation directives'
<> foldMap forEachInputFieldDefinition fields
where where
ruleFilter accumulator (OperationDefinitionRule rule) = forEachInputFieldDefinition =
mapReaderT (runRule accumulator) $ rule operation inputValueDefinition context rule inputFieldDefinitionLocation
ruleFilter accumulator _ = pure accumulator
fragmentDefinition :: forall m. FragmentDefinition -> ValidateT m enumValueDefinition :: forall m
fragmentDefinition fragment = . Validation m
asks rules >>= foldM ruleFilter Seq.empty -> ApplyRule m Full.EnumValueDefinition
enumValueDefinition context rule (Full.EnumValueDefinition _ _ directives') =
directives context rule enumValueLocation directives'
fieldDefinition :: forall m. Validation m -> ApplyRule m Full.FieldDefinition
fieldDefinition context rule (Full.FieldDefinition _ _ arguments' _ directives')
= directives context rule fieldDefinitionLocation directives'
>< argumentsDefinition context rule arguments'
argumentsDefinition :: forall m
. Validation m
-> ApplyRule m Full.ArgumentsDefinition
argumentsDefinition context rule (Full.ArgumentsDefinition definitions) =
foldMap forEachArgument definitions
where where
ruleFilter accumulator (FragmentDefinitionRule rule) = forEachArgument =
mapReaderT (runRule accumulator) $ rule fragment inputValueDefinition context rule argumentDefinitionLocation
ruleFilter accumulator _ = pure accumulator
inputValueDefinition :: forall m
. Validation m
-> Validation.Rule m
-> DirectiveLocation
-> Full.InputValueDefinition
-> Seq (Validation.RuleT m)
inputValueDefinition context rule directiveLocation definition' =
let Full.InputValueDefinition _ _ _ _ directives' = definition'
in directives context rule directiveLocation directives'
operationDefinition :: forall m
. Validation.Rule m
-> Validation m
-> Full.OperationDefinition
-> Seq (Validation.RuleT m)
operationDefinition rule context operation
| Validation.OperationDefinitionRule operationRule <- rule =
pure $ operationRule operation
| Validation.VariablesRule variablesRule <- rule
, Full.OperationDefinition _ _ variables _ _ _ <- operation =
foldMap (variableDefinition context rule) variables |> variablesRule variables
| Full.SelectionSet selections _ <- operation =
selectionSet context types' rule queryRoot selections
| Full.OperationDefinition Full.Query _ _ directives' selections _ <- operation
= selectionSet context types' rule queryRoot selections
>< directives context rule queryLocation directives'
| Full.OperationDefinition Full.Mutation _ _ directives' selections _ <- operation =
let root = Out.NamedObjectType <$> Schema.mutation schema'
in selectionSet context types' rule root selections
>< directives context rule mutationLocation directives'
| Full.OperationDefinition Full.Subscription _ _ directives' selections _ <- operation =
let root = Out.NamedObjectType <$> Schema.subscription schema'
in selectionSet context types' rule root selections
>< directives context rule subscriptionLocation directives'
where
schema' = Validation.schema context
queryRoot = Just $ Out.NamedObjectType $ Schema.query schema'
types' = Schema.types schema'
typeToOut :: forall m. Schema.Type m -> Maybe (Out.Type m)
typeToOut (Schema.ObjectType objectType) =
Just $ Out.NamedObjectType objectType
typeToOut (Schema.InterfaceType interfaceType) =
Just $ Out.NamedInterfaceType interfaceType
typeToOut (Schema.UnionType unionType) = Just $ Out.NamedUnionType unionType
typeToOut (Schema.EnumType enumType) = Just $ Out.NamedEnumType enumType
typeToOut (Schema.ScalarType scalarType) = Just $ Out.NamedScalarType scalarType
typeToOut _ = Nothing
variableDefinition :: forall m
. Validation m
-> ApplyRule m Full.VariableDefinition
variableDefinition context rule (Full.VariableDefinition _ typeName value' _)
| Just defaultValue' <- value'
, types <- Schema.types $ Validation.schema context
, variableType <- Type.lookupInputType typeName types =
constValue rule variableType defaultValue'
variableDefinition _ _ _ = mempty
constValue :: forall m
. Validation.Rule m
-> Maybe In.Type
-> Full.Node Full.ConstValue
-> Seq (Validation.RuleT m)
constValue (Validation.ValueRule _ rule) valueType = go valueType
where
go inputObjectType value'@(Full.Node (Full.ConstObject fields) _)
= foldMap (forEach inputObjectType) (Seq.fromList fields)
|> rule inputObjectType value'
go anotherValue value' = pure $ rule anotherValue value'
forEach inputObjectType Full.ObjectField{value = value', ..} =
go (valueTypeByName name inputObjectType) value'
constValue _ _ = const mempty
inputFieldType :: In.InputField -> In.Type
inputFieldType (In.InputField _ inputFieldType' _) = inputFieldType'
valueTypeByName :: Full.Name -> Maybe In.Type -> Maybe In.Type
valueTypeByName fieldName (Just( In.InputObjectBaseType inputObjectType)) =
let In.InputObjectType _ _ fieldTypes = inputObjectType
in inputFieldType <$> HashMap.lookup fieldName fieldTypes
valueTypeByName _ _ = Nothing
fragmentDefinition :: forall m
. Validation.Rule m
-> Validation m
-> Full.FragmentDefinition
-> Seq (Validation.RuleT m)
fragmentDefinition (Validation.FragmentDefinitionRule rule) _ definition' =
pure $ rule definition'
fragmentDefinition rule context definition'
| Full.FragmentDefinition _ typeCondition directives' selections _ <- definition'
, Validation.FragmentRule definitionRule _ <- rule
= applyToChildren typeCondition directives' selections
|> definitionRule definition'
| Full.FragmentDefinition _ typeCondition directives' selections _ <- definition'
= applyToChildren typeCondition directives' selections
where
types' = Schema.types $ Validation.schema context
applyToChildren typeCondition directives' selections
= selectionSet context types' rule (lookupType' typeCondition) selections
>< directives context rule fragmentDefinitionLocation directives'
lookupType' = flip lookupType types'
lookupType :: forall m
. Full.TypeCondition
-> HashMap Full.Name (Schema.Type m)
-> Maybe (Out.Type m)
lookupType typeCondition types' = HashMap.lookup typeCondition types'
>>= typeToOut
selectionSet :: Traversable t
=> forall m
. Validation m
-> ApplySelectionRule m (t Full.Selection)
selectionSet context types' rule = foldMap . selection context types' rule
selection :: forall m. Validation m -> ApplySelectionRule m Full.Selection
selection context types' rule objectType selection'
| Validation.SelectionRule selectionRule <- rule =
applyToChildren |> selectionRule objectType selection'
| otherwise = applyToChildren
where
applyToChildren =
case selection' of
Full.FieldSelection field' ->
field context types' rule objectType field'
Full.InlineFragmentSelection inlineFragment' ->
inlineFragment context types' rule objectType inlineFragment'
Full.FragmentSpreadSelection fragmentSpread' ->
fragmentSpread context rule fragmentSpread'
field :: forall m. Validation m -> ApplySelectionRule m Full.Field
field context types' rule objectType field' = go field'
where
go (Full.Field _ fieldName _ _ _ _)
| Validation.FieldRule fieldRule <- rule =
applyToChildren fieldName |> fieldRule objectType field'
| Validation.ArgumentsRule argumentsRule _ <- rule =
applyToChildren fieldName |> argumentsRule objectType field'
| otherwise = applyToChildren fieldName
typeFieldType (Out.Field _ type' _) = type'
typeFieldArguments (Out.Field _ _ argumentTypes) = argumentTypes
applyToChildren fieldName =
let Full.Field _ _ arguments' directives' selections _ = field'
typeField = objectType >>= Type.lookupTypeField fieldName
argumentTypes = maybe mempty typeFieldArguments typeField
in selectionSet context types' rule (typeFieldType <$> typeField) selections
>< directives context rule fieldLocation directives'
>< arguments rule argumentTypes arguments'
arguments :: forall m
. Validation.Rule m
-> In.Arguments
-> [Full.Argument]
-> Seq (Validation.RuleT m)
arguments rule argumentTypes = foldMap forEach . Seq.fromList
where
forEach argument'@(Full.Argument argumentName _ _) =
let argumentType = HashMap.lookup argumentName argumentTypes
in argument rule argumentType argument'
argument :: forall m
. Validation.Rule m
-> Maybe In.Argument
-> Full.Argument
-> Seq (Validation.RuleT m)
argument rule argumentType (Full.Argument _ value' _) =
value rule (valueType <$> argumentType) value'
where
valueType (In.Argument _ valueType' _) = valueType'
value :: forall m
. Validation.Rule m
-> Maybe In.Type
-> Full.Node Full.Value
-> Seq (Validation.RuleT m)
value (Validation.ValueRule rule _) valueType = go valueType
where
go inputObjectType value'@(Full.Node (Full.Object fields) _)
= foldMap (forEach inputObjectType) (Seq.fromList fields)
|> rule inputObjectType value'
go anotherValue value' = pure $ rule anotherValue value'
forEach inputObjectType Full.ObjectField{value = value', ..} =
go (valueTypeByName name inputObjectType) value'
value _ _ = const mempty
inlineFragment :: forall m
. Validation m
-> ApplySelectionRule m Full.InlineFragment
inlineFragment context types' rule objectType inlineFragment' =
go inlineFragment'
where
go (Full.InlineFragment optionalType directives' selections _)
| Validation.FragmentRule _ fragmentRule <- rule
= applyToChildren (refineTarget optionalType) directives' selections
|> fragmentRule inlineFragment'
| otherwise = applyToChildren (refineTarget optionalType) directives' selections
refineTarget (Just typeCondition) = lookupType typeCondition types'
refineTarget Nothing = objectType
applyToChildren objectType' directives' selections
= selectionSet context types' rule objectType' selections
>< directives context rule inlineFragmentLocation directives'
fragmentSpread :: forall m. Validation m -> ApplyRule m Full.FragmentSpread
fragmentSpread context rule fragmentSpread'@(Full.FragmentSpread _ directives' _)
| Validation.FragmentSpreadRule fragmentRule <- rule =
applyToChildren |> fragmentRule fragmentSpread'
| otherwise = applyToChildren
where
applyToChildren = directives context rule fragmentSpreadLocation directives'
directives :: Traversable t
=> forall m
. Validation m
-> Validation.Rule m
-> DirectiveLocation
-> t Full.Directive
-> Seq (Validation.RuleT m)
directives context rule directiveLocation directives'
| Validation.DirectivesRule directivesRule <- rule =
applyToChildren |> directivesRule directiveLocation directiveList
| otherwise = applyToChildren
where
directiveList = toList directives'
applyToChildren = foldMap (directive context rule) directiveList
directive :: forall m. Validation m -> ApplyRule m Full.Directive
directive _ (Validation.ArgumentsRule _ argumentsRule) directive' =
pure $ argumentsRule directive'
directive context rule (Full.Directive directiveName arguments' _) =
let argumentTypes = maybe HashMap.empty directiveArguments
$ HashMap.lookup directiveName
$ Schema.directives
$ Validation.schema context
in arguments rule argumentTypes arguments'
where
directiveArguments (Schema.Directive _ _ argumentTypes) = argumentTypes

File diff suppressed because it is too large Load Diff

View File

@ -5,41 +5,29 @@
-- | Definitions used by the validation rules and the validator itself. -- | Definitions used by the validation rules and the validator itself.
module Language.GraphQL.Validate.Validation module Language.GraphQL.Validate.Validation
( Error(..) ( Error(..)
, Path(..)
, Rule(..) , Rule(..)
, RuleT , RuleT
, Validation(..) , Validation(..)
) where ) where
import Control.Monad.Trans.Reader (ReaderT(..)) import Control.Monad.Trans.Reader (ReaderT)
import Data.HashMap.Strict (HashMap) import Data.Sequence (Seq)
import Data.Text (Text) import Language.GraphQL.AST.DirectiveLocation (DirectiveLocation(..))
import Language.GraphQL.AST.Document import Language.GraphQL.AST.Document
import qualified Language.GraphQL.Type.In as In
import qualified Language.GraphQL.Type.Out as Out
import Language.GraphQL.Type.Schema (Schema) import Language.GraphQL.Type.Schema (Schema)
import qualified Language.GraphQL.Type.Schema as Schema
-- | If an error can be associated to a particular field in the GraphQL result,
-- it must contain an entry with the key path that details the path of the
-- response field which experienced the error. This allows clients to identify
-- whether a null result is intentional or caused by a runtime error.
data Path
= Segment Text -- ^ Field name.
| Index Int -- ^ List index if a field returned a list.
deriving (Eq, Show)
-- | Validation error. -- | Validation error.
data Error = Error data Error = Error
{ message :: String { message :: String
, locations :: [Location] , locations :: [Location]
, path :: [Path]
} deriving (Eq, Show) } deriving (Eq, Show)
-- | Validation rule context. -- | Validation rule context.
data Validation m = Validation data Validation m = Validation
{ ast :: Document { ast :: Document
, schema :: Schema m , schema :: Schema m
, types :: HashMap Name (Schema.Type m)
, rules :: [Rule m]
} }
-- | 'Rule' assigns a function to each AST node that can be validated. If the -- | 'Rule' assigns a function to each AST node that can be validated. If the
@ -49,6 +37,14 @@ data Rule m
= DefinitionRule (Definition -> RuleT m) = DefinitionRule (Definition -> RuleT m)
| OperationDefinitionRule (OperationDefinition -> RuleT m) | OperationDefinitionRule (OperationDefinition -> RuleT m)
| FragmentDefinitionRule (FragmentDefinition -> RuleT m) | FragmentDefinitionRule (FragmentDefinition -> RuleT m)
| SelectionRule (Maybe (Out.Type m) -> Selection -> RuleT m)
| FragmentRule (FragmentDefinition -> RuleT m) (InlineFragment -> RuleT m)
| FragmentSpreadRule (FragmentSpread -> RuleT m)
| FieldRule (Maybe (Out.Type m) -> Field -> RuleT m)
| ArgumentsRule (Maybe (Out.Type m) -> Field -> RuleT m) (Directive -> RuleT m)
| DirectivesRule (DirectiveLocation -> [Directive] -> RuleT m)
| VariablesRule ([VariableDefinition] -> RuleT m)
| ValueRule (Maybe In.Type -> Node Value -> RuleT m) (Maybe In.Type -> Node ConstValue -> RuleT m)
-- | Monad transformer used by the rules. -- | Monad transformer used by the rules.
type RuleT m = ReaderT (Validation m) Maybe Error type RuleT m = ReaderT (Validation m) Seq Error

View File

@ -1,9 +0,0 @@
resolver: lts-16.11
packages:
- .
extra-deps: []
flags: {}
pvp-bounds: both

View File

@ -0,0 +1,20 @@
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE OverloadedStrings #-}
module Language.GraphQL.AST.DocumentSpec
( spec
) where
import Language.GraphQL.AST.Document
import Test.Hspec (Spec, describe, it, shouldBe)
spec :: Spec
spec = do
describe "Document" $ do
it "shows objects" $
let zero = Location 0 0
object = ConstObject
[ ObjectField "field1" (Node (ConstFloat 1.2) zero) zero
, ObjectField "field2" (Node ConstNull zero) zero
]
expected = "{ field1: 1.2, field2: null }"
in show object `shouldBe` expected

View File

@ -4,7 +4,7 @@ module Language.GraphQL.AST.EncoderSpec
( spec ( spec
) where ) where
import Language.GraphQL.AST import qualified Language.GraphQL.AST.Document as Full
import Language.GraphQL.AST.Encoder import Language.GraphQL.AST.Encoder
import Test.Hspec (Spec, context, describe, it, shouldBe, shouldStartWith, shouldEndWith, shouldNotContain) import Test.Hspec (Spec, context, describe, it, shouldBe, shouldStartWith, shouldEndWith, shouldNotContain)
import Test.QuickCheck (choose, oneof, forAll) import Test.QuickCheck (choose, oneof, forAll)
@ -15,52 +15,52 @@ spec :: Spec
spec = do spec = do
describe "value" $ do describe "value" $ do
context "null value" $ do context "null value" $ do
let testNull formatter = value formatter Null `shouldBe` "null" let testNull formatter = value formatter Full.Null `shouldBe` "null"
it "minified" $ testNull minified it "minified" $ testNull minified
it "pretty" $ testNull pretty it "pretty" $ testNull pretty
context "minified" $ do context "minified" $ do
it "escapes \\" $ it "escapes \\" $
value minified (String "\\") `shouldBe` "\"\\\\\"" value minified (Full.String "\\") `shouldBe` "\"\\\\\""
it "escapes double quotes" $ it "escapes double quotes" $
value minified (String "\"") `shouldBe` "\"\\\"\"" value minified (Full.String "\"") `shouldBe` "\"\\\"\""
it "escapes \\f" $ it "escapes \\f" $
value minified (String "\f") `shouldBe` "\"\\f\"" value minified (Full.String "\f") `shouldBe` "\"\\f\""
it "escapes \\n" $ it "escapes \\n" $
value minified (String "\n") `shouldBe` "\"\\n\"" value minified (Full.String "\n") `shouldBe` "\"\\n\""
it "escapes \\r" $ it "escapes \\r" $
value minified (String "\r") `shouldBe` "\"\\r\"" value minified (Full.String "\r") `shouldBe` "\"\\r\""
it "escapes \\t" $ it "escapes \\t" $
value minified (String "\t") `shouldBe` "\"\\t\"" value minified (Full.String "\t") `shouldBe` "\"\\t\""
it "escapes backspace" $ it "escapes backspace" $
value minified (String "a\bc") `shouldBe` "\"a\\bc\"" value minified (Full.String "a\bc") `shouldBe` "\"a\\bc\""
context "escapes Unicode for chars less than 0010" $ do context "escapes Unicode for chars less than 0010" $ do
it "Null" $ value minified (String "\x0000") `shouldBe` "\"\\u0000\"" it "Null" $ value minified (Full.String "\x0000") `shouldBe` "\"\\u0000\""
it "bell" $ value minified (String "\x0007") `shouldBe` "\"\\u0007\"" it "bell" $ value minified (Full.String "\x0007") `shouldBe` "\"\\u0007\""
context "escapes Unicode for char less than 0020" $ do context "escapes Unicode for char less than 0020" $ do
it "DLE" $ value minified (String "\x0010") `shouldBe` "\"\\u0010\"" it "DLE" $ value minified (Full.String "\x0010") `shouldBe` "\"\\u0010\""
it "EM" $ value minified (String "\x0019") `shouldBe` "\"\\u0019\"" it "EM" $ value minified (Full.String "\x0019") `shouldBe` "\"\\u0019\""
context "encodes without escape" $ do context "encodes without escape" $ do
it "space" $ value minified (String "\x0020") `shouldBe` "\" \"" it "space" $ value minified (Full.String "\x0020") `shouldBe` "\" \""
it "~" $ value minified (String "\x007E") `shouldBe` "\"~\"" it "~" $ value minified (Full.String "\x007E") `shouldBe` "\"~\""
context "pretty" $ do context "pretty" $ do
it "uses strings for short string values" $ it "uses strings for short string values" $
value pretty (String "Short text") `shouldBe` "\"Short text\"" value pretty (Full.String "Short text") `shouldBe` "\"Short text\""
it "uses block strings for text with new lines, with newline symbol" $ it "uses block strings for text with new lines, with newline symbol" $
value pretty (String "Line 1\nLine 2") value pretty (Full.String "Line 1\nLine 2")
`shouldBe` [r|""" `shouldBe` [r|"""
Line 1 Line 1
Line 2 Line 2
"""|] """|]
it "uses block strings for text with new lines, with CR symbol" $ it "uses block strings for text with new lines, with CR symbol" $
value pretty (String "Line 1\rLine 2") value pretty (Full.String "Line 1\rLine 2")
`shouldBe` [r|""" `shouldBe` [r|"""
Line 1 Line 1
Line 2 Line 2
"""|] """|]
it "uses block strings for text with new lines, with CR symbol followed by newline" $ it "uses block strings for text with new lines, with CR symbol followed by newline" $
value pretty (String "Line 1\r\nLine 2") value pretty (Full.String "Line 1\r\nLine 2")
`shouldBe` [r|""" `shouldBe` [r|"""
Line 1 Line 1
Line 2 Line 2
@ -77,12 +77,12 @@ spec = do
forAll genNotAllowedSymbol $ \x -> do forAll genNotAllowedSymbol $ \x -> do
let let
rawValue = "Short \n" <> cons x "text" rawValue = "Short \n" <> cons x "text"
encoded = value pretty (String $ toStrict rawValue) encoded = value pretty (Full.String $ toStrict rawValue)
shouldStartWith (unpack encoded) "\"" shouldStartWith (unpack encoded) "\""
shouldEndWith (unpack encoded) "\"" shouldEndWith (unpack encoded) "\""
shouldNotContain (unpack encoded) "\"\"\"" shouldNotContain (unpack encoded) "\"\"\""
it "Hello world" $ value pretty (String "Hello,\n World!\n\nYours,\n GraphQL.") it "Hello world" $ value pretty (Full.String "Hello,\n World!\n\nYours,\n GraphQL.")
`shouldBe` [r|""" `shouldBe` [r|"""
Hello, Hello,
World! World!
@ -91,29 +91,29 @@ spec = do
GraphQL. GraphQL.
"""|] """|]
it "has only newlines" $ value pretty (String "\n") `shouldBe` [r|""" it "has only newlines" $ value pretty (Full.String "\n") `shouldBe` [r|"""
"""|] """|]
it "has newlines and one symbol at the begining" $ it "has newlines and one symbol at the begining" $
value pretty (String "a\n\n") `shouldBe` [r|""" value pretty (Full.String "a\n\n") `shouldBe` [r|"""
a a
"""|] """|]
it "has newlines and one symbol at the end" $ it "has newlines and one symbol at the end" $
value pretty (String "\n\na") `shouldBe` [r|""" value pretty (Full.String "\n\na") `shouldBe` [r|"""
a a
"""|] """|]
it "has newlines and one symbol in the middle" $ it "has newlines and one symbol in the middle" $
value pretty (String "\na\n") `shouldBe` [r|""" value pretty (Full.String "\na\n") `shouldBe` [r|"""
a a
"""|] """|]
it "skip trailing whitespaces" $ value pretty (String " Short\ntext ") it "skip trailing whitespaces" $ value pretty (Full.String " Short\ntext ")
`shouldBe` [r|""" `shouldBe` [r|"""
Short Short
text text
@ -121,11 +121,13 @@ spec = do
describe "definition" $ describe "definition" $
it "indents block strings in arguments" $ it "indents block strings in arguments" $
let arguments = [Argument "message" (String "line1\nline2")] let location = Full.Location 0 0
field = Field Nothing "field" arguments [] [] argumentValue = Full.Node (Full.String "line1\nline2") location
operation = DefinitionOperation arguments = [Full.Argument "message" argumentValue location]
$ SelectionSet (pure field) field = Full.Field Nothing "field" arguments [] [] location
$ Location 0 0 fieldSelection = pure $ Full.FieldSelection field
operation = Full.DefinitionOperation
$ Full.SelectionSet fieldSelection location
in definition pretty operation `shouldBe` [r|{ in definition pretty operation `shouldBe` [r|{
field(message: """ field(message: """
line1 line1

View File

@ -75,9 +75,9 @@ spec = describe "Lexer" $ do
parse dollar "" "$" `shouldParse` "$" parse dollar "" "$" `shouldParse` "$"
runBetween parens `shouldSucceedOn` "()" runBetween parens `shouldSucceedOn` "()"
parse spread "" "..." `shouldParse` "..." parse spread "" "..." `shouldParse` "..."
parse colon "" ":" `shouldParse` ":" parse colon "" `shouldSucceedOn` ":"
parse equals "" "=" `shouldParse` "=" parse equals "" "=" `shouldParse` "="
parse at "" "@" `shouldParse` "@" parse at "" `shouldSucceedOn` "@"
runBetween brackets `shouldSucceedOn` "[]" runBetween brackets `shouldSucceedOn` "[]"
runBetween braces `shouldSucceedOn` "{}" runBetween braces `shouldSucceedOn` "{}"
parse pipe "" "|" `shouldParse` "|" parse pipe "" "|" `shouldParse` "|"

View File

@ -6,6 +6,7 @@ module Language.GraphQL.AST.ParserSpec
import Data.List.NonEmpty (NonEmpty(..)) import Data.List.NonEmpty (NonEmpty(..))
import Language.GraphQL.AST.Document import Language.GraphQL.AST.Document
import qualified Language.GraphQL.AST.DirectiveLocation as DirLoc
import Language.GraphQL.AST.Parser import Language.GraphQL.AST.Parser
import Test.Hspec (Spec, describe, it) import Test.Hspec (Spec, describe, it)
import Test.Hspec.Megaparsec (shouldParse, shouldFailOn, shouldSucceedOn) import Test.Hspec.Megaparsec (shouldParse, shouldFailOn, shouldSucceedOn)
@ -119,6 +120,56 @@ spec = describe "Parser" $ do
| FRAGMENT_SPREAD | FRAGMENT_SPREAD
|] |]
it "parses two minimal directive definitions" $
let directive nm loc =
TypeSystemDefinition
(DirectiveDefinition
(Description Nothing)
nm
(ArgumentsDefinition [])
(loc :| []))
example1 =
directive "example1"
(DirLoc.TypeSystemDirectiveLocation DirLoc.FieldDefinition)
(Location {line = 2, column = 17})
example2 =
directive "example2"
(DirLoc.ExecutableDirectiveLocation DirLoc.Field)
(Location {line = 3, column = 17})
testSchemaExtension = example1 :| [ example2 ]
query = [r|
directive @example1 on FIELD_DEFINITION
directive @example2 on FIELD
|]
in parse document "" query `shouldParse` testSchemaExtension
it "parses a directive definition with a default empty list argument" $
let directive nm loc args =
TypeSystemDefinition
(DirectiveDefinition
(Description Nothing)
nm
(ArgumentsDefinition
[ InputValueDefinition
(Description Nothing)
argName
argType
argValue
[]
| (argName, argType, argValue) <- args])
(loc :| []))
defn =
directive "test"
(DirLoc.TypeSystemDirectiveLocation DirLoc.FieldDefinition)
[("foo",
TypeList (TypeNamed "String"),
Just
$ Node (ConstList [])
$ Location {line = 1, column = 33})]
(Location {line = 1, column = 1})
query = [r|directive @test(foo: [String] = []) on FIELD_DEFINITION|]
in parse document "" query `shouldParse` (defn :| [ ])
it "parses schema extension with a new directive" $ it "parses schema extension with a new directive" $
parse document "" `shouldSucceedOn`[r| parse document "" `shouldSucceedOn`[r|
extend schema @newDirective extend schema @newDirective
@ -128,7 +179,7 @@ spec = describe "Parser" $ do
parse document "" `shouldSucceedOn` [r|extend schema { query: Query }|] parse document "" `shouldSucceedOn` [r|extend schema { query: Query }|]
it "parses schema extension with an operation type and directive" $ it "parses schema extension with an operation type and directive" $
let newDirective = Directive "newDirective" [] let newDirective = Directive "newDirective" [] $ Location 1 15
schemaExtension = SchemaExtension schemaExtension = SchemaExtension
$ SchemaOperationExtension [newDirective] $ SchemaOperationExtension [newDirective]
$ OperationTypeDefinition Query "Query" :| [] $ OperationTypeDefinition Query "Query" :| []

View File

@ -8,17 +8,29 @@ module Language.GraphQL.ErrorSpec
) where ) where
import qualified Data.Aeson as Aeson import qualified Data.Aeson as Aeson
import qualified Data.Sequence as Seq import Data.List.NonEmpty (NonEmpty (..))
import Language.GraphQL.Error import Language.GraphQL.Error
import Test.Hspec ( Spec import Test.Hspec
( Spec
, describe , describe
, it , it
, shouldBe , shouldBe
) )
import Text.Megaparsec (PosState(..))
import Text.Megaparsec.Error (ParseError(..), ParseErrorBundle(..))
import Text.Megaparsec.Pos (SourcePos(..), mkPos)
spec :: Spec spec :: Spec
spec = describe "singleError" $ spec = describe "parseError" $
it "constructs an error with the given message" $ it "generates response with a single error" $ do
let errors'' = Seq.singleton $ Error "Message." [] let parseErrors = TrivialError 0 Nothing mempty :| []
expected = Response Aeson.Null errors'' posState = PosState
in singleError "Message." `shouldBe` expected { pstateInput = ""
, pstateOffset = 0
, pstateSourcePos = SourcePos "" (mkPos 1) (mkPos 1)
, pstateTabWidth = mkPos 1
, pstateLinePrefix = ""
}
Response Aeson.Null actual <-
parseError (ParseErrorBundle parseErrors posState)
length actual `shouldBe` 1

View File

@ -0,0 +1,72 @@
{- This Source Code Form is subject to the terms of the Mozilla Public License,
v. 2.0. If a copy of the MPL was not distributed with this file, You can
obtain one at https://mozilla.org/MPL/2.0/. -}
{-# LANGUAGE OverloadedStrings #-}
module Language.GraphQL.Execute.OrderedMapSpec
( spec
) where
import Language.GraphQL.Execute.OrderedMap (OrderedMap)
import qualified Language.GraphQL.Execute.OrderedMap as OrderedMap
import Test.Hspec (Spec, describe, it, shouldBe, shouldSatisfy)
spec :: Spec
spec =
describe "OrderedMap" $ do
it "creates an empty map" $
(mempty :: OrderedMap String) `shouldSatisfy` null
it "creates a singleton" $
let value :: String
value = "value"
in OrderedMap.size (OrderedMap.singleton "key" value) `shouldBe` 1
it "combines inserted vales" $
let key = "key"
map1 = OrderedMap.singleton key ("1" :: String)
map2 = OrderedMap.singleton key ("2" :: String)
in OrderedMap.lookup key (map1 <> map2) `shouldBe` Just "12"
it "shows the map" $
let actual = show
$ OrderedMap.insert "key1" "1"
$ OrderedMap.singleton "key2" ("2" :: String)
expected = "fromList [(\"key2\",\"2\"),(\"key1\",\"1\")]"
in actual `shouldBe` expected
it "traverses a map of just values" $
let actual = sequence
$ OrderedMap.insert "key1" (Just "2")
$ OrderedMap.singleton "key2" $ Just ("1" :: String)
expected = Just
$ OrderedMap.insert "key1" "2"
$ OrderedMap.singleton "key2" ("1" :: String)
in actual `shouldBe` expected
it "traverses a map with a Nothing" $
let actual = sequence
$ OrderedMap.insert "key1" Nothing
$ OrderedMap.singleton "key2" $ Just ("1" :: String)
expected = Nothing
in actual `shouldBe` expected
it "combines two maps preserving the order of the second one" $
let map1 :: OrderedMap String
map1 = OrderedMap.insert "key2" "2"
$ OrderedMap.singleton "key1" "1"
map2 :: OrderedMap String
map2 = OrderedMap.insert "key4" "4"
$ OrderedMap.singleton "key3" "3"
expected = OrderedMap.insert "key4" "4"
$ OrderedMap.insert "key3" "3"
$ OrderedMap.insert "key2" "2"
$ OrderedMap.singleton "key1" "1"
in (map1 <> map2) `shouldBe` expected
it "replaces existing values" $
let key = "key"
actual = OrderedMap.replace key ("2" :: String)
$ OrderedMap.singleton key ("1" :: String)
in OrderedMap.lookup key actual `shouldBe` Just "2"

View File

@ -3,40 +3,92 @@
obtain one at https://mozilla.org/MPL/2.0/. -} obtain one at https://mozilla.org/MPL/2.0/. -}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
module Language.GraphQL.ExecuteSpec module Language.GraphQL.ExecuteSpec
( spec ( spec
) where ) where
import Control.Exception (SomeException) import Control.Exception (Exception(..), SomeException)
import Control.Monad.Catch (throwM)
import Data.Aeson ((.=)) import Data.Aeson ((.=))
import qualified Data.Aeson as Aeson import qualified Data.Aeson as Aeson
import Data.Aeson.Types (emptyObject)
import Data.Conduit import Data.Conduit
import Data.HashMap.Strict (HashMap) import Data.HashMap.Strict (HashMap)
import qualified Data.HashMap.Strict as HashMap import qualified Data.HashMap.Strict as HashMap
import Language.GraphQL.AST (Name) import Data.Typeable (cast)
import Language.GraphQL.AST (Document, Location(..), Name)
import Language.GraphQL.AST.Parser (document) import Language.GraphQL.AST.Parser (document)
import Language.GraphQL.Error import Language.GraphQL.Error
import Language.GraphQL.Execute import Language.GraphQL.Execute (execute)
import Language.GraphQL.Type as Type import qualified Language.GraphQL.Type.Schema as Schema
import Language.GraphQL.Type.Out as Out import Language.GraphQL.Type
import qualified Language.GraphQL.Type.In as In
import qualified Language.GraphQL.Type.Out as Out
import Prelude hiding (id)
import Test.Hspec (Spec, context, describe, it, shouldBe) import Test.Hspec (Spec, context, describe, it, shouldBe)
import Text.Megaparsec (parse) import Text.Megaparsec (parse)
import Text.RawString.QQ (r)
schema :: Schema (Either SomeException) data PhilosopherException = PhilosopherException
schema = Schema deriving Show
{ query = queryType
, mutation = Nothing instance Exception PhilosopherException where
, subscription = Just subscriptionType toException = toException. ResolverException
} fromException e = do
ResolverException resolverException <- fromException e
cast resolverException
philosopherSchema :: Schema (Either SomeException)
philosopherSchema =
schemaWithTypes Nothing queryType Nothing subscriptionRoot extraTypes mempty
where
subscriptionRoot = Just subscriptionType
extraTypes =
[ Schema.ObjectType bookType
, Schema.ObjectType bookCollectionType
]
queryType :: Out.ObjectType (Either SomeException) queryType :: Out.ObjectType (Either SomeException)
queryType = Out.ObjectType "Query" Nothing [] queryType = Out.ObjectType "Query" Nothing []
$ HashMap.singleton "philosopher" $ HashMap.fromList
$ ValueResolver philosopherField [ ("philosopher", ValueResolver philosopherField philosopherResolver)
$ pure $ Type.Object mempty , ("genres", ValueResolver genresField genresResolver)
]
where where
philosopherField = philosopherField =
Out.Field Nothing (Out.NonNullObjectType philosopherType) HashMap.empty Out.Field Nothing (Out.NonNullObjectType philosopherType)
$ HashMap.singleton "id"
$ In.Argument Nothing (In.NamedScalarType id) Nothing
philosopherResolver = pure $ Object mempty
genresField =
let fieldType = Out.ListType $ Out.NonNullScalarType string
in Out.Field Nothing fieldType HashMap.empty
genresResolver :: Resolve (Either SomeException)
genresResolver = throwM PhilosopherException
musicType :: Out.ObjectType (Either SomeException)
musicType = Out.ObjectType "Music" Nothing []
$ HashMap.fromList resolvers
where
resolvers =
[ ("instrument", ValueResolver instrumentField instrumentResolver)
]
instrumentResolver = pure $ String "piano"
instrumentField = Out.Field Nothing (Out.NonNullScalarType string) HashMap.empty
poetryType :: Out.ObjectType (Either SomeException)
poetryType = Out.ObjectType "Poetry" Nothing []
$ HashMap.fromList resolvers
where
resolvers =
[ ("genre", ValueResolver genreField genreResolver)
]
genreResolver = pure $ String "Futurism"
genreField = Out.Field Nothing (Out.NonNullScalarType string) HashMap.empty
interestType :: Out.UnionType (Either SomeException)
interestType = Out.UnionType "Interest" Nothing [musicType, poetryType]
philosopherType :: Out.ObjectType (Either SomeException) philosopherType :: Out.ObjectType (Either SomeException)
philosopherType = Out.ObjectType "Philosopher" Nothing [] philosopherType = Out.ObjectType "Philosopher" Nothing []
@ -45,19 +97,68 @@ philosopherType = Out.ObjectType "Philosopher" Nothing []
resolvers = resolvers =
[ ("firstName", ValueResolver firstNameField firstNameResolver) [ ("firstName", ValueResolver firstNameField firstNameResolver)
, ("lastName", ValueResolver lastNameField lastNameResolver) , ("lastName", ValueResolver lastNameField lastNameResolver)
, ("school", ValueResolver schoolField schoolResolver)
, ("interest", ValueResolver interestField interestResolver)
, ("majorWork", ValueResolver majorWorkField majorWorkResolver)
, ("century", ValueResolver centuryField centuryResolver)
] ]
firstNameField = firstNameField =
Out.Field Nothing (Out.NonNullScalarType string) HashMap.empty Out.Field Nothing (Out.NonNullScalarType string) HashMap.empty
firstNameResolver = pure $ Type.String "Friedrich" firstNameResolver = pure $ String "Friedrich"
lastNameField lastNameField
= Out.Field Nothing (Out.NonNullScalarType string) HashMap.empty = Out.Field Nothing (Out.NonNullScalarType string) HashMap.empty
lastNameResolver = pure $ Type.String "Nietzsche" lastNameResolver = pure $ String "Nietzsche"
schoolField
= Out.Field Nothing (Out.NonNullEnumType schoolType) HashMap.empty
schoolResolver = pure $ Enum "EXISTENTIALISM"
interestField
= Out.Field Nothing (Out.NonNullUnionType interestType) HashMap.empty
interestResolver = pure
$ Object
$ HashMap.fromList [("instrument", "piano")]
majorWorkField
= Out.Field Nothing (Out.NonNullInterfaceType workType) HashMap.empty
majorWorkResolver = pure
$ Object
$ HashMap.fromList
[ ("title", "Also sprach Zarathustra: Ein Buch für Alle und Keinen")
]
centuryField =
Out.Field Nothing (Out.NonNullScalarType int) HashMap.empty
centuryResolver = pure $ Float 18.5
workType :: Out.InterfaceType (Either SomeException)
workType = Out.InterfaceType "Work" Nothing []
$ HashMap.fromList fields
where
fields = [("title", titleField)]
titleField = Out.Field Nothing (Out.NonNullScalarType string) HashMap.empty
bookType :: Out.ObjectType (Either SomeException)
bookType = Out.ObjectType "Book" Nothing [workType]
$ HashMap.fromList resolvers
where
resolvers =
[ ("title", ValueResolver titleField titleResolver)
]
titleField = Out.Field Nothing (Out.NonNullScalarType string) HashMap.empty
titleResolver = pure "Also sprach Zarathustra: Ein Buch für Alle und Keinen"
bookCollectionType :: Out.ObjectType (Either SomeException)
bookCollectionType = Out.ObjectType "Book" Nothing [workType]
$ HashMap.fromList resolvers
where
resolvers =
[ ("title", ValueResolver titleField titleResolver)
]
titleField = Out.Field Nothing (Out.NonNullScalarType string) HashMap.empty
titleResolver = pure "The Three Critiques"
subscriptionType :: Out.ObjectType (Either SomeException) subscriptionType :: Out.ObjectType (Either SomeException)
subscriptionType = Out.ObjectType "Subscription" Nothing [] subscriptionType = Out.ObjectType "Subscription" Nothing []
$ HashMap.singleton "newQuote" $ HashMap.singleton "newQuote"
$ EventStreamResolver quoteField (pure $ Type.Object mempty) $ EventStreamResolver quoteField (pure $ Object mempty)
$ pure $ yield $ Type.Object mempty $ pure $ yield $ Object mempty
where where
quoteField = quoteField =
Out.Field Nothing (Out.NonNullObjectType quoteType) HashMap.empty Out.Field Nothing (Out.NonNullObjectType quoteType) HashMap.empty
@ -71,9 +172,39 @@ quoteType = Out.ObjectType "Quote" Nothing []
quoteField = quoteField =
Out.Field Nothing (Out.NonNullScalarType string) HashMap.empty Out.Field Nothing (Out.NonNullScalarType string) HashMap.empty
schoolType :: EnumType
schoolType = EnumType "School" Nothing $ HashMap.fromList
[ ("NOMINALISM", EnumValue Nothing)
, ("REALISM", EnumValue Nothing)
, ("IDEALISM", EnumValue Nothing)
]
type EitherStreamOrValue = Either
(ResponseEventStream (Either SomeException) Aeson.Value)
(Response Aeson.Value)
execute' :: Document -> Either SomeException EitherStreamOrValue
execute' =
execute philosopherSchema Nothing (mempty :: HashMap Name Aeson.Value)
spec :: Spec spec :: Spec
spec = spec =
describe "execute" $ do describe "execute" $ do
it "rejects recursive fragments" $
let sourceQuery = [r|
{
...cyclicFragment
}
fragment cyclicFragment on Query {
...cyclicFragment
}
|]
expected = Response emptyObject mempty
Right (Right actual) = either (pure . parseError) execute'
$ parse document "" sourceQuery
in actual `shouldBe` expected
context "Query" $ do context "Query" $ do
it "skips unknown fields" $ it "skips unknown fields" $
let data'' = Aeson.object let data'' = Aeson.object
@ -82,7 +213,6 @@ spec =
] ]
] ]
expected = Response data'' mempty expected = Response data'' mempty
execute' = execute schema Nothing (mempty :: HashMap Name Aeson.Value)
Right (Right actual) = either (pure . parseError) execute' Right (Right actual) = either (pure . parseError) execute'
$ parse document "" "{ philosopher { firstName surname } }" $ parse document "" "{ philosopher { firstName surname } }"
in actual `shouldBe` expected in actual `shouldBe` expected
@ -94,10 +224,102 @@ spec =
] ]
] ]
expected = Response data'' mempty expected = Response data'' mempty
execute' = execute schema Nothing (mempty :: HashMap Name Aeson.Value)
Right (Right actual) = either (pure . parseError) execute' Right (Right actual) = either (pure . parseError) execute'
$ parse document "" "{ philosopher { firstName } philosopher { lastName } }" $ parse document "" "{ philosopher { firstName } philosopher { lastName } }"
in actual `shouldBe` expected in actual `shouldBe` expected
it "errors on invalid output enum values" $
let data'' = Aeson.object
[ "philosopher" .= Aeson.object
[ "school" .= Aeson.Null
]
]
executionErrors = pure $ Error
{ message = "Enum value completion failed."
, locations = [Location 1 17]
, path = []
}
expected = Response data'' executionErrors
Right (Right actual) = either (pure . parseError) execute'
$ parse document "" "{ philosopher { school } }"
in actual `shouldBe` expected
it "gives location information for non-null unions" $
let data'' = Aeson.object
[ "philosopher" .= Aeson.object
[ "interest" .= Aeson.Null
]
]
executionErrors = pure $ Error
{ message = "Union value completion failed."
, locations = [Location 1 17]
, path = []
}
expected = Response data'' executionErrors
Right (Right actual) = either (pure . parseError) execute'
$ parse document "" "{ philosopher { interest } }"
in actual `shouldBe` expected
it "gives location information for invalid interfaces" $
let data'' = Aeson.object
[ "philosopher" .= Aeson.object
[ "majorWork" .= Aeson.Null
]
]
executionErrors = pure $ Error
{ message = "Interface value completion failed."
, locations = [Location 1 17]
, path = []
}
expected = Response data'' executionErrors
Right (Right actual) = either (pure . parseError) execute'
$ parse document "" "{ philosopher { majorWork { title } } }"
in actual `shouldBe` expected
it "gives location information for invalid scalar arguments" $
let data'' = Aeson.object
[ "philosopher" .= Aeson.Null
]
executionErrors = pure $ Error
{ message = "Argument coercing failed."
, locations = [Location 1 15]
, path = []
}
expected = Response data'' executionErrors
Right (Right actual) = either (pure . parseError) execute'
$ parse document "" "{ philosopher(id: true) { lastName } }"
in actual `shouldBe` expected
it "gives location information for failed result coercion" $
let data'' = Aeson.object
[ "philosopher" .= Aeson.object
[ "century" .= Aeson.Null
]
]
executionErrors = pure $ Error
{ message = "Result coercion failed."
, locations = [Location 1 26]
, path = []
}
expected = Response data'' executionErrors
Right (Right actual) = either (pure . parseError) execute'
$ parse document "" "{ philosopher(id: \"1\") { century } }"
in actual `shouldBe` expected
it "gives location information for failed result coercion" $
let data'' = Aeson.object
[ "genres" .= Aeson.Null
]
executionErrors = pure $ Error
{ message = "PhilosopherException"
, locations = [Location 1 3]
, path = []
}
expected = Response data'' executionErrors
Right (Right actual) = either (pure . parseError) execute'
$ parse document "" "{ genres }"
in actual `shouldBe` expected
context "Subscription" $ context "Subscription" $
it "subscribes" $ it "subscribes" $
let data'' = Aeson.object let data'' = Aeson.object
@ -106,7 +328,6 @@ spec =
] ]
] ]
expected = Response data'' mempty expected = Response data'' mempty
execute' = execute schema Nothing (mempty :: HashMap Name Aeson.Value)
Right (Left stream) = either (pure . parseError) execute' Right (Left stream) = either (pure . parseError) execute'
$ parse document "" "subscription { newQuote { quote } }" $ parse document "" "subscription { newQuote { quote } }"
Right (Just actual) = runConduit $ stream .| await Right (Just actual) = runConduit $ stream .| await

View File

@ -0,0 +1,962 @@
{- This Source Code Form is subject to the terms of the Mozilla Public License,
v. 2.0. If a copy of the MPL was not distributed with this file, You can
obtain one at https://mozilla.org/MPL/2.0/. -}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
module Language.GraphQL.Validate.RulesSpec
( spec
) where
import Data.Foldable (toList)
import qualified Data.HashMap.Strict as HashMap
import Data.Text (Text)
import qualified Language.GraphQL.AST as AST
import Language.GraphQL.Type
import qualified Language.GraphQL.Type.In as In
import qualified Language.GraphQL.Type.Out as Out
import Language.GraphQL.Validate
import Test.Hspec (Spec, context, describe, it, shouldBe, shouldContain)
import Text.Megaparsec (parse, errorBundlePretty)
import Text.RawString.QQ (r)
petSchema :: Schema IO
petSchema = schema queryType Nothing (Just subscriptionType) mempty
queryType :: ObjectType IO
queryType = ObjectType "Query" Nothing [] $ HashMap.fromList
[ ("dog", dogResolver)
, ("cat", catResolver)
, ("findDog", findDogResolver)
]
where
dogField = Field Nothing (Out.NamedObjectType dogType) mempty
dogResolver = ValueResolver dogField $ pure Null
findDogArguments = HashMap.singleton "complex"
$ In.Argument Nothing (In.NonNullInputObjectType dogDataType) Nothing
findDogField = Field Nothing (Out.NamedObjectType dogType) findDogArguments
findDogResolver = ValueResolver findDogField $ pure Null
catField = Field Nothing (Out.NamedObjectType catType) mempty
catResolver = ValueResolver catField $ pure Null
catCommandType :: EnumType
catCommandType = EnumType "CatCommand" Nothing $ HashMap.fromList
[ ("JUMP", EnumValue Nothing)
]
catType :: ObjectType IO
catType = ObjectType "Cat" Nothing [petType] $ HashMap.fromList
[ ("name", nameResolver)
, ("nickname", nicknameResolver)
, ("doesKnowCommands", doesKnowCommandsResolver)
, ("meowVolume", meowVolumeResolver)
]
where
meowVolumeField = Field Nothing (Out.NamedScalarType int) mempty
meowVolumeResolver = ValueResolver meowVolumeField $ pure $ Int 3
doesKnowCommandsType = In.NonNullListType
$ In.NonNullEnumType catCommandType
doesKnowCommandsField = Field Nothing (Out.NonNullScalarType boolean)
$ HashMap.singleton "catCommands"
$ In.Argument Nothing doesKnowCommandsType Nothing
doesKnowCommandsResolver = ValueResolver doesKnowCommandsField
$ pure $ Boolean True
nameResolver :: Resolver IO
nameResolver = ValueResolver nameField $ pure "Name"
where
nameField = Field Nothing (Out.NonNullScalarType string) mempty
nicknameResolver :: Resolver IO
nicknameResolver = ValueResolver nicknameField $ pure "Nickname"
where
nicknameField = Field Nothing (Out.NamedScalarType string) mempty
dogCommandType :: EnumType
dogCommandType = EnumType "DogCommand" Nothing $ HashMap.fromList
[ ("SIT", EnumValue Nothing)
, ("DOWN", EnumValue Nothing)
, ("HEEL", EnumValue Nothing)
]
dogType :: ObjectType IO
dogType = ObjectType "Dog" Nothing [petType] $ HashMap.fromList
[ ("name", nameResolver)
, ("nickname", nicknameResolver)
, ("barkVolume", barkVolumeResolver)
, ("doesKnowCommand", doesKnowCommandResolver)
, ("isHousetrained", isHousetrainedResolver)
, ("owner", ownerResolver)
]
where
barkVolumeField = Field Nothing (Out.NamedScalarType int) mempty
barkVolumeResolver = ValueResolver barkVolumeField $ pure $ Int 3
doesKnowCommandField = Field Nothing (Out.NonNullScalarType boolean)
$ HashMap.singleton "dogCommand"
$ In.Argument Nothing (In.NonNullEnumType dogCommandType) Nothing
doesKnowCommandResolver = ValueResolver doesKnowCommandField
$ pure $ Boolean True
isHousetrainedField = Field Nothing (Out.NonNullScalarType boolean)
$ HashMap.singleton "atOtherHomes"
$ In.Argument Nothing (In.NamedScalarType boolean) Nothing
isHousetrainedResolver = ValueResolver isHousetrainedField
$ pure $ Boolean True
ownerField = Field Nothing (Out.NamedObjectType humanType) mempty
ownerResolver = ValueResolver ownerField $ pure Null
dogDataType :: InputObjectType
dogDataType = InputObjectType "DogData" Nothing
$ HashMap.singleton "name" nameInputField
where
nameInputField = InputField Nothing (In.NonNullScalarType string) Nothing
sentientType :: InterfaceType IO
sentientType = InterfaceType "Sentient" Nothing []
$ HashMap.singleton "name"
$ Field Nothing (Out.NonNullScalarType string) mempty
petType :: InterfaceType IO
petType = InterfaceType "Pet" Nothing []
$ HashMap.singleton "name"
$ Field Nothing (Out.NonNullScalarType string) mempty
subscriptionType :: ObjectType IO
subscriptionType = ObjectType "Subscription" Nothing [] $ HashMap.fromList
[ ("newMessage", newMessageResolver)
, ("disallowedSecondRootField", newMessageResolver)
]
where
newMessageField = Field Nothing (Out.NonNullObjectType messageType) mempty
newMessageResolver = ValueResolver newMessageField
$ pure $ Object HashMap.empty
messageType :: ObjectType IO
messageType = ObjectType "Message" Nothing [] $ HashMap.fromList
[ ("sender", senderResolver)
, ("body", bodyResolver)
]
where
senderField = Field Nothing (Out.NonNullScalarType string) mempty
senderResolver = ValueResolver senderField $ pure "Sender"
bodyField = Field Nothing (Out.NonNullScalarType string) mempty
bodyResolver = ValueResolver bodyField $ pure "Message body."
humanType :: ObjectType IO
humanType = ObjectType "Human" Nothing [sentientType] $ HashMap.fromList
[ ("name", nameResolver)
, ("pets", petsResolver)
]
where
petsField =
Field Nothing (Out.ListType $ Out.NonNullInterfaceType petType) mempty
petsResolver = ValueResolver petsField $ pure $ List []
validate :: Text -> [Error]
validate queryString =
case parse AST.document "" queryString of
Left parseErrors -> error $ errorBundlePretty parseErrors
Right ast -> toList $ document petSchema specifiedRules ast
spec :: Spec
spec =
describe "document" $ do
context "executableDefinitionsRule" $
it "rejects type definitions" $
let queryString = [r|
query getDogName {
dog {
name
color
}
}
extend type Dog {
color: String
}
|]
expected = Error
{ message =
"Definition must be OperationDefinition or \
\FragmentDefinition."
, locations = [AST.Location 9 19]
}
in validate queryString `shouldContain` [expected]
context "singleFieldSubscriptionsRule" $ do
it "rejects multiple subscription root fields" $
let queryString = [r|
subscription sub {
newMessage {
body
sender
}
disallowedSecondRootField
}
|]
expected = Error
{ message =
"Subscription \"sub\" must select only one top \
\level field."
, locations = [AST.Location 2 19]
}
in validate queryString `shouldContain` [expected]
it "rejects multiple subscription root fields coming from a fragment" $
let queryString = [r|
subscription sub {
...multipleSubscriptions
}
fragment multipleSubscriptions on Subscription {
newMessage {
body
sender
}
disallowedSecondRootField
}
|]
expected = Error
{ message =
"Subscription \"sub\" must select only one top \
\level field."
, locations = [AST.Location 2 19]
}
in validate queryString `shouldContain` [expected]
it "finds corresponding subscription fragment" $
let queryString = [r|
subscription sub {
...anotherSubscription
...multipleSubscriptions
}
fragment multipleSubscriptions on Subscription {
newMessage {
body
}
disallowedSecondRootField {
sender
}
}
fragment anotherSubscription on Subscription {
newMessage {
body
sender
}
}
|]
expected = Error
{ message =
"Subscription \"sub\" must select only one top \
\level field."
, locations = [AST.Location 2 19]
}
in validate queryString `shouldBe` [expected]
context "loneAnonymousOperationRule" $
it "rejects multiple anonymous operations" $
let queryString = [r|
{
dog {
name
}
}
query getName {
dog {
owner {
name
}
}
}
|]
expected = Error
{ message =
"This anonymous operation must be the only defined \
\operation."
, locations = [AST.Location 2 19]
}
in validate queryString `shouldBe` [expected]
context "uniqueOperationNamesRule" $
it "rejects operations with the same name" $
let queryString = [r|
query dogOperation {
dog {
name
}
}
mutation dogOperation {
mutateDog {
id
}
}
|]
expected = Error
{ message =
"There can be only one operation named \
\\"dogOperation\"."
, locations = [AST.Location 2 19, AST.Location 8 19]
}
in validate queryString `shouldBe` [expected]
context "uniqueFragmentNamesRule" $
it "rejects fragments with the same name" $
let queryString = [r|
{
dog {
...fragmentOne
}
}
fragment fragmentOne on Dog {
name
}
fragment fragmentOne on Dog {
owner {
name
}
}
|]
expected = Error
{ message =
"There can be only one fragment named \
\\"fragmentOne\"."
, locations = [AST.Location 8 19, AST.Location 12 19]
}
in validate queryString `shouldBe` [expected]
context "fragmentSpreadTargetDefinedRule" $
it "rejects the fragment spread without a target" $
let queryString = [r|
{
dog {
...undefinedFragment
}
}
|]
expected = Error
{ message =
"Fragment target \"undefinedFragment\" is \
\undefined."
, locations = [AST.Location 4 23]
}
in validate queryString `shouldBe` [expected]
context "fragmentSpreadTypeExistenceRule" $ do
it "rejects fragment spreads without an unknown target type" $
let queryString = [r|
{
dog {
...notOnExistingType
}
}
fragment notOnExistingType on NotInSchema {
name
}
|]
expected = Error
{ message =
"Fragment \"notOnExistingType\" is specified on \
\type \"NotInSchema\" which doesn't exist in the \
\schema."
, locations = [AST.Location 4 23]
}
in validate queryString `shouldBe` [expected]
it "rejects inline fragments without a target" $
let queryString = [r|
{
... on NotInSchema {
name
}
}
|]
expected = Error
{ message =
"Inline fragment is specified on type \
\\"NotInSchema\" which doesn't exist in the schema."
, locations = [AST.Location 3 21]
}
in validate queryString `shouldBe` [expected]
context "fragmentsOnCompositeTypesRule" $ do
it "rejects fragments on scalar types" $
let queryString = [r|
{
dog {
...fragOnScalar
}
}
fragment fragOnScalar on Int {
name
}
|]
expected = Error
{ message =
"Fragment cannot condition on non composite type \
\\"Int\"."
, locations = [AST.Location 7 19]
}
in validate queryString `shouldContain` [expected]
it "rejects inline fragments on scalar types" $
let queryString = [r|
{
... on Boolean {
name
}
}
|]
expected = Error
{ message =
"Fragment cannot condition on non composite type \
\\"Boolean\"."
, locations = [AST.Location 3 21]
}
in validate queryString `shouldContain` [expected]
context "noUnusedFragmentsRule" $
it "rejects unused fragments" $
let queryString = [r|
fragment nameFragment on Dog { # unused
name
}
{
dog {
name
}
}
|]
expected = Error
{ message =
"Fragment \"nameFragment\" is never used."
, locations = [AST.Location 2 19]
}
in validate queryString `shouldBe` [expected]
context "noFragmentCyclesRule" $
it "rejects spreads that form cycles" $
let queryString = [r|
{
dog {
...nameFragment
}
}
fragment nameFragment on Dog {
name
...barkVolumeFragment
}
fragment barkVolumeFragment on Dog {
barkVolume
...nameFragment
}
|]
error1 = Error
{ message =
"Cannot spread fragment \"barkVolumeFragment\" \
\within itself (via barkVolumeFragment -> \
\nameFragment -> barkVolumeFragment)."
, locations = [AST.Location 11 19]
}
error2 = Error
{ message =
"Cannot spread fragment \"nameFragment\" within \
\itself (via nameFragment -> barkVolumeFragment -> \
\nameFragment)."
, locations = [AST.Location 7 19]
}
in validate queryString `shouldBe` [error1, error2]
context "uniqueArgumentNamesRule" $
it "rejects duplicate field arguments" $
let queryString = [r|
{
dog {
isHousetrained(atOtherHomes: true, atOtherHomes: true)
}
}
|]
expected = Error
{ message =
"There can be only one argument named \
\\"atOtherHomes\"."
, locations = [AST.Location 4 38, AST.Location 4 58]
}
in validate queryString `shouldBe` [expected]
context "uniqueDirectiveNamesRule" $
it "rejects more than one directive per location" $
let queryString = [r|
query ($foo: Boolean = true, $bar: Boolean = false) {
dog @skip(if: $foo) @skip(if: $bar) {
name
}
}
|]
expected = Error
{ message =
"There can be only one directive named \"skip\"."
, locations = [AST.Location 3 25, AST.Location 3 41]
}
in validate queryString `shouldBe` [expected]
context "uniqueVariableNamesRule" $
it "rejects duplicate variables" $
let queryString = [r|
query houseTrainedQuery($atOtherHomes: Boolean, $atOtherHomes: Boolean) {
dog {
isHousetrained(atOtherHomes: $atOtherHomes)
}
}
|]
expected = Error
{ message =
"There can be only one variable named \
\\"atOtherHomes\"."
, locations = [AST.Location 2 43, AST.Location 2 67]
}
in validate queryString `shouldBe` [expected]
context "variablesAreInputTypesRule" $
it "rejects non-input types as variables" $
let queryString = [r|
query takesDogBang($dog: Dog!) {
dog {
isHousetrained(atOtherHomes: $dog)
}
}
|]
expected = Error
{ message =
"Variable \"$dog\" cannot be non-input type \
\\"Dog\"."
, locations = [AST.Location 2 38]
}
in validate queryString `shouldContain` [expected]
context "noUndefinedVariablesRule" $
it "rejects undefined variables" $
let queryString = [r|
query variableIsNotDefinedUsedInSingleFragment {
dog {
...isHousetrainedFragment
}
}
fragment isHousetrainedFragment on Dog {
isHousetrained(atOtherHomes: $atOtherHomes)
}
|]
expected = Error
{ message =
"Variable \"$atOtherHomes\" is not defined by \
\operation \
\\"variableIsNotDefinedUsedInSingleFragment\"."
, locations = [AST.Location 9 50]
}
in validate queryString `shouldBe` [expected]
context "noUnusedVariablesRule" $
it "rejects unused variables" $
let queryString = [r|
query variableUnused($atOtherHomes: Boolean) {
dog {
isHousetrained
}
}
|]
expected = Error
{ message =
"Variable \"$atOtherHomes\" is never used in \
\operation \"variableUnused\"."
, locations = [AST.Location 2 40]
}
in validate queryString `shouldBe` [expected]
context "uniqueInputFieldNamesRule" $
it "rejects duplicate fields in input objects" $
let queryString = [r|
{
findDog(complex: { name: "Fido", name: "Jack" }) {
name
}
}
|]
expected = Error
{ message =
"There can be only one input field named \"name\"."
, locations = [AST.Location 3 40, AST.Location 3 54]
}
in validate queryString `shouldBe` [expected]
context "fieldsOnCorrectTypeRule" $
it "rejects undefined fields" $
let queryString = [r|
{
dog {
meowVolume
}
}
|]
expected = Error
{ message =
"Cannot query field \"meowVolume\" on type \"Dog\"."
, locations = [AST.Location 4 23]
}
in validate queryString `shouldBe` [expected]
context "scalarLeafsRule" $
it "rejects scalar fields with not empty selection set" $
let queryString = [r|
{
dog {
barkVolume {
sinceWhen
}
}
}
|]
expected = Error
{ message =
"Field \"barkVolume\" must not have a selection \
\since type \"Int\" has no subfields."
, locations = [AST.Location 4 23]
}
in validate queryString `shouldBe` [expected]
context "knownArgumentNamesRule" $ do
it "rejects field arguments missing in the type" $
let queryString = [r|
{
dog {
doesKnowCommand(command: CLEAN_UP_HOUSE, dogCommand: SIT)
}
}
|]
expected = Error
{ message =
"Unknown argument \"command\" on field \
\\"Dog.doesKnowCommand\"."
, locations = [AST.Location 4 39]
}
in validate queryString `shouldBe` [expected]
it "rejects directive arguments missing in the definition" $
let queryString = [r|
{
dog {
isHousetrained(atOtherHomes: true) @include(unless: false, if: true)
}
}
|]
expected = Error
{ message =
"Unknown argument \"unless\" on directive \
\\"@include\"."
, locations = [AST.Location 4 67]
}
in validate queryString `shouldBe` [expected]
context "knownDirectiveNamesRule" $
it "rejects undefined directives" $
let queryString = [r|
{
dog {
isHousetrained(atOtherHomes: true) @ignore(if: true)
}
}
|]
expected = Error
{ message = "Unknown directive \"@ignore\"."
, locations = [AST.Location 4 58]
}
in validate queryString `shouldBe` [expected]
context "knownInputFieldNamesRule" $
it "rejects undefined input object fields" $
let queryString = [r|
{
findDog(complex: { favoriteCookieFlavor: "Bacon", name: "Jack" }) {
name
}
}
|]
expected = Error
{ message =
"Field \"favoriteCookieFlavor\" is not defined \
\by type \"DogData\"."
, locations = [AST.Location 3 40]
}
in validate queryString `shouldBe` [expected]
context "directivesInValidLocationsRule" $
it "rejects directives in invalid locations" $
let queryString = [r|
query @skip(if: $foo) {
dog {
name
}
}
|]
expected = Error
{ message =
"Directive \"@skip\" may not be used on QUERY."
, locations = [AST.Location 2 25]
}
in validate queryString `shouldBe` [expected]
context "overlappingFieldsCanBeMergedRule" $ do
it "fails to merge fields of mismatching types" $
let queryString = [r|
{
dog {
name: nickname
name
}
}
|]
expected = Error
{ message =
"Fields \"name\" conflict because \"nickname\" and \
\\"name\" are different fields. Use different \
\aliases on the fields to fetch both if this was \
\intentional."
, locations = [AST.Location 4 23, AST.Location 5 23]
}
in validate queryString `shouldBe` [expected]
it "fails if the arguments of the same field don't match" $
let queryString = [r|
{
dog {
doesKnowCommand(dogCommand: SIT)
doesKnowCommand(dogCommand: HEEL)
}
}
|]
expected = Error
{ message =
"Fields \"doesKnowCommand\" conflict because they \
\have different arguments. Use different aliases \
\on the fields to fetch both if this was \
\intentional."
, locations = [AST.Location 4 23, AST.Location 5 23]
}
in validate queryString `shouldBe` [expected]
it "fails to merge same-named field and alias" $
let queryString = [r|
{
dog {
doesKnowCommand(dogCommand: SIT)
doesKnowCommand: isHousetrained(atOtherHomes: true)
}
}
|]
expected = Error
{ message =
"Fields \"doesKnowCommand\" conflict because \
\\"doesKnowCommand\" and \"isHousetrained\" are \
\different fields. Use different aliases on the \
\fields to fetch both if this was intentional."
, locations = [AST.Location 4 23, AST.Location 5 23]
}
in validate queryString `shouldBe` [expected]
it "looks for fields after a successfully merged field pair" $
let queryString = [r|
{
dog {
name
doesKnowCommand(dogCommand: SIT)
}
dog {
name
doesKnowCommand: isHousetrained(atOtherHomes: true)
}
}
|]
expected = Error
{ message =
"Fields \"doesKnowCommand\" conflict because \
\\"doesKnowCommand\" and \"isHousetrained\" are \
\different fields. Use different aliases on the \
\fields to fetch both if this was intentional."
, locations = [AST.Location 5 23, AST.Location 9 23]
}
in validate queryString `shouldBe` [expected]
context "possibleFragmentSpreadsRule" $ do
it "rejects object inline spreads outside object scope" $
let queryString = [r|
{
dog {
... on Cat {
meowVolume
}
}
}
|]
expected = Error
{ message =
"Fragment cannot be spread here as objects of type \
\\"Dog\" can never be of type \"Cat\"."
, locations = [AST.Location 4 23]
}
in validate queryString `shouldBe` [expected]
it "rejects object named spreads outside object scope" $
let queryString = [r|
{
dog {
... catInDogFragmentInvalid
}
}
fragment catInDogFragmentInvalid on Cat {
meowVolume
}
|]
expected = Error
{ message =
"Fragment \"catInDogFragmentInvalid\" cannot be \
\spread here as objects of type \"Dog\" can never \
\be of type \"Cat\"."
, locations = [AST.Location 4 23]
}
in validate queryString `shouldBe` [expected]
context "providedRequiredInputFieldsRule" $
it "rejects missing required input fields" $
let queryString = [r|
{
findDog(complex: { name: null }) {
name
}
}
|]
expected = Error
{ message =
"Input field \"name\" of type \"DogData\" is \
\required, but it was not provided."
, locations = [AST.Location 3 38]
}
in validate queryString `shouldBe` [expected]
context "providedRequiredArgumentsRule" $ do
it "checks for (non-)nullable arguments" $
let queryString = [r|
{
dog {
doesKnowCommand(dogCommand: null)
}
}
|]
expected = Error
{ message =
"Field \"doesKnowCommand\" argument \"dogCommand\" \
\of type \"DogCommand\" is required, but it was \
\not provided."
, locations = [AST.Location 4 23]
}
in validate queryString `shouldBe` [expected]
context "variablesInAllowedPositionRule" $ do
it "rejects wrongly typed variable arguments" $
let queryString = [r|
query dogCommandArgQuery($dogCommandArg: DogCommand) {
dog {
doesKnowCommand(dogCommand: $dogCommandArg)
}
}
|]
expected = Error
{ message =
"Variable \"$dogCommandArg\" of type \
\\"DogCommand\" used in position expecting type \
\\"!DogCommand\"."
, locations = [AST.Location 2 44]
}
in validate queryString `shouldBe` [expected]
it "rejects wrongly typed variable arguments" $
let queryString = [r|
query intCannotGoIntoBoolean($intArg: Int) {
dog {
isHousetrained(atOtherHomes: $intArg)
}
}
|]
expected = Error
{ message =
"Variable \"$intArg\" of type \"Int\" used in \
\position expecting type \"Boolean\"."
, locations = [AST.Location 2 48]
}
in validate queryString `shouldBe` [expected]
context "valuesOfCorrectTypeRule" $ do
it "rejects values of incorrect types" $
let queryString = [r|
{
dog {
isHousetrained(atOtherHomes: 3)
}
}
|]
expected = Error
{ message =
"Value 3 cannot be coerced to type \"Boolean\"."
, locations = [AST.Location 4 52]
}
in validate queryString `shouldBe` [expected]
it "uses the location of a single list value" $
let queryString = [r|
{
cat {
doesKnowCommands(catCommands: [3])
}
}
|]
expected = Error
{ message =
"Value 3 cannot be coerced to type \"!CatCommand\"."
, locations = [AST.Location 4 54]
}
in validate queryString `shouldBe` [expected]
it "validates input object properties once" $
let queryString = [r|
{
findDog(complex: { name: 3 }) {
name
}
}
|]
expected = Error
{ message =
"Value 3 cannot be coerced to type \"!String\"."
, locations = [AST.Location 3 46]
}
in validate queryString `shouldBe` [expected]
it "checks for required list members" $
let queryString = [r|
{
cat {
doesKnowCommands(catCommands: [null])
}
}
|]
expected = Error
{ message =
"List of non-null values of type \"CatCommand\" \
\cannot contain null values."
, locations = [AST.Location 4 54]
}
in validate queryString `shouldBe` [expected]

View File

@ -1,283 +0,0 @@
{- This Source Code Form is subject to the terms of the Mozilla Public License,
v. 2.0. If a copy of the MPL was not distributed with this file, You can
obtain one at https://mozilla.org/MPL/2.0/. -}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
module Language.GraphQL.ValidateSpec
( spec
) where
import Data.Sequence (Seq(..))
import qualified Data.Sequence as Seq
import qualified Data.HashMap.Strict as HashMap
import Data.Text (Text)
import qualified Language.GraphQL.AST as AST
import Language.GraphQL.Type
import qualified Language.GraphQL.Type.In as In
import qualified Language.GraphQL.Type.Out as Out
import Language.GraphQL.Validate
import Test.Hspec (Spec, describe, it, shouldBe)
import Text.Megaparsec (parse)
import Text.RawString.QQ (r)
schema :: Schema IO
schema = Schema
{ query = queryType
, mutation = Nothing
, subscription = Nothing
}
queryType :: ObjectType IO
queryType = ObjectType "Query" Nothing []
$ HashMap.singleton "dog" dogResolver
where
dogField = Field Nothing (Out.NamedObjectType dogType) mempty
dogResolver = ValueResolver dogField $ pure Null
dogCommandType :: EnumType
dogCommandType = EnumType "DogCommand" Nothing $ HashMap.fromList
[ ("SIT", EnumValue Nothing)
, ("DOWN", EnumValue Nothing)
, ("HEEL", EnumValue Nothing)
]
dogType :: ObjectType IO
dogType = ObjectType "Dog" Nothing [petType] $ HashMap.fromList
[ ("name", nameResolver)
, ("nickname", nicknameResolver)
, ("barkVolume", barkVolumeResolver)
, ("doesKnowCommand", doesKnowCommandResolver)
, ("isHousetrained", isHousetrainedResolver)
, ("owner", ownerResolver)
]
where
nameField = Field Nothing (Out.NonNullScalarType string) mempty
nameResolver = ValueResolver nameField $ pure "Name"
nicknameField = Field Nothing (Out.NamedScalarType string) mempty
nicknameResolver = ValueResolver nicknameField $ pure "Nickname"
barkVolumeField = Field Nothing (Out.NamedScalarType int) mempty
barkVolumeResolver = ValueResolver barkVolumeField $ pure $ Int 3
doesKnowCommandField = Field Nothing (Out.NonNullScalarType boolean)
$ HashMap.singleton "dogCommand"
$ In.Argument Nothing (In.NonNullEnumType dogCommandType) Nothing
doesKnowCommandResolver = ValueResolver doesKnowCommandField
$ pure $ Boolean True
isHousetrainedField = Field Nothing (Out.NonNullScalarType boolean)
$ HashMap.singleton "atOtherHomes"
$ In.Argument Nothing (In.NamedScalarType boolean) Nothing
isHousetrainedResolver = ValueResolver isHousetrainedField
$ pure $ Boolean True
ownerField = Field Nothing (Out.NamedObjectType humanType) mempty
ownerResolver = ValueResolver ownerField $ pure Null
sentientType :: InterfaceType IO
sentientType = InterfaceType "Sentient" Nothing []
$ HashMap.singleton "name"
$ Field Nothing (Out.NonNullScalarType string) mempty
petType :: InterfaceType IO
petType = InterfaceType "Pet" Nothing []
$ HashMap.singleton "name"
$ Field Nothing (Out.NonNullScalarType string) mempty
{-
alienType :: ObjectType IO
alienType = ObjectType "Alien" Nothing [sentientType] $ HashMap.fromList
[ ("name", nameResolver)
, ("homePlanet", homePlanetResolver)
]
where
nameField = Field Nothing (Out.NonNullScalarType string) mempty
nameResolver = ValueResolver nameField $ pure "Name"
homePlanetField =
Field Nothing (Out.NamedScalarType string) mempty
homePlanetResolver = ValueResolver homePlanetField $ pure "Home planet"
-}
humanType :: ObjectType IO
humanType = ObjectType "Human" Nothing [sentientType] $ HashMap.fromList
[ ("name", nameResolver)
, ("pets", petsResolver)
]
where
nameField = Field Nothing (Out.NonNullScalarType string) mempty
nameResolver = ValueResolver nameField $ pure "Name"
petsField =
Field Nothing (Out.ListType $ Out.NonNullInterfaceType petType) mempty
petsResolver = ValueResolver petsField $ pure $ List []
{-
catCommandType :: EnumType
catCommandType = EnumType "CatCommand" Nothing $ HashMap.fromList
[ ("JUMP", EnumValue Nothing)
]
catType :: ObjectType IO
catType = ObjectType "Cat" Nothing [petType] $ HashMap.fromList
[ ("name", nameResolver)
, ("nickname", nicknameResolver)
, ("doesKnowCommand", doesKnowCommandResolver)
, ("meowVolume", meowVolumeResolver)
]
where
nameField = Field Nothing (Out.NonNullScalarType string) mempty
nameResolver = ValueResolver nameField $ pure "Name"
nicknameField = Field Nothing (Out.NamedScalarType string) mempty
nicknameResolver = ValueResolver nicknameField $ pure "Nickname"
doesKnowCommandField = Field Nothing (Out.NonNullScalarType boolean)
$ HashMap.singleton "catCommand"
$ In.Argument Nothing (In.NonNullEnumType catCommandType) Nothing
doesKnowCommandResolver = ValueResolver doesKnowCommandField
$ pure $ Boolean True
meowVolumeField = Field Nothing (Out.NamedScalarType int) mempty
meowVolumeResolver = ValueResolver meowVolumeField $ pure $ Int 2
catOrDogType :: UnionType IO
catOrDogType = UnionType "CatOrDog" Nothing [catType, dogType]
dogOrHumanType :: UnionType IO
dogOrHumanType = UnionType "DogOrHuman" Nothing [dogType, humanType]
humanOrAlienType :: UnionType IO
humanOrAlienType = UnionType "HumanOrAlien" Nothing [humanType, alienType]
-}
validate :: Text -> Seq Error
validate queryString =
case parse AST.document "" queryString of
Left _ -> Seq.empty
Right ast -> document schema specifiedRules ast
spec :: Spec
spec =
describe "document" $ do
it "rejects type definitions" $
let queryString = [r|
query getDogName {
dog {
name
color
}
}
extend type Dog {
color: String
}
|]
expected = Error
{ message =
"Definition must be OperationDefinition or FragmentDefinition."
, locations = [AST.Location 9 15]
, path = []
}
in validate queryString `shouldBe` Seq.singleton expected
it "rejects multiple subscription root fields" $
let queryString = [r|
subscription sub {
newMessage {
body
sender
}
disallowedSecondRootField
}
|]
expected = Error
{ message =
"Subscription sub must select only one top level field."
, locations = [AST.Location 2 15]
, path = []
}
in validate queryString `shouldBe` Seq.singleton expected
it "rejects multiple subscription root fields coming from a fragment" $
let queryString = [r|
subscription sub {
...multipleSubscriptions
}
fragment multipleSubscriptions on Subscription {
newMessage {
body
sender
}
disallowedSecondRootField
}
|]
expected = Error
{ message =
"Subscription sub must select only one top level field."
, locations = [AST.Location 2 15]
, path = []
}
in validate queryString `shouldBe` Seq.singleton expected
it "rejects multiple anonymous operations" $
let queryString = [r|
{
dog {
name
}
}
query getName {
dog {
owner {
name
}
}
}
|]
expected = Error
{ message =
"This anonymous operation must be the only defined operation."
, locations = [AST.Location 2 15]
, path = []
}
in validate queryString `shouldBe` Seq.singleton expected
it "rejects operations with the same name" $
let queryString = [r|
query dogOperation {
dog {
name
}
}
mutation dogOperation {
mutateDog {
id
}
}
|]
expected = Error
{ message =
"There can be only one operation named \"dogOperation\"."
, locations = [AST.Location 2 15, AST.Location 8 15]
, path = []
}
in validate queryString `shouldBe` Seq.singleton expected
it "rejects fragments with the same name" $
let queryString = [r|
{
dog {
...fragmentOne
}
}
fragment fragmentOne on Dog {
name
}
fragment fragmentOne on Dog {
owner {
name
}
}
|]
expected = Error
{ message =
"There can be only one fragment named \"fragmentOne\"."
, locations = [AST.Location 8 15, AST.Location 12 15]
, path = []
}
in validate queryString `shouldBe` Seq.singleton expected

View File

@ -19,8 +19,7 @@ import Test.Hspec.GraphQL
import Text.RawString.QQ (r) import Text.RawString.QQ (r)
experimentalResolver :: Schema IO experimentalResolver :: Schema IO
experimentalResolver = Schema experimentalResolver = schema queryType Nothing Nothing mempty
{ query = queryType, mutation = Nothing, subscription = Nothing }
where where
queryType = Out.ObjectType "Query" Nothing [] queryType = Out.ObjectType "Query" Nothing []
$ HashMap.singleton "experimentalField" $ HashMap.singleton "experimentalField"
@ -72,7 +71,7 @@ spec =
...experimentalFragment @skip(if: true) ...experimentalFragment @skip(if: true)
} }
fragment experimentalFragment on ExperimentalType { fragment experimentalFragment on Query {
experimentalField experimentalField
} }
|] |]
@ -83,7 +82,7 @@ spec =
it "should be able to @skip an inline fragment" $ do it "should be able to @skip an inline fragment" $ do
let sourceQuery = [r| let sourceQuery = [r|
{ {
... on ExperimentalType @skip(if: true) { ... on Query @skip(if: true) {
experimentalField experimentalField
} }
} }

View File

@ -46,15 +46,12 @@ inlineQuery = [r|{
}|] }|]
shirtType :: Out.ObjectType IO shirtType :: Out.ObjectType IO
shirtType = Out.ObjectType "Shirt" Nothing [] shirtType = Out.ObjectType "Shirt" Nothing [] $ HashMap.fromList
$ HashMap.fromList
[ ("size", sizeFieldType) [ ("size", sizeFieldType)
, ("circumference", circumferenceFieldType)
] ]
hatType :: Out.ObjectType IO hatType :: Out.ObjectType IO
hatType = Out.ObjectType "Hat" Nothing [] hatType = Out.ObjectType "Hat" Nothing [] $ HashMap.fromList
$ HashMap.fromList
[ ("size", sizeFieldType) [ ("size", sizeFieldType)
, ("circumference", circumferenceFieldType) , ("circumference", circumferenceFieldType)
] ]
@ -70,12 +67,11 @@ sizeFieldType
$ pure $ snd size $ pure $ snd size
toSchema :: Text -> (Text, Value) -> Schema IO toSchema :: Text -> (Text, Value) -> Schema IO
toSchema t (_, resolve) = Schema toSchema t (_, resolve) = schema queryType Nothing Nothing mempty
{ query = queryType, mutation = Nothing, subscription = Nothing }
where where
unionMember = if t == "Hat" then hatType else shirtType garmentType = Out.UnionType "Garment" Nothing [hatType, shirtType]
typeNameField = Out.Field Nothing (Out.NamedScalarType string) mempty typeNameField = Out.Field Nothing (Out.NamedScalarType string) mempty
garmentField = Out.Field Nothing (Out.NamedObjectType unionMember) mempty garmentField = Out.Field Nothing (Out.NamedUnionType garmentType) mempty
queryType = queryType =
case t of case t of
"circumference" -> hatType "circumference" -> hatType
@ -111,23 +107,17 @@ spec = do
it "embeds inline fragments without type" $ do it "embeds inline fragments without type" $ do
let sourceQuery = [r|{ let sourceQuery = [r|{
garment {
circumference circumference
... { ... {
size size
} }
}
}|] }|]
resolvers = ("garment", Object $ HashMap.fromList [circumference, size]) actual <- graphql (toSchema "circumference" circumference) sourceQuery
actual <- graphql (toSchema "garment" resolvers) sourceQuery
let expected = HashMap.singleton "data" let expected = HashMap.singleton "data"
$ Aeson.object $ Aeson.object
[ "garment" .= Aeson.object
[ "circumference" .= (60 :: Int) [ "circumference" .= (60 :: Int)
, "size" .= ("L" :: Text) , "size" .= ("L" :: Text)
] ]
]
in actual `shouldResolveTo` expected in actual `shouldResolveTo` expected
it "evaluates fragments on Query" $ do it "evaluates fragments on Query" $ do
@ -183,21 +173,6 @@ spec = do
] ]
in actual `shouldResolveTo` expected in actual `shouldResolveTo` expected
it "rejects recursive fragments" $ do
let expected = HashMap.singleton "data" $ Aeson.object []
sourceQuery = [r|
{
...circumferenceFragment
}
fragment circumferenceFragment on Hat {
...circumferenceFragment
}
|]
actual <- graphql (toSchema "circumference" circumference) sourceQuery
actual `shouldResolveTo` expected
it "considers type condition" $ do it "considers type condition" $ do
let sourceQuery = [r| let sourceQuery = [r|
{ {

View File

@ -23,13 +23,11 @@ hatType = Out.ObjectType "Hat" Nothing []
$ ValueResolver (Out.Field Nothing (Out.NamedScalarType int) mempty) $ ValueResolver (Out.Field Nothing (Out.NamedScalarType int) mempty)
$ pure $ Int 60 $ pure $ Int 60
schema :: Schema IO garmentSchema :: Schema IO
schema = Schema garmentSchema = schema queryType (Just mutationType) Nothing mempty
{ query = Out.ObjectType "Query" Nothing [] hatFieldResolver
, mutation = Just $ Out.ObjectType "Mutation" Nothing [] incrementFieldResolver
, subscription = Nothing
}
where where
queryType = Out.ObjectType "Query" Nothing [] hatFieldResolver
mutationType = Out.ObjectType "Mutation" Nothing [] incrementFieldResolver
garment = pure $ Object $ HashMap.fromList garment = pure $ Object $ HashMap.fromList
[ ("circumference", Int 60) [ ("circumference", Int 60)
] ]
@ -57,7 +55,7 @@ spec =
[ "circumference" .= (60 :: Int) [ "circumference" .= (60 :: Int)
] ]
] ]
actual <- graphql schema querySource actual <- graphql garmentSchema querySource
actual `shouldResolveTo` expected actual `shouldResolveTo` expected
it "chooses Mutation" $ do it "chooses Mutation" $ do
@ -70,5 +68,5 @@ spec =
$ object $ object
[ "incrementCircumference" .= (61 :: Int) [ "incrementCircumference" .= (61 :: Int)
] ]
actual <- graphql schema querySource actual <- graphql garmentSchema querySource
actual `shouldResolveTo` expected actual `shouldResolveTo` expected

View File

@ -1,204 +0,0 @@
{-# LANGUAGE OverloadedStrings #-}
module Test.StarWars.Data
( Character
, StarWarsException(..)
, appearsIn
, artoo
, getDroid
, getDroid'
, getEpisode
, getFriends
, getHero
, getHuman
, id_
, homePlanet
, name_
, secretBackstory
, typeName
) where
import Control.Monad.Catch (Exception(..), MonadThrow(..), SomeException)
import Control.Applicative (Alternative(..), liftA2)
import Data.Maybe (catMaybes)
import Data.Text (Text)
import Data.Typeable (cast)
import Language.GraphQL.Error
import Language.GraphQL.Type
-- * Data
-- See https://github.com/graphql/graphql-js/blob/master/src/__tests__/starWarsData.js
-- ** Characters
type ID = Text
data CharCommon = CharCommon
{ _id_ :: ID
, _name :: Text
, _friends :: [ID]
, _appearsIn :: [Int]
} deriving (Show)
data Human = Human
{ _humanChar :: CharCommon
, homePlanet :: Text
}
data Droid = Droid
{ _droidChar :: CharCommon
, primaryFunction :: Text
}
type Character = Either Droid Human
id_ :: Character -> ID
id_ (Left x) = _id_ . _droidChar $ x
id_ (Right x) = _id_ . _humanChar $ x
name_ :: Character -> Text
name_ (Left x) = _name . _droidChar $ x
name_ (Right x) = _name . _humanChar $ x
friends :: Character -> [ID]
friends (Left x) = _friends . _droidChar $ x
friends (Right x) = _friends . _humanChar $ x
appearsIn :: Character -> [Int]
appearsIn (Left x) = _appearsIn . _droidChar $ x
appearsIn (Right x) = _appearsIn . _humanChar $ x
data StarWarsException = SecretBackstory | InvalidArguments
instance Show StarWarsException where
show SecretBackstory = "secretBackstory is secret."
show InvalidArguments = "Invalid arguments."
instance Exception StarWarsException where
toException = toException . ResolverException
fromException e = do
ResolverException resolverException <- fromException e
cast resolverException
secretBackstory :: Resolve (Either SomeException)
secretBackstory = throwM SecretBackstory
typeName :: Character -> Text
typeName = either (const "Droid") (const "Human")
luke :: Character
luke = Right luke'
luke' :: Human
luke' = Human
{ _humanChar = CharCommon
{ _id_ = "1000"
, _name = "Luke Skywalker"
, _friends = ["1002","1003","2000","2001"]
, _appearsIn = [4,5,6]
}
, homePlanet = "Tatooine"
}
vader :: Human
vader = Human
{ _humanChar = CharCommon
{ _id_ = "1001"
, _name = "Darth Vader"
, _friends = ["1004"]
, _appearsIn = [4,5,6]
}
, homePlanet = "Tatooine"
}
han :: Human
han = Human
{ _humanChar = CharCommon
{ _id_ = "1002"
, _name = "Han Solo"
, _friends = ["1000","1003","2001" ]
, _appearsIn = [4,5,6]
}
, homePlanet = mempty
}
leia :: Human
leia = Human
{ _humanChar = CharCommon
{ _id_ = "1003"
, _name = "Leia Organa"
, _friends = ["1000","1002","2000","2001"]
, _appearsIn = [4,5,6]
}
, homePlanet = "Alderaan"
}
tarkin :: Human
tarkin = Human
{ _humanChar = CharCommon
{ _id_ = "1004"
, _name = "Wilhuff Tarkin"
, _friends = ["1001"]
, _appearsIn = [4]
}
, homePlanet = mempty
}
threepio :: Droid
threepio = Droid
{ _droidChar = CharCommon
{ _id_ = "2000"
, _name = "C-3PO"
, _friends = ["1000","1002","1003","2001" ]
, _appearsIn = [ 4, 5, 6 ]
}
, primaryFunction = "Protocol"
}
artoo :: Character
artoo = Left artoo'
artoo' :: Droid
artoo' = Droid
{ _droidChar = CharCommon
{ _id_ = "2001"
, _name = "R2-D2"
, _friends = ["1000","1002","1003"]
, _appearsIn = [4,5,6]
}
, primaryFunction = "Astrometch"
}
-- ** Helper functions
getHero :: Int -> Character
getHero 5 = luke
getHero _ = artoo
getHuman :: ID -> Maybe Character
getHuman = fmap Right . getHuman'
getHuman' :: ID -> Maybe Human
getHuman' "1000" = pure luke'
getHuman' "1001" = pure vader
getHuman' "1002" = pure han
getHuman' "1003" = pure leia
getHuman' "1004" = pure tarkin
getHuman' _ = empty
getDroid :: ID -> Maybe Character
getDroid = fmap Left . getDroid'
getDroid' :: ID -> Maybe Droid
getDroid' "2000" = pure threepio
getDroid' "2001" = pure artoo'
getDroid' _ = empty
getFriends :: Character -> [Character]
getFriends char = catMaybes $ liftA2 (<|>) getDroid getHuman <$> friends char
getEpisode :: Int -> Maybe Text
getEpisode 4 = pure "NEW_HOPE"
getEpisode 5 = pure "EMPIRE"
getEpisode 6 = pure "JEDI"
getEpisode _ = empty

View File

@ -1,366 +0,0 @@
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
module Test.StarWars.QuerySpec
( spec
) where
import qualified Data.Aeson as Aeson
import Data.Aeson ((.=))
import qualified Data.HashMap.Strict as HashMap
import Data.Text (Text)
import Language.GraphQL
import Text.RawString.QQ (r)
import Test.Hspec.Expectations (Expectation, shouldBe)
import Test.Hspec (Spec, describe, it)
import Test.StarWars.Schema
-- * Test
-- See https://github.com/graphql/graphql-js/blob/master/src/__tests__/starWarsQueryTests.js
spec :: Spec
spec = describe "Star Wars Query Tests" $ do
describe "Basic Queries" $ do
it "R2-D2 hero" $ testQuery
[r| query HeroNameQuery {
hero {
id
}
}
|]
$ Aeson.object
[ "data" .= Aeson.object
[ "hero" .= Aeson.object ["id" .= ("2001" :: Text)]
]
]
it "R2-D2 ID and friends" $ testQuery
[r| query HeroNameAndFriendsQuery {
hero {
id
name
friends {
name
}
}
}
|]
$ Aeson.object [ "data" .= Aeson.object [
"hero" .= Aeson.object
[ "id" .= ("2001" :: Text)
, r2d2Name
, "friends" .=
[ Aeson.object [lukeName]
, Aeson.object [hanName]
, Aeson.object [leiaName]
]
]
]]
describe "Nested Queries" $ do
it "R2-D2 friends" $ testQuery
[r| query NestedQuery {
hero {
name
friends {
name
appearsIn
friends {
name
}
}
}
}
|]
$ Aeson.object [ "data" .= Aeson.object [
"hero" .= Aeson.object [
"name" .= ("R2-D2" :: Text)
, "friends" .= [
Aeson.object [
"name" .= ("Luke Skywalker" :: Text)
, "appearsIn" .= ["NEW_HOPE", "EMPIRE", "JEDI" :: Text]
, "friends" .= [
Aeson.object [hanName]
, Aeson.object [leiaName]
, Aeson.object [c3poName]
, Aeson.object [r2d2Name]
]
]
, Aeson.object [
hanName
, "appearsIn" .= ["NEW_HOPE", "EMPIRE", "JEDI" :: Text]
, "friends" .=
[ Aeson.object [lukeName]
, Aeson.object [leiaName]
, Aeson.object [r2d2Name]
]
]
, Aeson.object [
leiaName
, "appearsIn" .= ["NEW_HOPE", "EMPIRE", "JEDI" :: Text]
, "friends" .=
[ Aeson.object [lukeName]
, Aeson.object [hanName]
, Aeson.object [c3poName]
, Aeson.object [r2d2Name]
]
]
]
]
]]
it "Luke ID" $ testQuery
[r| query FetchLukeQuery {
human(id: "1000") {
name
}
}
|]
$ Aeson.object [ "data" .= Aeson.object
[ "human" .= Aeson.object [lukeName]
]]
it "Luke ID with variable" $ testQueryParams
(HashMap.singleton "someId" "1000")
[r| query FetchSomeIDQuery($someId: String!) {
human(id: $someId) {
name
}
}
|]
$ Aeson.object [ "data" .= Aeson.object [
"human" .= Aeson.object [lukeName]
]]
it "Han ID with variable" $ testQueryParams
(HashMap.singleton "someId" "1002")
[r| query FetchSomeIDQuery($someId: String!) {
human(id: $someId) {
name
}
}
|]
$ Aeson.object [ "data" .= Aeson.object [
"human" .= Aeson.object [hanName]
]]
it "Invalid ID" $ testQueryParams
(HashMap.singleton "id" "Not a valid ID")
[r| query humanQuery($id: String!) {
human(id: $id) {
name
}
}
|] $ Aeson.object ["data" .= Aeson.object ["human" .= Aeson.Null]]
it "Luke aliased" $ testQuery
[r| query FetchLukeAliased {
luke: human(id: "1000") {
name
}
}
|]
$ Aeson.object [ "data" .= Aeson.object [
"luke" .= Aeson.object [lukeName]
]]
it "R2-D2 ID and friends aliased" $ testQuery
[r| query HeroNameAndFriendsQuery {
hero {
id
name
friends {
friendName: name
}
}
}
|]
$ Aeson.object [ "data" .= Aeson.object [
"hero" .= Aeson.object [
"id" .= ("2001" :: Text)
, r2d2Name
, "friends" .=
[ Aeson.object ["friendName" .= ("Luke Skywalker" :: Text)]
, Aeson.object ["friendName" .= ("Han Solo" :: Text)]
, Aeson.object ["friendName" .= ("Leia Organa" :: Text)]
]
]
]]
it "Luke and Leia aliased" $ testQuery
[r| query FetchLukeAndLeiaAliased {
luke: human(id: "1000") {
name
}
leia: human(id: "1003") {
name
}
}
|]
$ Aeson.object [ "data" .= Aeson.object
[ "luke" .= Aeson.object [lukeName]
, "leia" .= Aeson.object [leiaName]
]]
describe "Fragments for complex queries" $ do
it "Aliases to query for duplicate content" $ testQuery
[r| query DuplicateFields {
luke: human(id: "1000") {
name
homePlanet
}
leia: human(id: "1003") {
name
homePlanet
}
}
|]
$ Aeson.object [ "data" .= Aeson.object [
"luke" .= Aeson.object [lukeName, tatooine]
, "leia" .= Aeson.object [leiaName, alderaan]
]]
it "Fragment for duplicate content" $ testQuery
[r| query UseFragment {
luke: human(id: "1000") {
...HumanFragment
}
leia: human(id: "1003") {
...HumanFragment
}
}
fragment HumanFragment on Human {
name
homePlanet
}
|]
$ Aeson.object [ "data" .= Aeson.object [
"luke" .= Aeson.object [lukeName, tatooine]
, "leia" .= Aeson.object [leiaName, alderaan]
]]
describe "__typename" $ do
it "R2D2 is a Droid" $ testQuery
[r| query CheckTypeOfR2 {
hero {
__typename
name
}
}
|]
$ Aeson.object ["data" .= Aeson.object [
"hero" .= Aeson.object
[ "__typename" .= ("Droid" :: Text)
, r2d2Name
]
]]
it "Luke is a human" $ testQuery
[r| query CheckTypeOfLuke {
hero(episode: EMPIRE) {
__typename
name
}
}
|]
$ Aeson.object ["data" .= Aeson.object [
"hero" .= Aeson.object
[ "__typename" .= ("Human" :: Text)
, lukeName
]
]]
describe "Errors in resolvers" $ do
it "error on secretBackstory" $ testQuery
[r|
query HeroNameQuery {
hero {
name
secretBackstory
}
}
|]
$ Aeson.object
[ "data" .= Aeson.object
[ "hero" .= Aeson.object
[ "name" .= ("R2-D2" :: Text)
, "secretBackstory" .= Aeson.Null
]
]
, "errors" .=
[ Aeson.object
["message" .= ("secretBackstory is secret." :: Text)]
]
]
it "Error in a list" $ testQuery
[r| query HeroNameQuery {
hero {
name
friends {
name
secretBackstory
}
}
}
|]
$ Aeson.object ["data" .= Aeson.object
[ "hero" .= Aeson.object
[ "name" .= ("R2-D2" :: Text)
, "friends" .=
[ Aeson.object
[ "name" .= ("Luke Skywalker" :: Text)
, "secretBackstory" .= Aeson.Null
]
, Aeson.object
[ "name" .= ("Han Solo" :: Text)
, "secretBackstory" .= Aeson.Null
]
, Aeson.object
[ "name" .= ("Leia Organa" :: Text)
, "secretBackstory" .= Aeson.Null
]
]
]
]
, "errors" .=
[ 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
[r| query HeroNameQuery {
mainHero: hero {
name
story: secretBackstory
}
}
|]
$ Aeson.object
[ "data" .= Aeson.object
[ "mainHero" .= Aeson.object
[ "name" .= ("R2-D2" :: Text)
, "story" .= Aeson.Null
]
]
, "errors" .=
[ Aeson.object
[ "message" .= ("secretBackstory is secret." :: Text)
]
]
]
where
lukeName = "name" .= ("Luke Skywalker" :: Text)
leiaName = "name" .= ("Leia Organa" :: Text)
hanName = "name" .= ("Han Solo" :: Text)
r2d2Name = "name" .= ("R2-D2" :: Text)
c3poName = "name" .= ("C-3PO" :: Text)
tatooine = "homePlanet" .= ("Tatooine" :: Text)
alderaan = "homePlanet" .= ("Alderaan" :: Text)
testQuery :: Text -> Aeson.Value -> Expectation
testQuery q expected =
let Right (Right actual) = graphql schema q
in Aeson.Object actual `shouldBe` expected
testQueryParams :: Aeson.Object -> Text -> Aeson.Value -> Expectation
testQueryParams f q expected =
let Right (Right actual) = graphqlSubs schema Nothing f q
in Aeson.Object actual `shouldBe` expected

View File

@ -1,154 +0,0 @@
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Test.StarWars.Schema
( schema
) where
import Control.Monad.Catch (MonadThrow(..), SomeException)
import Control.Monad.Trans.Reader (asks)
import qualified Data.HashMap.Strict as HashMap
import Data.Maybe (catMaybes)
import Data.Text (Text)
import Language.GraphQL.Type
import qualified Language.GraphQL.Type.In as In
import qualified Language.GraphQL.Type.Out as Out
import Test.StarWars.Data
import Prelude hiding (id)
-- See https://github.com/graphql/graphql-js/blob/master/src/__tests__/starWarsSchema.js
schema :: Schema (Either SomeException)
schema = Schema
{ query = queryType
, mutation = Nothing
, subscription = Nothing
}
where
queryType = Out.ObjectType "Query" Nothing [] $ HashMap.fromList
[ ("hero", heroFieldResolver)
, ("human", humanFieldResolver)
, ("droid", droidFieldResolver)
]
heroField = Out.Field Nothing (Out.NamedObjectType heroObject)
$ HashMap.singleton "episode"
$ In.Argument Nothing (In.NamedEnumType episodeEnum) Nothing
heroFieldResolver = ValueResolver heroField hero
humanField = Out.Field Nothing (Out.NamedObjectType heroObject)
$ HashMap.singleton "id"
$ In.Argument Nothing (In.NonNullScalarType string) Nothing
humanFieldResolver = ValueResolver humanField human
droidField = Out.Field Nothing (Out.NamedObjectType droidObject) mempty
droidFieldResolver = ValueResolver droidField droid
heroObject :: Out.ObjectType (Either SomeException)
heroObject = Out.ObjectType "Human" Nothing [] $ HashMap.fromList
[ ("id", idFieldType)
, ("name", nameFieldType)
, ("friends", friendsFieldType)
, ("appearsIn", appearsInField)
, ("homePlanet", homePlanetFieldType)
, ("secretBackstory", secretBackstoryFieldType)
, ("__typename", typenameFieldType)
]
where
homePlanetFieldType
= ValueResolver (Out.Field Nothing (Out.NamedScalarType string) mempty)
$ idField "homePlanet"
droidObject :: Out.ObjectType (Either SomeException)
droidObject = Out.ObjectType "Droid" Nothing [] $ HashMap.fromList
[ ("id", idFieldType)
, ("name", nameFieldType)
, ("friends", friendsFieldType)
, ("appearsIn", appearsInField)
, ("primaryFunction", primaryFunctionFieldType)
, ("secretBackstory", secretBackstoryFieldType)
, ("__typename", typenameFieldType)
]
where
primaryFunctionFieldType
= ValueResolver (Out.Field Nothing (Out.NamedScalarType string) mempty)
$ idField "primaryFunction"
typenameFieldType :: Resolver (Either SomeException)
typenameFieldType
= ValueResolver (Out.Field Nothing (Out.NamedScalarType string) mempty)
$ idField "__typename"
idFieldType :: Resolver (Either SomeException)
idFieldType
= ValueResolver (Out.Field Nothing (Out.NamedScalarType id) mempty)
$ idField "id"
nameFieldType :: Resolver (Either SomeException)
nameFieldType
= ValueResolver (Out.Field Nothing (Out.NamedScalarType string) mempty)
$ idField "name"
friendsFieldType :: Resolver (Either SomeException)
friendsFieldType
= ValueResolver (Out.Field Nothing fieldType mempty)
$ idField "friends"
where
fieldType = Out.ListType $ Out.NamedObjectType droidObject
appearsInField :: Resolver (Either SomeException)
appearsInField
= ValueResolver (Out.Field (Just description) fieldType mempty)
$ idField "appearsIn"
where
fieldType = Out.ListType $ Out.NamedEnumType episodeEnum
description = "Which movies they appear in."
secretBackstoryFieldType :: Resolver (Either SomeException)
secretBackstoryFieldType = ValueResolver field secretBackstory
where
field = Out.Field Nothing (Out.NamedScalarType string) mempty
idField :: Text -> Resolve (Either SomeException)
idField f = do
v <- asks values
let (Object v') = v
pure $ v' HashMap.! f
episodeEnum :: EnumType
episodeEnum = EnumType "Episode" (Just description)
$ HashMap.fromList [newHope, empire, jedi]
where
description = "One of the films in the Star Wars Trilogy"
newHope = ("NEW_HOPE", EnumValue $ Just "Released in 1977.")
empire = ("EMPIRE", EnumValue $ Just "Released in 1980.")
jedi = ("JEDI", EnumValue $ Just "Released in 1983.")
hero :: Resolve (Either SomeException)
hero = do
episode <- argument "episode"
pure $ character $ case episode of
Enum "NEW_HOPE" -> getHero 4
Enum "EMPIRE" -> getHero 5
Enum "JEDI" -> getHero 6
_ -> artoo
human :: Resolve (Either SomeException)
human = do
id' <- argument "id"
case id' of
String i -> pure $ maybe Null character $ getHuman i >>= Just
_ -> throwM InvalidArguments
droid :: Resolve (Either SomeException)
droid = do
id' <- argument "id"
case id' of
String i -> pure $ maybe Null character $ getDroid i >>= Just
_ -> throwM InvalidArguments
character :: Character -> Value
character char = Object $ HashMap.fromList
[ ("id", String $ id_ char)
, ("name", String $ name_ char)
, ("friends", List $ character <$> getFriends char)
, ("appearsIn", List $ Enum <$> catMaybes (getEpisode <$> appearsIn char))
, ("homePlanet", String $ either mempty homePlanet char)
, ("__typename", String $ typeName char)
]