Skip to content

Commit a70d822

Browse files
authored
Merge pull request #78 from wravery/master
Add more documentation
2 parents bbc8b73 + 49e5b0f commit a70d822

File tree

8 files changed

+432
-0
lines changed

8 files changed

+432
-0
lines changed

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,20 @@ the `graphql::service` namespace. Take a look at [UnifiedToday.h](samples/today/
130130
[UnifiedToday.cpp](samples/today/UnifiedToday.cpp) to see a sample implementation of a custom schema defined
131131
in [schema.today.graphql](samples/today/schema.today.graphql) for testing purposes.
132132

133+
### Additional Documentation
134+
135+
There are some more targeted documents in the [doc](./doc) directory:
136+
137+
* [Parsing GraphQL](./doc/parsing.md)
138+
* [Query Responses](./doc/responses.md)
139+
* [JSON Representation](./doc/json.md)
140+
* [Field Resolvers](./doc/resolvers.md)
141+
* [Field Parameters](./doc/fieldparams.md)
142+
* [Directives](./doc/directives.md)
143+
* [Subscriptions](./doc/subscriptions.md)
144+
145+
### Samples
146+
133147
All of the generated files are in the [samples](samples/) directory. There are two different versions of
134148
the generated code, one which creates a single pair of files (`samples/unified/`), and one which uses the
135149
`--separate-files` flag with `schemagen` to generate individual header and source files (`samples/separate/`)

doc/directives.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Directives
2+
3+
Directives in GraphQL are extensible annotations which alter the runtime
4+
evaluation of a query or which add information to the `schema` definition.
5+
They always begin with an `@`. There are three built-in directives which this
6+
library automatically handles:
7+
8+
1. `@include(if: Boolean!)`: Only resolve this field and include it in the
9+
results if the `if` argument evaluates to `true`.
10+
2. `@skip(if: Boolean!)`: Only resolve this field and include it in the
11+
results if the `if` argument evaluates to `false`.
12+
3. `@deprecated(reason: String)`: Mark the field or enum value as deprecated
13+
through introspection with the specified `reason` string.
14+
15+
The `schema` can also define custom `directives` which are valid on different
16+
elements of the `query`. The library does not handle them automatically, but it
17+
will pass them to the `getField` implementations through the
18+
`graphql::service::FieldParams` struct (see [fieldparams.md](fieldparams.md)
19+
for more information).

doc/fieldparams.md

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# Common Field Parameters
2+
3+
The `resolveField` methods generated by `schemagen` will unpack any arguments
4+
matching the `schema` from the `query` and pass those to the `getField` method
5+
defined by the implementer. However, the implementer might need to inspect
6+
shared state or `directives` from the `query`, so the `resolveField` method
7+
also packs that information into a `graphql::service::FieldParams` struct and
8+
passes it to every `getField` method as the first parameter.
9+
10+
## Details of Field Parameters
11+
12+
The `graphql::service::FieldParams` struct is declared in [GraphQLService.h](../include/graphqlservice/GraphQLService.h):
13+
```cpp
14+
// Pass a common bundle of parameters to all of the generated Object::getField accessors in a SelectionSet
15+
struct SelectionSetParams
16+
{
17+
// The lifetime of each of these borrowed references is guaranteed until the future returned
18+
// by the accessor is resolved or destroyed. They are owned by the OperationData shared pointer.
19+
const std::shared_ptr<RequestState>& state;
20+
const response::Value& operationDirectives;
21+
const response::Value& fragmentDefinitionDirectives;
22+
23+
// Fragment directives are shared for all fields in that fragment, but they aren't kept alive
24+
// after the call to the last accessor in the fragment. If you need to keep them alive longer,
25+
// you'll need to explicitly copy them into other instances of response::Value.
26+
const response::Value& fragmentSpreadDirectives;
27+
const response::Value& inlineFragmentDirectives;
28+
};
29+
30+
// Pass a common bundle of parameters to all of the generated Object::getField accessors.
31+
struct FieldParams : SelectionSetParams
32+
{
33+
explicit FieldParams(const SelectionSetParams& selectionSetParams, response::Value&& directives);
34+
35+
// Each field owns its own field-specific directives. Once the accessor returns it will be destroyed,
36+
// but you can move it into another instance of response::Value to keep it alive longer.
37+
response::Value fieldDirectives;
38+
};
39+
```
40+
41+
### Request State
42+
43+
The `SelectionSetParams::state` member is a reference to the
44+
`std::shared_ptr<graphql::service::RequestState>` parameter passed to
45+
`Request::resolve` (see [resolvers.md](./resolvers.md) for more info):
46+
```cpp
47+
// The RequestState is nullable, but if you have multiple threads processing requests and there's any
48+
// per-request state that you want to maintain throughout the request (e.g. optimizing or batching
49+
// backend requests), you can inherit from RequestState and pass it to Request::resolve to correlate the
50+
// asynchronous/recursive callbacks and accumulate state in it.
51+
struct RequestState : std::enable_shared_from_this<RequestState>
52+
{
53+
};
54+
```
55+
56+
### Scoped Directives
57+
58+
Each of the `directives` members contains the values of the `directives` and
59+
any of their arguments which were in effect at that scope of the `query`.
60+
Implementers may inspect those values in the call to `getField` and alter their
61+
behavior based on those custom `directives`.
62+
63+
As noted in the comments, the `fragmentSpreadDirectives` and
64+
`inlineFragmentDirectives` are borrowed `const` references, shared accross
65+
calls to multiple `getField` methods, but they will not be kept alive after
66+
the relevant `SelectionSet` has been resolved. The `fieldDirectives` member is
67+
passed by value and is not shared with other `getField` method calls, but it
68+
will not be kept alive after that call returns. It's up to the implementer to
69+
capture the values in these `directives` which they might need for asynchronous
70+
evaulation after the call to the current `getField` method has returned.
71+
72+
The implementer does not need to capture the values of `operationDirectives`
73+
or `fragmentDefinitionDirectives` because those are kept alive until the
74+
`operation` and all of its `std::future` results are resolved. Although they
75+
passed by `const` reference, the reference should always be valid as long as
76+
there's a pending result from the `getField` call.
77+
78+
## Related Documents
79+
80+
1. The `getField` methods are discussed in more detail in [resolvers.md](./resolvers.md).
81+
2. Built-in and custom `directives` are discussed in [directives.md](./directives.md).

doc/json.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Converting to/from JSON
2+
3+
## `graphqljson` Library Target
4+
5+
Converting between `graphql::response::Value` in [GraphQLResponse.h](../include/GraphQLResponse.h)
6+
and JSON strings is done in an optional library target called `graphqljson`.
7+
8+
## Default RapidJSON Implementation
9+
10+
The included implementation uses [RapidJSON](https://github.com/Tencent/rapidjson)
11+
release 1.1.0, but if you don't need JSON support, or you want to integrate
12+
a different JSON library, you can set `GRAPHQL_USE_RAPIDJSON=OFF` in your
13+
CMake configuration.
14+
15+
## Using Custom JSON Libraries
16+
17+
If you want to use a different JSON library, you can add implementations of
18+
the functions in [JSONResponse.h](../include/JSONResponse.h):
19+
```cpp
20+
namespace graphql::response {
21+
22+
std::string toJSON(Value&& response);
23+
24+
Value parseJSON(const std::string& json);
25+
26+
} /* namespace graphql::response */
27+
```
28+
29+
You will also need to update the [CMakeLists.txt](../src/CMakeLists.txt) file
30+
in the [../src](../src) directory to add your own implementation. See the
31+
comment in that file for more information:
32+
```cmake
33+
# RapidJSON is the only option for JSON serialization used in this project, but if you want
34+
# to use another JSON library you can implement an alternate version of the functions in
35+
# JSONResponse.cpp to serialize to and from GraphQLResponse and build graphqljson from that.
36+
# You will also need to define how to build the graphqljson library target with your
37+
# implementation, and you should set BUILD_GRAPHQLJSON so that the test dependencies know
38+
# about your version of graphqljson.
39+
option(GRAPHQL_USE_RAPIDJSON "Use RapidJSON for JSON serialization." ON)
40+
```

doc/parsing.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Parsing GraphQL Documents
2+
3+
## PEGTL
4+
5+
As mentioned in the [README](../README.md), `cppgraphqlgen` uses the
6+
[Parsing Expression Grammar Template Library (PEGTL)](https://github.com/taocpp/PEGTL)
7+
release 3.0.0, which is part of [The Art of C++](https://taocpp.github.io/)
8+
library collection. I've added this as a sub-module, so you do not need to
9+
install this separately. If you already have 3.0.0 installed where CMake can
10+
find it, it will use that instead of the sub-module and avoid installing
11+
another copy of PEGTL. _Note: PEGTL 3.0.0 is currently at pre-release._
12+
13+
It uses the [contrib/parse_tree.hpp](../PEGTL/include/tao/pegtl/contrib/parse_tree.hpp)
14+
module to build an AST automatically while parsing the document. The AST and
15+
the underlying grammar rules are tuned to the needs of `cppgraphqlgen`, but if
16+
you have another use for a GraphQL parser you could probably make a few small
17+
tweaks to include additional information in the rules or in the resulting AST.
18+
You could also use the grammar without the AST module if you want to handle
19+
the parsing callbacks another way. The grammar itself is defined in
20+
[GraphQLGrammar.h](../include/graphqlservice/GraphQLGrammar.h), and the AST
21+
selector callbacks are all defined in [GraphQLTree.cpp](../src/GraphQLTree.cpp).
22+
The grammar handles both the schema definition syntax which is used in
23+
`schemagen`, and the query/mutation/subscription operation syntax used in
24+
`Request::resolve` and `Request::subscribe`.
25+
26+
## Utilities
27+
28+
The [GraphQLParse.h](../include/graphqlservice/GraphQLParse.h) header includes
29+
several utility methods to help generate an AST from a `std::string_view`
30+
(`parseString`), an input file (`parseFile`), or using a
31+
[UDL](https://en.cppreference.com/w/cpp/language/user_literal) (`_graphql`)
32+
for hardcoded documents.
33+
34+
The UDL is used throughout the sample unit tests and in `schemagen` for the
35+
hard-coded introspection schema. It will be useful for additional unit tests
36+
against your own custom schema.
37+
38+
At runtime, you will probably call `parseString` most often to handle dynamic
39+
queries. If you have persisted queries saved to the file system or you are
40+
using a snapshot/[Approval Testing](https://approvaltests.com/) strategy you
41+
might also use `parseFile` to parse queries saved to text files.
42+
43+
## Encoding
44+
45+
The document must use a UTF-8 encoding. If you need to handle documents in
46+
another encoding you will need to convert them to UTF-8 before parsing.
47+
48+
If you need to convert the encoding at runtime, I would recommend using
49+
`std::wstring_convert`, with the cavevat that it has been
50+
[deprecated](https://en.cppreference.com/w/cpp/locale/wstring_convert) in
51+
C++17. You could keep using it until it is replaced in the standard, you
52+
could use a portable non-standard library like
53+
[ICU](http://site.icu-project.org/design/cpp), or you could use
54+
platform-specific conversion routines like
55+
[WideCharToMultiByte](https://docs.microsoft.com/en-us/windows/win32/api/stringapiset/nf-stringapiset-widechartomultibyte) on Windows.

doc/resolvers.md

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# Field Resolvers
2+
3+
GraphQL schemas define types with named fields, and each of those fields may
4+
take arguments which alter the behavior of that field. You can think of
5+
`fields` much like methods on an object instance in OOP (Object Oriented
6+
Programming). Each field is implemented using a `resolver`, which may
7+
recursively invoke additional `resolvers` for fields of the resulting objects,
8+
e.g.:
9+
```graphql
10+
query {
11+
foo(id: "bar") {
12+
baz
13+
}
14+
}
15+
```
16+
17+
This query would invoke the `resolver` for the `foo field` on the top-level
18+
`query` object, passing it the string `"bar"` as the `id` argument. Then it
19+
would invoke the `resolver` for the `baz` field on the result of the `foo
20+
field resolver`.
21+
22+
## Top-level Resolvers
23+
24+
The `schema` type in GraphQL defines the types for top-level operation types.
25+
By convention, these are often named after the operation type, although you
26+
could give them different names:
27+
```graphql
28+
schema {
29+
query: Query
30+
mutation: Mutation
31+
subscription: Subscription
32+
}
33+
```
34+
35+
Executing a query or mutation starts by calling `Request::resolve` from [GraphQLService.h](../include/graphqlservice/GraphQLService.h):
36+
```cpp
37+
std::future<response::Value> resolve(const std::shared_ptr<RequestState>& state, const peg::ast_node& root, const std::string& operationName, response::Value&& variables) const;
38+
```
39+
By default, the `std::future` results are resolved on-demand but synchronously,
40+
using `std::launch::deferred` with the `std::async` function. You can also use
41+
an override of `Request::resolve` which lets you substitute the
42+
`std::launch::async` option to begin executing the query on multiple threads
43+
in parallel:
44+
```cpp
45+
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;
46+
```
47+
48+
### `graphql::service::Request` and `graphql::<schema>::Operations`
49+
50+
Anywhere in the documentation where it mentions `graphql::service::Request`
51+
methods, the concrete type will actually be `graphql::<schema>::Operations`.
52+
This `class` is defined by `schemagen` and inherits from
53+
`graphql::service::Request`. It links the top-level objects for the custom
54+
schema to the `resolve` methods on its base class. See
55+
`graphql::today::Operations` in [TodaySchema.h](../samples/separate/TodaySchema.h)
56+
for an example.
57+
58+
## Generated Service Schema
59+
60+
The `schemagen` tool generates C++ types in the `graphql::<schema>::object`
61+
namespace with `resolveField` methods for each `field` which parse the
62+
arguments from the `query` and automatically dispatch the call to a `getField`
63+
virtual method to retrieve the `field` result. On `object` types, it will also
64+
recursively call the `resolvers` for each of the `fields` in the nested
65+
`SelectionSet`. See for example the generated
66+
`graphql::today::object::Appointment` object from the `today` sample in
67+
[AppointmentObject.h](../samples/separate/AppointmentObject.h).
68+
```cpp
69+
std::future<response::Value> resolveId(service::ResolverParams&& params);
70+
```
71+
In this example, the `resolveId` method invokes `getId`:
72+
```cpp
73+
virtual service::FieldResult<response::IdType> getId(service::FieldParams&& params) const override;
74+
```
75+
76+
There are a couple of interesting quirks in this example:
77+
1. The `Appointment object` implements and inherits from the `Node interface`,
78+
which already declared `getId` as a pure-virtual method. That's what the
79+
`override` keyword refers to.
80+
2. This schema was generated with default stub implementations (without the
81+
`schemagen --no-stubs` parameter) which speed up initial development with NYI
82+
(Not Yet Implemented) stubs. With that parameter, there would be no
83+
declaration of `Appointment::getId` since it would inherit a pure-virtual
84+
declaration and the implementer would need to define an override on the
85+
concrete implementation of `graphql::today::object::Appointment`. The NYI stub
86+
will throw a `std::runtime_error`, which the `resolver` converts into an entry
87+
in the `response errors` collection:
88+
```cpp
89+
throw std::runtime_error(R"ex(Appointment::getId is not implemented)ex");
90+
```
91+
92+
Although the `id field` does not take any arguments according to the sample
93+
[schema](../samples/today/schema.today.graphql), this example also shows how
94+
every `getField` method takes a `graphql::service::FieldParams` struct as
95+
its first parameter. There are more details on this in the [fieldparams.md](./fieldparams.md)
96+
document.

doc/responses.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Query Responses
2+
3+
## Value Types
4+
5+
As the comment in
6+
[GraphQLResponse.h](../include/graphqlservice/GraphQLResponse.h) says, GraphQL
7+
responses are not technically JSON-specific, although that is probably the most
8+
common way of representing them. These are the primitive types that may be
9+
represented in GraphQL, as of the
10+
[June 2018 spec](https://facebook.github.io/graphql/June2018/#sec-Serialization-Format):
11+
12+
```c++
13+
enum class Type : uint8_t
14+
{
15+
Map, // JSON Object
16+
List, // JSON Array
17+
String, // JSON String
18+
Null, // JSON null
19+
Boolean, // JSON true or false
20+
Int, // JSON Number
21+
Float, // JSON Number
22+
EnumValue, // JSON String
23+
Scalar, // JSON any type
24+
};
25+
```
26+
27+
## Common Accessors
28+
29+
Anywhere that a GraphQL result, a scalar type, or a GraphQL value literal is
30+
used, it's represented in `cppgraphqlgen` using an instance of
31+
`graphql::response::Value`. These can be constructed with any of the types in
32+
the `graphql::response::Type` enum, and depending on the type with which they
33+
are initialized, different accessors will be enabled.
34+
35+
Every type implements specializations for some subset of `get()` which does
36+
not allocate any memory, `set(...)` which takes an r-value, and `release()`
37+
which transfers ownership along with any extra allocations to the caller.
38+
Which of these methods are supported and what C++ types they use are
39+
determined by the `ValueTypeTraits<ValueType>` template and its
40+
specializations.
41+
42+
## Map and List
43+
44+
`Map` and `List` types enable collection methods like `reserve(size_t)`,
45+
`size()`, and `emplace_back(...)`. `Map` additionally implements `begin()`
46+
and `end()` for range-based for loops and `find(const std::string&)` and
47+
`operator[](const std::string&)` for key-based lookups. `List` has an
48+
`operator[](size_t)` for index-based instead of key-based lookups.

0 commit comments

Comments
 (0)