GraphQLScalar

Derive Macro GraphQLScalar 

Source
#[derive(GraphQLScalar)]
{
    // Attributes available to this derive:
    #[graphql]
}
Expand description

#[derive(GraphQLScalar)] macro for deriving a GraphQL scalar implementation.

§Transparent delegation

Quite often we want to create a custom GraphQL scalar type by just wrapping an existing one, inheriting all its behavior. In Rust, this is often called as “newtype pattern”. This could be achieved by annotating the definition with the #[graphql(transparent)] attribute:

#[derive(GraphQLScalar)]
#[graphql(transparent)]
struct UserId(String);

#[derive(GraphQLScalar)]
#[graphql(transparent)]
struct DroidId {
    value: String,
}

#[derive(GraphQLObject)]
struct Pair {
  user_id: UserId,
  droid_id: DroidId,
}

The inherited behaviour may also be customized:

/// Doc comments are used for the GraphQL type description.
#[derive(GraphQLScalar)]
#[graphql(
    // Custom GraphQL name.
    name = "MyUserId",
    // Description can also specified in the attribute.
    // This will the doc comment, if one exists.
    description = "...",
    // Optional specification URL.
    specified_by_url = "https://tools.ietf.org/html/rfc4122",
    // Explicit generic scalar.
    scalar = S: juniper::ScalarValue,
    transparent,
)]
struct UserId(String);

All the methods inherited from Newtype’s field may also be overridden with the attributes described below.

§Custom resolving

Customization of a GraphQL scalar type resolving is possible via #[graphql(to_output_with = <fn path>)] attribute:

#[derive(GraphQLScalar)]
#[graphql(to_output_with = to_output, transparent)]
struct Incremented(i32);

fn to_output(v: &Incremented) -> i32 {
    //                           ^^^ any concrete type having `ToScalarValue` implementation
    //                               could be used
    v.0 + 1
}

The provided function is polymorphic by its output type:

#[derive(GraphQLScalar)]
#[graphql(to_output_with = Self::to_output, transparent)]
struct Incremented(i32);

impl Incremented {
    fn to_output<S: ScalarValue>(v: &Incremented) -> S {
        //       ^^^^^^^^^^^^^^ returning generic or concrete `ScalarValue` is also OK
        (v.0 + 1).into()
    }
}

#[derive(GraphQLScalar)]
#[graphql(to_output_with = Self::to_output, transparent)]
struct CustomDateTime(jiff::Timestamp);

impl CustomDateTime {
    fn to_output(&self) -> impl Display {
        //                 ^^^^^^^^^^^^ in this case macro expansion uses the
        //                              `ScalarValue::from_displayable_non_static()` conversion
        self.0.strftime("%Y-%m-%d %H:%M:%S%.fZ")
    }
}

§Custom parsing

Customization of a GraphQL scalar type parsing is possible via #[graphql(from_input_with = <fn path>)] attribute:

#[derive(GraphQLScalar)]
#[graphql(from_input_with = Self::from_input, transparent)]
struct UserId(String);

impl UserId {
    /// Checks whether the [`ScalarValue`] is a [`String`] beginning with `id: ` and strips it.
    fn from_input(
        input: &str,
        //     ^^^^ any concrete type having `FromScalarValue` implementation could be used
    ) -> Result<Self, Box<str>> {
        //            ^^^^^^^^ must implement `IntoFieldError`
        input
            .strip_prefix("id: ")
            .ok_or_else(|| {
                format!("Expected `UserId` to begin with `id: `, found: {input}").into()
            })
            .map(|id| Self(id.into()))
    }
}

The provided function is polymorphic by its input and output types:

#[derive(GraphQLScalar)]
#[graphql(from_input_with = Self::from_input, transparent)]
struct UserId(String);

impl UserId {
    fn from_input(
        input: &Scalar<impl ScalarValue>,
        //      ^^^^^^ for generic argument using `Scalar` transparent wrapper is required,
        //             otherwise Rust won't be able to infer the required type
    ) -> Self {
    //   ^^^^ if the result is infallible, it's OK to omit `Result`
        Self(
            input
                .try_to_int().map(|i| i.to_string())
                .or_else(|| input.try_to_bool().map(|f| f.to_string()))
                .or_else(|| input.try_to_float().map(|b| b.to_string()))
                .or_else(|| input.try_to_string())
                .unwrap_or_else(|| {
                    unreachable!("`ScalarValue` is at least one of primitive GraphQL types")
                }),
        )
    }
}

§Custom token parsing

Customization of which tokens a GraphQL scalar type should be parsed is possible via #[graphql(parse_token_with = <fn path>)] or #[graphql(parse_token(<types>)] attributes:

#[derive(GraphQLScalar)]
#[graphql(
    to_output_with = to_output,
    from_input_with = from_input,
    parse_token_with = parse_token,
)]
//  ^^^^^^^^^^^^^^^^ Can be replaced with `parse_token(String, i32)`, which
//                   tries to parse as `String` first, and then as `i32` if
//                   prior fails.
enum StringOrInt {
    String(String),
    Int(i32),
}

