Skip to content

Commit 0ff52a7

Browse files
committed
Merge branch 'main' into multi-request
2 parents cc3daf3 + 6610946 commit 0ff52a7

File tree

123 files changed

+2839
-536
lines changed

Some content is hidden

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

123 files changed

+2839
-536
lines changed

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,11 @@ There are several helper functions for `CMake` declared in
218218
other samples sub-directories for examples of how to use them to automatically rerun the code generators and update
219219
the files in your source directory.
220220

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

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

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.2.0

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 the 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 `const` and `noexcept`. 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`.

include/RequestLoader.h

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
#include "graphqlservice/internal/Grammar.h"
1515
#include "graphqlservice/internal/Schema.h"
1616

17+
#include <unordered_set>
18+
1719
namespace graphql::generator {
1820

1921
using RequestSchemaType = std::shared_ptr<const schema::BaseType>;
@@ -40,9 +42,18 @@ struct ResponseField
4042
ResponseFieldList children;
4143
};
4244

43-
struct RequestVariable
45+
struct RequestInputType
4446
{
4547
RequestSchemaType type;
48+
std::unordered_set<std::string_view> dependencies {};
49+
std::vector<std::string_view> declarations {};
50+
};
51+
52+
using RequestInputTypeList = std::vector<RequestInputType>;
53+
54+
struct RequestVariable
55+
{
56+
RequestInputType inputType;
4657
TypeModifierStack modifiers;
4758
std::string_view name;
4859
std::string_view cppName;
@@ -76,10 +87,12 @@ class RequestLoader
7687
const ResponseType& getResponseType() const noexcept;
7788
const RequestVariableList& getVariables() const noexcept;
7889

79-
const RequestSchemaTypeList& getReferencedInputTypes() const noexcept;
90+
const RequestInputTypeList& getReferencedInputTypes() const noexcept;
8091
const RequestSchemaTypeList& getReferencedEnums() const noexcept;
8192

8293
std::string getInputCppType(const RequestSchemaType& wrappedInputType) const noexcept;
94+
std::string getInputCppType(const RequestSchemaType& inputType,
95+
const TypeModifierStack& modifiers) const noexcept;
8396
static std::string getOutputCppType(
8497
std::string_view outputCppType, const TypeModifierStack& modifiers) noexcept;
8598

@@ -99,7 +112,7 @@ class RequestLoader
99112
void collectFragments() noexcept;
100113
void collectVariables() noexcept;
101114
void collectInputTypes(const RequestSchemaType& variableType) noexcept;
102-
void reorderInputTypeDependencies() noexcept;
115+
void reorderInputTypeDependencies();
103116
void collectEnums(const RequestSchemaType& variableType) noexcept;
104117
void collectEnums(const ResponseField& responseField) noexcept;
105118

@@ -146,7 +159,7 @@ class RequestLoader
146159
ResponseType _responseType;
147160
RequestVariableList _variables;
148161
internal::string_view_set _inputTypeNames;
149-
RequestSchemaTypeList _referencedInputTypes;
162+
RequestInputTypeList _referencedInputTypes;
150163
internal::string_view_set _enumNames;
151164
RequestSchemaTypeList _referencedEnums;
152165
};

include/SchemaLoader.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ struct InputType
106106
InputFieldList fields;
107107
std::string_view description;
108108
std::unordered_set<std::string_view> dependencies {};
109+
std::vector<std::string_view> declarations {};
109110
};
110111

111112
using InputTypeList = std::vector<InputType>;

include/graphqlservice/GraphQLClient.h

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -71,17 +71,32 @@ enum class TypeModifier
7171
List,
7272
};
7373

74+
// Specialized to return true for all INPUT_OBJECT types.
75+
template <typename Type>
76+
constexpr bool isInputType() noexcept
77+
{
78+
return false;
79+
}
80+
7481
// Serialize variable input values with chained type modifiers which add nullable or list wrappers.
7582
template <typename Type>
7683
struct ModifiedVariable
7784
{
85+
// Special-case an innermost nullable INPUT_OBJECT type.
86+
template <TypeModifier... Other>
87+
static constexpr bool onlyNoneModifiers() noexcept
88+
{
89+
return (... && (Other == TypeModifier::None));
90+
}
91+
7892
// Peel off modifiers until we get to the underlying type.
7993
template <typename U, TypeModifier Modifier = TypeModifier::None, TypeModifier... Other>
8094
struct VariableTraits
8195
{
8296
// Peel off modifiers until we get to the underlying type.
8397
using type = typename std::conditional_t<TypeModifier::Nullable == Modifier,
84-
std::optional<typename VariableTraits<U, Other...>::type>,
98+
typename std::conditional_t<isInputType<U>() && onlyNoneModifiers<Other...>(),
99+
std::unique_ptr<U>, std::optional<typename VariableTraits<U, Other...>::type>>,
85100
typename std::conditional_t<TypeModifier::List == Modifier,
86101
std::vector<typename VariableTraits<U, Other...>::type>, U>>;
87102
};
@@ -115,7 +130,7 @@ struct ModifiedVariable
115130
if (nullableValue)
116131
{
117132
result = serialize<Other...>(std::move(*nullableValue));
118-
nullableValue = std::nullopt;
133+
nullableValue.reset();
119134
}
120135

121136
return result;
@@ -188,12 +203,12 @@ struct ModifiedResponse
188203
};
189204

190205
// Parse a single value of the response document.
191-
static Type parse(response::Value response);
206+
static Type parse(response::Value&& response);
192207

193208
// Peel off the none modifier. If it's included, it should always be last in the list.
194209
template <TypeModifier Modifier = TypeModifier::None, TypeModifier... Other>
195210
static typename std::enable_if_t<TypeModifier::None == Modifier && sizeof...(Other) == 0, Type>
196-
parse(response::Value response)
211+
parse(response::Value&& response)
197212
{
198213
return parse(std::move(response));
199214
}
@@ -202,7 +217,7 @@ struct ModifiedResponse
202217
template <TypeModifier Modifier, TypeModifier... Other>
203218
static typename std::enable_if_t<TypeModifier::Nullable == Modifier,
204219
std::optional<typename ResponseTraits<Type, Other...>::type>>
205-
parse(response::Value response)
220+
parse(response::Value&& response)
206221
{
207222
if (response.type() == response::Type::Null)
208223
{
@@ -217,7 +232,7 @@ struct ModifiedResponse
217232
template <TypeModifier Modifier, TypeModifier... Other>
218233
static typename std::enable_if_t<TypeModifier::List == Modifier,
219234
std::vector<typename ResponseTraits<Type, Other...>::type>>
220-
parse(response::Value response)
235+
parse(response::Value&& response)
221236
{
222237
std::vector<typename ResponseTraits<Type, Other...>::type> result;
223238

@@ -251,19 +266,19 @@ using ScalarResponse = ModifiedResponse<response::Value>;
251266
#ifdef GRAPHQL_DLLEXPORTS
252267
// Export all of the built-in converters
253268
template <>
254-
GRAPHQLCLIENT_EXPORT int ModifiedResponse<int>::parse(response::Value response);
269+
GRAPHQLCLIENT_EXPORT int ModifiedResponse<int>::parse(response::Value&& response);
255270
template <>
256-
GRAPHQLCLIENT_EXPORT double ModifiedResponse<double>::parse(response::Value response);
271+
GRAPHQLCLIENT_EXPORT double ModifiedResponse<double>::parse(response::Value&& response);
257272
template <>
258-
GRAPHQLCLIENT_EXPORT std::string ModifiedResponse<std::string>::parse(response::Value response);
273+
GRAPHQLCLIENT_EXPORT std::string ModifiedResponse<std::string>::parse(response::Value&& response);
259274
template <>
260-
GRAPHQLCLIENT_EXPORT bool ModifiedResponse<bool>::parse(response::Value response);
275+
GRAPHQLCLIENT_EXPORT bool ModifiedResponse<bool>::parse(response::Value&& response);
261276
template <>
262277
GRAPHQLCLIENT_EXPORT response::IdType ModifiedResponse<response::IdType>::parse(
263-
response::Value response);
278+
response::Value&& response);
264279
template <>
265280
GRAPHQLCLIENT_EXPORT response::Value ModifiedResponse<response::Value>::parse(
266-
response::Value response);
281+
response::Value&& response);
267282
#endif // GRAPHQL_DLLEXPORTS
268283

269284
} // namespace graphql::client

include/graphqlservice/GraphQLParse.h

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,17 @@ struct ast
3434
bool validated = false;
3535
};
3636

37-
GRAPHQLPEG_EXPORT ast parseSchemaString(std::string_view input);
38-
GRAPHQLPEG_EXPORT ast parseSchemaFile(std::string_view filename);
37+
// By default, we want to limit the depth of nested nodes. You can override this with
38+
// another value for the depthLimit parameter in these parse functions.
39+
constexpr size_t c_defaultDepthLimit = 25;
3940

40-
GRAPHQLPEG_EXPORT ast parseString(std::string_view input);
41-
GRAPHQLPEG_EXPORT ast parseFile(std::string_view filename);
41+
GRAPHQLPEG_EXPORT ast parseSchemaString(
42+
std::string_view input, size_t depthLimit = c_defaultDepthLimit);
43+
GRAPHQLPEG_EXPORT ast parseSchemaFile(
44+
std::string_view filename, size_t depthLimit = c_defaultDepthLimit);
45+
46+
GRAPHQLPEG_EXPORT ast parseString(std::string_view input, size_t depthLimit = c_defaultDepthLimit);
47+
GRAPHQLPEG_EXPORT ast parseFile(std::string_view filename, size_t depthLimit = c_defaultDepthLimit);
4248

4349
} // namespace peg
4450

0 commit comments

Comments
 (0)