Derive Macro juniper::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 is 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 of 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);

/// Increments [`Incremented`] before converting into a [`Value`].
fn to_output<S: ScalarValue>(v: &Incremented) -> Value<S> {
    let inc = v.0 + 1;
    Value::from(inc)
}

§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 [`InputValue`] is `String` beginning with `id: ` and
    /// strips it.
    fn from_input<S: ScalarValue>(
        input: &InputValue<S>,
    ) -> Result<Self, String> {
        //            ^^^^^^ must implement `IntoFieldError`
        input.as_string_value()
            .ok_or_else(|| format!("Expected `String`, found: {input}"))
            .and_then(|str| {
                str.strip_prefix("id: ")
                    .ok_or_else(|| {
                        format!(
                            "Expected `UserId` to begin with `id: `, \
                             found: {input}",
                        )
                    })
            })
            .map(|id| Self(id.into()))
    }
}

§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) -> Value<S> {
    match v {
        StringOrInt::String(s) => Value::scalar(s.to_owned()),
        StringOrInt::Int(i) => Value::scalar(*i),
    }
}

fn from_input<S: ScalarValue>(v: &InputValue<S>) -> Result<StringOrInt, String> {
    v.as_string_value()
        .map(|s| StringOrInt::String(s.into()))
        .or_else(|| v.as_int_value().map(StringOrInt::Int))
        .ok_or_else(|| format!("Expected `String` or `Int`, found: {v}"))
}

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) -> Value<S> {
        match v {
            StringOrInt::String(s) => Value::scalar(s.to_owned()),
            StringOrInt::Int(i) => Value::scalar(*i),
        }
    }

    pub(super) fn from_input<S: ScalarValue>(v: &InputValue<S>) -> Result<StringOrInt, String> {
        v.as_string_value()
            .map(|s| StringOrInt::String(s.into()))
            .or_else(|| v.as_int_value().map(StringOrInt::Int))
            .ok_or_else(|| format!("Expected `String` or `Int`, found: {v}"))
    }

    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) -> Value<S> {
        match self {
            Self::String(s) => Value::scalar(s.to_owned()),
            Self::Int(i) => Value::scalar(*i),
        }
    }

    fn from_input<S>(v: &InputValue<S>) -> Result<Self, String>
    where
        S: ScalarValue
    {
        v.as_string_value()
            .map(|s| Self::String(s.into()))
            .or_else(|| v.as_int_value().map(Self::Int))
            .ok_or_else(|| format!("Expected `String` or `Int`, found: {v}"))
    }

    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>(v: &StringOrInt) -> Value<S>
    where
        S: ScalarValue,
    {
        match v {
            StringOrInt::String(s) => Value::scalar(s.to_owned()),
            StringOrInt::Int(i) => Value::scalar(*i),
        }
    }

    pub(super) fn from_input<S>(v: &InputValue<S>) -> Result<StringOrInt, String>
    where
        S: ScalarValue,
    {
        v.as_string_value()
            .map(|s| StringOrInt::String(s.into()))
            .or_else(|| v.as_int_value().map(StringOrInt::Int))
            .ok_or_else(|| format!("Expected `String` or `Int`, found: {v}"))
    }

    // 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 #[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))].