|
| 1 | +# Custom Scalars |
| 2 | + |
| 3 | +GraphQL lets a schema declare [custom scalar](https://spec.graphql.org/October2021/#sec-Scalars.Custom-Scalars) |
| 4 | +types, e.g.: |
| 5 | + |
| 6 | +```graphql |
| 7 | +scalar DateTime |
| 8 | +scalar BigInt |
| 9 | +``` |
| 10 | + |
| 11 | +The wire representation of a scalar is always a `graphql::response::Value` (the JSON-like |
| 12 | +discriminated union described in [Query Responses](./responses.md)). By default `schemagen` |
| 13 | +generates the resolvers and argument accessors for every custom scalar in terms of |
| 14 | +`response::Value`, so you receive and return the value as generic JSON-like data: |
| 15 | + |
| 16 | +```cpp |
| 17 | +service::AwaitableScalar<response::Value> getWhen(service::FieldParams&& params) const override; |
| 18 | +``` |
| 19 | +
|
| 20 | +That is convenient, but it forces your implementation to store and manipulate the value as a |
| 21 | +`response::Value` even when a dedicated C++ type would be much more natural or efficient (for |
| 22 | +example a `std::chrono::system_clock::time_point` for `DateTime`, or a 64-bit integer for a |
| 23 | +`BigInt` that does not fit in the 32-bit `response::IntType`). |
| 24 | +
|
| 25 | +## The `@cppType` directive |
| 26 | +
|
| 27 | +You can map a custom scalar onto any C++ type using the `@cppType` directive on the `scalar` |
| 28 | +declaration: |
| 29 | +
|
| 30 | +```graphql |
| 31 | +scalar BigInt @cppType( |
| 32 | + name: "bigint::BigInt" |
| 33 | + header: "BigIntScalar.h" |
| 34 | +) |
| 35 | +``` |
| 36 | + |
| 37 | +* `name` (required) is the fully-qualified C++ type that `schemagen` uses everywhere the scalar |
| 38 | + appears in the generated code, instead of `response::Value`. It may be qualified with any |
| 39 | + namespace (`bigint::BigInt`, `my::project::BigInt`, ...). |
| 40 | +* `header` (optional) is added as an `#include` at the top of the generated schema header so the |
| 41 | + custom type and its conversion specializations are visible to the generated code. |
| 42 | + |
| 43 | +With the directive above, the generated resolver interface changes to use your type directly: |
| 44 | + |
| 45 | +```cpp |
| 46 | +service::AwaitableScalar<bigint::BigInt> getEcho( |
| 47 | + service::FieldParams&& params, bigint::BigInt&& valueArg) const override; |
| 48 | +``` |
| 49 | +
|
| 50 | +The scalar is still declared to clients exactly as `scalar BigInt` in introspection; `@cppType` |
| 51 | +is a code-generation hint only and never appears in the served schema. |
| 52 | +
|
| 53 | +## Providing the conversions |
| 54 | +
|
| 55 | +Because `schemagen` no longer knows how to translate the scalar to and from the `response::Value` |
| 56 | +wire representation, you provide that translation by specializing three members of the |
| 57 | +`graphql::service` templates. These are the type-erased boundary between your C++ type and the |
| 58 | +JSON-like data on the wire: |
| 59 | +
|
| 60 | +```cpp |
| 61 | +namespace graphql::service { |
| 62 | +
|
| 63 | +// Deserialize an argument or input value into your custom type. |
| 64 | +template <> |
| 65 | +bigint::BigInt Argument<bigint::BigInt>::convert(const response::Value& value); |
| 66 | +
|
| 67 | +// Serialize a resolver result of your custom type back into a response::Value. |
| 68 | +template <> |
| 69 | +AwaitableResolver Result<bigint::BigInt>::convert( |
| 70 | + AwaitableScalar<bigint::BigInt> result, ResolverParams&& params); |
| 71 | +
|
| 72 | +// Validate a cached response::Value (returned directly by a resolver) for this scalar. |
| 73 | +template <> |
| 74 | +void Result<bigint::BigInt>::validateScalar(const response::Value& value); |
| 75 | +
|
| 76 | +} // namespace graphql::service |
| 77 | +``` |
| 78 | + |
| 79 | +`Result<T>::convert` is usually implemented with the `ModifiedResult<T>::resolve` helper, passing a |
| 80 | +callback that turns a `T&&` into a `response::Value`: |
| 81 | + |
| 82 | +```cpp |
| 83 | +template <> |
| 84 | +AwaitableResolver Result<bigint::BigInt>::convert( |
| 85 | + AwaitableScalar<bigint::BigInt> result, ResolverParams&& params) |
| 86 | +{ |
| 87 | + return ModifiedResult<bigint::BigInt>::resolve(std::move(result), |
| 88 | + std::move(params), |
| 89 | + [](bigint::BigInt&& value, const ResolverParams&) { |
| 90 | + return response::Value { std::to_string(value.value) }; |
| 91 | + }); |
| 92 | +} |
| 93 | +``` |
| 94 | + |
| 95 | +Declare these specializations in the header named by `@cppType`'s `header` argument so they are |
| 96 | +visible to the generated code, and define them in a matching `.cpp` which is compiled and linked |
| 97 | +into the schema library. |
| 98 | + |
| 99 | +## Nullable scalar arguments |
| 100 | + |
| 101 | +Nullable arguments of built-in scalar types are passed as `std::optional<T>`. So that your custom |
| 102 | +scalar behaves the same way (instead of being treated like a generated `INPUT_OBJECT` type, which |
| 103 | +would be wrapped in `std::unique_ptr<T>`), specialize the `CustomScalarArgument` trait and inherit |
| 104 | +from `std::true_type`: |
| 105 | + |
| 106 | +```cpp |
| 107 | +namespace graphql::service { |
| 108 | + |
| 109 | +template <> |
| 110 | +struct CustomScalarArgument<bigint::BigInt> : std::true_type |
| 111 | +{ |
| 112 | +}; |
| 113 | + |
| 114 | +} // namespace graphql::service |
| 115 | +``` |
| 116 | + |
| 117 | +## Complete example |
| 118 | + |
| 119 | +See [`samples/scalar`](../samples/scalar) for a complete, buildable example of a `BigInt` custom |
| 120 | +scalar whose C++ storage is a `std::int64_t` (serialized on the wire as a JSON `String` so it keeps |
| 121 | +full precision), along with the [`test/ScalarTests.cpp`](../test/ScalarTests.cpp) unit tests which |
| 122 | +exercise serializing, deserializing, nullable arguments, and error handling through live queries. |
0 commit comments