fn to_output<S: ScalarValue>(v: &StringOrInt) -> S {
    match v {
        StringOrInt::String(s) => S::from_displayable(s),
        //                        ^^^^^^^^^^^^^^^^^^^ preferable conversion for types
        //                                            represented by string token
        StringOrInt::Int(i) => (*i).into(),
    }
}

fn from_input(v: &Scalar<impl ScalarValue>) -> Result<StringOrInt, Box<str>> {
    v.try_to_string()
        .map(StringOrInt::String)
        .or_else(|| v.try_to_int().map(StringOrInt::Int))
        .ok_or_else(|| format!("Expected `String` or `Int`, found: {v}").into())
}

fn parse_token<S: ScalarValue>(value: ScalarToken<'_>) -> ParseScalarResult<S> {
    <String as ParseScalarValue<S>>::from_str(value)
        .or_else(|_| <i32 as ParseScalarValue<S>>::from_str(value))
}

NOTE: Once we provide all 3 custom functions, there is no sense in following the newtype pattern anymore.

§Full behavior

Instead of providing all custom functions separately, it’s possible to provide a module holding the appropriate to_output(), from_input() and parse_token() functions:

#[derive(GraphQLScalar)]
#[graphql(with = string_or_int)]
enum StringOrInt {
    String(String),
    Int(i32),
}

mod string_or_int {
    use super::*;

    pub(super) fn to_output<S: ScalarValue>(v: &StringOrInt) -> S {
        match v {
            StringOrInt::String(s) => S::from_displayable(s),
            StringOrInt::Int(i) => (*i).into(),
        }
    }

    pub(super) fn from_input(v: &Scalar<impl ScalarValue>) -> Result<StringOrInt, Box<str>> {
        v.try_to_string()
            .map(StringOrInt::String)
            .or_else(|| v.try_to_int().map(StringOrInt::Int))
            .ok_or_else(|| format!("Expected `String` or `Int`, found: {v}").into())
    }

    pub(super) fn parse_token<S: ScalarValue>(t: ScalarToken<'_>) -> ParseScalarResult<S> {
        <String as ParseScalarValue<S>>::from_str(t)
            .or_else(|_| <i32 as ParseScalarValue<S>>::from_str(t))
    }
}

A regular impl block is also suitable for that:

#[derive(GraphQLScalar)]
// #[graphql(with = Self)] <- default behaviour, so can be omitted
enum StringOrInt {
    String(String),
    Int(i32),
}

impl StringOrInt {
    fn to_output<S: ScalarValue>(&self) -> S {
        match self {
            Self::String(s) => S::from_displayable(s),
            Self::Int(i) => (*i).into(),
        }
    }

    fn from_input(v: &Scalar<impl ScalarValue>) -> Result<Self, Box<str>> {
        v.try_to_string()
            .map(Self::String)
            .or_else(|| v.try_to_int().map(Self::Int))
            .ok_or_else(|| format!("Expected `String` or `Int`, found: {v}").into())
    }

    fn parse_token<S>(value: ScalarToken<'_>) -> ParseScalarResult<S>
    where
        S: ScalarValue
    {
        <String as ParseScalarValue<S>>::from_str(value)
            .or_else(|_| <i32 as ParseScalarValue<S>>::from_str(value))
    }
}

At the same time, any custom function still may be specified separately:

#[derive(GraphQLScalar)]
#[graphql(
    with = string_or_int,
    parse_token(String, i32)
)]
enum StringOrInt {
    String(String),
    Int(i32),
}

mod string_or_int {
    use super::*;

    pub(super) fn to_output<S: ScalarValue>(v: &StringOrInt) -> S {
        match v {
            StringOrInt::String(s) => S::from_displayable(s),
            StringOrInt::Int(i) => (*i).into(),
        }
    }

    pub(super) fn from_input(v: &Scalar<impl ScalarValue>) -> Result<StringOrInt, Box<str>> {
        v.try_to_string()
            .map(StringOrInt::String)
            .or_else(|| v.try_to_int().map(StringOrInt::Int))
            .ok_or_else(|| format!("Expected `String` or `Int`, found: {v}").into())
    }

    // No need in `parse_token()` function.
}

§Custom ScalarValue

By default, this macro generates code, which is generic over a ScalarValue type. Concrete ScalarValue type may be specified via the #[graphql(scalar = <type>)] attribute.

It also may be used to provide additional bounds to the ScalarValue generic, like the following: #[graphql(scalar = S: Trait)].

§Additional arbitrary trait bounds

GraphQL scalar type implementation may be bound with any additional trait bounds via #[graphql(where(<bounds>))] attribute, like the following: #[graphql(where(S: Trait, Self: fmt::Debug + fmt::Display))].