Skip to content

Commit

Permalink
feat(sample): add non-trivial async field resolvers to proxy client
Browse files Browse the repository at this point in the history
  • Loading branch information
wravery committed Sep 18, 2024
1 parent fa15fb7 commit be11263
Show file tree
Hide file tree
Showing 14 changed files with 450 additions and 19 deletions.
128 changes: 121 additions & 7 deletions samples/proxy/client.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

#include "schema/ProxySchema.h"
#include "schema/QueryObject.h"
#include "schema/ResultsObject.h"

#include "graphqlservice/JSONResponse.h"

Expand All @@ -21,6 +22,7 @@
#include <boost/asio/use_awaitable.hpp>
#include <boost/asio/use_future.hpp>

#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <functional>
Expand All @@ -45,13 +47,111 @@ constexpr auto c_port = "8080"sv;
constexpr auto c_target = "/graphql"sv;
constexpr int c_version = 11; // HTTP 1.1

struct AsyncIoWorker : service::RequestState
{
AsyncIoWorker()
: worker { std::make_shared<service::await_worker_thread>() }
{
}

const service::await_async worker;
};

class Results
{
public:
explicit Results(response::Value&& data, std::vector<client::Error> errors) noexcept;

service::AwaitableScalar<std::optional<std::string>> getData(
service::FieldParams&& fieldParams) const;
service::AwaitableScalar<std::optional<std::vector<std::optional<std::string>>>> getErrors(
service::FieldParams&& fieldParams) const;

private:
mutable response::Value m_data;
mutable std::vector<client::Error> m_errors;
};

Results::Results(response::Value&& data, std::vector<client::Error> errors) noexcept
: m_data { std::move(data) }
, m_errors { std::move(errors) }
{
}

service::AwaitableScalar<std::optional<std::string>> Results::getData(
service::FieldParams&& fieldParams) const
{
auto asyncIoWorker = std::static_pointer_cast<AsyncIoWorker>(fieldParams.state);
auto data = std::move(m_data);

// Jump to a worker thread for the resolver where we can run a separate I/O context without
// blocking the I/O context in Query::getRelay. This simulates how you might fan out to
// additional async I/O tasks for sub-field resolvers.
co_await asyncIoWorker->worker;

net::io_context ioc;
auto future = net::co_spawn(
ioc,
[](response::Value&& data) -> net::awaitable<std::optional<std::string>> {
co_return (data.type() == response::Type::Null)
? std::nullopt
: std::make_optional(response::toJSON(std::move(data)));
}(std::move(data)),
net::use_future);

ioc.run();

co_return future.get();
}

service::AwaitableScalar<std::optional<std::vector<std::optional<std::string>>>> Results::getErrors(
service::FieldParams&& fieldParams) const
{
auto asyncIoWorker = std::static_pointer_cast<AsyncIoWorker>(fieldParams.state);
auto errors = std::move(m_errors);

// Jump to a worker thread for the resolver where we can run a separate I/O context without
// blocking the I/O context in Query::getRelay. This simulates how you might fan out to
// additional async I/O tasks for sub-field resolvers.
co_await asyncIoWorker->worker;

net::io_context ioc;
auto future = net::co_spawn(
ioc,
[](std::vector<client::Error> errors)
-> net::awaitable<std::optional<std::vector<std::optional<std::string>>>> {
if (errors.empty())
{
co_return std::nullopt;
}

std::vector<std::optional<std::string>> results { errors.size() };

std::transform(errors.begin(),
errors.end(),
results.begin(),
[](auto& error) noexcept -> std::optional<std::string> {
return error.message.empty()
? std::nullopt
: std::make_optional<std::string>(std::move(error.message));
});

co_return std::make_optional(results);
}(std::move(errors)),
net::use_future);

ioc.run();

co_return future.get();
}

