Skip to content

Commit 777ef34

Browse files
authored
Merge pull request #186 from wravery/next
Documentation pass and a little bit of code cleanup
2 parents 9ffd6f0 + 59eb713 commit 777ef34

17 files changed

+846
-415
lines changed

README.md

Lines changed: 103 additions & 45 deletions
Large diffs are not rendered by default.

cmake/test_filesystem.cpp

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// This is a dummy program that just needs to compile and link to tell us if
2+
// the C++17 std::filesystem API requires any additional libraries.
3+
4+
#include <filesystem>
5+
6+
int main()
7+
{
8+
try
9+
{
10+
throw std::filesystem::filesystem_error("instantiate one to make sure it links",
11+
std::make_error_code(std::errc::function_not_supported));
12+
}
13+
catch (const std::filesystem::filesystem_error& error)
14+
{
15+
return -1;
16+
}
17+
18+
return !std::filesystem::temp_directory_path().is_absolute();
19+
}

cmake/test_filesystem.cpp.in

Lines changed: 0 additions & 22 deletions
This file was deleted.

doc/awaitable.md

Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
# Awaitable
2+
3+
## Launch Policy
4+
5+
In previous versions, this was a `std::launch` enum value used with the
6+
`std::async` standard library function. Now, this is a C++20 `Awaitable`,
7+
specifically a type-erased `graphql::service::await_async` class in
8+
[GraphQLService.h](../include/graphqlservice/GraphQLService.h):
9+
```cpp
10+
// Type-erased awaitable.
11+
class await_async : public coro::suspend_always
12+
{
13+
private:
14+
struct Concept
15+
{
16+
virtual ~Concept() = default;
17+
18+
virtual bool await_ready() const = 0;
19+
virtual void await_suspend(coro::coroutine_handle<> h) const = 0;
20+
virtual void await_resume() const = 0;
21+
};
22+
...
23+
24+
public:
25+
// Type-erased explicit constructor for a custom awaitable.
26+
template <class T>
27+
explicit await_async(std::shared_ptr<T> pimpl)
28+
: _pimpl { std::make_shared<Model<T>>(std::move(pimpl)) }
29+
{
30+
}
31+
32+
// Default to immediate synchronous execution.
33+
await_async()
34+
: _pimpl { std::static_pointer_cast<Concept>(
35+
std::make_shared<Model<coro::suspend_never>>(std::make_shared<coro::suspend_never>())) }
36+
{
37+
}
38+
39+
// Implicitly convert a std::launch parameter used with std::async to an awaitable.
40+
await_async(std::launch launch)
41+
: _pimpl { ((launch & std::launch::async) == std::launch::async)
42+
? std::static_pointer_cast<Concept>(std::make_shared<Model<await_worker_thread>>(
43+
std::make_shared<await_worker_thread>()))
44+
: std::static_pointer_cast<Concept>(std::make_shared<Model<coro::suspend_never>>(
45+
std::make_shared<coro::suspend_never>())) }
46+
{
47+
}
48+
...
49+
};
50+
```
51+
For convenience, it will use `graphql::service::await_worker_thread` if you specify `std::launch::async`,
52+
which should have the same behavior as calling `std::async(std::launch::async, ...)` did before.
53+
54+
If you specify any other flags for `std::launch`, it does not honor them. It will use `coro::suspend_never`
55+
(an alias for `std::suspend_never` or `std::experimental::suspend_never`), which as the name suggests,
56+
continues executing the coroutine without suspending. In other words, `std::launch::deferred` will no
57+
longer defer execution as in previous versions, it will execute immediately.
58+
59+
There is also a default constructor which also uses `coro::suspend_never`, so that is the default
60+
behavior anywhere that `await_async` is default-initialized with `{}`.
61+
62+
Other than simplification, the big advantage this brings is in the type-erased template constructor.
63+
If you are using another C++20 library or thread/task pool with coroutine support, you can implement
64+
your own `Awaitable` for it and wrap that in `graphql::service::await_async`. It should automatically
65+
start parallelizing all of its resolvers using your custom scheduler, which can pause and resume the
66+
coroutine when and where it likes.
67+
68+
## Awaitable Results
69+
70+
Many APIs which used to return some sort of `std::future` now return an alias for
71+
`graphql::internal::Awaitable<...>`. This template is defined in [Awaitable.h](../include/graphqlservice/internal/Awaitable.h):
72+
```cpp
73+
template <typename T>
74+
class Awaitable
75+
{
76+
public:
77+
Awaitable(std::future<T> value)
78+
: _value { std::move(value) }
79+
{
80+
}
81+
82+
T get()
83+
{
84+
return _value.get();
85+
}
86+
87+
struct promise_type
88+
{
89+
Awaitable get_return_object() noexcept
90+
{
91+
return { _promise.get_future() };
92+
}
93+
94+
...
95+
96+
void return_value(T&& value) noexcept(std::is_nothrow_move_constructible_v<T>)
97+
{
98+
_promise.set_value(std::move(value));
99+
}
100+
101+
...
102+
103+
private:
104+
std::promise<T> _promise;
105+
106+
};
107+
108+
constexpr bool await_ready() const noexcept
109+
{
110+
return true;
111+
}
112+
113+
void await_suspend(coro::coroutine_handle<> h) const
114+
{
115+
h.resume();
116+
}
117+
118+
T await_resume()
119+
{
120+
return _value.get();
121+
}
122+
123+
private:
124+
std::future<T> _value;
125+
};
126+
```
127+
128+
The key details are that it implements the required `promise_type` and `await_` methods so
129+
that you can turn any `co_return` statement into a `std::future<T>`, and it can either
130+
`co_await` for that `std::future<T>` from a coroutine, or call `T get()` to block a regular
131+
function until it completes.
132+
133+
## AwaitableScalar and AwaitableObject
134+
135+
In previous versions, `service::FieldResult<T>` created an abstraction over return types `T` and
136+
`std::future<T>`, when returning from a field getter you could return either and it would
137+
implicitly convert that to a `service::FieldResult<T>` which looked and acted like a
138+
`std::future<T>`.
139+
140+
Now, `service::FieldResult<T>` is replaced with `service::AwaitableScalar` for `scalar` type
141+
fields without a selection set of sub-fields, or `service::AwaitableObject` for `object`
142+
type fields which must have a selection set of sub-fields. The difference between
143+
`service::AwaitableScalar` and `service::AwaitableObject` is that `scalar` type fields can
144+
also return `std::shared_ptr<const response::Value>` directly, which bypasses all of the
145+
conversion logic in `service::ModifiedResult` and just validates that the shape of the
146+
`response::Value` matches the `scalar` type with all of its modifiers. These are both defined
147+
in [GraphQLService.h](../include/graphqlservice/GraphQLService.h):
148+
```cpp
149+
// Field accessors may return either a result of T, an awaitable of T, or a std::future<T>, so at
150+
// runtime the implementer may choose to return by value or defer/parallelize expensive operations
151+
// by returning an async future or an awaitable coroutine.
152+
//
153+
// If the overhead of conversion to response::Value is too expensive, scalar type field accessors
154+
// can store and return a std::shared_ptr<const response::Value> directly.
155+
template <typename T>
156+
class AwaitableScalar
157+
{
158+
public:
159+
template <typename U>
160+
AwaitableScalar(U&& value)
161+
: _value { std::forward<U>(value) }
162+
{
163+
}
164+
165+
struct promise_type
166+
{
167+
AwaitableScalar<T> get_return_object() noexcept
168+
{
169+
return { _promise.get_future() };
170+
}
171+
172+
...
173+
174+
void return_value(const T& value) noexcept(std::is_nothrow_copy_constructible_v<T>)
175+
{
176+
_promise.set_value(value);
177+
}
178+
179+
void return_value(T&& value) noexcept(std::is_nothrow_move_constructible_v<T>)
180+
{
181+
_promise.set_value(std::move(value));
182+
}
183+
184+
...
185+
186+
private:
187+
std::promise<T> _promise;
188+
};
189+
190+
bool await_ready() const noexcept { ... }
191+
192+
void await_suspend(coro::coroutine_handle<> h) const { ... }
193+
194+
T await_resume()
195+
{
196+
... // Throws std::logic_error("Cannot await std::shared_ptr<const response::Value>") if called with that alternative
197+
}
198+
199+
std::shared_ptr<const response::Value> get_value() noexcept
200+
{
201+
... // Returns an empty std::shared_ptr if called with a different alternative
202+
}
203+
204+
private:
205+
std::variant<T, std::future<T>, std::shared_ptr<const response::Value>> _value;
206+
};
207+
208+
// Field accessors may return either a result of T, an awaitable of T, or a std::future<T>, so at
209+
// runtime the implementer may choose to return by value or defer/parallelize expensive operations
210+
// by returning an async future or an awaitable coroutine.
211+
template <typename T>
212+
class AwaitableObject
213+
{
214+
public:
215+
template <typename U>
216+
AwaitableObject(U&& value)
217+
: _value { std::forward<U>(value) }
218+
{
219+
}
220+
221+
struct promise_type
222+
{
223+
AwaitableObject<T> get_return_object() noexcept
224+
{
225+
return { _promise.get_future() };
226+
}
227+
228+
...
229+
230+
void return_value(const T& value) noexcept(std::is_nothrow_copy_constructible_v<T>)
231+
{
232+
_promise.set_value(value);
233+
}
234+
235+
void return_value(T&& value) noexcept(std::is_nothrow_move_constructible_v<T>)
236+
{
237+
_promise.set_value(std::move(value));
238+
}
239+
240+
...
241+
242+
private:
243+
std::promise<T> _promise;
244+
};
245+
246+
bool await_ready() const noexcept { ... }
247+
248+
void await_suspend(coro::coroutine_handle<> h) const { ... }
249+
250+
T await_resume() { ... }
251+
252+
private:
253+
std::variant<T, std::future<T>> _value;
254+
};
255+
```
256+
257+
These types both add a `promise_type` for `T`, but coroutines need their own return type to do that.
258+
Making `service::AwaitableScalar<T>` or `service::AwaitableObject<T>` the return type of a field
259+
getter means you can turn it into a coroutine by just replacing `return` with `co_return`, and
260+
potentially start to `co_await` other awaitables and coroutines.
261+
262+
Type-erasure made it so you do not need to use a special return type, the type-erased
263+
`Object::Model<T>` type just needs to be able to pass the return result from your field
264+
getter into a constructor for one of these return types. So if you want to implement
265+
your field getters as coroutines, you should still wrap the return type in
266+
`service::AwaitableScalar<T>` or `service::AwaitableObject<T>`. Otherwise, you can remove
267+
the template wrapper from all of your field getters.

doc/directives.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
Directives in GraphQL are extensible annotations which alter the runtime
44
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
5+
They always begin with an `@`. There are four built-in directives which this
66
library automatically handles:
77

88
1. `@include(if: Boolean!)`: Only resolve this field and include it in the
@@ -11,9 +11,11 @@ results if the `if` argument evaluates to `true`.
1111
results if the `if` argument evaluates to `false`.
1212
3. `@deprecated(reason: String)`: Mark the field or enum value as deprecated
1313
through introspection with the specified `reason` string.
14+
4. `@specifiedBy(url: String!)`: Mark the custom scalar type through
15+
introspection as specified by a human readable page at the specified URL.
1416

1517
The `schema` can also define custom `directives` which are valid on different
1618
elements of the `query`. The library does not handle them automatically, but it
17-
will pass them to the `getField` implementations through the
19+
will pass them to the `getField` implementations through the optional
1820
`graphql::service::FieldParams` struct (see [fieldparams.md](fieldparams.md)
1921
for more information).

0 commit comments

Comments
 (0)