diff --git a/README.md b/README.md index d0e95987..5f8e6fce 100644 --- a/README.md +++ b/README.md @@ -23,8 +23,7 @@ service, you can use the same GraphQL client code to access your native data sou service online. You might even be able to share some more of that code between a progressive web app and your native/hybrid app. -If what you're after is a way to consume a GraphQL service from C++, as of -[v3.6.0](https://github.com/microsoft/cppgraphqlgen/releases/tag/v3.6.0) this project also includes +If what you're after is a way to consume a GraphQL service from C++, this project also includes a `graphqlclient` library and a `clientgen` utility to generate types matching a GraphQL request document, its variables, and all of the serialization code you need to talk to a `graphqlservice` implementation. If you want to consume another service, you will need access to the schema definition @@ -32,53 +31,85 @@ implementation. If you want to consume another service, you will need access to any variables to the service and parse its responses into a `graphql::response::Value` (e.g. with the `graphqljson` library) in your code. -# Getting Started - ## Related projects -I created a couple of sample projects that work with the latest version to demonstrate integrating the -[schema.today.graphql](./samples/schema.today.graphql) service into an Electron app. They're available under -my personal account, feel free to use either or both of these as a starting point to integrate your own generated -service with Node or Electron. PRs with links to your own samples are always welcome. - +The most complete examples I've built are related to [GqlMAPI](https://github.com/microsoft/gqlmapi): +- [eMAPI](https://github.com/microsoft/eMAPI): Windows-only Electron app which lets you access +the [MAPI](https://en.wikipedia.org/wiki/MAPI) interface used by +[Microsoft Outlook](https://en.wikipedia.org/wiki/Microsoft_Outlook). Its goal is to be a spiritual +successor to a debugging and diagnostic tool called +[MFCMAPI](https://github.com/stephenegriffin/mfcmapi). +- [electron-gqlmapi](https://github.com/microsoft/electron-gqlmapi): Native module for Electron +which hosts `GqlMAPI` in `eMAPI`. It includes JS libraries for calling the native module across the +Electron IPC channel. +- [Tauri-GqlMAPI](https://github.com/wravery/tauri-gqlmapi): Reimplementation of `eMAPI` built +in [Rust](https://www.rust-lang.org/) and [TypeScript](https://www.typescriptlang.org/) on top of +[Tauri](https://tauri.studio/en). +- [gqlmapi-rs](https://github.com/wravery/gqlmapi-rs): `Rust` crate I built to expose safe +bindings for `GqlMAPI`. It is loosely based on `electron-gqlmapi`, and it is used by +`Tauri-GqlMAPI`. + +I created a couple of sample projects that worked with earlier versions of `cppgraphqlgen` to +demonstrate integrating the [schema.today.graphql](./samples/schema.today.graphql) service into an +Electron app. They're still available under my personal account, but I haven't updated them +recently: - [electron-cppgraphql](https://github.com/wravery/electron-cppgraphql): Node Native Module which compiles -against the version of the Node headers included in Electron. +against the version of the Node headers included in Electron. This was the starting point for +`electron-gqlmapi`, and it is still useful as a sample because it does not depend on a platform-specific +API like `MAPI`, so it works cross-platform. - [cppgraphiql](https://github.com/wravery/cppgraphiql): Electron app which consumes `electron-cppgraphql` and exposes an instance of [GraphiQL](https://github.com/graphql/graphiql) on top of it. -## Installation process +Feel free to use either or both of these as a starting point to integrate your own generated +service with `Node`, `Electron`, or `Tauri`. PRs with links to your own samples are always welcome. -I've tested this on Windows with both Visual Studio 2017 and 2019, and on Linux using an Ubuntu 20.04 LTS instance running in -WSL with both gcc 9.3.0 and clang 10.0.0. The key compiler requirement is support for C++17 including std::filesystem, earlier -versions of gcc and clang may not have enough support for that. +# Getting Started -The easiest way to get all of these and to build `cppgraphqlgen` in one step is to use -[microsoft/vcpkg](https://github.com/microsoft/vcpkg). To install with vcpkg, make sure you've pulled the latest version -and then run `vcpkg install cppgraphqlgen` (or `cppgraphqlgen:x64-windows`, `cppgraphqlgen:x86-windows-static`, etc. -depending on your platform). To install just the dependencies and work in a clone of this repo, you'll need some subset -of `vcpkg install pegtl boost-program-options rapidjson gtest`. It works for Windows, Linux, and Mac, -but if you want to try building for another platform (e.g. Android or iOS), you'll need to do more of this manually. +## Installation process -Manual installation will work best if you clone the GitHub repos for each of the dependencies and follow the installation -instructions for each project. You might also be able to find pre-built packages depending on your platform, but the -versions need to match. +The minimum OS and toolchain versions I've tested with this version of `cppgraphqlgen` are: +- Microsoft Windows: Visual Studio 2019 +- Linux: Ubuntu 20.04 LTS with gcc 10.3.0 +- macOS: 11 (Big Sur) with AppleClang 13.0.0. + +The key compiler requirement is support for C++20 including coroutines and concepts. Some of these compiler +versions still treat coroutine support as experimental, and the CMake configuration can auto-detect that, +but earlier versions of gcc and clang do not seem to have enough support for C++20. + +The easiest way to build and install `cppgraphqlgen` is to use [microsoft/vcpkg](https://github.com/microsoft/vcpkg). +See the [Getting Started](https://github.com/microsoft/vcpkg#getting-started) section of the `vcpkg` README +for details. Once you have that configured, run `vcpkg install cppgraphqlgen` (or `cppgraphqlgen:x64-windows`, +`cppgraphqlgen:x86-windows-static`, etc. depending on your platform). That will build and install all of the +dependencies for `cppgraphqlgen`, and then build `cppgraphqlgen` itself without any other setup. The `cppgraphqlgen` +package (and its dependencies) are advertised to the `CMake` `find_package` function through the +`-DCMAKE_TOOLCHAIN_FILE=<...>/scripts/buildsystems/vcpkg.cmake` parameter/variable. There are more details about +this in the `vcpkg` documentation. + +If you want to build `cppgraphqlgen` yourself, you can do that with `CMake` from a clone or archive of this repo. +See the [Build and Test](#build-and-test) section below for instructions. You will need to install the dependencies +first where `find_package` can find them. If `vcpkg` works otherwise, you can do that with `vcpkg install pegtl +boost-program-options rapidjson gtest`. Some of these are optional, if for example you do not build the tests. If +`vcpkg` does not work, please see the documentation for each of those dependencies, as well as your +platform/toolchain documentation for perferred installation mechanisms. You may need to build some or all of them +separately from source. ## Software dependencies The build system for this project uses [CMake](http://www.cmake.org/). You will need to have CMake (at least version -3.8.0) installed, and the library dependencies need to be where CMake can find them. Otherwise you need to disable the +3.15.0) installed, and the library dependencies need to be where CMake can find them. Otherwise you need to disable the options which depend on them. -I also picked a few other projects as dependencies, most of which are optional when consuming this project. If you -redistribute any binaries built from these libraries, you should still follow the terms of their individual licenses. As -of this writing, this library and all of its redistributable dependencies are available under the MIT license, which -means you need to include an acknowledgement along with the license text. +Besides the MIT license for this project, if you redistribute any source code or binaries built from these library +dependencies, you should still follow the terms of their individual licenses. As of this writing, this library and +all of its dependencies are available under either the MIT License or the Boost Software License (BSL). Both +licenses roughly mean that you may redistribute them freely as long as you include an acknowledgement along with +the license text. Please see the license or copyright notice which comes with each project for more details. ### graphqlpeg -- GraphQL parsing: [Parsing Expression Grammar Template Library (PEGTL)](https://github.com/taocpp/PEGTL) release 3.2.0, +- GraphQL parsing: [Parsing Expression Grammar Template Library (PEGTL)](https://github.com/taocpp/PEGTL) release 3.2.2, 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.2.0 installed where CMake can find it, it will use that +do not need to install this separately. If you already have 3.2.2 installed where CMake can find it, it will use that instead of the sub-module and avoid installing another copy of PEGTL. ### graphqlservice @@ -157,7 +188,7 @@ The generated code depends on the `graphqlclient` library for serialization of b code, you'll also need to link `graphqlclient`, `graphqlpeg` for the pre-parsed, pre-validated request AST, and `graphqlresponse` for the `graphql::response::Value` implementation. -Sample output for `clientgen` is in the [samples/client](samples/client) directory, and each sample is consumed by +Sample output for `clientgen` is in the sub-directories of [samples/client](samples/client), and each sample is consumed by a unit test in [test/ClientTests.cpp](test/ClientTests.cpp). ### tests (`GRAPHQL_BUILD_TESTS=ON`) @@ -169,14 +200,27 @@ configuration. ## API references See [GraphQLService.h](include/graphqlservice/GraphQLService.h) for the base types implemented in -the `graphql::service` namespace. Take a look at [TodayMock.h](samples/today/TodayMock.h) and -[TodayMock.cpp](samples/today/TodayMock.cpp) to see a sample implementation of a custom schema defined -in [schema.today.graphql](samples/schema.today.graphql) for testing purposes. +the `graphql::service` namespace. + +Take a look at the [samples/learn](samples/learn) directory, starting with +[StarWarsData.cpp](samples/learn/StarWarsData.cpp) to see a sample implementation of a custom schema defined in +[schema.learn.graphql](samples/learn/schema/schema.learn.graphql). This is the same schema and sample data used in the +GraphQL tutorial on https://graphql.org/learn/. This directory builds an interactive command line application which +can execute query and mutation operations against the sample data in memory. + +There are several helper functions for `CMake` declared in +[cmake/cppgraphqlgen-functions.cmake](cmake/cppgraphqlgen-functions.cmake), which is automatically included if you use +`find_package(cppgraphqlgen)` in your own `CMake` project. See +[samples/learn/schema/CMakeLists.txt](samples/learn/schema/CMakeLists.txt) and +[samples/learn/CMakeLists.txt](samples/learn/CMakeLists.txt), or the `CMakeLists.txt` files in some of the +other samples sub-directories for examples of how to use them to automatically rerun the code generators and update +the files in your source directory. ### Additional Documentation There are some more targeted documents in the [doc](./doc) directory: +* [Awaitable Types](./doc/awaitable.md) * [Parsing GraphQL](./doc/parsing.md) * [Query Responses](./doc/responses.md) * [JSON Representation](./doc/json.md) @@ -187,14 +231,28 @@ There are some more targeted documents in the [doc](./doc) directory: ### 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/`) -for each of the object types which need to be implemeneted. The only difference between -[TodayMock.h](samples/today/TodayMock.h) with and without `IMPL_SEPARATE_TODAY` defined should be that the -`--separate-files` option generates a [TodayObjects.h](samples/separate/TodayObjects.h) convenience header -which includes all of the inidividual object header along with the rest of the schema in -[TodaySchema.h](samples/separate/TodaySchema.h). +All of the samples are under [samples](samples/), with nested sub-directories for generated files: +- [samples/today](samples/today/): There are two different samples generated from +[schema.today.graphql](samples/today/schema.today.graphql) in this directory. The default +[schema](samples/today/schema/) target includes Introspection support (which is the default), while the +[nointrospection](samples/today/nointrospection/) target demonstrates how to disable Introspection support +with the `schemagen --no-introspection` parameter. The mock implementation of the service for both schemas is in +[samples/today/TodayMock.h](samples/today/TodayMock.h) and [samples/today/TodayMock.cpp](samples/today/TodayMock.cpp). +It builds an interactive `sample`/`sample_nointrospection` and `benchmark`/`benchmark_nointrospection` target for +each version, and it uses each of them in several unit tests. +- [samples/client](samples/client/): Several sample queries built with `clientgen` against the +[schema.today.graphql](samples/today/schema.today.graphql) schema shared with [samples/today](samples/today/). It +includes a `client_benchmark` executable for comparison with benchmark executables using the same hardcoded query +in [samples/today/]. The benchmark links with the default [schema](samples/today/schema/) target in +[samples/today](samples/today/) to handle the benchmark query. +- [samples/learn](samples/learn/): Simpler standalone which builds a `learn_star_wars` executable that follows +the tutorial examples on https://graphql.org/learn/. +- [samples/validation](samples/validation/): This schema is based on the examples and counter-examples from the +[Validation](https://spec.graphql.org/October2021/#sec-Validation) section of the October 2021 GraphQL spec. There +is no implementation of this schema, it relies entirely generated stubs (created with `schemagen --stubs`) to build +successfully without defining more than placeholder objects fo the Query, Mutation, and Subscription operations in +[samples/validation/ValidationMock.h](samples/validation/ValidationMock.h). It is used to test the validation logic +with every example or counter-example in the spec in [test/ValidationTests.cpp](test/ValidationTests.cpp). # Build and Test @@ -211,7 +269,7 @@ can run all of them from there. Your experience will vary depending on your build toolchain. The same instructions should work for any platform that CMake supports. These basic steps will build and run the tests. You can add options to build in another target directory, -change the config from `Debug` (default) to `Release`, use another build tool like `Ninja`, etc. If you are using vcpkg +change the config from `Debug` (default) to `Release`, use another build tool like `Ninja`, etc. If you are using `vcpkg` to install the dependencies, remember to specify the `-DCMAKE_TOOLCHAIN_FILE=...` option when you run the initial build configuration. @@ -226,8 +284,8 @@ You can then optionally install the public outputs by configuring it with `Relea ## Interactive tests -If you want to try an interactive version, you can run `samples/sample` and paste in queries against -the same mock service or load a query from a file on the command line. +If you want to try an interactive version, you can run `samples/today/sample` or `samples/today/sample_nointrospection` +and paste in queries against the same mock service or load a query from a file on the command line. ## Reporting Security Issues diff --git a/cmake/test_filesystem.cpp b/cmake/test_filesystem.cpp new file mode 100644 index 00000000..240e7d6b --- /dev/null +++ b/cmake/test_filesystem.cpp @@ -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 + +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(); +} \ No newline at end of file diff --git a/cmake/test_filesystem.cpp.in b/cmake/test_filesystem.cpp.in deleted file mode 100644 index e29d850d..00000000 --- a/cmake/test_filesystem.cpp.in +++ /dev/null @@ -1,22 +0,0 @@ -// This is a dummy program that just needs to compile and link to tell us if -// the C++17 std::filesystem API is available. Use CMake's configure_file -// command to replace the FILESYSTEM_HEADER and FILESYSTEM_NAMESPACE tokens -// for each combination of headers and namespaces which we want to pass to the -// CMake try_compile command. - -#include <@FILESYSTEM_HEADER@> - -int main() -{ - try - { - throw @FILESYSTEM_NAMESPACE@::filesystem_error("instantiate one to make sure it links", - std::make_error_code(std::errc::function_not_supported)); - } - catch (const @FILESYSTEM_NAMESPACE@::filesystem_error& error) - { - return -1; - } - - return !@FILESYSTEM_NAMESPACE@::temp_directory_path().is_absolute(); -} \ No newline at end of file diff --git a/doc/awaitable.md b/doc/awaitable.md new file mode 100644 index 00000000..0d41be36 --- /dev/null +++ b/doc/awaitable.md @@ -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 + explicit await_async(std::shared_ptr pimpl) + : _pimpl { std::make_shared>(std::move(pimpl)) } + { + } + + // Default to immediate synchronous execution. + await_async() + : _pimpl { std::static_pointer_cast( + std::make_shared>(std::make_shared())) } + { + } + + // 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(std::make_shared>( + std::make_shared())) + : std::static_pointer_cast(std::make_shared>( + std::make_shared())) } + { + } +... +}; +``` +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 +class Awaitable +{ +public: + Awaitable(std::future 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) + { + _promise.set_value(std::move(value)); + } + + ... + + private: + std::promise _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 _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`, and it can either +`co_await` for that `std::future` from a coroutine, or call `T get()` to block a regular +function until it completes. + +## AwaitableScalar and AwaitableObject + +In previous versions, `service::FieldResult` created an abstraction over return types `T` and +`std::future`, when returning from a field getter you could return either and it would +implicitly convert that to a `service::FieldResult` which looked and acted like a +`std::future`. + +Now, `service::FieldResult` 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` 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, 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 directly. +template +class AwaitableScalar +{ +public: + template + AwaitableScalar(U&& value) + : _value { std::forward(value) } + { + } + + struct promise_type + { + AwaitableScalar get_return_object() noexcept + { + return { _promise.get_future() }; + } + + ... + + void return_value(const T& value) noexcept(std::is_nothrow_copy_constructible_v) + { + _promise.set_value(value); + } + + void return_value(T&& value) noexcept(std::is_nothrow_move_constructible_v) + { + _promise.set_value(std::move(value)); + } + + ... + + private: + std::promise _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") if called with that alternative + } + + std::shared_ptr get_value() noexcept + { + ... // Returns an empty std::shared_ptr if called with a different alternative + } + +private: + std::variant, std::shared_ptr> _value; +}; + +// Field accessors may return either a result of T, an awaitable of T, or a std::future, 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 +class AwaitableObject +{ +public: + template + AwaitableObject(U&& value) + : _value { std::forward(value) } + { + } + + struct promise_type + { + AwaitableObject get_return_object() noexcept + { + return { _promise.get_future() }; + } + + ... + + void return_value(const T& value) noexcept(std::is_nothrow_copy_constructible_v) + { + _promise.set_value(value); + } + + void return_value(T&& value) noexcept(std::is_nothrow_move_constructible_v) + { + _promise.set_value(std::move(value)); + } + + ... + + private: + std::promise _promise; + }; + + bool await_ready() const noexcept { ... } + + void await_suspend(coro::coroutine_handle<> h) const { ... } + + T await_resume() { ... } + +private: + std::variant> _value; +}; +``` + +These types both add a `promise_type` for `T`, but coroutines need their own return type to do that. +Making `service::AwaitableScalar` or `service::AwaitableObject` 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` 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` or `service::AwaitableObject`. Otherwise, you can remove +the template wrapper from all of your field getters. diff --git a/doc/directives.md b/doc/directives.md index cc13f849..faa67a9b 100644 --- a/doc/directives.md +++ b/doc/directives.md @@ -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 @@ -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). \ No newline at end of file diff --git a/doc/fieldparams.md b/doc/fieldparams.md index 8d33d6c4..7ca8d7c1 100644 --- a/doc/fieldparams.md +++ b/doc/fieldparams.md @@ -7,31 +7,18 @@ 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. +This parameter is optional. The type-erased implementation of `graphql::service::Object` +for each `Object` type in the schema declares a pair of `methods::ObjectHas::getFieldWithParams` +and `methods::ObjectHas::getField` concepts for each field getter. If the implementation +type supports passing the `graphql::service::FieldParams` struct as the first parameter, +the type-erased `object::Object` invokes the `getFieldWithParams` version, otherwise it +drops the parameter and calls the `getField` version with whatever other field +arguments the schema specified. + ## Details of Field Parameters The `graphql::service::FieldParams` struct is declared in [GraphQLService.h](../include/graphqlservice/GraphQLService.h): ```cpp -// Resolvers may be called in multiple different Operation contexts. -enum class ResolverContext -{ - // Resolving a Query operation. - Query, - - // Resolving a Mutation operation. - Mutation, - - // Adding a Subscription. If you need to prepare to send events for this Subsciption - // (e.g. registering an event sink of your own), this is a chance to do that. - NotifySubscribe, - - // Resolving a Subscription event. - Subscription, - - // Removing a Subscription. If there are no more Subscriptions registered this is an - // opportunity to release resources which are no longer needed. - NotifyUnsubscribe, -}; - // Pass a common bundle of parameters to all of the generated Object::getField accessors in a // SelectionSet struct SelectionSetParams @@ -42,39 +29,61 @@ 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& state; - const response::Value& operationDirectives; - const response::Value& fragmentDefinitionDirectives; + const Directives& operationDirectives; + const std::shared_ptr 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; + // you'll need to explicitly copy them into other instances of Directives. + const std::shared_ptr fragmentSpreadDirectives; + const std::shared_ptr inlineFragmentDirectives; // Field error path to this selection set. std::optional errorPath; // Async launch policy for sub-field resolvers. - const std::launch launch = std::launch::deferred; + const await_async launch {}; }; // Pass a common bundle of parameters to all of the generated Object::getField accessors. struct FieldParams : SelectionSetParams { GRAPHQLSERVICE_EXPORT explicit FieldParams( - SelectionSetParams&& selectionSetParams, response::Value&& directives); + SelectionSetParams&& selectionSetParams, Directives 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; + Directives fieldDirectives; }; ``` ### Resolver Context The `SelectionSetParams::resolverContext` enum member informs the `getField` -accessors about what type of operation is being resolved. +accessors about what type of operation is being resolved: +```cpp +// Resolvers may be called in multiple different Operation contexts. +enum class ResolverContext +{ + // Resolving a Query operation. + Query, + + // Resolving a Mutation operation. + Mutation, + + // Adding a Subscription. If you need to prepare to send events for this Subsciption + // (e.g. registering an event sink of your own), this is a chance to do that. + NotifySubscribe, + + // Resolving a Subscription event. + Subscription, + + // Removing a Subscription. If there are no more Subscriptions registered this is an + // opportunity to release resources which are no longer needed. + NotifyUnsubscribe, +}; +``` ### Request State @@ -82,10 +91,10 @@ The `SelectionSetParams::state` member is a reference to the `std::shared_ptr` 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. +// 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 { }; @@ -96,16 +105,27 @@ struct RequestState : std::enable_shared_from_this 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`. +behavior based on those custom `directives`: +```cpp +// Directive order matters, and some of them are repeatable. So rather than passing them in a +// response::Value, pass directives in something like the underlying response::MapType which +// preserves the order of the elements without complete uniqueness. +using Directives = std::vector>; + +// Traversing a fragment spread adds a new set of directives. +using FragmentDefinitionDirectiveStack = std::list>; +using FragmentSpreadDirectiveStack = std::list; +``` 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. +`inlineFragmentDirectives` are stacks of directives passed down through nested +inline fragments and fragment spreads. The `Directives` object for each frame of +the stack is shared accross calls to multiple `getField` methods in a single fragment, +but they will be popped from the stack when the last field has been visited. The +`fieldDirectives` member is passed by value and is not shared with other `getField` +method calls. 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 @@ -124,18 +144,13 @@ report, accoring to the [spec](https://spec.graphql.org/October2021/#sec-Errors) ### Launch Policy -The `graphqlservice` library uses the `SelectionSetParams::launch` parameter to -determine how it should handle async resolvers in the same selection set or -elements in the same list. It is passed from the top-most `resolve`, `deliver`, -or async `subscribe`/`unsubscribe` call. The `getField` accessors get a copy of -this member in their `FieldParams` argument, and they may change their own -behavior based on that, but they cannot alter the launch policy which -`graphqlservice` uses for the resolvers themselves. +See the [Awaitable](./awaitable.md) document for more information about +`service::await_async`. ## 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). -3. Subscription resolvers get called up to 3 times depending on which -`subscribe`/`unsubscribe` overrides you call. See [subscriptions.md](./subscriptions.md) -for more details. \ No newline at end of file +2. Awaitable types are covered in [awaitable.md](./awaitable.md). +3. Built-in and custom `directives` are discussed in [directives.md](./directives.md). +4. Subscription resolvers may be called 2 extra times, inside of subscribe` and `unsubscribe`. +See [subscriptions.md](./subscriptions.md) for more details. \ No newline at end of file diff --git a/doc/json.md b/doc/json.md index 57202daa..8ea8b0c4 100644 --- a/doc/json.md +++ b/doc/json.md @@ -37,4 +37,49 @@ comment in that file for more information: # 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) -``` \ No newline at end of file +``` + +## response::Writer + +You can plug-in a type-erased streaming `response::Writer` to serialize a `response::Value` +to some other output mechanism, without building a single string buffer for the entire +document in memory. For example, you might use this to write directly to a buffered IPC pipe +or network connection: +```cpp +class Writer +{ +private: + struct Concept + { + virtual ~Concept() = default; + + virtual void start_object() const = 0; + virtual void add_member(const std::string& key) const = 0; + virtual void end_object() const = 0; + + virtual void start_array() const = 0; + virtual void end_arrary() const = 0; + + virtual void write_null() const = 0; + virtual void write_string(const std::string& value) const = 0; + virtual void write_bool(bool value) const = 0; + virtual void write_int(int value) const = 0; + virtual void write_float(double value) const = 0; + }; +... + +public: + template + Writer(std::unique_ptr writer) + : _concept { std::static_pointer_cast( + std::make_shared>(std::move(writer))) } + { + } + + GRAPHQLRESPONSE_EXPORT void write(Value value) const; +}; +``` + +Internally, this is what `graphqljson` uses to implement `response::toJSON` with RapidJSON. +It wraps a `rapidjson::Writer` in `response::Writer` and then writes into a +`rapidjson::StringBuffer` through that. diff --git a/doc/parsing.md b/doc/parsing.md index 018e0fbb..300fe816 100644 --- a/doc/parsing.md +++ b/doc/parsing.md @@ -49,7 +49,7 @@ mix of executable and schema definitions. There are `parseSchemaString` and `parseSchemaFile` functions which do the opposite, but unless you are building additional tooling on top of the `graphqlpeg` library, you will probably not need them. They have only been used -by `schemagen` in this project. +by `schemagen` and `clientgen` in this project. ## Encoding diff --git a/doc/resolvers.md b/doc/resolvers.md index a66e3b4e..a294392d 100644 --- a/doc/resolvers.md +++ b/doc/resolvers.md @@ -34,20 +34,37 @@ schema { Executing a query or mutation starts by calling `Request::resolve` from [GraphQLService.h](../include/graphqlservice/GraphQLService.h): ```cpp -GRAPHQLSERVICE_EXPORT response::AwaitableValue resolve( - const std::shared_ptr& state, peg::ast& query, - const std::string& operationName, response::Value&& variables) const; +GRAPHQLSERVICE_EXPORT response::AwaitableValue resolve(RequestResolveParams params) const; ``` -By default, the `std::future` results are resolved on-demand but synchronously. -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: + +The `RequestResolveParams` struct is defined in the same header: ```cpp -GRAPHQLSERVICE_EXPORT response::AwaitableValue resolve(std::launch launch, - const std::shared_ptr& state, peg::ast& query, - const std::string& operationName, response::Value&& variables) const; +struct RequestResolveParams +{ + // Required query information. + peg::ast& query; + std::string_view operationName {}; + response::Value variables { response::Type::Map }; + + // Optional async execution awaitable. + await_async launch; + + // Optional sub-class of RequestState which will be passed to each resolver and field accessor. + std::shared_ptr state; +}; ``` +The only parameter which cannot be default initialized is `query`. + +The `service::await_async` launch policy is described in [awaitable.md](./awaitable.md). +By default, the resolvers will run on the same thread synchronously. + +The `response::AwaitableValue` return type is a type alias in [GraphQLResponse.h](../include/graphqlservice/GraphQLResponse.h): +```cpp +using AwaitableValue = internal::Awaitable; +``` +The `internal::Awaitable` template is described in [awaitable.md](./awaitable.md). + ### `graphql::service::Request` and `graphql::::Operations` Anywhere in the documentation where it mentions `graphql::service::Request` @@ -55,45 +72,104 @@ methods, the concrete type will actually be `graphql::::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) +`graphql::today::Operations` in [TodaySchema.h](../samples/today/schema/TodaySchema.h) for an example. ## Generated Service Schema -The `schemagen` tool generates C++ types in the `graphql::::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). +The `schemagen` tool generates type-erased C++ types in the `graphql::::object` +namespace with `resolveField` methods for each `field` which parse the arguments from +the `query` and automatically dispatch the call to a `getField` method on the +implementation type 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.cpp](../samples/today/schema/AppointmentObject.cpp): ```cpp -service::AwaitableResolver resolveId(service::ResolverParams&& params); +service::AwaitableResolver Appointment::resolveId(service::ResolverParams&& params) const +{ + std::unique_lock resolverLock(_resolverMutex); + auto directives = std::move(params.fieldDirectives); + auto result = _pimpl->getId(service::FieldParams(service::SelectionSetParams{ params }, std::move(directives))); + resolverLock.unlock(); + + return service::ModifiedResult::convert(std::move(result), std::move(params)); +} ``` -In this example, the `resolveId` method invokes `getId`: +In this example, the `resolveId` method invokes `Concept::getId(service::FieldParams&&)`, +which is implemented by `Model::getId(service::FieldParams&&)`: ```cpp -virtual service::AwaitableScalar getId(service::FieldParams&& params) const override; +service::AwaitableScalar getId(service::FieldParams&& params) const final +{ + if constexpr (methods::AppointmentHas::getIdWithParams) + { + return { _pimpl->getId(std::move(params)) }; + } + else if constexpr (methods::AppointmentHas::getId) + { + return { _pimpl->getId() }; + } + else + { + throw std::runtime_error(R"ex(Appointment::getId is not implemented)ex"); + } +} ``` -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: +There are a couple of interesting points in this example: +1. The `methods::AppointmentHas::getIdWithParams` and +`methods::AppointmentHas::getIdWith` concepts are automatically generated at the top of +[AppointmentObject.h](../samples/today/schema/AppointmentObject.h). The implementation +of the virtual method from the `object::Appointment::Concept` interface uses +`if constexpr (...)` to conditionally compile just one of the 3 method bodies, depending +on whether or not `T` matches those concepts: ```cpp -throw std::runtime_error(R"ex(Appointment::getId is not implemented)ex"); +namespace methods::AppointmentHas { + +template +concept getIdWithParams = requires (TImpl impl, service::FieldParams params) +{ + { service::AwaitableScalar { impl.getId(std::move(params)) } }; +}; + +template +concept getId = requires (TImpl impl) +{ + { service::AwaitableScalar { impl.getId() } }; +}; + +... + +} // namespace methods::AppointmentHas +``` +2. This schema was generated with default stub implementations (using the +`schemagen --stubs` parameter) which speeds up initial development with NYI +(Not Yet Implemented) stubs. If the implementation type `T` does not match either +concept, it will still implement this method on `object::Appointment::Model`, but +it will always throw a `std::runtime_error` indicating that the method was not implemented. +Compared to the type-erased objects generated for the [learn](../samples/learn/), such as +[HumanObject.h](../samples/learn/schema/HumanObject.h), without `schemagen --stubs` it +adds a `static_assert` instead, so it will trigger a compile-time error if you do not +implement all of the field getters: +```cpp +service::AwaitableScalar getId(service::FieldParams&& params) const final +{ + if constexpr (methods::HumanHas::getIdWithParams) + { + return { _pimpl->getId(std::move(params)) }; + } + else + { + static_assert(methods::HumanHas::getId, R"msg(Human::getId is not implemented)msg"); + return { _pimpl->getId() }; + } +} ``` Although the `id field` does not take any arguments according to the sample -[schema](../samples/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. \ No newline at end of file +[schema](../samples/today/schema.today.graphql), this example also shows how every `getField` +method on the `object::Appointment::Concept` takes a `graphql::service::FieldParams` struct +as its first parameter from the resolver. If the implementation type can take that parameter +and matches the concept, the `object::Appointment::Model` `getField` method will pass +it through to the implementation type. If it does not, it will silently ignore that +parameter and invoke the implementation type `getField` method without it. There are more +details on this in the [fieldparams.md](./fieldparams.md) document. \ No newline at end of file diff --git a/doc/subscriptions.md b/doc/subscriptions.md index d5929820..3ee8f02a 100644 --- a/doc/subscriptions.md +++ b/doc/subscriptions.md @@ -8,48 +8,84 @@ provide a way to register callbacks when adding a subscription, and you can define trigger conditions when delivering an update to selectively dispatch the subscriptions to those listeners. -## Adding/Removing a Listener +## Adding a Listener -Subscriptions are created or removed by calling the `Request::subscribe` -and `Request::unsubscribe` methods in [GraphQLService.h](../include/graphqlservice/GraphQLService.h): +Subscriptions are created by calling the `Request::subscribe` method in +[GraphQLService.h](../include/graphqlservice/GraphQLService.h): ```cpp -GRAPHQLSERVICE_EXPORT SubscriptionKey subscribe( - RequestSubscribeParams&& params, SubscriptionCallback&& callback); -GRAPHQLSERVICE_EXPORT AwaitableSubscribe subscribe( - std::launch launch, RequestSubscribeParams&& params, SubscriptionCallback&& callback); - -GRAPHQLSERVICE_EXPORT void unsubscribe(SubscriptionKey key); -GRAPHQLSERVICE_EXPORT AwaitableUnsubscribe unsubscribe(std::launch launch, SubscriptionKey key); +GRAPHQLSERVICE_EXPORT AwaitableSubscribe subscribe(RequestSubscribeParams params); ``` -You need to fill in a `RequestSubscribeParams` struct with the [parsed](./parsing.md) -query and any other relevant operation parameters: + +You need to fill in a `RequestSubscribeParams` struct with the subscription event +callback, the [parsed](./parsing.md) `query` and any other relevant operation parameters: ```cpp -// You can still sub-class RequestState and use that in the state parameter to Request::subscribe -// to add your own state to the service callbacks that you receive while executing the subscription -// query. struct RequestSubscribeParams { - std::shared_ptr state; + // Callback which receives the event data. + SubscriptionCallback callback; + + // Required query information. peg::ast query; - std::string operationName; - response::Value variables; + std::string operationName {}; + response::Value variables { response::Type::Map }; + + // Optional async execution awaitable. + await_async launch; + + // Optional sub-class of RequestState which will be passed to each resolver and field accessor. + std::shared_ptr state; }; ``` + The `SubscriptionCallback` signature is: ```cpp // Subscription callbacks receive the response::Value representing the result of evaluating the // SelectionSet against the payload. -using SubscriptionCallback = std::function; +using SubscriptionCallback = std::function; +``` + +The `service::await_async` launch policy is described in [awaitable.md](./awaitable.md). +By default, the resolvers will run on the same thread synchronously. + +The `std::shared_ptr` state is described in [fieldparams.md](./fieldparams.md). + +The `AwaitableSubscribe` return type is a type alias in +[GraphQLResponse.h](../include/graphqlservice/GraphQLResponse.h): +```cpp +using AwaitableSubscribe = internal::Awaitable; +``` +The `internal::Awaitable` template is described in [awaitable.md](./awaitable.md). + +## Removing a Listener + +Subscriptions are removed by calling the `Request::unsubscribe` method in +[GraphQLService.h](../include/graphqlservice/GraphQLService.h): +```cpp +GRAPHQLSERVICE_EXPORT AwaitableUnsubscribe unsubscribe(RequestUnsubscribeParams params); +``` + +You need to fill in a `RequestUnsubscribeParams` struct with the `SubscriptionKey` +returned by `Request::subscribe` in `AwaitableSubscribe`: +```cpp +struct RequestUnsubscribeParams +{ + // Key returned by a previous call to subscribe. + SubscriptionKey key; + + // Optional async execution awaitable. + await_async launch; +}; ``` +The `service::await_async` launch policy is described in [awaitable.md](./awaitable.md). +By default, the resolvers will run on the same thread synchronously. + ## `ResolverContext::NotifySubscribe` and `ResolverContext::NotifyUnsubscribe` -If you use the async version of `subscribe` and `unsubscribe` which take a -`std::launch` parameter, and you provide a default instance of the -`Subscription` object to the `Request`/`Operations` constructor, you will get -additional callbacks with the `ResolverContext::NotifySubscribe` and -`ResolverContext::NotifyUnsubscribe` values for the -`FieldParams::resolverContext` member. These are passed by the async +If you provide a default instance of the `Subscription` object to the `Request`/ +`Operations` constructor, you will get additional callbacks with the +`ResolverContext::NotifySubscribe` and `ResolverContext::NotifyUnsubscribe` values +for the `FieldParams::resolverContext` member. These are passed by the `subscribe` and `unsubscribe` calls to the default subscription object, and they provide an opportunity to acquire or release resources that are required to implement the subscription. @@ -62,79 +98,77 @@ event payload for a `deliver` call will be resolved with ## Delivering Subscription Updates -If you pass an empty `std::shared_ptr` for the `subscriptionObject` -parameter, `deliver` will fall back to resolving the query against the default -`Subscription` object passed to the `Request`/`Operations` constructor. If both -`Subscription` object parameters are empty, `deliver` will throw an exception. - -There are currently five `Request::deliver` overrides you can choose from when -sending updates to any subscribed listeners. The first one is the simplest, -it will evaluate each subscribed query against the `subscriptionObject` -parameter (which should match the `Subscription` type in the `schema`). It will -unconditionally invoke every subscribed `SubscriptionCallback` callback with -the response to its query: +If you pass an empty `std::shared_ptr` for the +`subscriptionObject` parameter, `deliver` will fall back to resolving the query +against the default `Subscription` object passed to the `Request`/`Operations` +constructor. If both `Subscription` object parameters are empty, `deliver` +will throw an exception: ```cpp -GRAPHQLSERVICE_EXPORT void deliver( - const SubscriptionName& name, const std::shared_ptr& subscriptionObject) const; +GRAPHQLSERVICE_EXPORT AwaitableDeliver deliver(RequestDeliverParams params) const; ``` -The second override adds argument filtering. It will look at the field -arguments in the subscription `query`, and if all of the required parameters -in the `arguments` parameter are present and are an exact match it will -dispatch the callback to that subscription: +The `Request::deliver` method determines which subscriptions should receive +an event based on several factors, which makes the `RequestDeliverParams` struct +more complicated: ```cpp -GRAPHQLSERVICE_EXPORT void deliver(const SubscriptionName& name, - const SubscriptionArguments& arguments, - const std::shared_ptr& subscriptionObject) const; -``` +struct RequestDeliverParams +{ + // Deliver to subscriptions on this field. + std::string_view field; -The third override adds directive filtering. It will look at both the field -arguments and the directives with their own arguments in the subscription -`query`, and if all of them match it will dispatch the callback to that -subscription: -```cpp -GRAPHQLSERVICE_EXPORT void deliver(const SubscriptionName& name, - const SubscriptionArguments& arguments, const SubscriptionArguments& directives, - const std::shared_ptr& subscriptionObject) const; + // Optional filter to control which subscriptions will receive the event. If not specified, + // every subscription on this field will receive the event and evaluate their queries. + RequestDeliverFilter filter; + + // Optional async execution awaitable. + await_async launch; + + // Optional override for the default Subscription operation object. + std::shared_ptr subscriptionObject; +}; ``` -The last two overrides let you customize the the way that the required -arguments and directives are matched. Instead of an exact match or making all -of the arguments required, it will dispatch the callback if the `apply` -function parameters return true for every required field and directive in the -subscription `query`. +First, the `Request::deliver` method selects only the subscriptions which are listening +to events for this field. Then, if you specify the `RequestDeliverFilter`, it filters the set +of subscriptions down to the ones which it matches: ```cpp -GRAPHQLSERVICE_EXPORT void deliver(const SubscriptionName& name, - const SubscriptionFilterCallback& applyArguments, - const std::shared_ptr& subscriptionObject) const; -GRAPHQLSERVICE_EXPORT void deliver(const SubscriptionName& name, - const SubscriptionFilterCallback& applyArguments, - const SubscriptionFilterCallback& applyDirectives, - const std::shared_ptr& subscriptionObject) const; +// Deliver to a specific subscription key, or apply custom criteria for the field name, arguments, +// and directives in the Subscription query. +using RequestDeliverFilter = std::optional>; ``` -By default, `deliver` invokes all of the `SubscriptionCallback` listeners with -`std::future` payloads which are resolved on-demand but synchronously. You can -also use an override of `Request::resolve` which lets you substitute the -`std::launch::async` option to begin executing the queries and invoke the -callbacks on multiple threads in parallel: +The simplest filter is the `SubscriptionKey`. If you specify that, and the subscription is +listening to `field` events, `Request::deliver` will only deliver to that one subscription. + +The `SubscriptionFilter struct` adds alternative filters for the field arguments and any field +directives which might have been specified in the subscription query. For each one, you can either +specify a callback which will test each argument or directive, or you can specify a set of required +arguments or directives which must all be present: ```cpp -GRAPHQLSERVICE_EXPORT AwaitableDeliver deliver(std::launch launch, const SubscriptionName& name, - std::shared_ptr subscriptionObject) const; -GRAPHQLSERVICE_EXPORT AwaitableDeliver deliver(std::launch launch, const SubscriptionName& name, - const SubscriptionArguments& arguments, std::shared_ptr subscriptionObject) const; -GRAPHQLSERVICE_EXPORT AwaitableDeliver deliver(std::launch launch, const SubscriptionName& name, - const SubscriptionArguments& arguments, const SubscriptionArguments& directives, - std::shared_ptr subscriptionObject) const; -GRAPHQLSERVICE_EXPORT AwaitableDeliver deliver(std::launch launch, const SubscriptionName& name, - const SubscriptionFilterCallback& applyArguments, - std::shared_ptr subscriptionObject) const; -GRAPHQLSERVICE_EXPORT AwaitableDeliver deliver(std::launch launch, const SubscriptionName& name, - const SubscriptionFilterCallback& applyArguments, - const SubscriptionFilterCallback& applyDirectives, - std::shared_ptr subscriptionObject) const; +using SubscriptionArguments = std::map; +using SubscriptionArgumentFilterCallback = std::function; +using SubscriptionDirectiveFilterCallback = std::function; + +struct SubscriptionFilter +{ + // Optional field argument filter, which can either be a set of required arguments, or a + // callback which returns true if the arguments match custom criteria. + std::optional> + arguments; + + // Optional field directives filter, which can either be a set of required directives and + // arguments, or a callback which returns true if the directives match custom criteria. + std::optional> directives; +}; ``` +The `service::await_async` launch policy is described in [awaitable.md](./awaitable.md). +By default, the resolvers will run on the same thread synchronously. + +The optional `std::shared_ptr subscriptionObject` parameter can override the +default Subscription operation object passed to the `Operations` constructor, or supply +one if no default instance was included. + ## Handling Multiple Operation Types Some service implementations (e.g. Apollo over HTTP) use a single pipe to diff --git a/include/graphqlservice/GraphQLService.h b/include/graphqlservice/GraphQLService.h index 4c020298..1f08e3d3 100644 --- a/include/graphqlservice/GraphQLService.h +++ b/include/graphqlservice/GraphQLService.h @@ -1233,9 +1233,6 @@ using SubscriptionDirectiveFilterCallback = std::function> @@ -1252,8 +1249,11 @@ using RequestDeliverFilter = std::optional SubscriptionKey addSubscription(RequestSubscribeParams&& params); void removeSubscription(SubscriptionKey key); std::vector> collectRegistrations( - RequestDeliverFilter&& filter) const noexcept; + std::string_view field, RequestDeliverFilter&& filter) const noexcept; const TypeMap _operations; std::unique_ptr _validation; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index abd456f6..3a798528 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -245,18 +245,12 @@ endif() # Common schemagen and clientgen filesystem and Boost dependencies if(GRAPHQL_BUILD_SCHEMAGEN OR GRAPHQL_BUILD_CLIENTGEN) - set(BOOST_COMPONENTS program_options) - set(BOOST_LIBRARIES Boost::program_options) - # Try compiling a test program with std::filesystem or one of its alternatives. - function(check_filesystem_impl FILESYSTEM_HEADER FILESYSTEM_NAMESPACE OPTIONAL_LIBS OUT_RESULT) - set(TEST_FILE "test_${OUT_RESULT}.cpp") - configure_file(${CMAKE_CURRENT_SOURCE_DIR}/../cmake/test_filesystem.cpp.in ${TEST_FILE} @ONLY) - + function(check_filesystem_impl OPTIONAL_LIBS) try_compile(TEST_RESULT ${CMAKE_CURRENT_BINARY_DIR} - ${CMAKE_CURRENT_BINARY_DIR}/${TEST_FILE} - CXX_STANDARD 17) + ${CMAKE_CURRENT_SOURCE_DIR}/../cmake/test_filesystem.cpp + CXX_STANDARD 20) if(NOT TEST_RESULT) # Retry with each of the optional libraries. @@ -265,7 +259,7 @@ if(GRAPHQL_BUILD_SCHEMAGEN OR GRAPHQL_BUILD_CLIENTGEN) ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_BINARY_DIR}/${TEST_FILE} LINK_LIBRARIES ${OPTIONAL_LIB} - CXX_STANDARD 17) + CXX_STANDARD 20) if(TEST_RESULT) # Looks like the optional library was required, go ahead and add it to the link options. @@ -279,49 +273,19 @@ if(GRAPHQL_BUILD_SCHEMAGEN OR GRAPHQL_BUILD_CLIENTGEN) endif() endforeach(OPTIONAL_LIB) endif() - - set(${OUT_RESULT} ${TEST_RESULT} PARENT_SCOPE) endfunction(check_filesystem_impl) - # Try compiling a minimal program with each header/namespace, in order of preference: - # C++17: #include // std::filesystem - # Experimental C++17: #include // std::experimental::filesystem - # Boost.Filesystem: #include // boost::filesystem - check_filesystem_impl("filesystem" "std::filesystem" "stdc++fs;c++fs" STD_FILESYTEM) - if(STD_FILESYTEM) - if(GRAPHQL_BUILD_SCHEMAGEN) - target_compile_definitions(schemagen PRIVATE USE_STD_FILESYSTEM) - endif() - if(GRAPHQL_BUILD_CLIENTGEN) - target_compile_definitions(clientgen PRIVATE USE_STD_FILESYSTEM) - endif() - else() - check_filesystem_impl("experimental/filesystem" "std::experimental::filesystem" "stdc++fs;c++fs" STD_EXPERIMENTAL_FILESYTEM) - if(STD_EXPERIMENTAL_FILESYTEM) - if(GRAPHQL_BUILD_SCHEMAGEN) - target_compile_definitions(schemagen PRIVATE USE_STD_EXPERIMENTAL_FILESYSTEM) - endif() - if(GRAPHQL_BUILD_CLIENTGEN) - target_compile_definitions(clientgen PRIVATE USE_STD_EXPERIMENTAL_FILESYSTEM) - endif() - else() - set(BOOST_COMPONENTS ${BOOST_COMPONENTS} filesystem) - set(BOOST_LIBRARIES ${BOOST_LIBRARIES} Boost::filesystem) - if(GRAPHQL_BUILD_SCHEMAGEN) - target_compile_definitions(schemagen PRIVATE USE_BOOST_FILESYSTEM) - endif() - if(GRAPHQL_BUILD_CLIENTGEN) - target_compile_definitions(clientgen PRIVATE USE_BOOST_FILESYSTEM) - endif() - endif() - endif() + # Try compiling a minimal program without any extra libraries, then with each optional library until it succeeded: + # stdc++fs + # c++fs + check_filesystem_impl("stdc++fs;c++fs" STD_FILESYTEM) - find_package(Boost REQUIRED COMPONENTS ${BOOST_COMPONENTS}) + find_package(Boost REQUIRED COMPONENTS program_options) if(GRAPHQL_BUILD_SCHEMAGEN) - target_link_libraries(schemagen PRIVATE ${BOOST_LIBRARIES}) + target_link_libraries(schemagen PRIVATE Boost::program_options) endif() if(GRAPHQL_BUILD_CLIENTGEN) - target_link_libraries(clientgen PRIVATE ${BOOST_LIBRARIES}) + target_link_libraries(clientgen PRIVATE Boost::program_options) endif() endif() diff --git a/src/ClientGenerator.cpp b/src/ClientGenerator.cpp index 1d099f55..14b7d9a6 100644 --- a/src/ClientGenerator.cpp +++ b/src/ClientGenerator.cpp @@ -20,26 +20,8 @@ #pragma warning(pop) #endif // _MSC_VER -// clang-format off -#ifdef USE_STD_FILESYSTEM - #include - namespace fs = std::filesystem; -#else - #ifdef USE_STD_EXPERIMENTAL_FILESYSTEM - #include - namespace fs = std::experimental::filesystem; - #else - #ifdef USE_BOOST_FILESYSTEM - #include - namespace fs = boost::filesystem; - #else - #error "No std::filesystem implementation defined" - #endif - #endif -#endif -// clang-format on - #include +#include #include #include #include @@ -66,7 +48,7 @@ std::string Generator::getHeaderDir() const noexcept { if (!_options.paths.headerPath.empty()) { - return fs::path { _options.paths.headerPath }.string(); + return std::filesystem::path { _options.paths.headerPath }.string(); } else { @@ -78,7 +60,7 @@ std::string Generator::getSourceDir() const noexcept { if (!_options.paths.sourcePath.empty()) { - return fs::path(_options.paths.sourcePath).string(); + return std::filesystem::path(_options.paths.sourcePath).string(); } else { @@ -88,7 +70,7 @@ std::string Generator::getSourceDir() const noexcept std::string Generator::getHeaderPath() const noexcept { - fs::path fullPath { _headerDir }; + std::filesystem::path fullPath { _headerDir }; fullPath /= (std::string { _schemaLoader.getFilenamePrefix() } + "Client.h"); @@ -97,7 +79,7 @@ std::string Generator::getHeaderPath() const noexcept std::string Generator::getSourcePath() const noexcept { - fs::path fullPath { _sourceDir }; + std::filesystem::path fullPath { _sourceDir }; fullPath /= (std::string { _schemaLoader.getFilenamePrefix() } + "Client.cpp"); @@ -188,7 +170,8 @@ std::vector Generator::Build() const noexcept bool Generator::outputHeader() const noexcept { std::ofstream headerFile(_headerPath, std::ios_base::trunc); - IncludeGuardScope includeGuard { headerFile, fs::path(_headerPath).filename().string() }; + IncludeGuardScope includeGuard { headerFile, + std::filesystem::path(_headerPath).filename().string() }; headerFile << R"cpp(#include "graphqlservice/GraphQLClient.h" #include "graphqlservice/GraphQLParse.h" @@ -223,7 +206,8 @@ static_assert(graphql::internal::MinorVersion == )cpp" { pendingSeparator.reset(); - headerFile << R"cpp(enum class )cpp" << _schemaLoader.getCppType(enumType->name()) << R"cpp( + headerFile << R"cpp(enum class )cpp" << _schemaLoader.getCppType(enumType->name()) + << R"cpp( { )cpp"; for (const auto& enumValue : enumType->enumValues()) diff --git a/src/GraphQLService.cpp b/src/GraphQLService.cpp index 3e4b8051..4d0b77ef 100644 --- a/src/GraphQLService.cpp +++ b/src/GraphQLService.cpp @@ -1837,7 +1837,7 @@ AwaitableDeliver Request::deliver(RequestDeliverParams params) const throw std::invalid_argument("Missing subscriptionObject"); } - const auto registrations = collectRegistrations(std::move(params.filter)); + const auto registrations = collectRegistrations(params.field, std::move(params.filter)); if (registrations.empty()) { @@ -1987,38 +1987,41 @@ void Request::removeSubscription(SubscriptionKey key) } std::vector> Request::collectRegistrations( - RequestDeliverFilter&& filter) const noexcept + std::string_view field, RequestDeliverFilter&& filter) const noexcept { std::vector> registrations; + const auto itrListeners = _listeners.find(field); - if (!filter) + if (itrListeners != _listeners.end()) { - // Return all of the registered subscriptions. - registrations.reserve(_subscriptions.size()); - std::transform(_subscriptions.begin(), - _subscriptions.end(), - std::back_inserter(registrations), - [](const auto& entry) noexcept { - return entry.second; - }); - } - else if (std::holds_alternative(*filter)) - { - // Return the specific subscription for this key. - const auto itr = _subscriptions.find(std::get(*filter)); - - if (itr != _subscriptions.end()) + if (!filter) { - registrations.push_back(itr->second); + // Return all of the registered subscriptions for this field. + registrations.reserve(itrListeners->second.size()); + std::transform(itrListeners->second.begin(), + itrListeners->second.end(), + std::back_inserter(registrations), + [this](const auto& key) noexcept { + const auto itr = _subscriptions.find(key); + + return itr == _subscriptions.end() ? std::shared_ptr {} + : itr->second; + }); } - } - else if (std::holds_alternative(*filter)) - { - auto& subscriptionFilter = std::get(*filter); - const auto itrListeners = _listeners.find(subscriptionFilter.field); + else if (std::holds_alternative(*filter)) + { + // Return the specific subscription for this key. + const auto itr = _subscriptions.find(std::get(*filter)); - if (itrListeners != _listeners.end()) + if (itr != _subscriptions.end() && itr->second->field == field) + { + registrations.push_back(itr->second); + } + } + else if (std::holds_alternative(*filter)) { + auto& subscriptionFilter = std::get(*filter); + registrations.reserve(itrListeners->second.size()); std::optional argumentsMatch; diff --git a/src/SchemaGenerator.cpp b/src/SchemaGenerator.cpp index 580fe487..3ff453f0 100644 --- a/src/SchemaGenerator.cpp +++ b/src/SchemaGenerator.cpp @@ -16,26 +16,8 @@ #pragma warning(pop) #endif // _MSC_VER -// clang-format off -#ifdef USE_STD_FILESYSTEM - #include - namespace fs = std::filesystem; -#else - #ifdef USE_STD_EXPERIMENTAL_FILESYSTEM - #include - namespace fs = std::experimental::filesystem; - #else - #ifdef USE_BOOST_FILESYSTEM - #include - namespace fs = boost::filesystem; - #else - #error "No std::filesystem implementation defined" - #endif - #endif -#endif -// clang-format on - #include +#include #include #include #include @@ -61,7 +43,7 @@ std::string Generator::getHeaderDir() const noexcept { if (!_options.paths.headerPath.empty()) { - return fs::path { _options.paths.headerPath }.string(); + return std::filesystem::path { _options.paths.headerPath }.string(); } else { @@ -73,7 +55,7 @@ std::string Generator::getSourceDir() const noexcept { if (!_options.paths.sourcePath.empty()) { - return fs::path(_options.paths.sourcePath).string(); + return std::filesystem::path(_options.paths.sourcePath).string(); } else { @@ -83,7 +65,7 @@ std::string Generator::getSourceDir() const noexcept std::string Generator::getHeaderPath() const noexcept { - fs::path fullPath { _headerDir }; + std::filesystem::path fullPath { _headerDir }; fullPath /= (std::string { _loader.getFilenamePrefix() } + "Schema.h"); return fullPath.string(); @@ -91,7 +73,7 @@ std::string Generator::getHeaderPath() const noexcept std::string Generator::getSourcePath() const noexcept { - fs::path fullPath { _sourceDir }; + std::filesystem::path fullPath { _sourceDir }; fullPath /= (std::string { _loader.getFilenamePrefix() } + "Schema.cpp"); return fullPath.string(); @@ -124,7 +106,8 @@ std::vector Generator::Build() const noexcept bool Generator::outputHeader() const noexcept { std::ofstream headerFile(_headerPath, std::ios_base::trunc); - IncludeGuardScope includeGuard { headerFile, fs::path(_headerPath).filename().string() }; + IncludeGuardScope includeGuard { headerFile, + std::filesystem::path(_headerPath).filename().string() }; headerFile << R"cpp(#include "graphqlservice/internal/Schema.h" @@ -977,7 +960,8 @@ void Generator::outputObjectDeclaration( for (auto unionName : objectType.unions) { - headerFile << R"cpp( friend )cpp" << _loader.getSafeCppName(unionName) << R"cpp(; + headerFile << R"cpp( friend )cpp" << _loader.getSafeCppName(unionName) + << R"cpp(; )cpp"; } @@ -1758,7 +1742,8 @@ Operations::Operations()cpp"; )cpp"; } sourceFile << R"cpp(}, )cpp" - << (directive.isRepeatable ? R"cpp(true)cpp" : R"cpp(false)cpp") << R"cpp()); + << (directive.isRepeatable ? R"cpp(true)cpp" : R"cpp(false)cpp") + << R"cpp()); )cpp"; } } @@ -2655,8 +2640,8 @@ std::string Generator::getIntrospectionType( std::vector Generator::outputSeparateFiles() const noexcept { - const fs::path headerDir(_headerDir); - const fs::path sourceDir(_sourceDir); + const std::filesystem::path headerDir(_headerDir); + const std::filesystem::path sourceDir(_sourceDir); std::vector files; std::string_view queryType; @@ -2676,7 +2661,8 @@ std::vector Generator::outputSeparateFiles() const noexcept std::ofstream headerFile(headerPath, std::ios_base::trunc); IncludeGuardScope includeGuard { headerFile, headerFilename }; - headerFile << R"cpp(#include ")cpp" << fs::path(_headerPath).filename().string() << R"cpp(" + headerFile << R"cpp(#include ")cpp" + << std::filesystem::path(_headerPath).filename().string() << R"cpp(" )cpp"; @@ -2766,7 +2752,8 @@ using namespace std::literals; std::ofstream headerFile(headerPath, std::ios_base::trunc); IncludeGuardScope includeGuard { headerFile, headerFilename }; - headerFile << R"cpp(#include ")cpp" << fs::path(_headerPath).filename().string() << R"cpp(" + headerFile << R"cpp(#include ")cpp" + << std::filesystem::path(_headerPath).filename().string() << R"cpp(" )cpp"; @@ -2856,7 +2843,8 @@ using namespace std::literals; std::ofstream headerFile(headerPath, std::ios_base::trunc); IncludeGuardScope includeGuard { headerFile, headerFilename }; - headerFile << R"cpp(#include ")cpp" << fs::path(_headerPath).filename().string() << R"cpp(" + headerFile << R"cpp(#include ")cpp" + << std::filesystem::path(_headerPath).filename().string() << R"cpp(" )cpp"; @@ -3053,8 +3041,8 @@ int main(int argc, char** argv) po::value(&headerDir), "Target path for the Schema.h header file")("stubs", po::bool_switch(&stubs), - "Unimplemented fields throw runtime exceptions instead of compiler errors")( - "no-introspection", + "Unimplemented fields throw runtime exceptions instead of compiler errors")("no-" + "introspection", po::bool_switch(&noIntrospection), "Do not generate support for Introspection"); positional.add("schema", 1).add("prefix", 1).add("namespace", 1); diff --git a/test/ClientTests.cpp b/test/ClientTests.cpp index ba4f5b9a..37068041 100644 --- a/test/ClientTests.cpp +++ b/test/ClientTests.cpp @@ -247,7 +247,7 @@ TEST_F(ClientCase, SubscribeNextAppointmentChangeDefault) {}, state }) .get(); - _service->deliver({ { service::SubscriptionFilter { "nextAppointmentChange"sv } } }).get(); + _service->deliver({ "nextAppointmentChange"sv }).get(); _service->unsubscribe({ key }).get(); try diff --git a/test/TodayTests.cpp b/test/TodayTests.cpp index f4ea145d..8130f23b 100644 --- a/test/TodayTests.cpp +++ b/test/TodayTests.cpp @@ -618,7 +618,7 @@ TEST_F(TodayServiceCase, SubscribeNextAppointmentChangeDefault) {}, state }) .get(); - _service->deliver({ { service::SubscriptionFilter { "nextAppointmentChange"sv } } }).get(); + _service->deliver({ "nextAppointmentChange"sv }).get(); _service->unsubscribe({ key }).get(); try @@ -682,8 +682,9 @@ TEST_F(TodayServiceCase, SubscribeNextAppointmentChangeOverride) state }) .get(); _service - ->deliver({ { service::SubscriptionFilter { "nextAppointmentChange"sv } }, - {}, + ->deliver({ "nextAppointmentChange"sv, + {}, // filter + {}, // launch std::make_shared(std::move(subscriptionObject)) }) .get(); _service->unsubscribe({ key }).get(); @@ -722,7 +723,7 @@ TEST_F(TodayServiceCase, DeliverNextAppointmentChangeNoSubscriptionObject) try { - service->deliver({ { service::SubscriptionFilter { "nextAppointmentChange"sv } } }).get(); + service->deliver({ "nextAppointmentChange"sv }).get(); } catch (const std::invalid_argument& ex) { @@ -740,7 +741,7 @@ TEST_F(TodayServiceCase, DeliverNextAppointmentChangeNoSubscriptionSupport) try { - service->deliver({ { service::SubscriptionFilter { "nextAppointmentChange"sv } } }).get(); + service->deliver({ "nextAppointmentChange"sv }).get(); } catch (const std::logic_error& ex) { @@ -1269,10 +1270,10 @@ TEST_F(TodayServiceCase, SubscribeNodeChangeMatchingId) state }) .get(); _service - ->deliver({ { service::SubscriptionFilter { "nodeChange"sv, - { service::SubscriptionArguments { - { "id", response::Value("ZmFrZVRhc2tJZA=="s) } } } } }, - {}, + ->deliver({ "nodeChange"sv, + { service::SubscriptionFilter { { service::SubscriptionArguments { + { "id", response::Value("ZmFrZVRhc2tJZA=="s) } } } } }, + {}, // launch std::make_shared(std::move(subscriptionObject)) }) .get(); _service->unsubscribe({ key }).get(); @@ -1330,10 +1331,10 @@ TEST_F(TodayServiceCase, SubscribeNodeChangeMismatchedId) std::move(variables) }) .get(); _service - ->deliver({ { service::SubscriptionFilter { "nodeChange"sv, - { service::SubscriptionArguments { - { "id", response::Value("ZmFrZUFwcG9pbnRtZW50SWQ="s) } } } } }, - {}, + ->deliver({ "nodeChange"sv, + { service::SubscriptionFilter { { service::SubscriptionArguments { + { "id", response::Value("ZmFrZUFwcG9pbnRtZW50SWQ="s) } } } } }, + {}, // launch std::make_shared(std::move(subscriptionObject)) }) .get(); _service->unsubscribe({ key }).get(); @@ -1396,11 +1397,11 @@ TEST_F(TodayServiceCase, SubscribeNodeChangeFuzzyComparator) state }) .get(); _service - ->deliver( - { { service::SubscriptionFilter { "nodeChange"sv, - { service::SubscriptionArgumentFilterCallback { std::move(filterCallback) } } } }, - {}, - std::make_shared(std::move(subscriptionObject)) }) + ->deliver({ "nodeChange"sv, + { service::SubscriptionFilter { + { service::SubscriptionArgumentFilterCallback { std::move(filterCallback) } } } }, + {}, // launch + std::make_shared(std::move(subscriptionObject)) }) .get(); _service->unsubscribe({ key }).get(); @@ -1467,11 +1468,11 @@ TEST_F(TodayServiceCase, SubscribeNodeChangeFuzzyMismatch) std::move(variables) }) .get(); _service - ->deliver( - { { service::SubscriptionFilter { "nodeChange"sv, - { service::SubscriptionArgumentFilterCallback { std::move(filterCallback) } } } }, - {}, - std::make_shared(std::move(subscriptionObject)) }) + ->deliver({ "nodeChange"sv, + { service::SubscriptionFilter { + { service::SubscriptionArgumentFilterCallback { std::move(filterCallback) } } } }, + {}, // launch + std::make_shared(std::move(subscriptionObject)) }) .get(); _service->unsubscribe({ key }).get(); @@ -1524,10 +1525,10 @@ TEST_F(TodayServiceCase, SubscribeNodeChangeMatchingVariable) state }) .get(); _service - ->deliver({ { service::SubscriptionFilter { "nodeChange"sv, - { service::SubscriptionArguments { - { "id", response::Value("ZmFrZVRhc2tJZA=="s) } } } } }, - {}, + ->deliver({ "nodeChange"sv, + { service::SubscriptionFilter { { service::SubscriptionArguments { + { "id", response::Value("ZmFrZVRhc2tJZA=="s) } } } } }, + {}, // launch std::make_shared(std::move(subscriptionObject)) }) .get(); _service->unsubscribe({ key }).get(); @@ -1722,10 +1723,7 @@ TEST_F(TodayServiceCase, SubscribeNextAppointmentChangeAsync) {}, state }) .get(); - _service - ->deliver( - { { service::SubscriptionFilter { "nextAppointmentChange"sv } }, std::launch::async }) - .get(); + _service->deliver({ "nextAppointmentChange"sv, {}, std::launch::async }).get(); _service->unsubscribe({ key }).get(); try