Skip to content

Strip Value from to_output() function for GraphQL scalars #1330

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
3 changes: 2 additions & 1 deletion book/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ publish = false
anyhow = "1.0"
dataloader = "0.18"
derive_more = { version = "2.0", features = ["display", "from", "try_into"] }
juniper = { path = "../juniper", features = ["anyhow", "schema-language"] }
jiff = { version = "0.2", features = ["std"], default-features = false }
juniper = { path = "../juniper", features = ["anyhow", "jiff", "schema-language"] }
juniper_subscriptions = { path = "../juniper_subscriptions" }
serde_json = "1.0"
tokio = { version = "1.0", features = ["sync"] }
Expand Down
1 change: 1 addition & 0 deletions book/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
use anyhow as _;
use dataloader as _;
use derive_more as _;
use jiff as _;
use juniper as _;
use juniper_subscriptions as _;
use serde_json as _;
Expand Down
90 changes: 60 additions & 30 deletions book/src/types/scalars.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,15 +85,49 @@ pub struct UserId(String);
In case we need to customize [resolving][7] of a [custom GraphQL scalar][2] value (change the way it gets executed), the `#[graphql(to_output_with = <fn path>)]` attribute is the way to do so:
```rust
# extern crate juniper;
# use juniper::{GraphQLScalar, IntoValue as _, ScalarValue, Value};
# use juniper::GraphQLScalar;
#
#[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> {
(v.0 + 1).into_value()
fn to_output(v: &Incremented) -> i32 {
// ^^^ any concrete type having `ToScalarValue` implementation
// could be used
v.0 + 1
}
#
# fn main() {}
```

The provided function is polymorphic by its output type:
```rust
# extern crate jiff;
# extern crate juniper;
# use std::fmt::Display;
# use juniper::{GraphQLScalar, ScalarValue};
#
#[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")
}
}
#
# fn main() {}
Expand All @@ -117,7 +151,7 @@ impl UserId {
input: &str,
// ^^^^ any concrete type having `FromScalarValue` implementation could be used
) -> Result<Self, Box<str>> {
// ^^^^^^^^ must implement `IntoFieldError`
// ^^^^^^^^ must implement `IntoFieldError`
input
.strip_prefix("id: ")
.ok_or_else(|| {
Expand All @@ -130,7 +164,7 @@ impl UserId {
# fn main() {}
```

The provided function is polymorphic by input and output types:
The provided function is polymorphic by its input and output types:
```rust
# extern crate juniper;
# use juniper::{GraphQLScalar, Scalar, ScalarValue};
Expand Down Expand Up @@ -169,7 +203,7 @@ Customization of which tokens a [custom GraphQL scalar][0] type should be parsed
```rust
# extern crate juniper;
# use juniper::{
# GraphQLScalar, ParseScalarResult, ParseScalarValue, Scalar, ScalarToken, ScalarValue, Value,
# GraphQLScalar, ParseScalarResult, ParseScalarValue, Scalar, ScalarToken, ScalarValue,
# };
#
#[derive(GraphQLScalar)]
Expand All @@ -186,10 +220,12 @@ enum StringOrInt {
Int(i32),
}

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

Expand All @@ -216,7 +252,7 @@ Instead of providing all custom functions separately, it's possible to provide a
```rust
# extern crate juniper;
# use juniper::{
# GraphQLScalar, ParseScalarResult, ParseScalarValue, Scalar, ScalarToken, ScalarValue, Value,
# GraphQLScalar, ParseScalarResult, ParseScalarValue, Scalar, ScalarToken, ScalarValue,
# };
#
#[derive(GraphQLScalar)]
Expand All @@ -229,10 +265,10 @@ enum StringOrInt {
mod string_or_int {
use super::*;

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

Expand All @@ -256,7 +292,7 @@ A regular `impl` block is also suitable for that:
```rust
# extern crate juniper;
# use juniper::{
# GraphQLScalar, ParseScalarResult, ParseScalarValue, Scalar, ScalarToken, ScalarValue, Value,
# GraphQLScalar, ParseScalarResult, ParseScalarValue, Scalar, ScalarToken, ScalarValue,
# };
#
#[derive(GraphQLScalar)]
Expand All @@ -267,10 +303,10 @@ enum StringOrInt {
}

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