class Query
{
public:
explicit Query(std::string_view host, std::string_view port, std::string_view target,
int version) noexcept;

std::future<std::optional<std::string>> getRelay(std::string&& queryArg,
std::future<std::shared_ptr<proxy::object::Results>> getRelay(std::string&& queryArg,
std::optional<std::string>&& operationNameArg,
std::optional<std::string>&& variablesArg) const;

Expand All @@ -73,7 +173,7 @@ Query::Query(

// Based on:
// https://www.boost.org/doc/libs/1_82_0/libs/beast/example/http/client/awaitable/http_client_awaitable.cpp
std::future<std::optional<std::string>> Query::getRelay(std::string&& queryArg,
std::future<std::shared_ptr<proxy::object::Results>> Query::getRelay(std::string&& queryArg,
std::optional<std::string>&& operationNameArg, std::optional<std::string>&& variablesArg) const
{
response::Value payload { response::Type::Map };
Expand All @@ -99,7 +199,7 @@ std::future<std::optional<std::string>> Query::getRelay(std::string&& queryArg,
const char* port,
const char* target,
int version,
std::string requestBody) -> net::awaitable<std::optional<std::string>> {
std::string requestBody) -> net::awaitable<std::shared_ptr<proxy::object::Results>> {
// These objects perform our I/O. They use an executor with a default completion token
// of use_awaitable. This makes our code easy, but will use exceptions as the default
// error handling, i.e. if the connection drops, we might see an exception.
Expand Down Expand Up @@ -150,7 +250,10 @@ std::future<std::optional<std::string>> Query::getRelay(std::string&& queryArg,
throw boost::system::system_error(ec, "shutdown");
}

co_return std::make_optional<std::string>(std::move(res.body()));
auto [data, errors] = client::parseServiceResponse(response::parseJSON(res.body()));

co_return std::make_shared<proxy::object::Results>(
std::make_shared<Results>(std::move(data), std::move(errors)));
}(m_host.c_str(), m_port.c_str(), m_target.c_str(), m_version, std::move(requestBody)),
net::use_future);

Expand Down Expand Up @@ -179,14 +282,25 @@ int main(int argc, char** argv)
auto variables = serializeVariables(
{ input, ((argc > 1) ? std::make_optional(argv[1]) : std::nullopt) });
auto launch = service::await_async { std::make_shared<service::await_worker_queue>() };
auto state = std::make_shared<AsyncIoWorker>();
auto serviceResponse = client::parseServiceResponse(
service->resolve({ query, GetOperationName(), std::move(variables), launch }).get());
service->resolve({ query, GetOperationName(), std::move(variables), launch, state })
.get());
auto result = client::query::relayQuery::parseResponse(std::move(serviceResponse.data));
auto errors = std::move(serviceResponse.errors);

if (result.relay)
if (result.relay.data)
{
std::cout << "Data: " << *result.relay.data << std::endl;
}

if (result.relay.errors)
{
std::cout << *result.relay << std::endl;
for (const auto& message : *result.relay.errors)
{
std::cerr << "Remote Error: "
<< (message ? std::string_view { *message } : "<empty>"sv) << std::endl;
}
}

if (!errors.empty())
Expand Down
34 changes: 32 additions & 2 deletions samples/proxy/query/ProxyClient.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@ const std::string& GetRequestText() noexcept
# Licensed under the MIT License.
query relayQuery($query: String!, $operationName: String, $variables: String) {
relay(query: $query, operationName: $operationName, variables: $variables)
relay(query: $query, operationName: $operationName, variables: $variables) {
data
errors
}
}
)gql"s;

Expand All @@ -51,6 +54,33 @@ const peg::ast& GetRequestObject() noexcept

using namespace proxy;

template <>
query::relayQuery::Response::relay_Results Response<query::relayQuery::Response::relay_Results>::parse(response::Value&& response)
{
query::relayQuery::Response::relay_Results result;

if (response.type() == response::Type::Map)
{
auto members = response.release<response::MapType>();

for (auto& member : members)
{
if (member.first == R"js(data)js"sv)
{
result.data = ModifiedResponse<std::string>::parse<TypeModifier::Nullable>(std::move(member.second));
continue;
}
if (member.first == R"js(errors)js"sv)
{
result.errors = ModifiedResponse<std::string>::parse<TypeModifier::Nullable, TypeModifier::List, TypeModifier::Nullable>(std::move(member.second));
continue;
}
}
}

return result;
}

namespace query::relayQuery {

const std::string& GetOperationName() noexcept
Expand Down Expand Up @@ -83,7 +113,7 @@ Response parseResponse(response::Value&& response)
{
if (member.first == R"js(relay)js"sv)
{
result.relay = ModifiedResponse<std::string>::parse<TypeModifier::Nullable>(std::move(member.second));
result.relay = ModifiedResponse<query::relayQuery::Response::relay_Results>::parse(std::move(member.second));
continue;
}
}
Expand Down
13 changes: 11 additions & 2 deletions samples/proxy/query/ProxyClient.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ namespace graphql::client {
/// # Licensed under the MIT License.
///
/// query relayQuery($query: String!, $operationName: String, $variables: String) {
/// relay(query: $query, operationName: $operationName, variables: $variables)
/// relay(query: $query, operationName: $operationName, variables: $variables) {
/// data
/// errors
/// }
/// }
/// </code>
namespace proxy {
Expand Down Expand Up @@ -64,7 +67,13 @@ struct [[nodiscard("unnecessary construction")]] Variables

struct [[nodiscard("unnecessary construction")]] Response
{
std::optional<std::string> relay {};
struct [[nodiscard("unnecessary construction")]] relay_Results
{
std::optional<std::string> data {};
std::optional<std::vector<std::optional<std::string>>> errors {};
};

relay_Results relay {};
};

[[nodiscard("unnecessary conversion")]] Response parseResponse(response::Value&& response);
Expand Down
5 changes: 4 additions & 1 deletion samples/proxy/query/query.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,8 @@
# Licensed under the MIT License.

query relayQuery($query: String!, $operationName: String, $variables: String) {
relay(query: $query, operationName: $operationName, variables: $variables)
relay(query: $query, operationName: $operationName, variables: $variables) {
data
errors
}
}
3 changes: 3 additions & 0 deletions samples/proxy/schema/ProxySchema.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,11 @@ void AddTypesToSchema(const std::shared_ptr<schema::Schema>& schema)
{
auto typeQuery = schema::ObjectType::Make(R"gql(Query)gql"sv, R"md()md"sv);
schema->AddType(R"gql(Query)gql"sv, typeQuery);
auto typeResults = schema::ObjectType::Make(R"gql(Results)gql"sv, R"md()md"sv);
schema->AddType(R"gql(Results)gql"sv, typeResults);

AddQueryDetails(typeQuery, schema);
AddResultsDetails(typeResults, schema);

schema->AddQueryType(typeQuery);
}
Expand Down
2 changes: 2 additions & 0 deletions samples/proxy/schema/ProxySchema.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ namespace proxy {
namespace object {

class Query;
class Results;

} // namespace object

Expand All @@ -50,6 +51,7 @@ class [[nodiscard("unnecessary construction")]] Operations final
};

void AddQueryDetails(const std::shared_ptr<schema::ObjectType>& typeQuery, const std::shared_ptr<schema::Schema>& schema);
void AddResultsDetails(const std::shared_ptr<schema::ObjectType>& typeResults, const std::shared_ptr<schema::Schema>& schema);

std::shared_ptr<schema::Schema> GetSchema();

Expand Down
2 changes: 2 additions & 0 deletions samples/proxy/schema/ProxySchema.ixx
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@ module;
export module GraphQL.Proxy.ProxySchema;

export import GraphQL.Proxy.QueryObject;
export import GraphQL.Proxy.ResultsObject;

export namespace graphql::proxy {

using proxy::Operations;

using proxy::AddQueryDetails;
using proxy::AddResultsDetails;

using proxy::GetSchema;

Expand Down
5 changes: 3 additions & 2 deletions samples/proxy/schema/QueryObject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
// WARNING! Do not edit this file manually, your changes will be overwritten.

#include "QueryObject.h"
#include "ResultsObject.h"

#include "graphqlservice/internal/Introspection.h"

Expand Down Expand Up @@ -65,7 +66,7 @@ service::AwaitableResolver Query::resolveRelay(service::ResolverParams&& params)
auto result = _pimpl->getRelay(service::FieldParams { std::move(selectionSetParams), std::move(directives) }, std::move(argQuery), std::move(argOperationName), std::move(argVariables));
resolverLock.unlock();

return service::ModifiedResult<std::string>::convert<service::TypeModifier::Nullable>(std::move(result), std::move(params));
return service::ModifiedResult<Results>::convert(std::move(result), std::move(params));
}

service::AwaitableResolver Query::resolve_typename(service::ResolverParams&& params) const
Expand All @@ -92,7 +93,7 @@ service::AwaitableResolver Query::resolve_type(service::ResolverParams&& params)
void AddQueryDetails(const std::shared_ptr<schema::ObjectType>& typeQuery, const std::shared_ptr<schema::Schema>& schema)
{
typeQuery->AddFields({
schema::Field::Make(R"gql(relay)gql"sv, R"md()md"sv, std::nullopt, schema->LookupType(R"gql(String)gql"sv), {
schema::Field::Make(R"gql(relay)gql"sv, R"md()md"sv, std::nullopt, schema->WrapType(introspection::TypeKind::NON_NULL, schema->LookupType(R"gql(Results)gql"sv)), {
schema::InputValue::Make(R"gql(query)gql"sv, R"md()md"sv, schema->WrapType(introspection::TypeKind::NON_NULL, schema->LookupType(R"gql(String)gql"sv)), R"gql()gql"sv),
schema::InputValue::Make(R"gql(operationName)gql"sv, R"md()md"sv, schema->LookupType(R"gql(String)gql"sv), R"gql()gql"sv),
schema::InputValue::Make(R"gql(variables)gql"sv, R"md()md"sv, schema->LookupType(R"gql(String)gql"sv), R"gql()gql"sv)
Expand Down
8 changes: 4 additions & 4 deletions samples/proxy/schema/QueryObject.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@ namespace methods::QueryHas {
template <class TImpl>
concept getRelayWithParams = requires (TImpl impl, service::FieldParams params, std::string queryArg, std::optional<std::string> operationNameArg, std::optional<std::string> variablesArg)
{
{ service::AwaitableScalar<std::optional<std::string>> { impl.getRelay(std::move(params), std::move(queryArg), std::move(operationNameArg), std::move(variablesArg)) } };
{ service::AwaitableObject<std::shared_ptr<Results>> { impl.getRelay(std::move(params), std::move(queryArg), std::move(operationNameArg), std::move(variablesArg)) } };
};

template <class TImpl>
concept getRelay = requires (TImpl impl, std::string queryArg, std::optional<std::string> operationNameArg, std::optional<std::string> variablesArg)
{
{ service::AwaitableScalar<std::optional<std::string>> { impl.getRelay(std::move(queryArg), std::move(operationNameArg), std::move(variablesArg)) } };
{ service::AwaitableObject<std::shared_ptr<Results>> { impl.getRelay(std::move(queryArg), std::move(operationNameArg), std::move(variablesArg)) } };
};

template <class TImpl>
Expand Down Expand Up @@ -58,7 +58,7 @@ class [[nodiscard("unnecessary construction")]] Query final
virtual void beginSelectionSet(const service::SelectionSetParams& params) const = 0;
virtual void endSelectionSet(const service::SelectionSetParams& params) const = 0;

[[nodiscard("unnecessary call")]] virtual service::AwaitableScalar<std::optional<std::string>> getRelay(service::FieldParams&& params, std::string&& queryArg, std::optional<std::string>&& operationNameArg, std::optional<std::string>&& variablesArg) const = 0;
[[nodiscard("unnecessary call")]] virtual service::AwaitableObject<std::shared_ptr<Results>> getRelay(service::FieldParams&& params, std::string&& queryArg, std::optional<std::string>&& operationNameArg, std::optional<std::string>&& variablesArg) const = 0;
};

template <class T>
Expand All @@ -70,7 +70,7 @@ class [[nodiscard("unnecessary construction")]] Query final
{
}

[[nodiscard("unnecessary call")]] service::AwaitableScalar<std::optional<std::string>> getRelay(service::FieldParams&& params, std::string&& queryArg, std::optional<std::string>&& operationNameArg, std::optional<std::string>&& variablesArg) const override
[[nodiscard("unnecessary call")]] service::AwaitableObject<std::shared_ptr<Results>> getRelay(service::FieldParams&& params, std::string&& queryArg, std::optional<std::string>&& operationNameArg, std::optional<std::string>&& variablesArg) const override
{
if constexpr (methods::QueryHas::getRelayWithParams<T>)
{
Expand Down
Loading

0 comments on commit be11263

Please sign in to comment.