Skip to content

Strip InputValue from from_input for GraphQL scalars #1327

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

Merged
merged 22 commits into from
Jun 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 73 additions & 58 deletions book/src/types/scalars.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,31 +105,57 @@ fn to_output<S: ScalarValue>(v: &Incremented) -> Value<S> {
Customization of a [custom GraphQL scalar][2] value parsing is possible via `#[graphql(from_input_with = <fn path>)]` attribute:
```rust
# extern crate juniper;
# use juniper::{GraphQLScalar, InputValue, ScalarValue};
# use juniper::{GraphQLScalar, ScalarValue};
#
#[derive(GraphQLScalar)]
#[graphql(from_input_with = Self::from_input, transparent)]
struct UserId(String);

impl UserId {
/// Checks whether the [`InputValue`] is a [`String`] beginning with `id: `
/// and strips it.
fn from_input<S>(input: &InputValue<S>) -> Result<Self, String>
where
S: ScalarValue
{
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}",
)
})
/// Checks whether the [`InputValue`] is a [`String`] beginning with `id: ` and strips it.
fn from_input(
input: &str,
// ^^^^ any concrete type having `TryScalarValueTo` 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.to_owned()))
.map(|id| Self(id.into()))
}
}
#
# fn main() {}
```

The provided function is polymorphic by input and output types:
```rust
# extern crate juniper;
# use juniper::{GraphQLScalar, Scalar, ScalarValue};
#
#[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 not use `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")
}),
)
}
}
#
Expand All @@ -143,8 +169,7 @@ Customization of which tokens a [custom GraphQL scalar][0] type should be parsed
```rust
# extern crate juniper;
# use juniper::{
# GraphQLScalar, InputValue, ParseScalarResult, ParseScalarValue,
# ScalarValue, ScalarToken, Value,
# GraphQLScalar, ParseScalarResult, ParseScalarValue, Scalar, ScalarToken, ScalarValue, Value,
# };
#
#[derive(GraphQLScalar)]
Expand All @@ -168,11 +193,11 @@ fn to_output<S: ScalarValue>(v: &StringOrInt) -> Value<S> {
}
}

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 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> {
Expand All @@ -191,8 +216,7 @@ Instead of providing all custom functions separately, it's possible to provide a
```rust
# extern crate juniper;
# use juniper::{
# GraphQLScalar, InputValue, ParseScalarResult, ParseScalarValue,
# ScalarValue, ScalarToken, Value,
# GraphQLScalar, ParseScalarResult, ParseScalarValue, Scalar, ScalarToken, ScalarValue, Value,
# };
#
#[derive(GraphQLScalar)]
Expand All @@ -212,11 +236,11 @@ mod string_or_int {
}
}

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 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> {
Expand All @@ -232,8 +256,7 @@ A regular `impl` block is also suitable for that:
```rust
# extern crate juniper;
# use juniper::{
# GraphQLScalar, InputValue, ParseScalarResult, ParseScalarValue,
# ScalarValue, ScalarToken, Value,
# GraphQLScalar, ParseScalarResult, ParseScalarValue, Scalar, ScalarToken, ScalarValue, Value,
# };
#
#[derive(GraphQLScalar)]
Expand All @@ -251,14 +274,11 @@ impl StringOrInt {
}
}

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 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>
Expand All @@ -277,8 +297,7 @@ At the same time, any custom function still may be specified separately, if requ
```rust
# extern crate juniper;
# use juniper::{
# GraphQLScalar, InputValue, ParseScalarResult, ScalarValue,
# ScalarToken, Value
# GraphQLScalar, ParseScalarResult, Scalar, ScalarToken, ScalarValue, Value,
# };
#
#[derive(GraphQLScalar)]
Expand All @@ -304,14 +323,11 @@ mod string_or_int {
}
}

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}"))
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.
Expand Down Expand Up @@ -351,9 +367,10 @@ For implementing [custom scalars][2] on foreign types there is [`#[graphql_scala
# }
#
# use juniper::DefaultScalarValue as CustomScalarValue;
use juniper::{InputValue, ScalarValue, Value, graphql_scalar};
use juniper::{ScalarValue, Value, graphql_scalar};

#[graphql_scalar(
#[graphql_scalar]
#[graphql(
with = date_scalar,
parse_token(String),
scalar = CustomScalarValue,
Expand All @@ -369,10 +386,8 @@ mod date_scalar {
Value::scalar(v.to_string())
}

pub(super) fn from_input(v: &InputValue<CustomScalarValue>) -> Result<Date, String> {
v.as_string_value()
.ok_or_else(|| format!("Expected `String`, found: {v}"))
.and_then(|s| s.parse().map_err(|e| format!("Failed to parse `Date`: {e}")))
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
36 changes: 35 additions & 1 deletion juniper/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,31 @@ All user visible changes to `juniper` crate will be documented in this file. Thi
- Switched `ParseError::UnexpectedToken` to `compact_str::CompactString` instead of `smartstring::SmartString`. ([20609366])
- Replaced `Value`'s `From` implementations with `IntoValue` ones. ([#1324])
- Replaced `InputValue`'s `From` implementations with `IntoInputValue` ones. ([#1324])
- `Value` enum: ([#1327])
- Removed `as_float_value()`, `as_string_value()` and `as_scalar_value()` methods (use `as_scalar()` method and then `ScalarValue` methods instead).
- `InputValue` enum: ([#1327])
- Removed `as_float_value()`, `as_int_value()`, `as_string_value()` and `as_scalar_value()` methods (use `as_scalar()` method and then `ScalarValue` methods instead).
- `ScalarValue` trait: ([#1327])
- Switched from `From` conversions to `TryScalarValueTo` conversions.
- Made to require `TryScalarValueTo` conversions for `bool`, `f64`, `i32`, `String` and `&str` types (could be derived with `#[value(<conversion>)]` attributes of `#[derive(ScalarValue)]` macro).
- Made to require `TryInto<String>` conversion (could be derived with `derive_more::TryInto`).
- Made `is_type()` method required and to accept `Any` type.
- Renamed `as_bool()` method as `try_to_bool()` and made it defined by default as `TryScalarValueTo<bool>` alias.
- Renamed `as_float()` method as `try_to_float()` and made it defined by default as `TryScalarValueTo<f64>` alias.
- Renamed `as_int()` method as `try_to_int()` and made it defined by default as `TryScalarValueTo<i32>` alias.
- Renamed `as_string()` method as `try_to_string()` and made it defined by default as `TryScalarValueTo<String>` alias.
- Renamed `as_str()` method as `try_as_str()` and made it defined by default as `TryScalarValueTo<&str>` alias.
- Renamed `into_string()` method as `try_into_string()` and made it defined by default as `TryInto<String>` alias.
- `#[derive(ScalarValue)]` macro: ([#1327])
- Renamed `#[value(as_bool)]` attribute as `#[value(to_bool)]`.
- Renamed `#[value(as_float)]` attribute as `#[value(to_float)]`.
- Renamed `#[value(as_int)]` attribute as `#[value(to_int)]`.
- Renamed `#[value(as_string)]` attribute as `#[value(to_string)]`.
- Removed `#[value(into_string)]` attribute.
- Removed `#[value(allow_missing_attributes)]` attribute (now attributes can always be omitted).
- `From` and `Display` implementations are not derived anymore (recommended way is to use [`derive_more` crate] for this).
- `#[derive(GraphQLScalar)]` and `#[graphql_scalar]` macros: ([#1327])
- Made provided `from_input()` function to accept `ScalarValue` (or anything `TryScalarValueTo`-convertible) directly instead of `InputValue`.

### Added

Expand All @@ -82,8 +107,15 @@ All user visible changes to `juniper` crate will be documented in this file. Thi
- `http::GraphQLResponse::into_result()` method. ([#1293])
- `String` scalar implementation for `arcstr::ArcStr`. ([#1247])
- `String` scalar implementation for `compact_str::CompactString`. ([20609366])
- `ScalarValue::from_displayable()` method allowing to specialize `ScalarValue` conversion from custom string types. ([#1324], [#819])
- `IntoValue` and `IntoInputValue` conversion traits allowing to work around orphan rules with custom `ScalarValue`. ([#1324])
- `ScalarValue` trait:
- `from_displayable()` method allowing to specialize `ScalarValue` conversion from custom string types. ([#1324], [#819])
- `try_to::<T>()` method defined by default as `TryScalarValueTo<T>` alias. ([#1327])
- `TryScalarValueTo` conversion trait aiding `ScalarValue` trait. ([#1327])
- `#[derive(GraphQLScalar)]` and `#[graphql_scalar]` macros: ([#1327])
- Support for specifying concrete types as input argument in provided `from_input()` function.
- Support for non-`Result` return type in provided `from_input()` function.
- `Scalar` transparent wrapper for aiding type inference in `from_input()` function when input argument is generic `ScalarValue`.

### Changed

Expand All @@ -110,6 +142,7 @@ All user visible changes to `juniper` crate will be documented in this file. Thi
[#1318]: /../../pull/1318
[#1324]: /../../pull/1324
[#1326]: /../../pull/1326
[#1327]: /../../pull/1327
[1b1fc618]: /../../commit/1b1fc61879ffdd640d741e187dc20678bf7ab295
[20609366]: /../../commit/2060936635609b0186d46d8fbd06eb30fce660e3

Expand Down Expand Up @@ -313,6 +346,7 @@ See [old CHANGELOG](/../../blob/juniper-v0.15.12/juniper/CHANGELOG.md).
[`bson` crate]: https://docs.rs/bson
[`chrono` crate]: https://docs.rs/chrono
[`chrono-tz` crate]: https://docs.rs/chrono-tz
[`derive_more` crate]: https://docs.rs/derive_more
[`jiff` crate]: https://docs.rs/jiff
[`time` crate]: https://docs.rs/time
[Cargo feature]: https://doc.rust-lang.org/cargo/reference/features.html
Expand Down
3 changes: 2 additions & 1 deletion juniper/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,15 @@ bson = { version = "2.4", optional = true }
chrono = { version = "0.4.30", features = ["alloc"], default-features = false, optional = true }
chrono-tz = { version = "0.10", default-features = false, optional = true }
compact_str = "0.9"
derive_more = { version = "2.0", features = ["debug", "deref", "display", "error", "from", "into", "into_iterator"] }
derive_more = { version = "2.0", features = ["debug", "deref", "display", "error", "from", "into", "into_iterator", "try_into"] }
fnv = "1.0.5"
futures = { version = "0.3.22", features = ["alloc"], default-features = false }
graphql-parser = { version = "0.4", optional = true }
indexmap = { version = "2.0", features = ["serde"] }
itertools = "0.14"
jiff = { version = "0.2", features = ["std"], default-features = false, optional = true }
juniper_codegen = { version = "0.16.0", path = "../juniper_codegen" }
ref-cast = "1.0"
rust_decimal = { version = "1.20", default-features = false, optional = true }
ryu = { version = "1.0", optional = true }
serde = { version = "1.0.122", features = ["derive"] }
Expand Down
43 changes: 2 additions & 41 deletions juniper/src/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use indexmap::IndexMap;
use crate::{
executor::Variables,
parser::Spanning,
value::{DefaultScalarValue, ScalarValue},
value::{DefaultScalarValue, ScalarValue, ScalarValueFmt},
};

/// Type literal in a syntax tree.
Expand Down Expand Up @@ -359,30 +359,6 @@ impl<S> InputValue<S> {
}
}

/// View the underlying int value, if present.
pub fn as_int_value(&self) -> Option<i32>
where
S: ScalarValue,
{
self.as_scalar_value().and_then(|s| s.as_int())
}

/// View the underlying float value, if present.
pub fn as_float_value(&self) -> Option<f64>
where
S: ScalarValue,
{
self.as_scalar_value().and_then(|s| s.as_float())
}

/// View the underlying string value, if present.
pub fn as_string_value(&self) -> Option<&str>
where
S: ScalarValue,
{
self.as_scalar_value().and_then(|s| s.as_str())
}

/// View the underlying scalar value, if present.
pub fn as_scalar(&self) -> Option<&S> {
match self {
Expand All @@ -391,15 +367,6 @@ impl<S> InputValue<S> {
}
}

/// View the underlying scalar value, if present.
pub fn as_scalar_value<'a, T>(&'a self) -> Option<&'a T>
where
T: 'a,
Option<&'a T>: From<&'a S>,
{
self.as_scalar().and_then(Into::into)
}

/// Converts this [`InputValue`] to a [`Spanning::unlocated`] object value.
///
/// This constructs a new [`IndexMap`] containing references to the keys
Expand Down Expand Up @@ -473,13 +440,7 @@ impl<S: ScalarValue> fmt::Display for InputValue<S> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Null => write!(f, "null"),
Self::Scalar(s) => {
if let Some(s) = s.as_str() {
write!(f, "\"{s}\"")
} else {
write!(f, "{s}")
}
}
Self::Scalar(s) => fmt::Display::fmt(&ScalarValueFmt(s), f),
Self::Enum(v) => write!(f, "{v}"),
Self::Variable(v) => write!(f, "${v}"),
Self::List(v) => {
Expand Down
4 changes: 2 additions & 2 deletions juniper/src/executor/look_ahead.rs
Original file line number Diff line number Diff line change
Expand Up @@ -772,7 +772,7 @@ impl<'a, S: ScalarValue> ChildrenBuilder<'a, '_, S> {
LookAheadValue::from_input_value(v.as_ref(), Some(self.vars))
.item
{
s.as_bool().unwrap_or(false)
s.try_to_bool().unwrap_or(false)
} else {
false
}
Expand All @@ -788,7 +788,7 @@ impl<'a, S: ScalarValue> ChildrenBuilder<'a, '_, S> {
LookAheadValue::from_input_value(v.as_ref(), Some(self.vars))
.item
{
b.as_bool().map(Not::not).unwrap_or(false)
b.try_to_bool().map(Not::not).unwrap_or(false)
} else {
false
}
Expand Down
Loading