Expand All @@ -297,7 +333,7 @@ At the same time, any custom function still may be specified separately, if requ
```rust
# extern crate juniper;
# use juniper::{
# GraphQLScalar, ParseScalarResult, Scalar, ScalarToken, ScalarValue, Value,
# GraphQLScalar, ParseScalarResult, Scalar, ScalarToken, ScalarValue,
# };
#
#[derive(GraphQLScalar)]
Expand All @@ -313,13 +349,10 @@ enum StringOrInt {
mod string_or_int {
use super::*;

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

Expand Down Expand Up @@ -367,11 +400,12 @@ For implementing [custom scalars][2] on foreign types there is [`#[graphql_scala
# }
#
# use juniper::DefaultScalarValue as CustomScalarValue;
use juniper::{ScalarValue, Value, graphql_scalar};
use juniper::{ScalarValue, graphql_scalar};

#[graphql_scalar]
#[graphql(
with = date_scalar,
to_output_with = ScalarValue::from_displayable, // use `Display` representation
parse_token(String),
scalar = CustomScalarValue,
)]
Expand All @@ -381,10 +415,6 @@ type Date = date::Date;

mod date_scalar {
use super::*;

pub(super) fn to_output(v: &Date) -> Value<CustomScalarValue> {
Value::scalar(v.to_string())
}

pub(super) fn from_input(s: &str) -> Result<Date, Box<str>> {
s.parse().map_err(|e| format!("Failed to parse `Date`: {e}").into())
Expand Down
11 changes: 10 additions & 1 deletion juniper/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ All user visible changes to `juniper` crate will be documented in this file. Thi
- `From` and `Display` implementations are not derived anymore (recommended way is to use [`derive_more` crate] for this).
- `#[derive(GraphQLScalar)]` and `#[graphql_scalar]` macros:
- Made provided `from_input()` function to accept `ScalarValue` (or anything `FromScalarValue`-convertible) directly instead of `InputValue`. ([#1327])
- Made provided `to_output()` function to return `ScalarValue` directly instead of `Value`. ([#1330])
- Removed `LocalBoxFuture` usage from `http::tests::WsIntegration` trait. ([4b14c015])

### Added
Expand All @@ -112,18 +113,25 @@ All user visible changes to `juniper` crate will be documented in this file. Thi
- `IntoValue` and `IntoInputValue` conversion traits allowing to work around orphan rules with custom `ScalarValue`. ([#1324])
- `FromScalarValue` conversion trait. ([#1329])
- `TryToPrimitive` conversion trait aiding `ScalarValue` trait. ([#1327], [#1329])
- `ToScalarValue` conversion trait. ([#1330])
- `ScalarValue` trait:
- `from_displayable()` method allowing to specialize `ScalarValue` conversion from custom string types. ([#1324], [#819])
- `from_displayable()` and `from_displayable_non_static()` methods allowing to specialize `ScalarValue` conversion from/for custom string types. ([#1324], [#1330], [#819])
- `try_to::<T>()` method defined by default as `FromScalarValue<T>` alias. ([#1327], [#1329])
- `#[derive(ScalarValue)]` macro:
- Support of top-level `#[value(from_displayable_with = ...)]` attribute. ([#1324])
- Support of top-level `#[value(from_displayable_non_static_with = ...)]` attribute. ([#1330])
- `#[derive(GraphQLScalar)]` and `#[graphql_scalar]` macros:
- Support for specifying concrete types as input argument in provided `from_input()` function. ([#1327])
- Support for non-`Result` return type in provided `from_input()` function. ([#1327])
- `Scalar` transparent wrapper for aiding type inference in `from_input()` function when input argument is generic `ScalarValue`. ([#1327])
- Generating of `FromScalarValue` implementation. ([#1329])
- Support for concrete and `impl Display` return types in provided `to_output()` function. ([#1330])
- Generating of `ToScalarValue` implementation. ([#1330])

### Changed

- Upgraded [GraphiQL] to [5.0.0 version](https://github.com/graphql/graphiql/blob/graphiql%405.0.0/packages/graphiql/CHANGELOG.md#500). ([#1331])
- Lifted `Sized` requirement from `ToInputValue` conversion trait. ([#1330])

### Fixed

Expand All @@ -147,6 +155,7 @@ All user visible changes to `juniper` crate will be documented in this file. Thi
[#1324]: /../../pull/1324
[#1327]: /../../pull/1327
[#1329]: /../../pull/1329
[#1330]: /../../pull/1330
[#1331]: /../../pull/1331
[1b1fc618]: /../../commit/1b1fc61879ffdd640d741e187dc20678bf7ab295
[20609366]: /../../commit/2060936635609b0186d46d8fbd06eb30fce660e3
Expand Down
1 change: 1 addition & 0 deletions juniper/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ tap = { version = "1.0.1", optional = true }
void = { version = "1.0.2", optional = true }

[dev-dependencies]
arcstr = { version = "1.1", features = ["serde"] }
bencher = "0.1.2"
chrono = { version = "0.4.30", features = ["alloc"], default-features = false }
compact_str = { version = "0.9", features = ["serde"] }
Expand Down
4 changes: 2 additions & 2 deletions juniper/src/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -231,8 +231,8 @@ pub trait FromInputValue<S = DefaultScalarValue>: Sized {
}
}

/// Losslessly clones a Rust data type into an InputValue.
pub trait ToInputValue<S = DefaultScalarValue>: Sized {
/// Losslessly clones a Rust data type into an [`InputValue`].
pub trait ToInputValue<S = DefaultScalarValue> {
/// Performs the conversion.
fn to_input_value(&self) -> InputValue<S>;
}
Expand Down
6 changes: 3 additions & 3 deletions juniper/src/executor_tests/variables.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::{
GraphQLInputObject, GraphQLScalar, ScalarValue, Value,
GraphQLInputObject, GraphQLScalar,
executor::Variables,
graphql_object, graphql_value, graphql_vars,
parser::SourcePosition,
Expand All @@ -14,8 +14,8 @@ use crate::{
struct TestComplexScalar;

impl TestComplexScalar {
fn to_output<S: ScalarValue>(&self) -> Value<S> {
graphql_value!("SerializedValue")
fn to_output(&self) -> &'static str {
"SerializedValue"
}

fn from_input(s: &str) -> Result<Self, Box<str>> {
Expand Down
26 changes: 11 additions & 15 deletions juniper/src/integrations/bigdecimal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@
//!
//! [`BigDecimal`]: bigdecimal::BigDecimal

use std::str::FromStr as _;

use crate::{Scalar, ScalarValue, Value, graphql_scalar};
use crate::{ScalarValue, graphql_scalar};

// TODO: Try remove on upgrade of `bigdecimal` crate.
mod for_minimal_versions_check_only {
Expand All @@ -29,19 +27,18 @@ mod for_minimal_versions_check_only {
/// See also [`bigdecimal`] crate for details.
///
/// [`bigdecimal`]: https://docs.rs/bigdecimal
#[graphql_scalar(
#[graphql_scalar]
#[graphql(
with = bigdecimal_scalar,
to_output_with = ScalarValue::from_displayable,
parse_token(i32, f64, String),
specified_by_url = "https://docs.rs/bigdecimal",
)]
type BigDecimal = bigdecimal::BigDecimal;

mod bigdecimal_scalar {
use super::*;

pub(super) fn to_output<S: ScalarValue>(v: &BigDecimal) -> Value<S> {
Value::scalar(v.to_string())
}
use super::BigDecimal;
use crate::{Scalar, ScalarValue};

pub(super) fn from_input(v: &Scalar<impl ScalarValue>) -> Result<BigDecimal, Box<str>> {
if let Some(i) = v.try_to_int() {
Expand All @@ -50,13 +47,14 @@ mod bigdecimal_scalar {
// See akubera/bigdecimal-rs#103 for details:
// https://github.com/akubera/bigdecimal-rs/issues/103
let mut buf = ryu::Buffer::new();
BigDecimal::from_str(buf.format(f))
buf.format(f)
.parse::<BigDecimal>()
.map_err(|e| format!("Failed to parse `BigDecimal` from `Float`: {e}").into())
} else {
v.try_to::<&str>()
.map_err(|e| e.to_string().into())
.and_then(|s| {
BigDecimal::from_str(s).map_err(|e| {
s.parse::<BigDecimal>().map_err(|e| {
format!("Failed to parse `BigDecimal` from `String`: {e}").into()
})
})
Expand All @@ -66,8 +64,6 @@ mod bigdecimal_scalar {

#[cfg(test)]
mod test {
use std::str::FromStr as _;

use crate::{FromInputValue as _, InputValue, ToInputValue as _, graphql_input_value};

use super::BigDecimal;
Expand All @@ -91,7 +87,7 @@ mod test {
] {
let input: InputValue = input;
let parsed = BigDecimal::from_input_value(&input);
let expected = BigDecimal::from_str(expected).unwrap();
let expected = expected.parse::<BigDecimal>().unwrap();

assert!(
parsed.is_ok(),
Expand Down Expand Up @@ -130,7 +126,7 @@ mod test {
"123",
"43.44",
] {
let actual: InputValue = BigDecimal::from_str(raw).unwrap().to_input_value();
let actual: InputValue = raw.parse::<BigDecimal>().unwrap().to_input_value();

assert_eq!(actual, graphql_input_value!((raw)), "on value: {raw}");
}
Expand Down
Loading
Loading