Skip to content

Commit

Permalink
Merge pull request #78 from wravery/master
Browse files Browse the repository at this point in the history
Add more documentation
  • Loading branch information
wravery committed Sep 30, 2019
2 parents bbc8b73 + 49e5b0f commit a70d822
Show file tree
Hide file tree
Showing 8 changed files with 432 additions and 0 deletions.
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,20 @@ the `graphql::service` namespace. Take a look at [UnifiedToday.h](samples/today/
[UnifiedToday.cpp](samples/today/UnifiedToday.cpp) to see a sample implementation of a custom schema defined
in [schema.today.graphql](samples/today/schema.today.graphql) for testing purposes.

### Additional Documentation

There are some more targeted documents in the [doc](./doc) directory:

* [Parsing GraphQL](./doc/parsing.md)
* [Query Responses](./doc/responses.md)
* [JSON Representation](./doc/json.md)
* [Field Resolvers](./doc/resolvers.md)
* [Field Parameters](./doc/fieldparams.md)
* [Directives](./doc/directives.md)
* [Subscriptions](./doc/subscriptions.md)

### Samples

All of the generated files are in the [samples](samples/) directory. There are two different versions of
the generated code, one which creates a single pair of files (`samples/unified/`), and one which uses the
`--separate-files` flag with `schemagen` to generate individual header and source files (`samples/separate/`)
Expand Down
19 changes: 19 additions & 0 deletions doc/directives.md
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).
81 changes: 81 additions & 0 deletions doc/fieldparams.md
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).
40 changes: 40 additions & 0 deletions doc/json.md
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)
```
55 changes: 55 additions & 0 deletions doc/parsing.md
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.
96 changes: 96 additions & 0 deletions doc/resolvers.md
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.
48 changes: 48 additions & 0 deletions doc/responses.md
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.
Loading

0 comments on commit a70d822

Please sign in to comment.