-
Notifications
You must be signed in to change notification settings - Fork 44
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #78 from wravery/master
Add more documentation
- Loading branch information
Showing
8 changed files
with
432 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
# Directives | ||
|
||
Directives in GraphQL are extensible annotations which alter the runtime | ||
evaluation of a query or which add information to the `schema` definition. | ||
They always begin with an `@`. There are three built-in directives which this | ||
library automatically handles: | ||
|
||
1. `@include(if: Boolean!)`: Only resolve this field and include it in the | ||
results if the `if` argument evaluates to `true`. | ||
2. `@skip(if: Boolean!)`: Only resolve this field and include it in the | ||
results if the `if` argument evaluates to `false`. | ||
3. `@deprecated(reason: String)`: Mark the field or enum value as deprecated | ||
through introspection with the specified `reason` string. | ||
|
||
The `schema` can also define custom `directives` which are valid on different | ||
elements of the `query`. The library does not handle them automatically, but it | ||
will pass them to the `getField` implementations through the | ||
`graphql::service::FieldParams` struct (see [fieldparams.md](fieldparams.md) | ||
for more information). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
# Common Field Parameters | ||
|
||
The `resolveField` methods generated by `schemagen` will unpack any arguments | ||
matching the `schema` from the `query` and pass those to the `getField` method | ||
defined by the implementer. However, the implementer might need to inspect | ||
shared state or `directives` from the `query`, so the `resolveField` method | ||
also packs that information into a `graphql::service::FieldParams` struct and | ||
passes it to every `getField` method as the first parameter. | ||
|
||
## Details of Field Parameters | ||
|
||
The `graphql::service::FieldParams` struct is declared in [GraphQLService.h](../include/graphqlservice/GraphQLService.h): | ||
```cpp | ||
// Pass a common bundle of parameters to all of the generated Object::getField accessors in a SelectionSet | ||
struct SelectionSetParams | ||
{ | ||
// The lifetime of each of these borrowed references is guaranteed until the future returned | ||
// by the accessor is resolved or destroyed. They are owned by the OperationData shared pointer. | ||
const std::shared_ptr<RequestState>& state; | ||
const response::Value& operationDirectives; | ||
const response::Value& fragmentDefinitionDirectives; | ||
|
||
// Fragment directives are shared for all fields in that fragment, but they aren't kept alive | ||
// after the call to the last accessor in the fragment. If you need to keep them alive longer, | ||
// you'll need to explicitly copy them into other instances of response::Value. | ||
const response::Value& fragmentSpreadDirectives; | ||
const response::Value& inlineFragmentDirectives; | ||
}; | ||
|
||
// Pass a common bundle of parameters to all of the generated Object::getField accessors. | ||
struct FieldParams : SelectionSetParams | ||
{ | ||
explicit FieldParams(const SelectionSetParams& selectionSetParams, response::Value&& directives); | ||
|
||
// Each field owns its own field-specific directives. Once the accessor returns it will be destroyed, | ||
// but you can move it into another instance of response::Value to keep it alive longer. | ||
response::Value fieldDirectives; | ||
}; | ||
``` | ||
|
||
### Request State | ||
|
||
The `SelectionSetParams::state` member is a reference to the | ||
`std::shared_ptr<graphql::service::RequestState>` parameter passed to | ||
`Request::resolve` (see [resolvers.md](./resolvers.md) for more info): | ||
```cpp | ||
// The RequestState is nullable, but if you have multiple threads processing requests and there's any | ||
// per-request state that you want to maintain throughout the request (e.g. optimizing or batching | ||
// backend requests), you can inherit from RequestState and pass it to Request::resolve to correlate the | ||
// asynchronous/recursive callbacks and accumulate state in it. | ||
struct RequestState : std::enable_shared_from_this<RequestState> | ||
{ | ||
}; | ||
``` | ||
|
||
### Scoped Directives | ||
|
||
Each of the `directives` members contains the values of the `directives` and | ||
any of their arguments which were in effect at that scope of the `query`. | ||
Implementers may inspect those values in the call to `getField` and alter their | ||
behavior based on those custom `directives`. | ||
|
||
As noted in the comments, the `fragmentSpreadDirectives` and | ||
`inlineFragmentDirectives` are borrowed `const` references, shared accross | ||
calls to multiple `getField` methods, but they will not be kept alive after | ||
the relevant `SelectionSet` has been resolved. The `fieldDirectives` member is | ||
passed by value and is not shared with other `getField` method calls, but it | ||
will not be kept alive after that call returns. It's up to the implementer to | ||
capture the values in these `directives` which they might need for asynchronous | ||
evaulation after the call to the current `getField` method has returned. | ||
|
||
The implementer does not need to capture the values of `operationDirectives` | ||
or `fragmentDefinitionDirectives` because those are kept alive until the | ||
`operation` and all of its `std::future` results are resolved. Although they | ||
passed by `const` reference, the reference should always be valid as long as | ||
there's a pending result from the `getField` call. | ||
|
||
## Related Documents | ||
|
||
1. The `getField` methods are discussed in more detail in [resolvers.md](./resolvers.md). | ||
2. Built-in and custom `directives` are discussed in [directives.md](./directives.md). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
# Converting to/from JSON | ||
|
||
## `graphqljson` Library Target | ||
|
||
Converting between `graphql::response::Value` in [GraphQLResponse.h](../include/GraphQLResponse.h) | ||
and JSON strings is done in an optional library target called `graphqljson`. | ||
|
||
## Default RapidJSON Implementation | ||
|
||
The included implementation uses [RapidJSON](https://github.com/Tencent/rapidjson) | ||
release 1.1.0, but if you don't need JSON support, or you want to integrate | ||
a different JSON library, you can set `GRAPHQL_USE_RAPIDJSON=OFF` in your | ||
CMake configuration. | ||
|
||
## Using Custom JSON Libraries | ||
|
||
If you want to use a different JSON library, you can add implementations of | ||
the functions in [JSONResponse.h](../include/JSONResponse.h): | ||
```cpp | ||
namespace graphql::response { | ||
|
||
std::string toJSON(Value&& response); | ||
|
||
Value parseJSON(const std::string& json); | ||
|
||
} /* namespace graphql::response */ | ||
``` | ||
You will also need to update the [CMakeLists.txt](../src/CMakeLists.txt) file | ||
in the [../src](../src) directory to add your own implementation. See the | ||
comment in that file for more information: | ||
```cmake | ||
# RapidJSON is the only option for JSON serialization used in this project, but if you want | ||
# to use another JSON library you can implement an alternate version of the functions in | ||
# JSONResponse.cpp to serialize to and from GraphQLResponse and build graphqljson from that. | ||
# You will also need to define how to build the graphqljson library target with your | ||
# implementation, and you should set BUILD_GRAPHQLJSON so that the test dependencies know | ||
# about your version of graphqljson. | ||
option(GRAPHQL_USE_RAPIDJSON "Use RapidJSON for JSON serialization." ON) | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
# Parsing GraphQL Documents | ||
|
||
## PEGTL | ||
|
||
As mentioned in the [README](../README.md), `cppgraphqlgen` uses the | ||
[Parsing Expression Grammar Template Library (PEGTL)](https://github.com/taocpp/PEGTL) | ||
release 3.0.0, which is part of [The Art of C++](https://taocpp.github.io/) | ||
library collection. I've added this as a sub-module, so you do not need to | ||
install this separately. If you already have 3.0.0 installed where CMake can | ||
find it, it will use that instead of the sub-module and avoid installing | ||
another copy of PEGTL. _Note: PEGTL 3.0.0 is currently at pre-release._ | ||
|
||
It uses the [contrib/parse_tree.hpp](../PEGTL/include/tao/pegtl/contrib/parse_tree.hpp) | ||
module to build an AST automatically while parsing the document. The AST and | ||
the underlying grammar rules are tuned to the needs of `cppgraphqlgen`, but if | ||
you have another use for a GraphQL parser you could probably make a few small | ||
tweaks to include additional information in the rules or in the resulting AST. | ||
You could also use the grammar without the AST module if you want to handle | ||
the parsing callbacks another way. The grammar itself is defined in | ||
[GraphQLGrammar.h](../include/graphqlservice/GraphQLGrammar.h), and the AST | ||
selector callbacks are all defined in [GraphQLTree.cpp](../src/GraphQLTree.cpp). | ||
The grammar handles both the schema definition syntax which is used in | ||
`schemagen`, and the query/mutation/subscription operation syntax used in | ||
`Request::resolve` and `Request::subscribe`. | ||
|
||
## Utilities | ||
|
||
The [GraphQLParse.h](../include/graphqlservice/GraphQLParse.h) header includes | ||
several utility methods to help generate an AST from a `std::string_view` | ||
(`parseString`), an input file (`parseFile`), or using a | ||
[UDL](https://en.cppreference.com/w/cpp/language/user_literal) (`_graphql`) | ||
for hardcoded documents. | ||
|
||
The UDL is used throughout the sample unit tests and in `schemagen` for the | ||
hard-coded introspection schema. It will be useful for additional unit tests | ||
against your own custom schema. | ||
|
||
At runtime, you will probably call `parseString` most often to handle dynamic | ||
queries. If you have persisted queries saved to the file system or you are | ||
using a snapshot/[Approval Testing](https://approvaltests.com/) strategy you | ||
might also use `parseFile` to parse queries saved to text files. | ||
|
||
## Encoding | ||
|
||
The document must use a UTF-8 encoding. If you need to handle documents in | ||
another encoding you will need to convert them to UTF-8 before parsing. | ||
|
||
If you need to convert the encoding at runtime, I would recommend using | ||
`std::wstring_convert`, with the cavevat that it has been | ||
[deprecated](https://en.cppreference.com/w/cpp/locale/wstring_convert) in | ||
C++17. You could keep using it until it is replaced in the standard, you | ||
could use a portable non-standard library like | ||
[ICU](http://site.icu-project.org/design/cpp), or you could use | ||
platform-specific conversion routines like | ||
[WideCharToMultiByte](https://docs.microsoft.com/en-us/windows/win32/api/stringapiset/nf-stringapiset-widechartomultibyte) on Windows. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
# Field Resolvers | ||
|
||
GraphQL schemas define types with named fields, and each of those fields may | ||
take arguments which alter the behavior of that field. You can think of | ||
`fields` much like methods on an object instance in OOP (Object Oriented | ||
Programming). Each field is implemented using a `resolver`, which may | ||
recursively invoke additional `resolvers` for fields of the resulting objects, | ||
e.g.: | ||
```graphql | ||
query { | ||
foo(id: "bar") { | ||
baz | ||
} | ||
} | ||
``` | ||
|
||
This query would invoke the `resolver` for the `foo field` on the top-level | ||
`query` object, passing it the string `"bar"` as the `id` argument. Then it | ||
would invoke the `resolver` for the `baz` field on the result of the `foo | ||
field resolver`. | ||
|
||
## Top-level Resolvers | ||
|
||
The `schema` type in GraphQL defines the types for top-level operation types. | ||
By convention, these are often named after the operation type, although you | ||
could give them different names: | ||
```graphql | ||
schema { | ||
query: Query | ||
mutation: Mutation | ||
subscription: Subscription | ||
} | ||
``` | ||
|
||
Executing a query or mutation starts by calling `Request::resolve` from [GraphQLService.h](../include/graphqlservice/GraphQLService.h): | ||
```cpp | ||
std::future<response::Value> resolve(const std::shared_ptr<RequestState>& state, const peg::ast_node& root, const std::string& operationName, response::Value&& variables) const; | ||
``` | ||
By default, the `std::future` results are resolved on-demand but synchronously, | ||
using `std::launch::deferred` with the `std::async` function. You can also use | ||
an override of `Request::resolve` which lets you substitute the | ||
`std::launch::async` option to begin executing the query on multiple threads | ||
in parallel: | ||
```cpp | ||
std::future<response::Value> resolve(std::launch launch, const std::shared_ptr<RequestState>& state, const peg::ast_node& root, const std::string& operationName, response::Value&& variables) const; | ||
``` | ||
|
||
### `graphql::service::Request` and `graphql::<schema>::Operations` | ||
|
||
Anywhere in the documentation where it mentions `graphql::service::Request` | ||
methods, the concrete type will actually be `graphql::<schema>::Operations`. | ||
This `class` is defined by `schemagen` and inherits from | ||
`graphql::service::Request`. It links the top-level objects for the custom | ||
schema to the `resolve` methods on its base class. See | ||
`graphql::today::Operations` in [TodaySchema.h](../samples/separate/TodaySchema.h) | ||
for an example. | ||
|
||
## Generated Service Schema | ||
|
||
The `schemagen` tool generates C++ types in the `graphql::<schema>::object` | ||
namespace with `resolveField` methods for each `field` which parse the | ||
arguments from the `query` and automatically dispatch the call to a `getField` | ||
virtual method to retrieve the `field` result. On `object` types, it will also | ||
recursively call the `resolvers` for each of the `fields` in the nested | ||
`SelectionSet`. See for example the generated | ||
`graphql::today::object::Appointment` object from the `today` sample in | ||
[AppointmentObject.h](../samples/separate/AppointmentObject.h). | ||
```cpp | ||
std::future<response::Value> resolveId(service::ResolverParams&& params); | ||
``` | ||
In this example, the `resolveId` method invokes `getId`: | ||
```cpp | ||
virtual service::FieldResult<response::IdType> getId(service::FieldParams&& params) const override; | ||
``` | ||
|
||
There are a couple of interesting quirks in this example: | ||
1. The `Appointment object` implements and inherits from the `Node interface`, | ||
which already declared `getId` as a pure-virtual method. That's what the | ||
`override` keyword refers to. | ||
2. This schema was generated with default stub implementations (without the | ||
`schemagen --no-stubs` parameter) which speed up initial development with NYI | ||
(Not Yet Implemented) stubs. With that parameter, there would be no | ||
declaration of `Appointment::getId` since it would inherit a pure-virtual | ||
declaration and the implementer would need to define an override on the | ||
concrete implementation of `graphql::today::object::Appointment`. The NYI stub | ||
will throw a `std::runtime_error`, which the `resolver` converts into an entry | ||
in the `response errors` collection: | ||
```cpp | ||
throw std::runtime_error(R"ex(Appointment::getId is not implemented)ex"); | ||
``` | ||
Although the `id field` does not take any arguments according to the sample | ||
[schema](../samples/today/schema.today.graphql), this example also shows how | ||
every `getField` method takes a `graphql::service::FieldParams` struct as | ||
its first parameter. There are more details on this in the [fieldparams.md](./fieldparams.md) | ||
document. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
# Query Responses | ||
|
||
## Value Types | ||
|
||
As the comment in | ||
[GraphQLResponse.h](../include/graphqlservice/GraphQLResponse.h) says, GraphQL | ||
responses are not technically JSON-specific, although that is probably the most | ||
common way of representing them. These are the primitive types that may be | ||
represented in GraphQL, as of the | ||
[June 2018 spec](https://facebook.github.io/graphql/June2018/#sec-Serialization-Format): | ||
|
||
```c++ | ||
enum class Type : uint8_t | ||
{ | ||
Map, // JSON Object | ||
List, // JSON Array | ||
String, // JSON String | ||
Null, // JSON null | ||
Boolean, // JSON true or false | ||
Int, // JSON Number | ||
Float, // JSON Number | ||
EnumValue, // JSON String | ||
Scalar, // JSON any type | ||
}; | ||
``` | ||
|
||
## Common Accessors | ||
|
||
Anywhere that a GraphQL result, a scalar type, or a GraphQL value literal is | ||
used, it's represented in `cppgraphqlgen` using an instance of | ||
`graphql::response::Value`. These can be constructed with any of the types in | ||
the `graphql::response::Type` enum, and depending on the type with which they | ||
are initialized, different accessors will be enabled. | ||
|
||
Every type implements specializations for some subset of `get()` which does | ||
not allocate any memory, `set(...)` which takes an r-value, and `release()` | ||
which transfers ownership along with any extra allocations to the caller. | ||
Which of these methods are supported and what C++ types they use are | ||
determined by the `ValueTypeTraits<ValueType>` template and its | ||
specializations. | ||
|
||
## Map and List | ||
|
||
`Map` and `List` types enable collection methods like `reserve(size_t)`, | ||
`size()`, and `emplace_back(...)`. `Map` additionally implements `begin()` | ||
and `end()` for range-based for loops and `find(const std::string&)` and | ||
`operator[](const std::string&)` for key-based lookups. `List` has an | ||
`operator[](size_t)` for index-based instead of key-based lookups. |
Oops, something went wrong.