Skip to content

Documentation pass and a little bit of code cleanup #186

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Dec 16, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 103 additions & 45 deletions README.md

Large diffs are not rendered by default.

19 changes: 19 additions & 0 deletions cmake/test_filesystem.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// This is a dummy program that just needs to compile and link to tell us if
// the C++17 std::filesystem API requires any additional libraries.

#include <filesystem>

int main()
{
try
{
throw std::filesystem::filesystem_error("instantiate one to make sure it links",
std::make_error_code(std::errc::function_not_supported));
}
catch (const std::filesystem::filesystem_error& error)
{
return -1;
}

return !std::filesystem::temp_directory_path().is_absolute();
}
22 changes: 0 additions & 22 deletions cmake/test_filesystem.cpp.in

This file was deleted.

267 changes: 267 additions & 0 deletions doc/awaitable.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
# Awaitable

## Launch Policy

In previous versions, this was a `std::launch` enum value used with the
`std::async` standard library function. Now, this is a C++20 `Awaitable`,
specifically a type-erased `graphql::service::await_async` class in
[GraphQLService.h](../include/graphqlservice/GraphQLService.h):
```cpp
// Type-erased awaitable.
class await_async : public coro::suspend_always
{
private:
struct Concept
{
virtual ~Concept() = default;

virtual bool await_ready() const = 0;
virtual void await_suspend(coro::coroutine_handle<> h) const = 0;
virtual void await_resume() const = 0;
};
...

public:
// Type-erased explicit constructor for a custom awaitable.
template <class T>
explicit await_async(std::shared_ptr<T> pimpl)
: _pimpl { std::make_shared<Model<T>>(std::move(pimpl)) }
{
}

// Default to immediate synchronous execution.
await_async()
: _pimpl { std::static_pointer_cast<Concept>(
std::make_shared<Model<coro::suspend_never>>(std::make_shared<coro::suspend_never>())) }
{
}

// Implicitly convert a std::launch parameter used with std::async to an awaitable.
await_async(std::launch launch)
: _pimpl { ((launch & std::launch::async) == std::launch::async)
? std::static_pointer_cast<Concept>(std::make_shared<Model<await_worker_thread>>(
std::make_shared<await_worker_thread>()))
: std::static_pointer_cast<Concept>(std::make_shared<Model<coro::suspend_never>>(
std::make_shared<coro::suspend_never>())) }
{
}
...
};
```
For convenience, it will use `graphql::service::await_worker_thread` if you specify `std::launch::async`,
which should have the same behavior as calling `std::async(std::launch::async, ...)` did before.

If you specify any other flags for `std::launch`, it does not honor them. It will use `coro::suspend_never`
(an alias for `std::suspend_never` or `std::experimental::suspend_never`), which as the name suggests,
continues executing the coroutine without suspending. In other words, `std::launch::deferred` will no
longer defer execution as in previous versions, it will execute immediately.

There is also a default constructor which also uses `coro::suspend_never`, so that is the default
behavior anywhere that `await_async` is default-initialized with `{}`.

Other than simplification, the big advantage this brings is in the type-erased template constructor.
If you are using another C++20 library or thread/task pool with coroutine support, you can implement
your own `Awaitable` for it and wrap that in `graphql::service::await_async`. It should automatically
start parallelizing all of its resolvers using your custom scheduler, which can pause and resume the
coroutine when and where it likes.

## Awaitable Results

Many APIs which used to return some sort of `std::future` now return an alias for
`graphql::internal::Awaitable<...>`. This template is defined in [Awaitable.h](../include/graphqlservice/internal/Awaitable.h):
```cpp
template <typename T>
class Awaitable
{
public:
Awaitable(std::future<T> value)
: _value { std::move(value) }
{
}

T get()
{
return _value.get();
}

struct promise_type
{
Awaitable get_return_object() noexcept
{
return { _promise.get_future() };
}

...

void return_value(T&& value) noexcept(std::is_nothrow_move_constructible_v<T>)
{
_promise.set_value(std::move(value));
}

...

private:
std::promise<T> _promise;

};

constexpr bool await_ready() const noexcept
{
return true;
}

void await_suspend(coro::coroutine_handle<> h) const
{
h.resume();
}

T await_resume()
{
return _value.get();
}

private:
std::future<T> _value;
};
```

The key details are that it implements the required `promise_type` and `await_` methods so
that you can turn any `co_return` statement into a `std::future<T>`, and it can either
`co_await` for that `std::future<T>` from a coroutine, or call `T get()` to block a regular
function until it completes.

## AwaitableScalar and AwaitableObject

