Skip to content

Commit 6135c77

Browse files
committed
Merge upstream/main into custom-parse-tree
2 parents eb71fc3 + 8c502ed commit 6135c77

File tree

174 files changed

+9280
-2970
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

174 files changed

+9280
-2970
lines changed

README.md

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -181,17 +181,16 @@ Command line options:
181181
--no-introspection Do not expect support for Introspection
182182
```
183183

184-
This utility should output one header and one source file for each request document. A request document may contain
185-
more than one operation, in which case you must specify the `--operation` (or `-o`) parameter to indicate which one
186-
should be used to generate the files. If you want to generate client code for more than one operation in the same
187-
document, you will need to run `clientgen` more than once and specify another operation name each time.
184+
This utility should output one header and one source file for each request document. A request document may contain more
185+
than one operation, in which case it will output definitions for all of them together. You may limit the output to a
186+
single operation from the request document by specifying the `--operation` (or `-o`) argument with the operation name.
188187

189188
The generated code depends on the `graphqlclient` library for serialization of built-in types. If you link the generated
190189
code, you'll also need to link `graphqlclient`, `graphqlpeg` for the pre-parsed, pre-validated request AST, and
191190
`graphqlresponse` for the `graphql::response::Value` implementation.
192191

193-
Sample output for `clientgen` is in the sub-directories of [samples/client](samples/client), and each sample is consumed by
194-
a unit test in [test/ClientTests.cpp](test/ClientTests.cpp).
192+
Sample output for `clientgen` is in the sub-directories of [samples/client](samples/client), and several of them are
193+
consumed by unit tests in [test/ClientTests.cpp](test/ClientTests.cpp).
195194

196195
### tests (`GRAPHQL_BUILD_TESTS=ON`)
197196

@@ -218,6 +217,11 @@ There are several helper functions for `CMake` declared in
218217
other samples sub-directories for examples of how to use them to automatically rerun the code generators and update
219218
the files in your source directory.
220219

220+
### Migrating from v3.x to main
221+
222+
Please see the [Migration Guide for v4.x](./doc/migration.md) for more details about upgrading to from the `v3.x` branch
223+
to the `main` branch. Active development takes place almost entirely in `main`.
224+
221225
### Additional Documentation
222226

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

cmake/cppgraphqlgen-update-client-files.cmake

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,12 @@ cmake_policy(SET CMP0057 NEW)
4848
file(GLOB OLD_FILES ${CLIENT_SOURCE_DIR}/*.h ${CLIENT_SOURCE_DIR}/*.cpp)
4949
foreach(OLD_FILE ${OLD_FILES})
5050
get_filename_component(OLD_FILE ${OLD_FILE} NAME)
51-
if(NOT OLD_FILE IN_LIST FILE_NAMES AND
52-
NOT OLD_FILE STREQUAL "${CLIENT_PREFIX}Client.h" AND
53-
NOT OLD_FILE STREQUAL "${CLIENT_PREFIX}Client.cpp")
54-
message(WARNING "Unexpected file in ${CLIENT_TARGET} client sources: ${OLD_FILE}")
51+
if(NOT OLD_FILE IN_LIST FILE_NAMES)
52+
if(OLD_FILE MATCHES "Client\\.h$" OR OLD_FILE MATCHES "Client\\.cpp$")
53+
file(REMOVE "${CLIENT_SOURCE_DIR}/${OLD_FILE}")
54+
else()
55+
message(WARNING "Unexpected file in ${CLIENT_TARGET} client sources: ${OLD_FILE}")
56+
endif()
5557
endif()
5658
endforeach()
5759

cmake/version.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
4.1.1
1+
4.4.0

doc/awaitable.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,14 @@ specifically a type-erased `graphql::service::await_async` class in
88
[GraphQLService.h](../include/graphqlservice/GraphQLService.h):
99
```cpp
1010
// Type-erased awaitable.
11-
class await_async final
11+
class [[nodiscard]] await_async final
1212
{
1313
private:
14-
struct Concept
14+
struct [[nodiscard]] Concept
1515
{
1616
virtual ~Concept() = default;
1717

18-
virtual bool await_ready() const = 0;
18+
[[nodiscard]] virtual bool await_ready() const = 0;
1919
virtual void await_suspend(coro::coroutine_handle<> h) const = 0;
2020
virtual void await_resume() const = 0;
2121
};
@@ -71,22 +71,22 @@ Many APIs which used to return some sort of `std::future` now return an alias fo
7171
`graphql::internal::Awaitable<...>`. This template is defined in [Awaitable.h](../include/graphqlservice/internal/Awaitable.h):
7272
```cpp
7373
template <typename T>
74-
class Awaitable
74+
class [[nodiscard]] Awaitable
7575
{
7676
public:
7777
Awaitable(std::future<T> value)
7878
: _value { std::move(value) }
7979
{
8080
}
8181

82-
T get()
82+
[[nodiscard]] T get()
8383
{
8484
return _value.get();
8585
}
8686

8787
struct promise_type
8888
{
89-
Awaitable get_return_object() noexcept
89+
[[nodiscard]] Awaitable get_return_object() noexcept
9090
{
9191
return { _promise.get_future() };
9292
}
@@ -105,7 +105,7 @@ public:
105105

106106
};
107107

108-
constexpr bool await_ready() const noexcept
108+
[[nodiscard]] constexpr bool await_ready() const noexcept
109109
{
110110
return true;
111111
}
@@ -115,7 +115,7 @@ public:
115115
h.resume();
116116
}
117117

118-
T await_resume()
118+
[[nodiscard]] T await_resume()
119119
{
120120
return _value.get();
121121
}

doc/fieldparams.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ The `graphql::service::FieldParams` struct is declared in [GraphQLService.h](../
2121
```cpp
2222
// Pass a common bundle of parameters to all of the generated Object::getField accessors in a
2323
// SelectionSet
24-
struct SelectionSetParams
24+
struct [[nodiscard]] SelectionSetParams
2525
{
2626
// Context for this selection set.
2727
const ResolverContext resolverContext;
@@ -46,7 +46,7 @@ struct SelectionSetParams
4646
};
4747

4848
// Pass a common bundle of parameters to all of the generated Object::getField accessors.
49-
struct FieldParams : SelectionSetParams
49+
struct [[nodiscard]] FieldParams : SelectionSetParams
5050
{
5151
GRAPHQLSERVICE_EXPORT explicit FieldParams(
5252
SelectionSetParams&& selectionSetParams, Directives directives);
@@ -64,8 +64,7 @@ The `SelectionSetParams::resolverContext` enum member informs the `getField`
6464
accessors about what type of operation is being resolved:
6565
```cpp
6666
// Resolvers may be called in multiple different Operation contexts.
67-
enum class ResolverContext
68-
{
67+
enum class [[nodiscard]] ResolverContext {
6968
// Resolving a Query operation.
7069
Query,
7170

@@ -95,8 +94,9 @@ The `SelectionSetParams::state` member is a reference to the
9594
// any per-request state that you want to maintain throughout the request (e.g. optimizing or
9695
// batching backend requests), you can inherit from RequestState and pass it to Request::resolve to
9796
// correlate the asynchronous/recursive callbacks and accumulate state in it.
98-
struct RequestState : std::enable_shared_from_this<RequestState>
97+
struct [[nodiscard]] RequestState : std::enable_shared_from_this<RequestState>
9998
{
99+
virtual ~RequestState() = default;
100100
};
101101
```
102102

@@ -152,5 +152,5 @@ See the [Awaitable](./awaitable.md) document for more information about
152152
1. The `getField` methods are discussed in more detail in [resolvers.md](./resolvers.md).
153153
2. Awaitable types are covered in [awaitable.md](./awaitable.md).
154154
3. Built-in and custom `directives` are discussed in [directives.md](./directives.md).
155-
4. Subscription resolvers may be called 2 extra times, inside of subscribe` and `unsubscribe`.
156-
See [subscriptions.md](./subscriptions.md) for more details.
155+
4. Subscription resolvers may be called 2 extra times, inside of `subscribe` and `unsubscribe`.
156+
See [subscriptions.md](./subscriptions.md) for more details.

doc/json.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ to some other output mechanism, without building a single string buffer for the
4646
document in memory. For example, you might use this to write directly to a buffered IPC pipe
4747
or network connection:
4848
```cpp
49-
class Writer final
49+
class [[nodiscard]] Writer final
5050
{
5151
private:
5252
struct Concept

doc/migration.md

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
# Migration Guide for v4.x
2+
3+
Most of the changes in v4.x affect services generated with `schemagen` more than clients generated with `clientgen`.
4+
5+
## Adopting C++20
6+
7+
This version takes advantage of and requires the following C++20 features:
8+
9+
- Coroutines (in either the `std` or `std::experimental` namespace)
10+
- Concepts
11+
12+
There is enough support for these features in the following compiler versions, your mileage may vary with anything older than these:
13+
14+
- Microsoft Windows: Visual Studio 2019
15+
- Linux: Ubuntu 20.04 LTS with gcc 10.3.0
16+
- macOS: 11 (Big Sur) with AppleClang 13.0.0.
17+
18+
## Using Coroutines
19+
20+
The template methods generated by `schemagen` will construct the expected [awaitable](awaitable.md) type using the result of your field getter methods. If you want to implement your field getter as a coroutine, you can explicitly return the expected awaitable type, otherwise return any type that is implicitly convertible to the awaitable-wrapped type.
21+
22+
Take a look at `Query::getNode` in [samples/today/TodayMock.cpp](../samples/today/TodayMock.cpp) for an example. The `auto operator co_await(std::chrono::duration<_Rep, _Period> delay)` operator overload in the same file is also an example of how you can integrate custom awaitables in your field getters.
23+
24+
## Type Erasure Instead of Inheritance
25+
26+
Change your class declarations so that they no longer inherit from the generated `object` namespace classes. If you need the `shared_from_this()` method, you can replace that with `std::enable_shared_from_this<T>`. Remove the `override` or `final` keyword from any virtual field getter method declarations which were inherited from the object type.
27+
28+
In cases where the return type has changed from `std::shared_ptr<service::Object>` to `std::shared_ptr<object::Interface>` or `std::shared_ptr<object::Union>`, wrap the concrete type in `std::make_shared<T>(...)` for the polymorphic type and return that.
29+
30+
## Simplify Field Accessor Signatures
31+
32+
Examine the generated object types and determine what the expected return type is from each field getter method. Replace the `service::FieldResult` wrapped type with the expected return type (possibly including the awaitable wrapper).
33+
34+
Make methods `const` or non-`const` as appropriate. The `const` type erased object has a `const std::shared_ptr<T>` to your type, but the type inside of the `std::shared_ptr` is not `const`, so it can call non-`const` methods on your type. You can get rid of a lot of `mutable` fields or `const_cast` calls by doing this and make your type `const` correct.
35+
36+
Parameters can be passed as a `const&` reference, a `&&` r-value reference, or by value. The generated template methods will forward an r-value reference which will be implicitly converted into any of these types when calling your method.
37+
38+
Remove any unused `service::FieldParams` arguments. If your method does not take that as the first parameter, the generated template method will drop it and pass the rest of the expected arguments to your method.
39+
40+
## Decoupling Implementation Types from the Generated Types
41+
42+
If your implementation is tightly coupled with the object hierarchy from the schema, here's an example of how you might decouple them. Let's assume that you have a schema that looks something like this:
43+
```graphql
44+
interface Node
45+
{
46+
id: ID!
47+
}
48+
49+
type NodeTypeA implements Node
50+
{
51+
id: ID!
52+
# More fields specific to NodeTypeA...
53+
}
54+
55+
type NodeTypeB implements Node
56+
{
57+
id: ID!
58+
# More fields specific to NodeTypeB...
59+
}
60+
61+
# ...and so on for NodeTypeC, NodeTypeD, etc.
62+
```
63+
If you want a collection of `Node` interface objects, the C++ implementation using inheritance in prior versions might look something like:
64+
```c++
65+
class NodeTypeA : public object::NodeTypeA
66+
{
67+
// Implement the field accessors with an exact match for the virtual method signature...
68+
service::FieldResult<response::IdType> getId(service::FieldParams&&) const override;
69+
};
70+
71+
class NodeTypeB : public object::NodeTypeB
72+
{
73+
// Implement the field accessors with an exact match for the virtual method signature...
74+
service::FieldResult<response::IdType> getId(service::FieldParams&&) const override;
75+
};
76+
77+
std::vector<std::shared_ptr<service::Object>> nodes {
78+
std::make_shared<NodeTypeA>(),
79+
std::make_shared<NodeTypeB>(),
80+
// Can insert any sub-class of service::Object...
81+
};
82+
```
83+
It's up to you to make sure the `nodes` vector in this example only contains objects which actually implement the `Node` interface. If you want to do something more sophisticated like performing a lookup by `id`, you'd either need to request that before inserting an element and up-casting to `std::shared_ptr<service::Object>`, or you'd need to preserve the concrete type of each element, e.g. in a `std::variant` to be able to safely down-cast to the concrete type.
84+
85+
As of 4.x, the implementation might look more like this:
86+
```c++
87+
class NodeTypeImpl
88+
{
89+
public:
90+
// Need to override this in the sub-classes to construct the correct sub-type wrappers.
91+
virtual std::shared_ptr<object::Node> make_node() const = 0;
92+
93+
const response::IdType& getId() const noexcept final;
94+
95+
// Implement/declare any other accessors you want to use without downcasting...
96+
97+
private:
98+
const response::IdType _id;
99+
};
100+
101+
class NodeTypeA
102+
: public NodeTypeImpl
103+
, public std::enable_shared_from_this<NodeTypeA>
104+
{
105+
public:
106+
// Convert to a type-erased Node.
107+
std::shared_ptr<object::Node> make_node() const final
108+
{
109+
return std::make_shared<object::Node>(std::make_shared<object::NodeTypeA>(shared_from_this()));
110+
}
111+
112+
// Implement NodeTypeA and any NodeTypeImpl override accessors...
113+
};
114+
115+
class NodeTypeB
116+
: public NodeTypeImpl
117+
, public std::enable_shared_from_this<NodeTypeB>
118+
{
119+
public:
120+
// Convert to a type-erased Node.
121+
std::shared_ptr<object::Node> make_node() const final
122+
{
123+
return std::make_shared<object::Node>(std::make_shared<object::NodeTypeB>(shared_from_this()));
124+
}
125+
126+
// Implement NodeTypeB and any NodeTypeImpl override accessors...
127+
};
128+
129+
std::vector<std::shared_ptr<NodeTypeImpl>> nodes {
130+
std::make_shared<NodeTypeA>(),
131+
std::make_shared<NodeTypeB>(),
132+
// Can only insert sub-classes of NodeTypeImpl...
133+
};
134+
135+
std::vector<std::shared_ptr<object::Node>> wrap_nodes()
136+
{
137+
std::vector<std::shared_ptr<object::Node>> result(nodes.size());
138+
139+
std::transform(nodes.cbegin(), nodes.cend(), result.begin(), [](const auto& node) {
140+
return node
141+
? node->make_node()
142+
: std::shared_ptr<object::Node> {};
143+
});
144+
145+
return result;
146+
}
147+
```
148+
This has several advantages over the previous version.
149+
150+
- You can declare your own inheritance heirarchy without any constraints inherited from `service::Object`, such as already inheriting from `std::enable_shared_from_this<service::Object>` and defininig `shared_from_this()` for that type.
151+
- You can add your own common implementation for the interface methods you want, e.g. `NodeTypeImpl::getId`.
152+
- Best of all, you no longer need to match an exact method signature to override the `object::NodeType*` accessors. For example, `NodeTypeImpl::getId` uses a more efficient return type, does not require a `service::FieldParams` argument (which is likely ignored anyway), and it can be non-`const` or `noexcept` if you like. All of that together means you can use it as an internal accessor from any of these types as well as the field getter implementation.
153+
154+
The type erased implementation gives you a lot more control over your class hierarchy and makes it easier to use outside of the GraphQL service.
155+
156+
## CMake Changes
157+
158+
By default, earlier versions of `schemagen` would generate a single header and a single source file for the entire schema, including the declaration and definition of all of the object types. For any significantly complex schema, this source file could get very big. Even the `Today` sample schema was large enough to require a special `/bigobj` flag when compiling with `MSVC`. It also made incremental builds take much longer if you only added/removed/modified a few types, because the entire schema needed to be recompiled.
159+
160+
For a long time, `schemagen` also supported a `--separate-files` flag which would output a separate header and source file for each object type in the schema. This requires more complicated build logic since the set of files that need to be built can change based on the schema, but the end result is much easier to read and incremental builds are faster.
161+
162+
In v4.x, the separate files option is not only the default, it's the only option. Supporting both modes of code generation would have added too much complexity and too many tradeoffs for the simplified build logic. Instead, v4.x adds several CMake helper functions in [cmake/cppgraphqlgen-functions.cmake](../cmake/cppgraphqlgen-functions.cmake) which encapsulate the best practices for regenerating and building the schema targets dynamically when the schema file changes. These functions are automatically included by `find_package(cppgraphqlgen)`.
163+
164+
Replace custom CMake logic to invoke `schemagen` and `clientgen` with these helper functions:
165+
- `update_graphql_schema_files`: Runs `schemagen` with required parameters and additional optional parameters.
166+
- `update_graphql_client_files`: Runs `clientgen` with required parameters and additional optional parameters.
167+
168+
The output is generated in the CMake build directory. The files are compared against the contents of the source directory, and any changed/added files will be copied over to the sources directory. Files which were not regenerated will be deleted from the source directory.
169+
170+
_IMPORTANT_: The `update_graphql_schema_files` and `update_graphql_client_files` functions expect to generate sources in a separate sub-directory from any other source code. They will check for any source files that don't match the naming patterns of the code generators and fail the build rather than deleting them. Just in case, it's a good idea to make sure you have your source code backed up or under source control (e.g. committed in a git repository) before invoking these CMake functions.
171+
172+
Declare library targets which automatically include all of the generated files with these helper functions:
173+
- `add_graphql_schema_target`: Declares a library target for the specified schema which depends on the output of `update_graphql_schema_files` and automatically links all of the shared library dependencies needed for a service.
174+
- `add_graphql_client_target`: Declares a library target for the specified client which depends on the output of `update_graphql_client_files` and automatically links all of the shared library dependencies needed for a client.
175+
176+
With all of the refactoring in v4.x, there ceased to be any separation between the `graphqlintrospection` and `graphqlservice` libraries. Even if you use the `--no-introspection` flag with `schemagen`, the generated code still depends on the general schema types which remained in `graphqlintrospection` to perform query validation. As part of the v4.x release, the 2 libraries were combined back into a single `graphqlservice` target. If you use `add_graphql_schema_target` you do not need to worry about this, otherwise you should replace any references to just `graphqlintrospection` with `graphqlservice`.

doc/responses.md

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,17 @@ represented in GraphQL, as of the
1010
[October 2021 spec](https://spec.graphql.org/October2021/#sec-Serialization-Format):
1111

1212
```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
13+
enum class [[nodiscard]] Type : std::uint8_t {
14+
Map, // JSON Object
15+
List, // JSON Array
16+
String, // JSON String
17+
Null, // JSON null
18+
Boolean, // JSON true or false
19+
Int, // JSON Number
20+
Float, // JSON Number
21+
EnumValue, // JSON String
22+
ID, // JSON String
23+
Scalar, // JSON any type
2424
};
2525
```
2626

0 commit comments

Comments
 (0)