In previous versions, `service::FieldResult<T>` created an abstraction over return types `T` and
`std::future<T>`, when returning from a field getter you could return either and it would
implicitly convert that to a `service::FieldResult<T>` which looked and acted like a
`std::future<T>`.

Now, `service::FieldResult<T>` is replaced with `service::AwaitableScalar` for `scalar` type
fields without a selection set of sub-fields, or `service::AwaitableObject` for `object`
type fields which must have a selection set of sub-fields. The difference between
`service::AwaitableScalar` and `service::AwaitableObject` is that `scalar` type fields can
also return `std::shared_ptr<const response::Value>` directly, which bypasses all of the
conversion logic in `service::ModifiedResult` and just validates that the shape of the
`response::Value` matches the `scalar` type with all of its modifiers. These are both defined
in [GraphQLService.h](../include/graphqlservice/GraphQLService.h):
```cpp
// Field accessors may return either a result of T, an awaitable of T, or a std::future<T>, so at
// runtime the implementer may choose to return by value or defer/parallelize expensive operations
// by returning an async future or an awaitable coroutine.
//
// If the overhead of conversion to response::Value is too expensive, scalar type field accessors
// can store and return a std::shared_ptr<const response::Value> directly.
template <typename T>
class AwaitableScalar
{
public:
template <typename U>
AwaitableScalar(U&& value)
: _value { std::forward<U>(value) }
{
}

struct promise_type
{
AwaitableScalar<T> get_return_object() noexcept
{
return { _promise.get_future() };
}

...

void return_value(const T& value) noexcept(std::is_nothrow_copy_constructible_v<T>)
{
_promise.set_value(value);
}

void return_value(T&& value) noexcept(std::is_nothrow_move_constructible_v<T>)
{
_promise.set_value(std::move(value));
}

...

private:
std::promise<T> _promise;
};

bool await_ready() const noexcept { ... }

void await_suspend(coro::coroutine_handle<> h) const { ... }

T await_resume()
{
... // Throws std::logic_error("Cannot await std::shared_ptr<const response::Value>") if called with that alternative
}

std::shared_ptr<const response::Value> get_value() noexcept
{
... // Returns an empty std::shared_ptr if called with a different alternative
}

private:
std::variant<T, std::future<T>, std::shared_ptr<const response::Value>> _value;
};

// Field accessors may return either a result of T, an awaitable of T, or a std::future<T>, so at
// runtime the implementer may choose to return by value or defer/parallelize expensive operations
// by returning an async future or an awaitable coroutine.
template <typename T>
class AwaitableObject
{
public:
template <typename U>
AwaitableObject(U&& value)
: _value { std::forward<U>(value) }
{
}

struct promise_type
{
AwaitableObject<T> get_return_object() noexcept
{
return { _promise.get_future() };
}

...

void return_value(const T& value) noexcept(std::is_nothrow_copy_constructible_v<T>)
{
_promise.set_value(value);
}

void return_value(T&& value) noexcept(std::is_nothrow_move_constructible_v<T>)
{
_promise.set_value(std::move(value));
}

...

private:
std::promise<T> _promise;
};

bool await_ready() const noexcept { ... }

void await_suspend(coro::coroutine_handle<> h) const { ... }

T await_resume() { ... }

private:
std::variant<T, std::future<T>> _value;
};
```

These types both add a `promise_type` for `T`, but coroutines need their own return type to do that.
Making `service::AwaitableScalar<T>` or `service::AwaitableObject<T>` the return type of a field
getter means you can turn it into a coroutine by just replacing `return` with `co_return`, and
potentially start to `co_await` other awaitables and coroutines.

Type-erasure made it so you do not need to use a special return type, the type-erased
`Object::Model<T>` type just needs to be able to pass the return result from your field
getter into a constructor for one of these return types. So if you want to implement
your field getters as coroutines, you should still wrap the return type in
`service::AwaitableScalar<T>` or `service::AwaitableObject<T>`. Otherwise, you can remove
the template wrapper from all of your field getters.
6 changes: 4 additions & 2 deletions doc/directives.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

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
They always begin with an `@`. There are four built-in directives which this
library automatically handles:

1. `@include(if: Boolean!)`: Only resolve this field and include it in the
Expand All @@ -11,9 +11,11 @@ results if the `if` argument evaluates to `true`.
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.
4. `@specifiedBy(url: String!)`: Mark the custom scalar type through
introspection as specified by a human readable page at the specified URL.

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
will pass them to the `getField` implementations through the optional
`graphql::service::FieldParams` struct (see [fieldparams.md](fieldparams.md)
for more information).
Loading