diff --git a/book/src/types/scalars.md b/book/src/types/scalars.md index 2d0ba81e5..9956bb77a 100644 --- a/book/src/types/scalars.md +++ b/book/src/types/scalars.md @@ -105,31 +105,57 @@ fn to_output(v: &Incremented) -> Value { Customization of a [custom GraphQL scalar][2] value parsing is possible via `#[graphql(from_input_with = )]` 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(input: &InputValue) -> Result - 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> { + // ^^^^^^^^ 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, + // ^^^^^^ 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") + }), + ) } } # @@ -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)] @@ -168,11 +193,11 @@ fn to_output(v: &StringOrInt) -> Value { } } -fn from_input(v: &InputValue) -> Result { - 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) -> Result> { + 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(value: ScalarToken<'_>) -> ParseScalarResult { @@ -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)] @@ -212,11 +236,11 @@ mod string_or_int { } } - pub(super) fn from_input(v: &InputValue) -> Result { - 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) -> Result> { + 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(t: ScalarToken<'_>) -> ParseScalarResult { @@ -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)] @@ -251,14 +274,11 @@ impl StringOrInt { } } - fn from_input(v: &InputValue) -> Result - 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) -> Result> { + 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(value: ScalarToken<'_>) -> ParseScalarResult @@ -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)] @@ -304,14 +323,11 @@ mod string_or_int { } } - pub(super) fn from_input(v: &InputValue) -> Result - 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) -> Result> { + 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. @@ -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, @@ -369,10 +386,8 @@ mod date_scalar { Value::scalar(v.to_string()) } - pub(super) fn from_input(v: &InputValue) -> Result { - 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> { + s.parse().map_err(|e| format!("Failed to parse `Date`: {e}").into()) } } # diff --git a/juniper/CHANGELOG.md b/juniper/CHANGELOG.md index 9efc8ffa2..f48d0415a 100644 --- a/juniper/CHANGELOG.md +++ b/juniper/CHANGELOG.md @@ -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()]` attributes of `#[derive(ScalarValue)]` macro). + - Made to require `TryInto` 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` alias. + - Renamed `as_float()` method as `try_to_float()` and made it defined by default as `TryScalarValueTo` alias. + - Renamed `as_int()` method as `try_to_int()` and made it defined by default as `TryScalarValueTo` alias. + - Renamed `as_string()` method as `try_to_string()` and made it defined by default as `TryScalarValueTo` 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` 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 @@ -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::()` method defined by default as `TryScalarValueTo` 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 @@ -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 @@ -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 diff --git a/juniper/Cargo.toml b/juniper/Cargo.toml index 552076d75..ae5b131e2 100644 --- a/juniper/Cargo.toml +++ b/juniper/Cargo.toml @@ -51,7 +51,7 @@ 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 } @@ -59,6 +59,7 @@ 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"] } diff --git a/juniper/src/ast.rs b/juniper/src/ast.rs index dc92cfcff..500ef072d 100644 --- a/juniper/src/ast.rs +++ b/juniper/src/ast.rs @@ -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. @@ -359,30 +359,6 @@ impl InputValue { } } - /// View the underlying int value, if present. - pub fn as_int_value(&self) -> Option - 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 - 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 { @@ -391,15 +367,6 @@ impl InputValue { } } - /// 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 @@ -473,13 +440,7 @@ impl fmt::Display for InputValue { 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) => { diff --git a/juniper/src/executor/look_ahead.rs b/juniper/src/executor/look_ahead.rs index 8c815a5c9..156a63789 100644 --- a/juniper/src/executor/look_ahead.rs +++ b/juniper/src/executor/look_ahead.rs @@ -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 } @@ -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 } diff --git a/juniper/src/executor/mod.rs b/juniper/src/executor/mod.rs index cc02440a3..ca00741eb 100644 --- a/juniper/src/executor/mod.rs +++ b/juniper/src/executor/mod.rs @@ -207,10 +207,10 @@ impl FieldError { /// Maps the [`ScalarValue`] type of this [`FieldError`] into the specified /// one. #[must_use] - pub fn map_scalar_value(self) -> FieldError + pub fn map_scalar_value(self) -> FieldError where S: ScalarValue, - Into: ScalarValue, + T: ScalarValue, { FieldError { message: self.message, @@ -282,6 +282,12 @@ impl IntoFieldError for Cow<'_, str> { } } +impl IntoFieldError for Box { + fn into_field_error(self) -> FieldError { + FieldError::::from(self) + } +} + #[doc(hidden)] pub trait IntoResolvable<'a, S, T, C> where diff --git a/juniper/src/executor_tests/variables.rs b/juniper/src/executor_tests/variables.rs index bcfc9b84a..387436ad0 100644 --- a/juniper/src/executor_tests/variables.rs +++ b/juniper/src/executor_tests/variables.rs @@ -1,5 +1,5 @@ use crate::{ - GraphQLInputObject, GraphQLScalar, InputValue, ScalarValue, Value, + GraphQLInputObject, GraphQLScalar, ScalarValue, Value, executor::Variables, graphql_object, graphql_value, graphql_vars, parser::SourcePosition, @@ -18,11 +18,12 @@ impl TestComplexScalar { graphql_value!("SerializedValue") } - fn from_input(v: &InputValue) -> Result { - v.as_string_value() - .filter(|s| *s == "SerializedValue") - .map(|_| Self) - .ok_or_else(|| format!(r#"Expected "SerializedValue" string, found: {v}"#)) + fn from_input(s: &str) -> Result> { + if s == "SerializedValue" { + Ok(Self) + } else { + Err(format!(r#"Expected "SerializedValue" string, found: "{s}""#).into()) + } } } diff --git a/juniper/src/integrations/bigdecimal.rs b/juniper/src/integrations/bigdecimal.rs index 6c9a39b01..1defe431b 100644 --- a/juniper/src/integrations/bigdecimal.rs +++ b/juniper/src/integrations/bigdecimal.rs @@ -10,7 +10,7 @@ use std::str::FromStr as _; -use crate::{InputValue, ScalarValue, Value, graphql_scalar}; +use crate::{Scalar, ScalarValue, Value, graphql_scalar}; // TODO: Try remove on upgrade of `bigdecimal` crate. mod for_minimal_versions_check_only { @@ -43,21 +43,22 @@ mod bigdecimal_scalar { Value::scalar(v.to_string()) } - pub(super) fn from_input(v: &InputValue) -> Result { - if let Some(i) = v.as_int_value() { + pub(super) fn from_input(v: &Scalar) -> Result> { + if let Some(i) = v.try_to_int() { Ok(BigDecimal::from(i)) - } else if let Some(f) = v.as_float_value() { + } else if let Some(f) = v.try_to_float() { // 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)) - .map_err(|e| format!("Failed to parse `BigDecimal` from `Float`: {e}")) + .map_err(|e| format!("Failed to parse `BigDecimal` from `Float`: {e}").into()) } else { - v.as_string_value() - .ok_or_else(|| format!("Expected `String`, found: {v}")) + v.try_to::<&str>() + .map_err(|e| e.to_string().into()) .and_then(|s| { - BigDecimal::from_str(s) - .map_err(|e| format!("Failed to parse `BigDecimal` from `String`: {e}")) + BigDecimal::from_str(s).map_err(|e| { + format!("Failed to parse `BigDecimal` from `String`: {e}").into() + }) }) } } diff --git a/juniper/src/integrations/bson.rs b/juniper/src/integrations/bson.rs index 246bb3a27..509b3bc12 100644 --- a/juniper/src/integrations/bson.rs +++ b/juniper/src/integrations/bson.rs @@ -13,7 +13,7 @@ //! [s1]: https://graphql-scalars.dev/docs/scalars/object-id //! [s4]: https://graphql-scalars.dev/docs/scalars/date-time -use crate::{InputValue, ScalarValue, Value, graphql_scalar}; +use crate::{ScalarValue, Value, graphql_scalar}; // TODO: Try remove on upgrade of `bson` crate. mod for_minimal_versions_check_only { @@ -44,12 +44,8 @@ mod object_id { Value::scalar(v.to_hex()) } - pub(super) fn from_input(v: &InputValue) -> Result { - v.as_string_value() - .ok_or_else(|| format!("Expected `String`, found: {v}")) - .and_then(|s| { - ObjectId::parse_str(s).map_err(|e| format!("Failed to parse `ObjectID`: {e}")) - }) + pub(super) fn from_input(s: &str) -> Result> { + ObjectId::parse_str(s).map_err(|e| format!("Failed to parse `ObjectID`: {e}").into()) } } @@ -83,13 +79,9 @@ mod date_time { ) } - pub(super) fn from_input(v: &InputValue) -> Result { - v.as_string_value() - .ok_or_else(|| format!("Expected `String`, found: {v}")) - .and_then(|s| { - DateTime::parse_rfc3339_str(s) - .map_err(|e| format!("Failed to parse `DateTime`: {e}")) - }) + pub(super) fn from_input(s: &str) -> Result> { + DateTime::parse_rfc3339_str(s) + .map_err(|e| format!("Failed to parse `DateTime`: {e}").into()) } } diff --git a/juniper/src/integrations/chrono.rs b/juniper/src/integrations/chrono.rs index 3b089734f..99e380c8b 100644 --- a/juniper/src/integrations/chrono.rs +++ b/juniper/src/integrations/chrono.rs @@ -23,7 +23,7 @@ use std::fmt; use chrono::{FixedOffset, TimeZone}; -use crate::{InputValue, ScalarValue, Value, graphql_scalar}; +use crate::{ScalarValue, Value, graphql_scalar}; /// Date in the proleptic Gregorian calendar (without time zone). /// @@ -58,16 +58,8 @@ mod local_date { Value::scalar(v.format(FORMAT).to_string()) } - pub(super) fn from_input(v: &InputValue) -> Result - where - S: ScalarValue, - { - v.as_string_value() - .ok_or_else(|| format!("Expected `String`, found: {v}")) - .and_then(|s| { - LocalDate::parse_from_str(s, FORMAT) - .map_err(|e| format!("Invalid `LocalDate`: {e}")) - }) + pub(super) fn from_input(s: &str) -> Result> { + LocalDate::parse_from_str(s, FORMAT).map_err(|e| format!("Invalid `LocalDate`: {e}").into()) } } @@ -124,21 +116,13 @@ mod local_time { ) } - pub(super) fn from_input(v: &InputValue) -> Result - where - S: ScalarValue, - { - v.as_string_value() - .ok_or_else(|| format!("Expected `String`, found: {v}")) - .and_then(|s| { - // First, try to parse the most used format. - // At the end, try to parse the full format for the parsing - // error to be most informative. - LocalTime::parse_from_str(s, FORMAT_NO_MILLIS) - .or_else(|_| LocalTime::parse_from_str(s, FORMAT_NO_SECS)) - .or_else(|_| LocalTime::parse_from_str(s, FORMAT)) - .map_err(|e| format!("Invalid `LocalTime`: {e}")) - }) + pub(super) fn from_input(s: &str) -> Result> { + // First, try to parse the most used format. + // At the end, try to parse the full format for the parsing error to be most informative. + LocalTime::parse_from_str(s, FORMAT_NO_MILLIS) + .or_else(|_| LocalTime::parse_from_str(s, FORMAT_NO_SECS)) + .or_else(|_| LocalTime::parse_from_str(s, FORMAT)) + .map_err(|e| format!("Invalid `LocalTime`: {e}").into()) } } @@ -172,16 +156,9 @@ mod local_date_time { Value::scalar(v.format(FORMAT).to_string()) } - pub(super) fn from_input(v: &InputValue) -> Result - where - S: ScalarValue, - { - v.as_string_value() - .ok_or_else(|| format!("Expected `String`, found: {v}")) - .and_then(|s| { - LocalDateTime::parse_from_str(s, FORMAT) - .map_err(|e| format!("Invalid `LocalDateTime`: {e}")) - }) + pub(super) fn from_input(s: &str) -> Result> { + LocalDateTime::parse_from_str(s, FORMAT) + .map_err(|e| format!("Invalid `LocalDateTime`: {e}").into()) } } @@ -225,18 +202,13 @@ mod date_time { ) } - pub(super) fn from_input(v: &InputValue) -> Result, String> + pub(super) fn from_input(s: &str) -> Result, Box> where - S: ScalarValue, Tz: TimeZone + FromFixedOffset, { - v.as_string_value() - .ok_or_else(|| format!("Expected `String`, found: {v}")) - .and_then(|s| { - DateTime::::parse_from_rfc3339(s) - .map_err(|e| format!("Invalid `DateTime`: {e}")) - .map(FromFixedOffset::from_fixed_offset) - }) + DateTime::::parse_from_rfc3339(s) + .map(FromFixedOffset::from_fixed_offset) + .map_err(|e| format!("Invalid `DateTime`: {e}").into()) } } diff --git a/juniper/src/integrations/chrono_tz.rs b/juniper/src/integrations/chrono_tz.rs index c6f208fc5..ed38c6d87 100644 --- a/juniper/src/integrations/chrono_tz.rs +++ b/juniper/src/integrations/chrono_tz.rs @@ -11,7 +11,7 @@ //! [1]: http://www.iana.org/time-zones //! [s1]: https://graphql-scalars.dev/docs/scalars/time-zone -use crate::{InputValue, ScalarValue, Value, graphql_scalar}; +use crate::{ScalarValue, Value, graphql_scalar}; // TODO: Try remove on upgrade of `chrono-tz` crate. mod for_minimal_versions_check_only { @@ -45,13 +45,9 @@ mod tz { Value::scalar(v.name().to_owned()) } - pub(super) fn from_input(v: &InputValue) -> Result { - v.as_string_value() - .ok_or_else(|| format!("Expected `String`, found: {v}")) - .and_then(|s| { - s.parse::() - .map_err(|e| format!("Failed to parse `TimeZone`: {e}")) - }) + pub(super) fn from_input(s: &str) -> Result> { + s.parse::() + .map_err(|e| format!("Failed to parse `TimeZone`: {e}").into()) } } diff --git a/juniper/src/integrations/jiff.rs b/juniper/src/integrations/jiff.rs index de452289b..045c95f39 100644 --- a/juniper/src/integrations/jiff.rs +++ b/juniper/src/integrations/jiff.rs @@ -54,7 +54,7 @@ use std::str; use derive_more::with_trait::{Debug, Display, Error, Into}; -use crate::{InputValue, ScalarValue, Value, graphql_scalar}; +use crate::{ScalarValue, Value, graphql_scalar}; /// Representation of a civil date in the Gregorian calendar. /// @@ -90,15 +90,8 @@ mod local_date { Value::scalar(v.strftime(FORMAT).to_string()) } - pub(super) fn from_input(v: &InputValue) -> Result - where - S: ScalarValue, - { - v.as_string_value() - .ok_or_else(|| format!("Expected `String`, found: {v}")) - .and_then(|s| { - LocalDate::strptime(FORMAT, s).map_err(|e| format!("Invalid `LocalDate`: {e}")) - }) + pub(super) fn from_input(s: &str) -> Result> { + LocalDate::strptime(FORMAT, s).map_err(|e| format!("Invalid `LocalDate`: {e}").into()) } } @@ -153,21 +146,13 @@ mod local_time { ) } - pub(super) fn from_input(v: &InputValue) -> Result - where - S: ScalarValue, - { - v.as_string_value() - .ok_or_else(|| format!("Expected `String`, found: {v}")) - .and_then(|s| { - // First, try to parse the most used format. - // At the end, try to parse the full format for the parsing - // error to be most informative. - LocalTime::strptime(FORMAT_NO_MILLIS, s) - .or_else(|_| LocalTime::strptime(FORMAT_NO_SECS, s)) - .or_else(|_| LocalTime::strptime(FORMAT, s)) - .map_err(|e| format!("Invalid `LocalTime`: {e}")) - }) + pub(super) fn from_input(s: &str) -> Result> { + // First, try to parse the most used format. + // At the end, try to parse the full format for the parsing error to be most informative. + LocalTime::strptime(FORMAT_NO_MILLIS, s) + .or_else(|_| LocalTime::strptime(FORMAT_NO_SECS, s)) + .or_else(|_| LocalTime::strptime(FORMAT, s)) + .map_err(|e| format!("Invalid `LocalTime`: {e}").into()) } } @@ -207,16 +192,9 @@ mod local_date_time { Value::scalar(v.strftime(FORMAT).to_string()) } - pub(super) fn from_input(v: &InputValue) -> Result - where - S: ScalarValue, - { - v.as_string_value() - .ok_or_else(|| format!("Expected `String`, found: {v}")) - .and_then(|s| { - LocalDateTime::strptime(FORMAT, s) - .map_err(|e| format!("Invalid `LocalDateTime`: {e}")) - }) + pub(super) fn from_input(s: &str) -> Result> { + LocalDateTime::strptime(FORMAT, s) + .map_err(|e| format!("Invalid `LocalDateTime`: {e}").into()) } } @@ -254,13 +232,8 @@ mod date_time { Value::scalar(v.strftime(FORMAT).to_string()) } - pub(super) fn from_input(v: &InputValue) -> Result - where - S: ScalarValue, - { - v.as_string_value() - .ok_or_else(|| format!("Expected `String`, found: {v}")) - .and_then(|s| DateTime::from_str(s).map_err(|e| format!("Invalid `DateTime`: {e}"))) + pub(super) fn from_input(s: &str) -> Result> { + DateTime::from_str(s).map_err(|e| format!("Invalid `DateTime`: {e}").into()) } } @@ -299,15 +272,8 @@ mod zoned_date_time { Value::scalar(v.to_string()) } - pub(super) fn from_input(v: &InputValue) -> Result - where - S: ScalarValue, - { - v.as_string_value() - .ok_or_else(|| format!("Expected `String`, found: {v}")) - .and_then(|s| { - ZonedDateTime::from_str(s).map_err(|e| format!("Invalid `ZonedDateTime`: {e}")) - }) + pub(super) fn from_input(s: &str) -> Result> { + ZonedDateTime::from_str(s).map_err(|e| format!("Invalid `ZonedDateTime`: {e}").into()) } } @@ -341,13 +307,8 @@ mod duration { Value::scalar(v.to_string()) } - pub(super) fn from_input(v: &InputValue) -> Result - where - S: ScalarValue, - { - v.as_string_value() - .ok_or_else(|| format!("Expected `String`, found: {v}")) - .and_then(|s| Duration::from_str(s).map_err(|e| format!("Invalid `Duration`: {e}"))) + pub(super) fn from_input(s: &str) -> Result> { + Duration::from_str(s).map_err(|e| format!("Invalid `Duration`: {e}").into()) } } @@ -395,18 +356,11 @@ mod time_zone_or_utc_offset { )) } - pub(super) fn from_input(v: &InputValue) -> Result - where - S: ScalarValue, - { - v.as_string_value() - .ok_or_else(|| format!("Expected `String`, found: {v}")) - .and_then(|s| { - TimeZoneOrUtcOffset::get(s) - .map_err(TimeZoneParsingError::InvalidTimeZone) - .or_else(|_| utc_offset::utc_offset_from_str(s).map(TimeZoneOrUtcOffset::fixed)) - .map_err(|e| format!("Invalid `TimeZoneOrUtcOffset`: {e}")) - }) + pub(super) fn from_input(s: &str) -> Result> { + TimeZoneOrUtcOffset::get(s) + .map_err(TimeZoneParsingError::InvalidTimeZone) + .or_else(|_| utc_offset::utc_offset_from_str(s).map(TimeZoneOrUtcOffset::fixed)) + .map_err(|e| format!("Invalid `TimeZoneOrUtcOffset`: {e}").into()) } } @@ -478,13 +432,9 @@ mod time_zone { Value::scalar(v.to_string()) } - pub(super) fn from_input(v: &InputValue) -> Result - where - S: ScalarValue, - { - v.as_string_value() - .ok_or_else(|| format!("Expected `String`, found: {v}")) - .and_then(|s| s.parse().map_err(|e| format!("Invalid `TimeZone`: {e}"))) + pub(super) fn from_input(s: &str) -> Result> { + s.parse() + .map_err(|e| format!("Invalid `TimeZone`: {e}").into()) } } @@ -531,13 +481,8 @@ mod utc_offset { Value::scalar(buf) } - pub(super) fn from_input(v: &InputValue) -> Result - where - S: ScalarValue, - { - v.as_string_value() - .ok_or_else(|| format!("Expected `String`, found: {v}")) - .and_then(|s| utc_offset_from_str(s).map_err(|e| format!("Invalid `UtcOffset`: {e}"))) + pub(super) fn from_input(s: &str) -> Result> { + utc_offset_from_str(s).map_err(|e| format!("Invalid `UtcOffset`: {e}").into()) } } diff --git a/juniper/src/integrations/rust_decimal.rs b/juniper/src/integrations/rust_decimal.rs index 50a5370c3..c8952e47a 100644 --- a/juniper/src/integrations/rust_decimal.rs +++ b/juniper/src/integrations/rust_decimal.rs @@ -10,7 +10,7 @@ use std::str::FromStr as _; -use crate::{InputValue, ScalarValue, Value, graphql_scalar}; +use crate::{Scalar, ScalarValue, Value, graphql_scalar}; /// 128 bit representation of a fixed-precision decimal number. /// @@ -40,17 +40,18 @@ mod rust_decimal_scalar { Value::scalar(v.to_string()) } - pub(super) fn from_input(v: &InputValue) -> Result { - if let Some(i) = v.as_int_value() { + pub(super) fn from_input(v: &Scalar) -> Result> { + if let Some(i) = v.try_to_int() { Ok(Decimal::from(i)) - } else if let Some(f) = v.as_float_value() { - Decimal::try_from(f).map_err(|e| format!("Failed to parse `Decimal` from `Float`: {e}")) + } else if let Some(f) = v.try_to_float() { + Decimal::try_from(f) + .map_err(|e| format!("Failed to parse `Decimal` from `Float`: {e}").into()) } else { - v.as_string_value() - .ok_or_else(|| format!("Expected `String`, found: {v}")) + v.try_to::<&str>() + .map_err(|e| e.to_string().into()) .and_then(|s| { Decimal::from_str(s) - .map_err(|e| format!("Failed to parse `Decimal` from `String`: {e}")) + .map_err(|e| format!("Failed to parse `Decimal` from `String`: {e}").into()) }) } } diff --git a/juniper/src/integrations/time.rs b/juniper/src/integrations/time.rs index 3c649d1d0..e31ebbfb8 100644 --- a/juniper/src/integrations/time.rs +++ b/juniper/src/integrations/time.rs @@ -27,7 +27,7 @@ use time::{ macros::format_description, }; -use crate::{InputValue, ScalarValue, Value, graphql_scalar}; +use crate::{ScalarValue, Value, graphql_scalar}; /// Date in the proleptic Gregorian calendar (without time zone). /// @@ -62,12 +62,8 @@ mod local_date { ) } - pub(super) fn from_input(v: &InputValue) -> Result { - v.as_string_value() - .ok_or_else(|| format!("Expected `String`, found: {v}")) - .and_then(|s| { - LocalDate::parse(s, FORMAT).map_err(|e| format!("Invalid `LocalDate`: {e}")) - }) + pub(super) fn from_input(s: &str) -> Result> { + LocalDate::parse(s, FORMAT).map_err(|e| format!("Invalid `LocalDate`: {e}").into()) } } @@ -121,18 +117,13 @@ mod local_time { ) } - pub(super) fn from_input(v: &InputValue) -> Result { - v.as_string_value() - .ok_or_else(|| format!("Expected `String`, found: {v}")) - .and_then(|s| { - // First, try to parse the most used format. - // At the end, try to parse the full format for the parsing - // error to be most informative. - LocalTime::parse(s, FORMAT_NO_MILLIS) - .or_else(|_| LocalTime::parse(s, FORMAT_NO_SECS)) - .or_else(|_| LocalTime::parse(s, FORMAT)) - .map_err(|e| format!("Invalid `LocalTime`: {e}")) - }) + pub(super) fn from_input(s: &str) -> Result> { + // First, try to parse the most used format. + // At the end, try to parse the full format for the parsing error to be most informative. + LocalTime::parse(s, FORMAT_NO_MILLIS) + .or_else(|_| LocalTime::parse(s, FORMAT_NO_SECS)) + .or_else(|_| LocalTime::parse(s, FORMAT)) + .map_err(|e| format!("Invalid `LocalTime`: {e}").into()) } } @@ -167,12 +158,8 @@ mod local_date_time { ) } - pub(super) fn from_input(v: &InputValue) -> Result { - v.as_string_value() - .ok_or_else(|| format!("Expected `String`, found: {v}")) - .and_then(|s| { - LocalDateTime::parse(s, FORMAT).map_err(|e| format!("Invalid `LocalDateTime`: {e}")) - }) + pub(super) fn from_input(s: &str) -> Result> { + LocalDateTime::parse(s, FORMAT).map_err(|e| format!("Invalid `LocalDateTime`: {e}").into()) } } @@ -206,13 +193,10 @@ mod date_time { ) } - pub(super) fn from_input(v: &InputValue) -> Result { - v.as_string_value() - .ok_or_else(|| format!("Expected `String`, found: {v}")) - .and_then(|s| { - DateTime::parse(s, &Rfc3339).map_err(|e| format!("Invalid `DateTime`: {e}")) - }) + pub(super) fn from_input(s: &str) -> Result> { + DateTime::parse(s, &Rfc3339) .map(|dt| dt.to_offset(UtcOffset::UTC)) + .map_err(|e| format!("Invalid `DateTime`: {e}").into()) } } @@ -248,13 +232,9 @@ mod utc_offset { ) } - pub(super) fn from_input(v: &InputValue) -> Result { - v.as_string_value() - .ok_or_else(|| format!("Expected `String`, found: {v}")) - .and_then(|s| { - UtcOffset::parse(s, UTC_OFFSET_FORMAT) - .map_err(|e| format!("Invalid `UtcOffset`: {e}")) - }) + pub(super) fn from_input(s: &str) -> Result> { + UtcOffset::parse(s, UTC_OFFSET_FORMAT) + .map_err(|e| format!("Invalid `UtcOffset`: {e}").into()) } } diff --git a/juniper/src/integrations/url.rs b/juniper/src/integrations/url.rs index dd2008cbe..8bae4896c 100644 --- a/juniper/src/integrations/url.rs +++ b/juniper/src/integrations/url.rs @@ -9,7 +9,7 @@ //! [`Url`]: url::Url //! [s1]: https://graphql-scalars.dev/docs/scalars/url -use crate::{InputValue, ScalarValue, Value, graphql_scalar}; +use crate::{ScalarValue, Value, graphql_scalar}; /// [Standard URL][0] format as specified in [RFC 3986]. /// @@ -36,10 +36,8 @@ mod url_scalar { Value::scalar(v.as_str().to_owned()) } - pub(super) fn from_input(v: &InputValue) -> Result { - v.as_string_value() - .ok_or_else(|| format!("Expected `String`, found: {v}")) - .and_then(|s| Url::parse(s).map_err(|e| format!("Failed to parse `URL`: {e}"))) + pub(super) fn from_input(s: &str) -> Result> { + Url::parse(s).map_err(|e| format!("Failed to parse `URL`: {e}").into()) } } diff --git a/juniper/src/integrations/uuid.rs b/juniper/src/integrations/uuid.rs index 7657d747f..70ce7c7cb 100644 --- a/juniper/src/integrations/uuid.rs +++ b/juniper/src/integrations/uuid.rs @@ -9,7 +9,7 @@ //! [`Uuid`]: uuid::Uuid //! [s1]: https://graphql-scalars.dev/docs/scalars/uuid -use crate::{InputValue, ScalarValue, Value, graphql_scalar}; +use crate::{ScalarValue, Value, graphql_scalar}; /// [Universally Unique Identifier][0] (UUID). /// @@ -35,10 +35,8 @@ mod uuid_scalar { Value::scalar(v.to_string()) } - pub(super) fn from_input(v: &InputValue) -> Result { - v.as_string_value() - .ok_or_else(|| format!("Expected `String`, found: {v}")) - .and_then(|s| Uuid::parse_str(s).map_err(|e| format!("Failed to parse `UUID`: {e}"))) + pub(super) fn from_input(s: &str) -> Result> { + Uuid::parse_str(s).map_err(|e| format!("Failed to parse `UUID`: {e}").into()) } } diff --git a/juniper/src/lib.rs b/juniper/src/lib.rs index c01c61cb5..151e0814a 100644 --- a/juniper/src/lib.rs +++ b/juniper/src/lib.rs @@ -103,8 +103,8 @@ pub use crate::{ }, validation::RuleError, value::{ - AnyExt, DefaultScalarValue, IntoValue, Object, ParseScalarResult, ParseScalarValue, - ScalarValue, Value, + AnyExt, DefaultScalarValue, IntoValue, Object, ParseScalarResult, ParseScalarValue, Scalar, + ScalarValue, TryScalarValueTo, Value, WrongInputScalarTypeError, }, }; diff --git a/juniper/src/macros/helper/mod.rs b/juniper/src/macros/helper/mod.rs index e3e31926d..75a414aa2 100644 --- a/juniper/src/macros/helper/mod.rs +++ b/juniper/src/macros/helper/mod.rs @@ -2,17 +2,17 @@ pub mod subscription; -use std::fmt; +use std::convert::Infallible; +use derive_more::with_trait::Display; use futures::future::{self, BoxFuture}; -use crate::FieldError; +use crate::{FieldError, InputValue, ScalarValue}; -/// This trait is used by [`graphql_scalar!`] macro to retrieve [`Error`] type -/// from a [`Result`]. +/// This trait is used by [`graphql_scalar`] macro to retrieve [`Error`] type from a [`Result`]. /// /// [`Error`]: Result::Error -/// [`graphql_scalar!`]: macro@crate::graphql_scalar +/// [`graphql_scalar`]: macro@crate::graphql_scalar pub trait ExtractError { /// Extracted [`Error`] type of this [`Result`]. /// @@ -28,7 +28,7 @@ impl ExtractError for Result { /// which immediately resolves into [`FieldError`]. pub fn err_fut<'ok, D, Ok, S>(msg: D) -> BoxFuture<'ok, Result>> where - D: fmt::Display, + D: Display, Ok: Send + 'ok, S: Send + 'static, { @@ -53,3 +53,44 @@ where { Box::pin(future::err(err_unnamed_type(name))) } + +/// Error of an [`InputValue`] not representing a [`ScalarValue`], used in macro expansions. +#[derive(Display)] +#[display("Expected GraphQL scalar, found: {_0}")] +pub struct NotScalarError<'a, S: ScalarValue>(pub &'a InputValue); + +/// [Autoref-based specialized][0] coercion into a [`Result`] for a function call for providing a +/// return-type polymorphism in macros. +/// +/// [0]: https://lukaskalbertodt.github.io/2019/12/05/generalized-autoref-based-specialization.html +pub trait ToResultCall { + /// Input of this function. + type Input; + /// Output of this function. + type Output; + /// Error of the [`Result`] coercion for this function. + type Error; + + /// Calls this function, coercing its output into a [`Result`]. + fn __to_result_call(&self, input: Self::Input) -> Result; +} + +impl ToResultCall for fn(I) -> O { + type Input = I; + type Output = O; + type Error = Infallible; + + fn __to_result_call(&self, input: Self::Input) -> Result { + Ok(self(input)) + } +} + +impl ToResultCall for &fn(I) -> Result { + type Input = I; + type Output = O; + type Error = E; + + fn __to_result_call(&self, input: Self::Input) -> Result { + self(input) + } +} diff --git a/juniper/src/schema/translate/graphql_parser.rs b/juniper/src/schema/translate/graphql_parser.rs index 9ad12d553..d62c263d5 100644 --- a/juniper/src/schema/translate/graphql_parser.rs +++ b/juniper/src/schema/translate/graphql_parser.rs @@ -107,13 +107,13 @@ impl GraphQLParserTranslator { match input { InputValue::Null => ExternalValue::Null, InputValue::Scalar(x) => { - if let Some(v) = x.as_string() { + if let Some(v) = x.try_to_string() { ExternalValue::String(v) - } else if let Some(v) = x.as_int() { + } else if let Some(v) = x.try_to_int() { ExternalValue::Int(ExternalNumber::from(v)) - } else if let Some(v) = x.as_float() { + } else if let Some(v) = x.try_to_float() { ExternalValue::Float(v) - } else if let Some(v) = x.as_bool() { + } else if let Some(v) = x.try_to_bool() { ExternalValue::Boolean(v) } else { panic!("unknown argument type") diff --git a/juniper/src/tests/introspection_tests.rs b/juniper/src/tests/introspection_tests.rs index 616065a96..210c3bab0 100644 --- a/juniper/src/tests/introspection_tests.rs +++ b/juniper/src/tests/introspection_tests.rs @@ -2,16 +2,15 @@ use std::collections::HashSet; use pretty_assertions::assert_eq; +use super::schema_introspection::*; use crate::{ - graphql_vars, + ScalarValue as _, graphql_vars, introspection::IntrospectionFormat, schema::model::RootNode, tests::fixtures::starwars::schema::{Database, Query}, types::scalars::{EmptyMutation, EmptySubscription}, }; -use super::schema_introspection::*; - #[tokio::test] async fn test_introspection_query_type_name() { let doc = r#" @@ -270,8 +269,9 @@ async fn test_introspection_possible_types() { .expect("possible type not an object") .get_field_value("name") .expect("'name' not present in type") - .as_scalar_value::() - .expect("'name' not a string") as &str + .as_scalar() + .and_then(|s| s.try_as_str()) + .expect("'name' not a string") }) .collect::>(); diff --git a/juniper/src/types/containers.rs b/juniper/src/types/containers.rs index b981711df..4ea31b990 100644 --- a/juniper/src/types/containers.rs +++ b/juniper/src/types/containers.rs @@ -668,13 +668,13 @@ mod coercion { assert_eq!( >::from_input_value(&v), Err(FromInputValueVecError::Item( - "Expected `Int`, found: null".into_field_error(), + "Expected GraphQL scalar, found: null".into_field_error(), )), ); assert_eq!( >>::from_input_value(&v), Err(FromInputValueVecError::Item( - "Expected `Int`, found: null".into_field_error(), + "Expected GraphQL scalar, found: null".into_field_error(), )), ); assert_eq!( @@ -778,13 +778,13 @@ mod coercion { assert_eq!( <[i32; 3]>::from_input_value(&v), Err(FromInputValueArrayError::Item( - "Expected `Int`, found: null".into_field_error(), + "Expected GraphQL scalar, found: null".into_field_error(), )), ); assert_eq!( >::from_input_value(&v), Err(FromInputValueArrayError::Item( - "Expected `Int`, found: null".into_field_error(), + "Expected GraphQL scalar, found: null".into_field_error(), )), ); assert_eq!( diff --git a/juniper/src/types/scalars.rs b/juniper/src/types/scalars.rs index c15af3a13..caf88c39a 100644 --- a/juniper/src/types/scalars.rs +++ b/juniper/src/types/scalars.rs @@ -1,10 +1,10 @@ -use std::{char, marker::PhantomData, rc::Rc, thread::JoinHandle}; +use std::{char, convert::identity, marker::PhantomData, rc::Rc, thread::JoinHandle}; use derive_more::with_trait::{Deref, Display, From, Into}; use serde::{Deserialize, Serialize}; use crate::{ - GraphQLScalar, + GraphQLScalar, Scalar, ast::{InputValue, Selection, ToInputValue}, executor::{ExecutionResult, Executor, Registry}, graphql_scalar, @@ -16,7 +16,7 @@ use crate::{ base::{GraphQLType, GraphQLValue}, subscriptions::GraphQLSubscriptionValue, }, - value::{ParseScalarResult, ScalarValue, Value}, + value::{ParseScalarResult, ScalarValue, Value, WrongInputScalarTypeError}, }; /// An ID as defined by the GraphQL specification @@ -26,32 +26,37 @@ use crate::{ Clone, Debug, Deref, Deserialize, Display, Eq, From, GraphQLScalar, Into, PartialEq, Serialize, )] #[deref(forward)] +#[from(Box, String)] +#[into(Box, String)] #[graphql(parse_token(String, i32))] -pub struct ID(String); +pub struct ID(Box); impl ID { fn to_output(&self) -> Value { - Value::scalar(self.0.clone()) + Value::scalar(self.0.clone().into_string()) } - fn from_input(v: &InputValue) -> Result { - v.as_string_value() - .map(str::to_owned) - .or_else(|| v.as_int_value().as_ref().map(ToString::to_string)) - .map(Self) - .ok_or_else(|| format!("Expected `String` or `Int`, found: {v}")) + fn from_input(v: &Scalar) -> Result> { + v.try_to_string() + .or_else(|| v.try_to_int().as_ref().map(ToString::to_string)) + .map(|s| Self(s.into())) + .ok_or_else(|| WrongInputScalarTypeError { + type_name: arcstr::literal!("String` or `Int"), + input: &**v, + }) } } impl ID { - /// Construct a new ID from anything implementing `Into` + /// Construct a new [`ID`] from anything implementing [`Into`]`<`[`String`]`>`. + #[must_use] pub fn new>(value: S) -> Self { - ID(value.into()) + ID(value.into().into()) } } #[graphql_scalar] -#[graphql(with = impl_string_scalar)] +#[graphql(with = impl_string_scalar, from_input_with = identity::)] type String = std::string::String; mod impl_string_scalar { @@ -61,12 +66,6 @@ mod impl_string_scalar { Value::scalar(v.to_owned()) } - pub(super) fn from_input(v: &InputValue) -> Result { - v.as_string_value() - .map(str::to_owned) - .ok_or_else(|| format!("Expected `String`, found: {v}")) - } - pub(super) fn parse_token(value: ScalarToken<'_>) -> ParseScalarResult { if let ScalarToken::String(value) = value { let mut ret = String::with_capacity(value.len()); @@ -176,18 +175,15 @@ where type ArcStr = arcstr::ArcStr; mod impl_arcstr_scalar { - use crate::{InputValue, IntoValue as _, ScalarValue, Value}; - use super::ArcStr; + use crate::{IntoValue as _, ScalarValue, Value}; pub(super) fn to_output(v: &ArcStr) -> Value { v.into_value() } - pub(super) fn from_input(v: &InputValue) -> Result { - v.as_string_value() - .map(Into::into) - .ok_or_else(|| format!("Expected `String`, found: {v}")) + pub(super) fn from_input(s: &str) -> ArcStr { + s.into() } } @@ -196,18 +192,15 @@ mod impl_arcstr_scalar { type CompactString = compact_str::CompactString; mod impl_compactstring_scalar { - use crate::{InputValue, IntoValue as _, ScalarValue, Value}; - use super::CompactString; + use crate::{IntoValue as _, ScalarValue, Value}; pub(super) fn to_output(v: &CompactString) -> Value { v.into_value() } - pub(super) fn from_input(v: &InputValue) -> Result { - v.as_string_value() - .map(Into::into) - .ok_or_else(|| format!("Expected `String`, found: {v}")) + pub(super) fn from_input(s: &str) -> CompactString { + s.into() } } @@ -282,7 +275,7 @@ where } #[graphql_scalar] -#[graphql(with = impl_boolean_scalar)] +#[graphql(with = impl_boolean_scalar, from_input_with = identity::)] type Boolean = bool; mod impl_boolean_scalar { @@ -292,12 +285,6 @@ mod impl_boolean_scalar { Value::scalar(*v) } - pub(super) fn from_input(v: &InputValue) -> Result { - v.as_scalar_value() - .and_then(ScalarValue::as_bool) - .ok_or_else(|| format!("Expected `Boolean`, found: {v}")) - } - pub(super) fn parse_token(value: ScalarToken<'_>) -> ParseScalarResult { // `Boolean`s are parsed separately, they shouldn't reach this code path. Err(ParseError::unexpected_token(Token::Scalar(value))) @@ -305,7 +292,7 @@ mod impl_boolean_scalar { } #[graphql_scalar] -#[graphql(with = impl_int_scalar)] +#[graphql(with = impl_int_scalar, from_input_with = identity::)] type Int = i32; mod impl_int_scalar { @@ -315,11 +302,6 @@ mod impl_int_scalar { Value::scalar(*v) } - pub(super) fn from_input(v: &InputValue) -> Result { - v.as_int_value() - .ok_or_else(|| format!("Expected `Int`, found: {v}")) - } - pub(super) fn parse_token(value: ScalarToken<'_>) -> ParseScalarResult { if let ScalarToken::Int(v) = value { v.parse() @@ -332,7 +314,7 @@ mod impl_int_scalar { } #[graphql_scalar] -#[graphql(with = impl_float_scalar)] +#[graphql(with = impl_float_scalar, from_input_with = identity::)] type Float = f64; mod impl_float_scalar { @@ -342,11 +324,6 @@ mod impl_float_scalar { Value::scalar(*v) } - pub(super) fn from_input(v: &InputValue) -> Result { - v.as_float_value() - .ok_or_else(|| format!("Expected `Float`, found: {v}")) - } - pub(super) fn parse_token(value: ScalarToken<'_>) -> ParseScalarResult { match value { ScalarToken::Int(v) => v @@ -483,7 +460,7 @@ impl Default for EmptySubscription { mod tests { use crate::{ parser::ScalarToken, - value::{DefaultScalarValue, ParseScalarValue}, + value::{DefaultScalarValue, ParseScalarValue, ScalarValue as _}, }; use super::{EmptyMutation, EmptySubscription, ID}; @@ -491,20 +468,20 @@ mod tests { #[test] fn test_id_from_string() { let actual = ID::from(String::from("foo")); - let expected = ID(String::from("foo")); + let expected = ID("foo".into()); assert_eq!(actual, expected); } #[test] fn test_id_new() { let actual = ID::new("foo"); - let expected = ID(String::from("foo")); + let expected = ID("foo".into()); assert_eq!(actual, expected); } #[test] fn test_id_deref() { - let id = ID(String::from("foo")); + let id = ID("foo".into()); assert_eq!(id.len(), 3); } @@ -520,7 +497,7 @@ mod tests { let s = >::from_str(ScalarToken::String(s)); assert!(s.is_ok(), "A parsing error occurred: {s:?}"); - let s: Option = s.unwrap().into(); + let s: Option = s.unwrap().try_to().ok(); assert!(s.is_some(), "No string returned"); assert_eq!(s.unwrap(), expected); } @@ -548,7 +525,7 @@ mod tests { let n = >::from_str(ScalarToken::Int(v)); assert!(n.is_ok(), "A parsing error occurred: {:?}", n.unwrap_err()); - let n: Option = n.unwrap().into(); + let n: Option = n.unwrap().try_to().ok(); assert!(n.is_some(), "No `f64` returned"); assert_eq!(n.unwrap(), f64::from(expected)); } @@ -566,7 +543,7 @@ mod tests { let n = >::from_str(ScalarToken::Float(v)); assert!(n.is_ok(), "A parsing error occurred: {:?}", n.unwrap_err()); - let n: Option = n.unwrap().into(); + let n: Option = n.unwrap().try_to().ok(); assert!(n.is_some(), "No `f64` returned"); assert_eq!(n.unwrap(), expected); } diff --git a/juniper/src/validation/input_value.rs b/juniper/src/validation/input_value.rs index b0a5c2d64..30fec7ecb 100644 --- a/juniper/src/validation/input_value.rs +++ b/juniper/src/validation/input_value.rs @@ -250,8 +250,8 @@ where match value { // TODO: avoid this bad duplicate as_str() call. (value system refactor) - InputValue::Scalar(scalar) if scalar.as_str().is_some() => { - if let Some(name) = scalar.as_str() { + InputValue::Scalar(scalar) if scalar.try_as_str().is_some() => { + if let Some(name) = scalar.try_as_str() { if !meta.values.iter().any(|ev| ev.name == *name) { errors.push(unification_error( var_name, diff --git a/juniper/src/validation/test_harness.rs b/juniper/src/validation/test_harness.rs index 576df2090..dcf765017 100644 --- a/juniper/src/validation/test_harness.rs +++ b/juniper/src/validation/test_harness.rs @@ -233,7 +233,7 @@ where { type Error = &'static str; - fn from_input_value<'a>(v: &InputValue) -> Result { + fn from_input_value(v: &InputValue) -> Result { match v.as_enum_value() { Some("SIT") => Ok(DogCommand::Sit), Some("HEEL") => Ok(DogCommand::Heel), @@ -335,7 +335,7 @@ where { type Error = &'static str; - fn from_input_value<'a>(v: &InputValue) -> Result { + fn from_input_value(v: &InputValue) -> Result { match v.as_enum_value() { Some("BROWN") => Ok(FurColor::Brown), Some("BLACK") => Ok(FurColor::Black), @@ -611,7 +611,7 @@ where { type Error = FieldError; - fn from_input_value<'a>(v: &InputValue) -> Result { + fn from_input_value(v: &InputValue) -> Result { let obj = v.to_object_value().ok_or("Expected object")?; Ok(ComplexInput { diff --git a/juniper/src/value/mod.rs b/juniper/src/value/mod.rs index 08259f60b..8defdb611 100644 --- a/juniper/src/value/mod.rs +++ b/juniper/src/value/mod.rs @@ -6,16 +6,19 @@ use std::{any::TypeId, borrow::Cow, fmt, mem}; use arcstr::ArcStr; use compact_str::CompactString; +pub(crate) use self::scalar::ScalarValueFmt; +pub use self::{ + object::Object, + scalar::{ + AnyExt, DefaultScalarValue, ParseScalarResult, ParseScalarValue, Scalar, ScalarValue, + TryScalarValueTo, WrongInputScalarTypeError, + }, +}; use crate::{ ast::{InputValue, ToInputValue}, parser::Spanning, }; -pub use self::{ - object::Object, - scalar::{AnyExt, DefaultScalarValue, ParseScalarResult, ParseScalarValue, ScalarValue}, -}; - /// Serializable value returned from query and field execution. /// /// Used by the execution engine and resolvers to build up the response @@ -64,28 +67,6 @@ impl Value { matches!(*self, Self::Null) } - /// View the underlying scalar value if present - pub fn as_scalar_value<'a, T>(&'a self) -> Option<&'a T> - where - Option<&'a T>: From<&'a S>, - { - match self { - Self::Scalar(s) => s.into(), - _ => None, - } - } - - /// View the underlying float value, if present. - pub fn as_float_value(&self) -> Option - where - S: ScalarValue, - { - match self { - Self::Scalar(s) => s.as_float(), - _ => None, - } - } - /// View the underlying object value, if present. pub fn as_object_value(&self) -> Option<&Object> { match self { @@ -128,27 +109,17 @@ impl Value { } } - /// View the underlying string value, if present. - pub fn as_string_value<'a>(&'a self) -> Option<&'a str> - where - Option<&'a String>: From<&'a S>, - { - self.as_scalar_value::().map(String::as_str) - } - /// Maps the [`ScalarValue`] type of this [`Value`] into the specified one. - pub fn map_scalar_value(self) -> Value + pub fn map_scalar_value(self) -> Value where S: ScalarValue, - Into: ScalarValue, + T: ScalarValue, { - if TypeId::of::() == TypeId::of::() { - // SAFETY: This is safe, because we're transmuting the value into - // itself, so no invariants may change and we're just - // satisfying the type checker. - // As `mem::transmute_copy` creates a copy of data, we need - // `mem::ManuallyDrop` here to omit double-free when - // `S: Drop`. + if TypeId::of::() == TypeId::of::() { + // SAFETY: This is safe, because we're transmuting the value into itself, so no + // invariants may change, and we're just satisfying the type checker. + // As `mem::transmute_copy` creates a copy of the data, we need the + // `mem::ManuallyDrop` here to omit double-free when `S: Drop`. let val = mem::ManuallyDrop::new(self); unsafe { mem::transmute_copy(&*val) } } else { @@ -194,13 +165,7 @@ impl fmt::Display for Value { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Null => write!(f, "null"), - Self::Scalar(s) => { - if let Some(string) = s.as_string() { - write!(f, "\"{string}\"") - } else { - write!(f, "{s}") - } - } + Self::Scalar(s) => fmt::Display::fmt(&ScalarValueFmt(s), f), Self::List(list) => { write!(f, "[")?; for (idx, item) in list.iter().enumerate() { diff --git a/juniper/src/value/scalar.rs b/juniper/src/value/scalar.rs index c3b6a8c32..3eaae254e 100644 --- a/juniper/src/value/scalar.rs +++ b/juniper/src/value/scalar.rs @@ -1,15 +1,21 @@ +use std::convert::Infallible; + +use arcstr::ArcStr; +use derive_more::with_trait::{Deref, Display, Error, From, TryInto}; +use ref_cast::RefCast; +use serde::{Serialize, de::DeserializeOwned}; use std::{ any::{Any, TypeId}, borrow::Cow, fmt, ptr, }; -use derive_more::with_trait::From; -use serde::{Serialize, de::DeserializeOwned}; - -use crate::parser::{ParseError, ScalarToken}; +use crate::{ + FieldError, IntoFieldError, + parser::{ParseError, ScalarToken}, +}; #[cfg(doc)] -use crate::{InputValue, Value}; +use crate::{GraphQLScalar, GraphQLValue, Value}; pub use juniper_codegen::ScalarValue; @@ -31,9 +37,10 @@ pub trait ParseScalarValue { /// # Deriving /// /// There is a custom derive (`#[derive(`[`ScalarValue`](macro@crate::ScalarValue)`)]`) available, -/// that implements most of the required traits automatically for an enum representing a -/// [`ScalarValue`]. However, [`Serialize`] and [`Deserialize`] implementations -/// are expected to be provided. +/// that implements most of the required [`juniper`] traits automatically for an enum representing a +/// [`ScalarValue`]. However, [`Serialize`] and [`Deserialize`] implementations are expected to be +/// provided, as we as [`Display`], [`From`] and [`TryInto`] ones (for which it's convenient to use +/// [`derive_more`]). /// /// # Example /// @@ -47,21 +54,31 @@ pub trait ParseScalarValue { /// # use std::{any::Any, fmt}; /// # /// # use compact_str::CompactString; -/// # use juniper::ScalarValue; -/// # use serde::{de, Deserialize, Deserializer, Serialize}; -/// # -/// #[derive(Clone, Debug, PartialEq, ScalarValue, Serialize)] +/// use derive_more::{Display, From, TryInto}; +/// use juniper::ScalarValue; +/// use serde::{de, Deserialize, Deserializer, Serialize}; +/// +/// #[derive(Clone, Debug, Display, From, PartialEq, ScalarValue, Serialize, TryInto)] /// #[serde(untagged)] /// #[value(from_displayable_with = from_compact_str)] /// enum MyScalarValue { -/// #[value(as_float, as_int)] +/// #[from] +/// #[value(to_float, to_int)] /// Int(i32), +/// +/// #[from] /// Long(i64), -/// #[value(as_float)] +/// +/// #[from] +/// #[value(to_float)] /// Float(f64), -/// #[value(as_str, as_string, into_string)] +/// +/// #[from(&str, String, CompactString)] +/// #[value(as_str, to_string)] /// String(CompactString), -/// #[value(as_bool)] +/// +/// #[from] +/// #[value(to_bool)] /// Boolean(bool), /// } /// @@ -77,10 +94,17 @@ pub trait ParseScalarValue { /// } /// } /// -/// // Macro cannot infer and generate this impl if a custom string type is used. -/// impl From for MyScalarValue { -/// fn from(value: String) -> Self { -/// Self::String(value.into()) +/// // `derive_more::TryInto` is not capable for transitive conversions yet, +/// // so this impl is manual as a custom string type is used instead of `String`. +/// impl TryFrom for String { +/// type Error = MyScalarValue; +/// +/// fn try_from(value: MyScalarValue) -> Result { +/// if let MyScalarValue::String(s) = value { +/// Ok(s.into()) +/// } else { +/// Err(value) +/// } /// } /// } /// @@ -150,6 +174,7 @@ pub trait ParseScalarValue { /// } /// ``` /// +/// [`juniper`]: crate /// [`CompactString`]: compact_str::CompactString /// [`Deserialize`]: trait@serde::Deserialize /// [`Serialize`]: trait@serde::Serialize @@ -164,12 +189,26 @@ pub trait ScalarValue: + From + From + From + + for<'a> TryScalarValueTo<'a, bool, Error: Display + IntoFieldError> + + for<'a> TryScalarValueTo<'a, i32, Error: Display + IntoFieldError> + + for<'a> TryScalarValueTo<'a, f64, Error: Display + IntoFieldError> + + for<'a> TryScalarValueTo<'a, String, Error: Display + IntoFieldError> + + for<'a> TryScalarValueTo<'a, &'a str, Error: Display + IntoFieldError> + + for<'a> TryScalarValueTo<'a, &'a Self, Error: Display + IntoFieldError> + + TryInto + 'static { - /// Checks whether this [`ScalarValue`] contains the value of the given - /// type. + /// Checks whether this [`ScalarValue`] contains the value of the provided type `T`. /// - /// ``` + /// # Implementation + /// + /// Implementations should implement this method. + /// + /// This is usually an enum dispatch with calling [`AnyExt::is::()`] method on each variant. + /// + /// # Example + /// + /// ```rust /// # use juniper::{ScalarValue, DefaultScalarValue}; /// # /// let value = DefaultScalarValue::Int(42); @@ -178,77 +217,153 @@ pub trait ScalarValue: /// assert_eq!(value.is_type::(), false); /// ``` #[must_use] - fn is_type<'a, T>(&'a self) -> bool + fn is_type(&self) -> bool; + + /// Tries to represent this [`ScalarValue`] as the specified type `T`. + /// + /// This method could be used instead of other helpers in case the [`TryScalarValueTo::Error`] + /// is needed. + /// + /// # Implementation + /// + /// This method is an ergonomic alias for the [`TryScalarValueTo`] conversion. + /// + /// Implementations should not implement this method, but rather implement the + /// [`TryScalarValueTo`] conversion directly. + fn try_to<'a, T>(&'a self) -> Result>::Error> where T: 'a, - Option<&'a T>: From<&'a Self>, + Self: TryScalarValueTo<'a, T, Error: IntoFieldError>, { - >::from(self).is_some() + self.try_scalar_value_to() } - /// Represents this [`ScalarValue`] as an integer value. + /// Tries to represent this [`ScalarValue`] as a [`bool`] value. + /// + /// Use the [`ScalarValue::try_to::()`] method in case the [`TryScalarValueTo::Error`] is + /// needed. /// - /// This function is used for implementing [`GraphQLValue`] for [`i32`] for - /// all possible [`ScalarValue`]s. Implementations should convert all the - /// supported integer types with 32 bit or less to an integer, if requested. + /// # Implementation /// - /// [`GraphQLValue`]: crate::GraphQLValue + /// This method is an ergonomic alias for the [`TryScalarValueTo`] conversion, which is + /// used for implementing [`GraphQLValue`] for [`bool`] for all possible [`ScalarValue`]s. + /// + /// Implementations should not implement this method, but rather implement the + /// [`TryScalarValueTo`] conversions for all the supported boolean types. #[must_use] - fn as_int(&self) -> Option; + fn try_to_bool(&self) -> Option { + self.try_to().ok() + } - /// Represents this [`ScalarValue`] as a [`String`] value. + /// Tries to represent this [`ScalarValue`] as an [`i32`] value. /// - /// This function is used for implementing [`GraphQLValue`] for [`String`] - /// for all possible [`ScalarValue`]s. + /// Use the [`ScalarValue::try_to::()`] method in case the [`TryScalarValueTo::Error`] is + /// needed. /// - /// [`GraphQLValue`]: crate::GraphQLValue + /// # Implementation + /// + /// This method is an ergonomic alias for the [`TryScalarValueTo`] conversion, which is + /// used for implementing [`GraphQLValue`] for [`i32`] for all possible [`ScalarValue`]s. + /// + /// Implementations should not implement this method, but rather implement the + /// [`TryScalarValueTo`] conversions for all the supported integer types with 32 bit or + /// less to an integer, if requested. #[must_use] - fn as_string(&self) -> Option; + fn try_to_int(&self) -> Option { + self.try_to().ok() + } - /// Converts this [`ScalarValue`] into a [`String`] value. + /// Tries to represent this [`ScalarValue`] as a [`f64`] value. + /// + /// Use the [`ScalarValue::try_to::()`] method in case the [`TryScalarValueTo::Error`] is + /// needed. + /// + /// # Implementation /// - /// Same as [`ScalarValue::as_string()`], but takes ownership, so allows to - /// omit redundant cloning. + /// This method is an ergonomic alias for the [`TryScalarValueTo`] conversion, which is + /// used for implementing [`GraphQLValue`] for [`f64`] for all possible [`ScalarValue`]s. + /// + /// Implementations should not implement this method, but rather implement the + /// [`TryScalarValueTo`] conversions for all the supported integer types with 64 bit and + /// all floating point values with 64 bit or less to a float, if requested. #[must_use] - fn into_string(self) -> Option; + fn try_to_float(&self) -> Option { + self.try_to().ok() + } - /// Represents this [`ScalarValue`] as a [`str`] value. + /// Tries to represent this [`ScalarValue`] as a [`String`] value. + /// + /// Allocates every time is called. For read-only and non-owning use of the underlying + /// [`String`] value, consider using the [`ScalarValue::try_as_str()`] method. + /// + /// Use the [`ScalarValue::try_to::()`] method in case the [`TryScalarValueTo::Error`] + /// is needed. + /// + /// # Implementation /// - /// This function is used for implementing [`GraphQLValue`] for [`str`] for - /// all possible [`ScalarValue`]s. + /// This method is an ergonomic alias for the [`TryScalarValueTo`] conversion, which is + /// used for implementing [`GraphQLValue`] for [`String`] for all possible [`ScalarValue`]s. /// - /// [`GraphQLValue`]: crate::GraphQLValue + /// Implementations should not implement this method, but rather implement the + /// [`TryScalarValueTo`] conversions for all the supported string types, if requested. #[must_use] - fn as_str(&self) -> Option<&str>; + fn try_to_string(&self) -> Option { + self.try_to().ok() + } - /// Represents this [`ScalarValue`] as a float value. + /// Tries to convert this [`ScalarValue`] into a [`String`] value. + /// + /// Similar to the [`ScalarValue::try_to_string()`] method, but takes ownership, so allows to + /// omit redundant [`Clone`]ing. + /// + /// Use the [`TryInto`] conversion in case the [`TryInto::Error`] is needed. + /// + /// # Implementation /// - /// This function is used for implementing [`GraphQLValue`] for [`f64`] for - /// all possible [`ScalarValue`]s. Implementations should convert all - /// supported integer types with 64 bit or less and all floating point - /// values with 64 bit or less to a float, if requested. + /// This method is an ergonomic alias for the [`TryInto`] conversion. /// - /// [`GraphQLValue`]: crate::GraphQLValue + /// Implementations should not implement this method, but rather implement the + /// [`TryInto`] conversion for all the supported string types, if requested. #[must_use] - fn as_float(&self) -> Option; + fn try_into_string(self) -> Option { + self.try_into().ok() + } - /// Represents this [`ScalarValue`] as a boolean value + /// Tries to represent this [`ScalarValue`] as a [`str`] value. + /// + /// Use the [`ScalarValue::try_to::<&str>()`] method in case the [`TryScalarValueTo::Error`] + /// is needed. + /// + /// # Implementation /// - /// This function is used for implementing [`GraphQLValue`] for [`bool`] for - /// all possible [`ScalarValue`]s. + /// This method is an ergonomic alias for the [`TryScalarValueTo`]`<&`[`str`]`>` conversion, + /// which is used for implementing [`GraphQLValue`] for [`String`] for all possible + /// [`ScalarValue`]s. /// - /// [`GraphQLValue`]: crate::GraphQLValue - fn as_bool(&self) -> Option; + /// Implementations should not implement this method, but rather implement the + /// [`TryScalarValueTo`]`<&`[`str`]`>` conversions for all the supported string types, if + /// requested. + #[must_use] + fn try_as_str(&self) -> Option<&str> { + self.try_to().ok() + } - /// Converts this [`ScalarValue`] into another one. + /// Converts this [`ScalarValue`] into another one via [`i32`], [`f64`], [`bool`] or [`String`] + /// conversion. + /// + /// # Panics + /// + /// If this [`ScalarValue`] doesn't represent at least one of [`i32`], [`f64`], [`bool`] or + /// [`String`]. + #[must_use] fn into_another(self) -> S { - if let Some(i) = self.as_int() { + if let Some(i) = self.try_to_int() { S::from(i) - } else if let Some(f) = self.as_float() { + } else if let Some(f) = self.try_to_float() { S::from(f) - } else if let Some(b) = self.as_bool() { + } else if let Some(b) = self.try_to_bool() { S::from(b) - } else if let Some(s) = self.into_string() { + } else if let Some(s) = self.try_into_string() { S::from(s) } else { unreachable!("`ScalarValue` must represent at least one of the GraphQL spec types") @@ -260,7 +375,7 @@ pub trait ScalarValue: /// This method should be implemented if [`ScalarValue`] implementation uses some custom string /// type inside to enable efficient conversion from values of this type. /// - /// Default implementation allocates by converting [`ToString`] and [`From`]`<`[`String`]`>`. + /// Default implementation allocates by converting [`ToString`] and [`From`]. /// /// # Example /// @@ -271,6 +386,84 @@ pub trait ScalarValue: } } +/// Fallible representation of a [`ScalarValue`] as one of the types it consists of, or derived ones +/// from them. +/// +/// # Implementation +/// +/// Implementing this trait for a type allows to specify this type directly in the `from_input()` +/// function when implementing a [`GraphQLScalar`] via [derive macro](macro@GraphQLScalar). +/// +/// `#[derive(`[`ScalarValue`](macro@crate::ScalarValue)`)]` automatically implements this trait for +/// all the required primitive types if `#[to_]` and `#[as_]` attributes are specified. +pub trait TryScalarValueTo<'me, T: 'me> { + /// Error if this [`ScalarValue`] doesn't represent the expected type. + type Error; + + /// Tries to represent this [`ScalarValue`] as the expected type. + /// + /// # Errors + /// + /// If this [`ScalarValue`] doesn't represent the expected type. + fn try_scalar_value_to(&'me self) -> Result; +} + +impl<'me, S: ScalarValue> TryScalarValueTo<'me, &'me S> for S { + type Error = Infallible; + + fn try_scalar_value_to(&'me self) -> Result<&'me S, Self::Error> { + Ok(self) + } +} + +impl<'me, S: ScalarValue> TryScalarValueTo<'me, &'me Scalar> for S { + type Error = Infallible; + + fn try_scalar_value_to(&'me self) -> Result<&'me Scalar, Self::Error> { + Ok(Scalar::ref_cast(self)) + } +} + +/// Error of a [`ScalarValue`] not matching the expected type. +#[derive(Clone, Debug, Display, Error)] +#[display("Expected `{type_name}`, found: {}", ScalarValueFmt(*input))] +pub struct WrongInputScalarTypeError<'a, S: ScalarValue> { + /// Type name of the expected GraphQL scalar. + pub type_name: ArcStr, + + /// Input [`ScalarValue`] not matching the expected type. + pub input: &'a S, +} + +impl<'a, S: ScalarValue> IntoFieldError for WrongInputScalarTypeError<'a, S> { + fn into_field_error(self) -> FieldError { + FieldError::::from(self) + } +} + +/// [`Display`]-formatter for a [`ScalarValue`] to render as a [`Value`]. +pub(crate) struct ScalarValueFmt<'a, S: ScalarValue>(pub &'a S); + +impl<'a, S: ScalarValue> Display for ScalarValueFmt<'a, S> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(s) = self.0.try_as_str() { + write!(f, "\"{s}\"") + } else { + Display::fmt(&self.0, f) + } + } +} + +/// Transparent wrapper over a value, indicating it being a [`ScalarValue`]. +/// +/// Used in [`GraphQLScalar`] definitions to distinguish a concrete type for a generic +/// [`ScalarValue`], since Rust type inference fail do so for a generic value directly in macro +/// expansions. +#[derive(Debug, Deref, Display, RefCast)] +#[display("{}", ScalarValueFmt(_0))] +#[repr(transparent)] +pub struct Scalar(T); + /// Extension of [`Any`] for using its methods directly on the value without `dyn`. pub trait AnyExt: Any { /// Returns `true` if the this type is the same as `T`. @@ -302,14 +495,14 @@ impl AnyExt for T {} /// These types closely follow the [GraphQL specification][0]. /// /// [0]: https://spec.graphql.org/October2021 -#[derive(Clone, Debug, From, PartialEq, ScalarValue, Serialize)] +#[derive(Clone, Debug, Display, From, PartialEq, ScalarValue, Serialize, TryInto)] #[serde(untagged)] pub enum DefaultScalarValue { /// [`Int` scalar][0] as a signed 32‐bit numeric non‐fractional value. /// /// [0]: https://spec.graphql.org/October2021#sec-Int - #[from(ignore)] - #[value(as_float, as_int)] + #[from] + #[value(to_float, to_int)] Int(i32), /// [`Float` scalar][0] as a signed double‐precision fractional values as @@ -317,22 +510,22 @@ pub enum DefaultScalarValue { /// /// [0]: https://spec.graphql.org/October2021#sec-Float /// [IEEE 754]: https://en.wikipedia.org/wiki/IEEE_floating_point - #[from(ignore)] - #[value(as_float)] + #[from] + #[value(to_float)] Float(f64), /// [`String` scalar][0] as a textual data, represented as UTF‐8 character /// sequences. /// /// [0]: https://spec.graphql.org/October2021#sec-String - #[from(&str, Cow<'_, str>)] - #[value(as_str, as_string, into_string)] + #[from(&str, Cow<'_, str>, String)] + #[value(as_str, to_string)] String(String), /// [`Boolean` scalar][0] as a `true` or `false` value. /// /// [0]: https://spec.graphql.org/October2021#sec-Boolean - #[from(ignore)] - #[value(as_bool)] + #[from] + #[value(to_bool)] Boolean(bool), } diff --git a/juniper_codegen/CHANGELOG.md b/juniper_codegen/CHANGELOG.md index d4d51a8c1..ef1814ee2 100644 --- a/juniper_codegen/CHANGELOG.md +++ b/juniper_codegen/CHANGELOG.md @@ -11,13 +11,28 @@ All user visible changes to `juniper_codegen` crate will be documented in this f ### BC Breaks - Bumped up [MSRV] to 1.85. ([#1272], [1b1fc618]) +- `#[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. + - `From` and `Display` implementations are not derived anymore. +- `#[derive(GraphQLScalar)]` and `#[graphql_scalar]` macros: ([#1327]) + - Made provided `from_input()` function to accept `ScalarValue` directly instead of `InputValue`. ### Added -- Support of top-level `#[value(from_displayable_with = ...)]` attribute in `derive(ScalarValue)`. ([#1324]) +- `#[derive(ScalarValue)]` macro: ([#1324]) + - Support of top-level `#[value(from_displayable_with = ...)]` attribute. +- `#[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. [#1272]: /../../pull/1272 [#1324]: /../../pull/1324 +[#1327]: /../../pull/1327 [1b1fc618]: /../../commit/1b1fc61879ffdd640d741e187dc20678bf7ab295 diff --git a/juniper_codegen/Cargo.toml b/juniper_codegen/Cargo.toml index 06a346a54..d1d241919 100644 --- a/juniper_codegen/Cargo.toml +++ b/juniper_codegen/Cargo.toml @@ -29,7 +29,7 @@ syn = { version = "2.0", features = ["extra-traits", "full", "visit", "visit-mut url = "2.0" [dev-dependencies] -derive_more = { version = "2.0", features = ["from"] } +derive_more = { version = "2.0", features = ["from", "try_into"] } futures = "0.3.22" juniper = { path = "../juniper" } serde = "1.0.122" diff --git a/juniper_codegen/src/graphql_enum/mod.rs b/juniper_codegen/src/graphql_enum/mod.rs index 2474f3a95..b8abf3a44 100644 --- a/juniper_codegen/src/graphql_enum/mod.rs +++ b/juniper_codegen/src/graphql_enum/mod.rs @@ -594,7 +594,9 @@ impl Definition { fn from_input_value( v: &::juniper::InputValue<#scalar>, ) -> ::core::result::Result { - match v.as_enum_value().or_else(|| v.as_string_value()) { + match v.as_enum_value() + .or_else(|| ::juniper::ScalarValue::try_as_str(v.as_scalar()?)) + { #( #variants )* _ => ::core::result::Result::Err( ::std::format!("Unknown enum value: {}", v), diff --git a/juniper_codegen/src/graphql_scalar/mod.rs b/juniper_codegen/src/graphql_scalar/mod.rs index 4338e1fad..beeecec7c 100644 --- a/juniper_codegen/src/graphql_scalar/mod.rs +++ b/juniper_codegen/src/graphql_scalar/mod.rs @@ -770,7 +770,16 @@ impl Methods { from_input: Some(from_input), .. } => { - quote! { #from_input(input) } + quote! { + use ::juniper::macros::helper::ToResultCall as _; + + let input = ::juniper::InputValue::as_scalar(input) + .ok_or_else(|| ::juniper::macros::helper::NotScalarError(input))?; + let input = ::juniper::TryScalarValueTo::try_scalar_value_to(input) + .map_err(::juniper::executor::IntoFieldError::<#scalar>::into_field_error)?; + let func: fn(_) -> _ = #from_input; + (&&func).__to_result_call(input) + } } Self::Delegated { field, .. } => { let field_ty = field.ty(); diff --git a/juniper_codegen/src/lib.rs b/juniper_codegen/src/lib.rs index 4100f6880..ab977ce91 100644 --- a/juniper_codegen/src/lib.rs +++ b/juniper_codegen/src/lib.rs @@ -365,15 +365,13 @@ pub fn derive_enum(input: TokenStream) -> TokenStream { }) } -/// `#[derive(GraphQLScalar)]` macro for deriving a [GraphQL scalar][0] -/// implementation. +/// `#[derive(GraphQLScalar)]` macro for deriving a [GraphQL scalar][0] implementation. /// /// # Transparent delegation /// -/// Quite often we want to create a custom [GraphQL scalar][0] type by just -/// wrapping an existing one, inheriting all its behavior. In Rust, this is -/// often called as ["newtype pattern"][1]. This is achieved by annotating -/// the definition with the `#[graphql(transparent)]` attribute: +/// Quite often we want to create a custom [GraphQL scalar][0] type by just wrapping an existing +/// one, inheriting all its behavior. In Rust, this is often called as ["newtype pattern"][1]. This +/// could be achieved by annotating the definition with the `#[graphql(transparent)]` attribute: /// ```rust /// # use juniper::{GraphQLObject, GraphQLScalar}; /// # @@ -415,8 +413,8 @@ pub fn derive_enum(input: TokenStream) -> TokenStream { /// struct UserId(String); /// ``` /// -/// All the methods inherited from `Newtype`'s field may also be overridden -/// with the attributes described below. +/// All the methods inherited from `Newtype`'s field may also be overridden with the attributes +/// described below. /// /// # Custom resolving /// @@ -440,44 +438,66 @@ pub fn derive_enum(input: TokenStream) -> TokenStream { /// Customization of a [GraphQL scalar][0] type parsing is possible via /// `#[graphql(from_input_with = )]` attribute: /// ```rust -/// # use juniper::{DefaultScalarValue, GraphQLScalar, InputValue, ScalarValue}; +/// # use juniper::{GraphQLScalar, ScalarValue}; /// # /// #[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( -/// input: &InputValue, -/// ) -> Result { -/// // ^^^^^^ 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}", -/// ) -/// }) +/// /// Checks whether the [`ScalarValue`] is a [`String`] beginning with `id: ` and strips it. +/// fn from_input( +/// input: &str, +/// // ^^^^ any concrete type having `TryScalarValueTo` implementation could be used +/// ) -> Result> { +/// // ^^^^^^^^ 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 input and output types: +/// ```rust +/// # 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, +/// // ^^^^^^ 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") +/// }), +/// ) +/// } +/// } +/// ``` +/// /// # Custom token parsing /// -/// Customization of which tokens a [GraphQL scalar][0] type should be parsed is -/// possible via `#[graphql(parse_token_with = )]` or -/// `#[graphql(parse_token()]` attributes: +/// Customization of which tokens a [GraphQL scalar][0] type should be parsed is possible via +/// `#[graphql(parse_token_with = )]` or `#[graphql(parse_token()]` attributes: /// ```rust /// # use juniper::{ -/// # GraphQLScalar, InputValue, ParseScalarResult, ParseScalarValue, -/// # ScalarValue, ScalarToken, Value, +/// # GraphQLScalar, ParseScalarResult, ParseScalarValue, Scalar, ScalarToken, ScalarValue, +/// # Value, /// # }; /// # /// #[derive(GraphQLScalar)] @@ -501,11 +521,11 @@ pub fn derive_enum(input: TokenStream) -> TokenStream { /// } /// } /// -/// fn from_input(v: &InputValue) -> Result { -/// 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) -> Result> { +/// 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(value: ScalarToken<'_>) -> ParseScalarResult { @@ -513,18 +533,17 @@ pub fn derive_enum(input: TokenStream) -> TokenStream { /// .or_else(|_| >::from_str(value)) /// } /// ``` -/// > __NOTE:__ Once we provide all 3 custom functions, there is no sense in -/// > following the [newtype pattern][1] anymore. +/// > __NOTE:__ Once we provide all 3 custom functions, there is no sense in following the +/// > [newtype pattern][1] 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: +/// 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: /// ```rust /// # use juniper::{ -/// # GraphQLScalar, InputValue, ParseScalarResult, ParseScalarValue, -/// # ScalarValue, ScalarToken, Value, +/// # GraphQLScalar, ParseScalarResult, ParseScalarValue, Scalar, ScalarToken, ScalarValue, +/// # Value, /// # }; /// # /// #[derive(GraphQLScalar)] @@ -544,11 +563,11 @@ pub fn derive_enum(input: TokenStream) -> TokenStream { /// } /// } /// -/// pub(super) fn from_input(v: &InputValue) -> Result { -/// 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) -> Result> { +/// 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(t: ScalarToken<'_>) -> ParseScalarResult { @@ -563,8 +582,8 @@ pub fn derive_enum(input: TokenStream) -> TokenStream { /// A regular `impl` block is also suitable for that: /// ```rust /// # use juniper::{ -/// # GraphQLScalar, InputValue, ParseScalarResult, ParseScalarValue, -/// # ScalarValue, ScalarToken, Value, +/// # GraphQLScalar, ParseScalarResult, ParseScalarValue, Scalar, ScalarToken, ScalarValue, +/// # Value, /// # }; /// # /// #[derive(GraphQLScalar)] @@ -582,14 +601,11 @@ pub fn derive_enum(input: TokenStream) -> TokenStream { /// } /// } /// -/// fn from_input(v: &InputValue) -> Result -/// 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) -> Result> { +/// 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(value: ScalarToken<'_>) -> ParseScalarResult @@ -606,10 +622,7 @@ pub fn derive_enum(input: TokenStream) -> TokenStream { /// /// At the same time, any custom function still may be specified separately: /// ```rust -/// # use juniper::{ -/// # GraphQLScalar, InputValue, ParseScalarResult, ScalarValue, -/// # ScalarToken, Value -/// # }; +/// # use juniper::{GraphQLScalar, ParseScalarResult, Scalar, ScalarToken, ScalarValue, Value}; /// # /// #[derive(GraphQLScalar)] /// #[graphql( @@ -634,14 +647,11 @@ pub fn derive_enum(input: TokenStream) -> TokenStream { /// } /// } /// -/// pub(super) fn from_input(v: &InputValue) -> Result -/// 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) -> Result> { +/// 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. @@ -652,18 +662,17 @@ pub fn derive_enum(input: TokenStream) -> TokenStream { /// /// # Custom `ScalarValue` /// -/// By default, this macro generates code, which is generic over a -/// [`ScalarValue`] type. Concrete [`ScalarValue`] type may be specified via -/// `#[graphql(scalar = )]` attribute. +/// By default, this macro generates code, which is generic over a [`ScalarValue`] type. Concrete +/// [`ScalarValue`] type may be specified via the `#[graphql(scalar = )]` attribute. /// -/// It also may be used to provide additional bounds to the [`ScalarValue`] -/// generic, like the following: `#[graphql(scalar = S: Trait)]`. +/// 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][0] type implementation may be bound with any additional -/// trait bounds via `#[graphql(where())]` attribute, like the -/// following: `#[graphql(where(S: Trait, Self: fmt::Debug + fmt::Display))]`. +/// [GraphQL scalar][0] type implementation may be bound with any additional trait bounds via +/// `#[graphql(where())]` attribute, like the following: +/// `#[graphql(where(S: Trait, Self: fmt::Debug + fmt::Display))]`. /// /// [0]: https://spec.graphql.org/October2021#sec-Scalars /// [1]: https://rust-unofficial.github.io/patterns/patterns/behavioural/newtype.html @@ -677,9 +686,8 @@ pub fn derive_scalar(input: TokenStream) -> TokenStream { }) } -/// `#[graphql_scalar]` macro.is interchangeable with -/// `#[derive(`[`GraphQLScalar`]`)]` macro, and is used for deriving a -/// [GraphQL scalar][0] implementation. +/// `#[graphql_scalar]` macro.is interchangeable with the `#[derive(`[`GraphQLScalar`]`)]` macro, +/// and is used for deriving a [GraphQL scalar][0] implementation. /// /// ```rust /// # use juniper::graphql_scalar; @@ -703,11 +711,10 @@ pub fn derive_scalar(input: TokenStream) -> TokenStream { /// /// # Foreign types /// -/// Additionally, `#[graphql_scalar]` can be used directly on foreign types via -/// type alias, without using the [newtype pattern][1]. +/// Additionally, `#[graphql_scalar]` can be used directly on foreign types via type alias, without +/// using the [newtype pattern][1]. /// -/// > __NOTE:__ To satisfy [orphan rules] you should provide local -/// > [`ScalarValue`] implementation. +/// > __NOTE:__ To satisfy [orphan rules] you should provide local [`ScalarValue`] implementation. /// /// ```rust /// # mod date { @@ -728,7 +735,7 @@ pub fn derive_scalar(input: TokenStream) -> TokenStream { /// # } /// # /// # use juniper::DefaultScalarValue as CustomScalarValue; -/// use juniper::{graphql_scalar, InputValue, ScalarValue, Value}; +/// use juniper::{graphql_scalar, ScalarValue, Value}; /// /// #[graphql_scalar] /// #[graphql( @@ -747,10 +754,8 @@ pub fn derive_scalar(input: TokenStream) -> TokenStream { /// Value::scalar(v.to_string()) /// } /// -/// pub(super) fn from_input(v: &InputValue) -> Result { -/// 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> { +/// s.parse().map_err(|e| format!("Failed to parse `Date`: {e}").into()) /// } /// } /// # @@ -773,43 +778,42 @@ pub fn graphql_scalar(attr: TokenStream, body: TokenStream) -> TokenStream { /// `#[derive(ScalarValue)]` macro for deriving a [`ScalarValue`] implementation. /// -/// To derive a [`ScalarValue`] on enum you should mark the corresponding enum -/// variants with `as_int`, `as_float`, `as_string`, `into_string`, `as_str` and -/// `as_bool` attribute arguments (names correspond to [`ScalarValue`] required -/// methods). +/// To derive a [`ScalarValue`] on enum you either could mark the corresponding enum variants with +/// `to_int`, `to_float`, `to_string`, `as_str` and `to_bool` attribute arguments (names correspond +/// to the similar [`ScalarValue`] methods aliasing the required [`TryScalarValueTo`] conversions), +/// or implement the required [`TryScalarValueTo`] conversions by hand. /// -/// Additional `from_displayable_with` argument could be used to specify a custom -/// implementation to override the default `ScalarValue::from_displayable()` method. +/// Additional `from_displayable_with` argument could be used to specify a custom function to +/// override the default `ScalarValue::from_displayable()` method. /// /// ```rust /// # use std::{any::Any, fmt}; /// # -/// # use serde::{de, Deserialize, Deserializer, Serialize}; +/// use derive_more::with_trait::{Display, From, TryInto}; /// # use juniper::ScalarValue; -/// # -/// #[derive(Clone, Debug, PartialEq, ScalarValue, Serialize)] +/// # use serde::{de, Deserialize, Deserializer, Serialize}; +/// #[derive(Clone, Debug, Display, From, PartialEq, ScalarValue, Serialize, TryInto)] /// #[serde(untagged)] /// #[value(from_displayable_with = from_custom_str)] /// enum MyScalarValue { -/// #[value(as_float, as_int)] +/// #[value(to_float, to_int)] /// Int(i32), /// Long(i64), -/// #[value(as_float)] +/// #[value(to_float)] /// Float(f64), /// #[value( -/// into_string, /// as_str, -/// as_string = String::clone, +/// to_string = String::clone, /// )] /// // ^^^^^^^^^^^^^ custom resolvers may be provided /// String(String), -/// #[value(as_bool)] +/// #[value(to_bool)] /// Boolean(bool), /// } /// /// // Custom implementation of `ScalarValue::from_displayable()` method for /// // possible efficient conversions into `MyScalarValue` from custom string types. -/// fn from_custom_str(s: &Str) -> MyScalarValue { +/// fn from_custom_str(s: &Str) -> MyScalarValue { /// use juniper::AnyExt as _; // allows downcasting directly on types without `dyn` /// /// // Imagine this is some custom optimized string type. @@ -891,6 +895,7 @@ pub fn graphql_scalar(attr: TokenStream, body: TokenStream) -> TokenStream { /// ``` /// /// [`ScalarValue`]: juniper::ScalarValue +/// [`TryScalarValueTo`]: juniper::TryScalarValueTo #[proc_macro_derive(ScalarValue, attributes(value))] pub fn derive_scalar_value(input: TokenStream) -> TokenStream { diagnostic::entry_point(|| { diff --git a/juniper_codegen/src/scalar_value/mod.rs b/juniper_codegen/src/scalar_value/mod.rs index 4af754262..3fe3ed9c1 100644 --- a/juniper_codegen/src/scalar_value/mod.rs +++ b/juniper_codegen/src/scalar_value/mod.rs @@ -9,7 +9,6 @@ use syn::{ parse_quote, spanned::Spanned as _, token, - visit::Visit, }; use crate::common::{ @@ -26,7 +25,6 @@ const ERR: diagnostic::Scope = diagnostic::Scope::ScalarValueDerive; /// Expands `#[derive(ScalarValue)]` macro into generated code. pub fn expand_derive(input: TokenStream) -> syn::Result { let ast = syn::parse2::(input)?; - let span = ast.span(); let data_enum = match ast.data { syn::Data::Enum(e) => e, @@ -48,34 +46,6 @@ pub fn expand_derive(input: TokenStream) -> syn::Result { } } - let missing_methods = [ - (Method::AsInt, "as_int"), - (Method::AsFloat, "as_float"), - (Method::AsStr, "as_str"), - (Method::AsString, "as_string"), - (Method::IntoString, "into_string"), - (Method::AsBool, "as_bool"), - ] - .iter() - .filter_map(|(method, err)| (!methods.contains_key(method)).then_some(err)) - .fold(None, |acc, &method| { - Some( - acc.map(|acc| format!("{acc}, {method}")) - .unwrap_or_else(|| method.into()), - ) - }) - .filter(|_| !attr.allow_missing_attrs); - if let Some(missing_methods) = missing_methods { - return Err(ERR.custom_error( - span, - format!( - "missing `#[value({missing_methods})]` attributes. In case you \ - are sure that it's ok, use `#[value(allow_missing_attributes)]` \ - to suppress this error.", - ), - )); - } - Ok(Definition { ident: ast.ident, generics: ast.generics, @@ -90,9 +60,6 @@ pub fn expand_derive(input: TokenStream) -> syn::Result { /// an enum definition. #[derive(Default)] struct Attr { - /// Allows missing [`Method`]s. - allow_missing_attrs: bool, - /// Explicitly specified function to be used as `ScalarValue::from_displayable()` /// implementation. from_displayable: Option>, @@ -104,9 +71,6 @@ impl Parse for Attr { while !input.is_empty() { let ident = input.parse::()?; match ident.to_string().as_str() { - "allow_missing_attributes" => { - out.allow_missing_attrs = true; - } "from_displayable_with" => { input.parse::()?; let scl = input.parse::()?; @@ -129,7 +93,6 @@ impl Attr { /// duplicates, if any. fn try_merge(self, mut another: Self) -> syn::Result { Ok(Self { - allow_missing_attrs: self.allow_missing_attrs || another.allow_missing_attrs, from_displayable: try_merge_opt!(from_displayable: self, another), }) } @@ -146,23 +109,20 @@ impl Attr { /// Possible attribute names of the `#[derive(ScalarValue)]`. #[derive(Eq, Hash, PartialEq)] enum Method { - /// `#[value(as_int)]`. - AsInt, + /// `#[value(to_int)]`. + ToInt, - /// `#[value(as_float)]`. - AsFloat, + /// `#[value(to_float)]`. + ToFloat, /// `#[value(as_str)]`. AsStr, - /// `#[value(as_string)]`. - AsString, - - /// `#[value(into_string)]`. - IntoString, + /// `#[value(to_string)]`. + ToString, - /// `#[value(as_bool)]`. - AsBool, + /// `#[value(to_bool)]`. + ToBool, } /// Available arguments behind `#[value]` attribute when generating code for an @@ -176,12 +136,11 @@ impl Parse for VariantAttr { while !input.is_empty() { let ident = input.parse::()?; let method = match ident.to_string().as_str() { - "as_int" => Method::AsInt, - "as_float" => Method::AsFloat, + "to_int" => Method::ToInt, + "to_float" => Method::ToFloat, "as_str" => Method::AsStr, - "as_string" => Method::AsString, - "into_string" => Method::IntoString, - "as_bool" => Method::AsBool, + "to_string" => Method::ToString, + "to_bool" => Method::ToBool, name => { return Err(err::unknown_arg(&ident, name)); } @@ -247,197 +206,135 @@ struct Definition { impl ToTokens for Definition { fn to_tokens(&self, into: &mut TokenStream) { self.impl_scalar_value_tokens().to_tokens(into); - self.impl_from_tokens().to_tokens(into); - self.impl_display_tokens().to_tokens(into); + self.impl_try_scalar_to_tokens().to_tokens(into); } } impl Definition { /// Returns generated code implementing `ScalarValue`. fn impl_scalar_value_tokens(&self) -> TokenStream { - let ident = &self.ident; + let ty_ident = &self.ident; let (impl_gens, ty_gens, where_clause) = self.generics.split_for_impl(); - let methods = [ - ( - Method::AsInt, - quote! { fn as_int(&self) -> ::core::option::Option<::core::primitive::i32> }, - quote! { ::core::primitive::i32::from(*v) }, - ), - ( - Method::AsFloat, - quote! { fn as_float(&self) -> ::core::option::Option<::core::primitive::f64> }, - quote! { ::core::primitive::f64::from(*v) }, - ), - ( - Method::AsStr, - quote! { fn as_str(&self) -> ::core::option::Option<&::core::primitive::str> }, - quote! { ::core::convert::AsRef::as_ref(v) }, - ), - ( - Method::AsString, - quote! { fn as_string(&self) -> ::core::option::Option<::std::string::String> }, - quote! { ::std::string::ToString::to_string(v) }, - ), - ( - Method::IntoString, - quote! { fn into_string(self) -> ::core::option::Option<::std::string::String> }, - quote! { ::std::string::String::from(v) }, - ), - ( - Method::AsBool, - quote! { fn as_bool(&self) -> ::core::option::Option<::core::primitive::bool> }, - quote! { ::core::primitive::bool::from(*v) }, - ), - ]; - let methods = methods.iter().map(|(m, sig, def)| { - let arms = self.methods.get(m).into_iter().flatten().map(|v| { - let arm = v.match_arm(); - let call = v.expr.as_ref().map_or(def.clone(), |f| quote! { #f(v) }); - quote! { #arm => ::core::option::Option::Some(#call), } + let is_type = { + let arms = self.variants.iter().map(|var| { + let var_ident = &var.ident; + let field = Field::try_from(var.fields.clone()) + .unwrap_or_else(|_| unreachable!("already checked")); + let var_pattern = field.match_arg(); + + quote! { + Self::#var_ident #var_pattern => ::juniper::AnyExt::is::<__T>(v), + } }); + quote! { - #sig { + fn is_type<__T: ::core::any::Any + ?::core::marker::Sized>(&self) -> bool { match self { - #(#arms)* - _ => ::core::option::Option::None, + #( #arms )* } } } - }); + }; let from_displayable = self.from_displayable.as_ref().map(|expr| { quote! { - fn from_displayable( - __s: &Str, - ) -> Self { - #expr(__s) + fn from_displayable< + __T: ::core::fmt::Display + ::core::any::Any + ?::core::marker::Sized, + >(__v: &__T) -> Self { + #expr(__v) } } }); quote! { #[automatically_derived] - impl #impl_gens ::juniper::ScalarValue for #ident #ty_gens - #where_clause - { - #( #methods )* + impl #impl_gens ::juniper::ScalarValue for #ty_ident #ty_gens #where_clause { + #is_type #from_displayable } } } - /// Returns generated code implementing: - /// - [`From`] each variant into enum itself. - /// - [`From`] enum into [`Option`] of each variant. - /// - [`From`] enum reference into [`Option`] of each variant reference. - fn impl_from_tokens(&self) -> TokenStream { + /// Returns generated code implementing `TryScalarValueTo`. + fn impl_try_scalar_to_tokens(&self) -> TokenStream { let ty_ident = &self.ident; - let (impl_gen, ty_gen, where_clause) = self.generics.split_for_impl(); + let (_, ty_gens, where_clause) = self.generics.split_for_impl(); + let ref_lt = quote! { '___a }; // We don't impose additional bounds on generic parameters, because // `ScalarValue` itself has `'static` bound. let mut generics = self.generics.clone(); - generics.params.push(parse_quote! { '___a }); - let (lf_impl_gen, _, _) = generics.split_for_impl(); - - self.variants - .iter() - .map(|v| { - let var_ident = &v.ident; - let field = v.fields.iter().next().unwrap(); - let var_ty = &field.ty; - let var_field = field - .ident - .as_ref() - .map_or_else(|| quote! { (v) }, |i| quote! { { #i: v } }); + generics.params.push(parse_quote! { #ref_lt }); + let (lt_impl_gens, _, _) = generics.split_for_impl(); + let methods = [ + ( + Method::ToInt, + "Int", + quote! { ::core::primitive::i32 }, + quote! { ::core::convert::Into::into(*v) }, + ), + ( + Method::ToFloat, + "Float", + quote! { ::core::primitive::f64 }, + quote! { ::core::convert::Into::into(*v) }, + ), + ( + Method::AsStr, + "String", + quote! { &#ref_lt ::core::primitive::str }, + quote! { ::core::convert::AsRef::as_ref(v) }, + ), + ( + Method::ToString, + "String", + quote! { ::std::string::String }, + quote! { ::std::string::ToString::to_string(v) }, + ), + ( + Method::ToBool, + "Bool", + quote! { ::core::primitive::bool }, + quote! { ::core::convert::Into::into(*v) }, + ), + ]; + let impls = methods.iter().filter_map(|(m, into_name, as_ty, default_expr)| { + let arms = self.methods.get(m)?.iter().map(|v| { + let arm_pattern = v.match_arm(); + let call = if let Some(func) = &v.expr { + quote! { #func(v) } + } else { + default_expr.clone() + }; quote! { - #[automatically_derived] - impl #impl_gen ::core::convert::From<#var_ty> for #ty_ident #ty_gen - #where_clause - { - fn from(v: #var_ty) -> Self { - Self::#var_ident #var_field - } - } - - #[automatically_derived] - impl #impl_gen ::core::convert::From<#ty_ident #ty_gen> - for ::core::option::Option<#var_ty> #where_clause - { - fn from(ty: #ty_ident #ty_gen) -> Self { - if let #ty_ident::#var_ident #var_field = ty { - ::core::option::Option::Some(v) - } else { - ::core::option::Option::None - } - } - } - - #[automatically_derived] - impl #lf_impl_gen ::core::convert::From<&'___a #ty_ident #ty_gen> - for ::core::option::Option<&'___a #var_ty> #where_clause - { - fn from(ty: &'___a #ty_ident #ty_gen) -> Self { - if let #ty_ident::#var_ident #var_field = ty { - ::core::option::Option::Some(v) - } else { - ::core::option::Option::None - } + #arm_pattern => ::core::result::Result::Ok(#call), + } + }); + Some(quote! { + #[automatically_derived] + impl #lt_impl_gens ::juniper::TryScalarValueTo<#ref_lt, #as_ty> + for #ty_ident #ty_gens #where_clause + { + type Error = ::juniper::WrongInputScalarTypeError<#ref_lt, #ty_ident #ty_gens>; + + fn try_scalar_value_to( + &#ref_lt self, + ) -> ::core::result::Result<#as_ty, Self::Error> { + match self { + #( #arms )* + _ => ::core::result::Result::Err(::juniper::WrongInputScalarTypeError { + type_name: ::juniper::arcstr::literal!(#into_name), + input: self, + }), } } } }) - .collect() - } - - /// Returns generated code implementing [`Display`] by matching over each - /// enum variant. - /// - /// [`Display`]: std::fmt::Display - fn impl_display_tokens(&self) -> TokenStream { - let ident = &self.ident; - - let mut generics = self.generics.clone(); - generics.make_where_clause(); - for var in &self.variants { - let var_ty = &var.fields.iter().next().unwrap().ty; - let mut check = IsVariantGeneric::new(&self.generics); - check.visit_type(var_ty); - if check.res { - generics - .where_clause - .as_mut() - .unwrap() - .predicates - .push(parse_quote! { #var_ty: ::core::fmt::Display }); - } - } - let (impl_gen, ty_gen, where_clause) = generics.split_for_impl(); - - let arms = self.variants.iter().map(|v| { - let var_ident = &v.ident; - let field = v.fields.iter().next().unwrap(); - let var_field = field - .ident - .as_ref() - .map_or_else(|| quote! { (v) }, |i| quote! { { #i: v } }); - - quote! { Self::#var_ident #var_field => ::core::fmt::Display::fmt(v, f), } }); - quote! { - #[automatically_derived] - impl #impl_gen ::core::fmt::Display for #ident #ty_gen - #where_clause - { - fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { - match self { - #( #arms )* - } - } - } + #( #impls )* } } } @@ -507,43 +404,3 @@ impl Field { } } } - -/// [`Visit`]or checking whether a [`Variant`]'s [`Field`] contains generic -/// parameters. -struct IsVariantGeneric<'a> { - /// Indicates whether the checked [`Variant`]'s [`Field`] contains generic - /// parameters. - res: bool, - - /// [`syn::Generics`] to search generic parameters in. - generics: &'a syn::Generics, -} - -impl<'a> IsVariantGeneric<'a> { - /// Constructs a new [`IsVariantGeneric`] [`Visit`]or. - fn new(generics: &'a syn::Generics) -> Self { - Self { - res: false, - generics, - } - } -} - -impl<'ast> Visit<'ast> for IsVariantGeneric<'_> { - fn visit_path(&mut self, path: &'ast syn::Path) { - if let Some(ident) = path.get_ident() { - let is_generic = self.generics.params.iter().any(|par| { - if let syn::GenericParam::Type(ty) = par { - ty.ident == *ident - } else { - false - } - }); - if is_generic { - self.res = true; - } else { - syn::visit::visit_path(self, path); - } - } - } -} diff --git a/tests/codegen/Cargo.toml b/tests/codegen/Cargo.toml index 813cb0bc4..d8679ec32 100644 --- a/tests/codegen/Cargo.toml +++ b/tests/codegen/Cargo.toml @@ -8,8 +8,10 @@ publish = false rustversion = "1.0" [dev-dependencies] +derive_more = { version = "2.0", features = ["display", "from", "try_into"] } futures = "0.3" juniper = { path = "../../juniper" } +serde = { version = "1.0", features = ["derive"] } trybuild = "1.0.63" [lints.clippy] diff --git a/tests/codegen/fail/scalar/derive_input/attr_invalid_url.rs b/tests/codegen/fail/scalar/derive_input/attr_invalid_url.rs index b28cb1fc3..59df8b501 100644 --- a/tests/codegen/fail/scalar/derive_input/attr_invalid_url.rs +++ b/tests/codegen/fail/scalar/derive_input/attr_invalid_url.rs @@ -1,6 +1,7 @@ use juniper::graphql_scalar; -#[graphql_scalar(specified_by_url = "not an url", transparent)] +#[graphql_scalar] +#[graphql(specified_by_url = "not an url", transparent)] struct ScalarSpecifiedByUrl(i32); fn main() {} diff --git a/tests/codegen/fail/scalar/derive_input/attr_invalid_url.stderr b/tests/codegen/fail/scalar/derive_input/attr_invalid_url.stderr index b1bd12105..759e02342 100644 --- a/tests/codegen/fail/scalar/derive_input/attr_invalid_url.stderr +++ b/tests/codegen/fail/scalar/derive_input/attr_invalid_url.stderr @@ -1,5 +1,11 @@ error: Invalid URL: relative URL without a base - --> fail/scalar/derive_input/attr_invalid_url.rs:3:37 + --> fail/scalar/derive_input/attr_invalid_url.rs:4:30 | -3 | #[graphql_scalar(specified_by_url = "not an url", transparent)] - | ^^^^^^^^^^^^ +4 | #[graphql(specified_by_url = "not an url", transparent)] + | ^^^^^^^^^^^^ + +error: cannot find attribute `graphql` in this scope + --> fail/scalar/derive_input/attr_invalid_url.rs:4:3 + | +4 | #[graphql(specified_by_url = "not an url", transparent)] + | ^^^^^^^ diff --git a/tests/codegen/fail/scalar/derive_input/attr_transparent_and_with.rs b/tests/codegen/fail/scalar/derive_input/attr_transparent_and_with.rs index 3b3190840..ff2174204 100644 --- a/tests/codegen/fail/scalar/derive_input/attr_transparent_and_with.rs +++ b/tests/codegen/fail/scalar/derive_input/attr_transparent_and_with.rs @@ -1,6 +1,7 @@ use juniper::graphql_scalar; -#[graphql_scalar(with = Self, transparent)] +#[graphql_scalar] +#[graphql(with = Self, transparent)] struct Scalar; fn main() {} diff --git a/tests/codegen/fail/scalar/derive_input/attr_transparent_and_with.stderr b/tests/codegen/fail/scalar/derive_input/attr_transparent_and_with.stderr index ce9f808d6..c7fc6ceb5 100644 --- a/tests/codegen/fail/scalar/derive_input/attr_transparent_and_with.stderr +++ b/tests/codegen/fail/scalar/derive_input/attr_transparent_and_with.stderr @@ -1,5 +1,11 @@ error: GraphQL scalar `with = ` attribute argument cannot be combined with `transparent`. You can specify custom resolvers with `to_output_with`, `from_input_with`, `parse_token`/`parse_token_with` attribute arguments and still use `transparent` for unspecified ones. - --> fail/scalar/derive_input/attr_transparent_and_with.rs:3:25 + --> fail/scalar/derive_input/attr_transparent_and_with.rs:4:18 | -3 | #[graphql_scalar(with = Self, transparent)] - | ^^^^ +4 | #[graphql(with = Self, transparent)] + | ^^^^ + +error: cannot find attribute `graphql` in this scope + --> fail/scalar/derive_input/attr_transparent_and_with.rs:4:3 + | +4 | #[graphql(with = Self, transparent)] + | ^^^^^^^ diff --git a/tests/codegen/fail/scalar/derive_input/attr_transparent_multiple_named_fields.rs b/tests/codegen/fail/scalar/derive_input/attr_transparent_multiple_named_fields.rs index 56658b548..3637f937e 100644 --- a/tests/codegen/fail/scalar/derive_input/attr_transparent_multiple_named_fields.rs +++ b/tests/codegen/fail/scalar/derive_input/attr_transparent_multiple_named_fields.rs @@ -1,6 +1,7 @@ use juniper::graphql_scalar; -#[graphql_scalar(transparent)] +#[graphql_scalar] +#[graphql(transparent)] struct Scalar { id: i32, another: i32, diff --git a/tests/codegen/fail/scalar/derive_input/attr_transparent_multiple_named_fields.stderr b/tests/codegen/fail/scalar/derive_input/attr_transparent_multiple_named_fields.stderr index f082ef001..b24aa24f4 100644 --- a/tests/codegen/fail/scalar/derive_input/attr_transparent_multiple_named_fields.stderr +++ b/tests/codegen/fail/scalar/derive_input/attr_transparent_multiple_named_fields.stderr @@ -1,5 +1,11 @@ error: GraphQL scalar `transparent` attribute argument requires exactly 1 field - --> fail/scalar/derive_input/attr_transparent_multiple_named_fields.rs:4:1 + --> fail/scalar/derive_input/attr_transparent_multiple_named_fields.rs:5:1 | -4 | struct Scalar { +5 | struct Scalar { | ^^^^^^ + +error: cannot find attribute `graphql` in this scope + --> fail/scalar/derive_input/attr_transparent_multiple_named_fields.rs:4:3 + | +4 | #[graphql(transparent)] + | ^^^^^^^ diff --git a/tests/codegen/fail/scalar/derive_input/attr_transparent_multiple_unnamed_fields.rs b/tests/codegen/fail/scalar/derive_input/attr_transparent_multiple_unnamed_fields.rs index bbf969364..3d6edbc2e 100644 --- a/tests/codegen/fail/scalar/derive_input/attr_transparent_multiple_unnamed_fields.rs +++ b/tests/codegen/fail/scalar/derive_input/attr_transparent_multiple_unnamed_fields.rs @@ -1,6 +1,7 @@ use juniper::graphql_scalar; -#[graphql_scalar(transparent)] +#[graphql_scalar] +#[graphql(transparent)] struct Scalar(i32, i32); fn main() {} diff --git a/tests/codegen/fail/scalar/derive_input/attr_transparent_multiple_unnamed_fields.stderr b/tests/codegen/fail/scalar/derive_input/attr_transparent_multiple_unnamed_fields.stderr index 0c17a53c6..e115e7f00 100644 --- a/tests/codegen/fail/scalar/derive_input/attr_transparent_multiple_unnamed_fields.stderr +++ b/tests/codegen/fail/scalar/derive_input/attr_transparent_multiple_unnamed_fields.stderr @@ -1,5 +1,11 @@ error: GraphQL scalar `transparent` attribute argument requires exactly 1 field - --> fail/scalar/derive_input/attr_transparent_multiple_unnamed_fields.rs:4:1 + --> fail/scalar/derive_input/attr_transparent_multiple_unnamed_fields.rs:5:1 | -4 | struct Scalar(i32, i32); +5 | struct Scalar(i32, i32); | ^^^^^^ + +error: cannot find attribute `graphql` in this scope + --> fail/scalar/derive_input/attr_transparent_multiple_unnamed_fields.rs:4:3 + | +4 | #[graphql(transparent)] + | ^^^^^^^ diff --git a/tests/codegen/fail/scalar/derive_input/attr_transparent_unit_struct.rs b/tests/codegen/fail/scalar/derive_input/attr_transparent_unit_struct.rs index 9f8d7568d..2b21c8794 100644 --- a/tests/codegen/fail/scalar/derive_input/attr_transparent_unit_struct.rs +++ b/tests/codegen/fail/scalar/derive_input/attr_transparent_unit_struct.rs @@ -1,6 +1,7 @@ use juniper::graphql_scalar; -#[graphql_scalar(transparent)] +#[graphql_scalar] +#[graphql(transparent)] struct ScalarSpecifiedByUrl; fn main() {} diff --git a/tests/codegen/fail/scalar/derive_input/attr_transparent_unit_struct.stderr b/tests/codegen/fail/scalar/derive_input/attr_transparent_unit_struct.stderr index e83780a81..0d4e298c8 100644 --- a/tests/codegen/fail/scalar/derive_input/attr_transparent_unit_struct.stderr +++ b/tests/codegen/fail/scalar/derive_input/attr_transparent_unit_struct.stderr @@ -1,5 +1,11 @@ error: GraphQL scalar `transparent` attribute argument requires exactly 1 field - --> fail/scalar/derive_input/attr_transparent_unit_struct.rs:4:1 + --> fail/scalar/derive_input/attr_transparent_unit_struct.rs:5:1 | -4 | struct ScalarSpecifiedByUrl; +5 | struct ScalarSpecifiedByUrl; | ^^^^^^ + +error: cannot find attribute `graphql` in this scope + --> fail/scalar/derive_input/attr_transparent_unit_struct.rs:4:3 + | +4 | #[graphql(transparent)] + | ^^^^^^^ diff --git a/tests/codegen/fail/scalar/type_alias/attr_invalid_url.rs b/tests/codegen/fail/scalar/type_alias/attr_invalid_url.rs index 61b9c340e..966576843 100644 --- a/tests/codegen/fail/scalar/type_alias/attr_invalid_url.rs +++ b/tests/codegen/fail/scalar/type_alias/attr_invalid_url.rs @@ -1,13 +1,14 @@ -use juniper::{graphql_scalar, InputValue, ScalarValue, Value}; +use juniper::{graphql_scalar, Scalar, ScalarValue, Value}; struct ScalarSpecifiedByUrl; -#[graphql_scalar( +#[graphql_scalar] +#[graphql( specified_by_url = "not an url", with = scalar, parse_token(i32), )] -type Scalar = ScalarSpecifiedByUrl; +type MyScalar = ScalarSpecifiedByUrl; mod scalar { use super::*; @@ -16,10 +17,8 @@ mod scalar { Value::scalar(0) } - pub(super) fn from_input( - _: &InputValue, - ) -> Result { - Ok(ScalarSpecifiedByUrl) + pub(super) fn from_input(_: &Scalar) -> ScalarSpecifiedByUrl { + ScalarSpecifiedByUrl } } diff --git a/tests/codegen/fail/scalar/type_alias/attr_invalid_url.stderr b/tests/codegen/fail/scalar/type_alias/attr_invalid_url.stderr index 4ecefa21a..b364f6970 100644 --- a/tests/codegen/fail/scalar/type_alias/attr_invalid_url.stderr +++ b/tests/codegen/fail/scalar/type_alias/attr_invalid_url.stderr @@ -1,5 +1,11 @@ error: Invalid URL: relative URL without a base - --> fail/scalar/type_alias/attr_invalid_url.rs:6:24 + --> fail/scalar/type_alias/attr_invalid_url.rs:7:24 | -6 | specified_by_url = "not an url", +7 | specified_by_url = "not an url", | ^^^^^^^^^^^^ + +error: cannot find attribute `graphql` in this scope + --> fail/scalar/type_alias/attr_invalid_url.rs:6:3 + | +6 | #[graphql( + | ^^^^^^^ diff --git a/tests/codegen/fail/scalar/type_alias/attr_with_not_all_resolvers.rs b/tests/codegen/fail/scalar/type_alias/attr_with_not_all_resolvers.rs index 3b37e214b..be209fa03 100644 --- a/tests/codegen/fail/scalar/type_alias/attr_with_not_all_resolvers.rs +++ b/tests/codegen/fail/scalar/type_alias/attr_with_not_all_resolvers.rs @@ -2,7 +2,8 @@ use juniper::{graphql_scalar, Value}; struct Scalar; -#[graphql_scalar(to_output_with = Scalar::to_output)] +#[graphql_scalar] +#[graphql(to_output_with = Scalar::to_output)] type CustomScalar = Scalar; impl Scalar { diff --git a/tests/codegen/fail/scalar/type_alias/attr_with_not_all_resolvers.stderr b/tests/codegen/fail/scalar/type_alias/attr_with_not_all_resolvers.stderr index 4ef029a4c..445ebfca2 100644 --- a/tests/codegen/fail/scalar/type_alias/attr_with_not_all_resolvers.stderr +++ b/tests/codegen/fail/scalar/type_alias/attr_with_not_all_resolvers.stderr @@ -1,5 +1,11 @@ error: GraphQL scalar all the resolvers have to be provided via `with` attribute argument or a combination of `to_output_with`, `from_input_with`, `parse_token_with`/`parse_token` attribute arguments - --> fail/scalar/type_alias/attr_with_not_all_resolvers.rs:6:1 + --> fail/scalar/type_alias/attr_with_not_all_resolvers.rs:7:1 | -6 | type CustomScalar = Scalar; +7 | type CustomScalar = Scalar; | ^^^^ + +error: cannot find attribute `graphql` in this scope + --> fail/scalar/type_alias/attr_with_not_all_resolvers.rs:6:3 + | +6 | #[graphql(to_output_with = Scalar::to_output)] + | ^^^^^^^ diff --git a/tests/codegen/fail/scalar_value/missing_attributes.rs b/tests/codegen/fail/scalar_value/missing_attributes.rs index fb8f9c9bf..fb7fe2c67 100644 --- a/tests/codegen/fail/scalar_value/missing_attributes.rs +++ b/tests/codegen/fail/scalar_value/missing_attributes.rs @@ -1,12 +1,15 @@ +use derive_more::{Display, From, TryInto}; use juniper::ScalarValue; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, ScalarValue)] +#[derive(Clone, Debug, Deserialize, Display, From, PartialEq, ScalarValue, Serialize, TryInto)] pub enum DefaultScalarValue { Int(i32), + #[value(to_float)] Float(f64), - #[value(as_str, as_string, into_string)] + #[value(as_str, to_string)] String(String), - #[value(as_bool)] + #[value(to_bool)] Boolean(bool), } diff --git a/tests/codegen/fail/scalar_value/missing_attributes.stderr b/tests/codegen/fail/scalar_value/missing_attributes.stderr index b269e5a0d..bcd187e16 100644 --- a/tests/codegen/fail/scalar_value/missing_attributes.stderr +++ b/tests/codegen/fail/scalar_value/missing_attributes.stderr @@ -1,5 +1,11 @@ -error: GraphQL built-in scalars missing `#[value(as_int, as_float)]` attributes. In case you are sure that it's ok, use `#[value(allow_missing_attributes)]` to suppress this error. - --> fail/scalar_value/missing_attributes.rs:4:1 +error[E0277]: the trait bound `for<'a> DefaultScalarValue: TryScalarValueTo<'a, i32>` is not satisfied + --> fail/scalar_value/missing_attributes.rs:6:10 | -4 | pub enum DefaultScalarValue { - | ^^^ +6 | pub enum DefaultScalarValue { + | ^^^^^^^^^^^^^^^^^^ the trait `for<'a> TryScalarValueTo<'a, i32>` is not implemented for `DefaultScalarValue` + | + = help: the following other types implement trait `TryScalarValueTo<'me, T>`: + `DefaultScalarValue` implements `TryScalarValueTo<'_, &str>` + `DefaultScalarValue` implements `TryScalarValueTo<'_, bool>` + `DefaultScalarValue` implements `TryScalarValueTo<'_, f64>` + `DefaultScalarValue` implements `TryScalarValueTo<'_, std::string::String>` diff --git a/tests/codegen/src/lib.rs b/tests/codegen/src/lib.rs index 219ebd956..1b223d18d 100644 --- a/tests/codegen/src/lib.rs +++ b/tests/codegen/src/lib.rs @@ -5,8 +5,10 @@ #[cfg(test)] mod for_codegen_tests_only { + use derive_more as _; use futures as _; use juniper as _; + use serde as _; } #[rustversion::stable] diff --git a/tests/integration/Cargo.toml b/tests/integration/Cargo.toml index c977e0025..0e41e45be 100644 --- a/tests/integration/Cargo.toml +++ b/tests/integration/Cargo.toml @@ -6,6 +6,7 @@ publish = false [dev-dependencies] chrono = { version = "0.4", default-features = false } +derive_more = { version = "2.0", features = ["display", "from", "try_into"] } futures = "0.3" itertools = "0.14" juniper = { path = "../../juniper", features = ["chrono"] } diff --git a/tests/integration/tests/codegen_scalar_attr_derive_input.rs b/tests/integration/tests/codegen_scalar_attr_derive_input.rs index d5ac7060d..b1422e656 100644 --- a/tests/integration/tests/codegen_scalar_attr_derive_input.rs +++ b/tests/integration/tests/codegen_scalar_attr_derive_input.rs @@ -8,7 +8,7 @@ use std::fmt; use chrono::{DateTime, TimeZone, Utc}; use juniper::{ - InputValue, ParseScalarResult, ParseScalarValue, ScalarToken, ScalarValue, Value, execute, + ParseScalarResult, ParseScalarValue, Scalar, ScalarToken, ScalarValue, Value, execute, graphql_object, graphql_scalar, graphql_value, graphql_vars, }; @@ -31,10 +31,8 @@ mod trivial { Value::scalar(self.0) } - fn from_input(v: &InputValue) -> prelude::Result { - v.as_int_value() - .map(Self) - .ok_or_else(|| format!("Expected `Counter`, found: {v}")) + fn from_input(i: i32) -> Self { + Self(i) } fn parse_token(t: ScalarToken<'_>) -> ParseScalarResult { @@ -99,7 +97,8 @@ mod trivial { mod transparent { use super::*; - #[graphql_scalar(transparent)] + #[graphql_scalar] + #[graphql(transparent)] struct Counter(i32); struct QueryRoot; @@ -159,6 +158,7 @@ mod transparent { mod transparent_with_resolver { use super::*; + #[graphql_scalar] #[graphql_scalar( transparent, to_output_with = Self::to_output, @@ -228,23 +228,18 @@ mod transparent_with_resolver { mod all_custom_resolvers { use super::*; - #[graphql_scalar( + #[graphql_scalar] + #[graphql( to_output_with = to_output, - from_input_with = from_input, + from_input_with = Counter, )] - #[graphql_scalar(parse_token_with = parse_token)] + #[graphql(parse_token_with = parse_token)] struct Counter(i32); fn to_output(v: &Counter) -> Value { Value::scalar(v.0) } - fn from_input(v: &InputValue) -> prelude::Result { - v.as_int_value() - .map(Counter) - .ok_or_else(|| format!("Expected `Counter`, found: {v}")) - } - fn parse_token(value: ScalarToken<'_>) -> ParseScalarResult { >::from_str(value) } @@ -306,7 +301,8 @@ mod all_custom_resolvers { mod explicit_name { use super::*; - #[graphql_scalar(name = "Counter")] + #[graphql_scalar] + #[graphql(name = "Counter")] struct CustomCounter(i32); impl CustomCounter { @@ -314,10 +310,8 @@ mod explicit_name { Value::scalar(self.0) } - fn from_input(v: &InputValue) -> prelude::Result { - v.as_int_value() - .map(Self) - .ok_or_else(|| format!("Expected `Counter`, found: {v}")) + fn from_input(i: i32) -> Self { + Self(i) } fn parse_token(value: ScalarToken<'_>) -> ParseScalarResult { @@ -382,7 +376,8 @@ mod explicit_name { mod delegated_parse_token { use super::*; - #[graphql_scalar(parse_token(i32))] + #[graphql_scalar] + #[graphql(parse_token(i32))] struct Counter(i32); impl Counter { @@ -390,10 +385,8 @@ mod delegated_parse_token { Value::scalar(self.0) } - fn from_input(v: &InputValue) -> prelude::Result { - v.as_int_value() - .map(Self) - .ok_or_else(|| format!("Expected `Counter`, found: {v}")) + fn from_input(i: i32) -> Self { + Self(i) } } @@ -454,7 +447,8 @@ mod delegated_parse_token { mod multiple_delegated_parse_token { use super::*; - #[graphql_scalar(parse_token(prelude::String, i32))] + #[graphql_scalar] + #[graphql(parse_token(prelude::String, i32))] enum StringOrInt { String(prelude::String), Int(i32), @@ -468,11 +462,11 @@ mod multiple_delegated_parse_token { } } - fn from_input(v: &InputValue) -> prelude::Result { - v.as_string_value() - .map(|s| Self::String(s.to_owned())) - .or_else(|| v.as_int_value().map(Self::Int)) - .ok_or_else(|| format!("Expected `String` or `Int`, found: {v}")) + fn from_input(v: &Scalar) -> prelude::Result> { + 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()) } } @@ -513,7 +507,8 @@ mod multiple_delegated_parse_token { mod where_attribute { use super::*; - #[graphql_scalar( + #[graphql_scalar] + #[graphql( to_output_with = to_output, from_input_with = from_input, parse_token(prelude::String), @@ -531,19 +526,14 @@ mod where_attribute { Value::scalar(v.0.to_rfc3339()) } - fn from_input(v: &InputValue) -> prelude::Result, prelude::String> + fn from_input(s: &str) -> prelude::Result, prelude::Box> where - S: ScalarValue, Tz: From + TimeZone, Tz::Offset: fmt::Display, { - v.as_string_value() - .ok_or_else(|| format!("Expected `String`, found: {v}")) - .and_then(|s| { - DateTime::parse_from_rfc3339(s) - .map(|dt| CustomDateTime(dt.with_timezone(&Tz::from(Utc)))) - .map_err(|e| format!("Failed to parse `CustomDateTime`: {e}")) - }) + DateTime::parse_from_rfc3339(s) + .map(|dt| CustomDateTime(dt.with_timezone(&Tz::from(Utc)))) + .map_err(|e| format!("Failed to parse `CustomDateTime`: {e}").into()) } struct QueryRoot; @@ -593,7 +583,8 @@ mod where_attribute { mod with_self { use super::*; - #[graphql_scalar(with = Self)] + #[graphql_scalar] + #[graphql(with = Self)] struct Counter(i32); impl Counter { @@ -601,10 +592,8 @@ mod with_self { Value::scalar(self.0) } - fn from_input(v: &InputValue) -> prelude::Result { - v.as_int_value() - .map(Self) - .ok_or_else(|| format!("Expected `Counter`, found: {v}")) + fn from_input(i: i32) -> Self { + Self(i) } fn parse_token(value: ScalarToken<'_>) -> ParseScalarResult { @@ -669,7 +658,8 @@ mod with_self { mod with_module { use super::*; - #[graphql_scalar( + #[graphql_scalar] + #[graphql( with = custom_date_time, parse_token(prelude::String), where(Tz: From, Tz::Offset: fmt::Display), @@ -689,21 +679,16 @@ mod with_module { Value::scalar(v.0.to_rfc3339()) } - pub(super) fn from_input( - v: &InputValue, - ) -> prelude::Result, prelude::String> + pub(super) fn from_input( + s: &str, + ) -> prelude::Result, prelude::Box> where - S: ScalarValue, Tz: From + TimeZone, Tz::Offset: fmt::Display, { - v.as_string_value() - .ok_or_else(|| format!("Expected `String`, found: {v}")) - .and_then(|s| { - DateTime::parse_from_rfc3339(s) - .map(|dt| CustomDateTime(dt.with_timezone(&Tz::from(Utc)))) - .map_err(|e| format!("Failed to parse `CustomDateTime`: {e}")) - }) + DateTime::parse_from_rfc3339(s) + .map(|dt| CustomDateTime(dt.with_timezone(&Tz::from(Utc)))) + .map_err(|e| format!("Failed to parse `CustomDateTime`: {e}").into()) } } @@ -755,7 +740,8 @@ mod description_from_doc_comment { use super::*; /// Description - #[graphql_scalar(parse_token(i32))] + #[graphql_scalar] + #[graphql(parse_token(i32))] struct Counter(i32); impl Counter { @@ -763,10 +749,8 @@ mod description_from_doc_comment { Value::scalar(self.0) } - fn from_input(v: &InputValue) -> prelude::Result { - v.as_int_value() - .map(Self) - .ok_or_else(|| format!("Expected `Counter`, found: {v}")) + fn from_input(i: i32) -> Self { + Self(i) } } @@ -831,7 +815,8 @@ mod description_from_attribute { use super::*; /// Doc comment - #[graphql_scalar(description = "Description from attribute", parse_token(i32))] + #[graphql_scalar] + #[graphql(description = "Description from attribute", parse_token(i32))] struct Counter(i32); impl Counter { @@ -839,10 +824,8 @@ mod description_from_attribute { Value::scalar(self.0) } - fn from_input(v: &InputValue) -> prelude::Result { - v.as_int_value() - .map(Self) - .ok_or_else(|| format!("Expected `Counter`, found: {v}")) + fn from_input(i: i32) -> Self { + Self(i) } } @@ -907,7 +890,8 @@ mod custom_scalar { use super::*; /// Description - #[graphql_scalar(scalar = MyScalarValue, parse_token(i32))] + #[graphql_scalar] + #[graphql(scalar = MyScalarValue, parse_token(i32))] struct Counter(i32); impl Counter { @@ -915,10 +899,8 @@ mod custom_scalar { Value::scalar(self.0) } - fn from_input(v: &InputValue) -> prelude::Result { - v.as_int_value() - .map(Self) - .ok_or_else(|| format!("Expected `Counter`, found: {v}")) + fn from_input(i: i32) -> Self { + Self(i) } } @@ -983,7 +965,8 @@ mod generic_scalar { use super::*; /// Description - #[graphql_scalar(scalar = S: ScalarValue, parse_token(i32))] + #[graphql_scalar] + #[graphql(scalar = S: ScalarValue, parse_token(i32))] struct Counter(i32); impl Counter { @@ -991,10 +974,8 @@ mod generic_scalar { Value::scalar(self.0) } - fn from_input(v: &InputValue) -> prelude::Result { - v.as_int_value() - .map(Self) - .ok_or_else(|| format!("Expected `Counter`, found: {v}")) + fn from_input(i: i32) -> Self { + Self(i) } } @@ -1058,7 +1039,8 @@ mod generic_scalar { mod bounded_generic_scalar { use super::*; - #[graphql_scalar(scalar = S: ScalarValue + prelude::Clone, parse_token(i32))] + #[graphql_scalar] + #[graphql(scalar = S: ScalarValue + prelude::Clone, parse_token(i32))] struct Counter(i32); impl Counter { @@ -1066,10 +1048,8 @@ mod bounded_generic_scalar { Value::scalar(self.0) } - fn from_input(v: &InputValue) -> prelude::Result { - v.as_int_value() - .map(Self) - .ok_or_else(|| format!("Expected `Counter`, found: {v}")) + fn from_input(i: i32) -> Self { + Self(i) } } diff --git a/tests/integration/tests/codegen_scalar_attr_type_alias.rs b/tests/integration/tests/codegen_scalar_attr_type_alias.rs index 25289ab80..a29716a67 100644 --- a/tests/integration/tests/codegen_scalar_attr_type_alias.rs +++ b/tests/integration/tests/codegen_scalar_attr_type_alias.rs @@ -6,7 +6,7 @@ use std::fmt; use chrono::{DateTime, TimeZone, Utc}; use juniper::{ - InputValue, ParseScalarResult, ParseScalarValue, ScalarToken, ScalarValue, Value, execute, + ParseScalarResult, ParseScalarValue, Scalar, ScalarToken, ScalarValue, Value, execute, graphql_object, graphql_scalar, graphql_value, graphql_vars, }; @@ -23,11 +23,12 @@ mod all_custom_resolvers { struct CustomCounter(i32); - #[graphql_scalar( + #[graphql_scalar] + #[graphql( to_output_with = to_output, - from_input_with = from_input, + from_input_with = CustomCounter, )] - #[graphql_scalar( + #[graphql( parse_token_with = parse_token, )] type Counter = CustomCounter; @@ -36,12 +37,6 @@ mod all_custom_resolvers { Value::scalar(v.0) } - fn from_input(v: &InputValue) -> prelude::Result { - v.as_int_value() - .map(CustomCounter) - .ok_or_else(|| format!("Expected `Counter`, found: {v}")) - } - fn parse_token(value: ScalarToken<'_>) -> ParseScalarResult { >::from_str(value) } @@ -105,10 +100,11 @@ mod explicit_name { struct CustomCounter(i32); - #[graphql_scalar( + #[graphql_scalar] + #[graphql( name = "Counter", to_output_with = to_output, - from_input_with = from_input, + from_input_with = CustomCounter, parse_token_with = parse_token, )] type CounterScalar = CustomCounter; @@ -117,14 +113,6 @@ mod explicit_name { Value::scalar(v.0) } - fn from_input( - v: &InputValue, - ) -> prelude::Result { - v.as_int_value() - .map(CustomCounter) - .ok_or_else(|| format!("Expected `Counter`, found: {v}")) - } - fn parse_token(value: ScalarToken<'_>) -> ParseScalarResult { >::from_str(value) } @@ -208,9 +196,10 @@ mod delegated_parse_token { struct CustomCounter(i32); - #[graphql_scalar( + #[graphql_scalar] + #[graphql( to_output_with = to_output, - from_input_with = from_input, + from_input_with = CustomCounter, parse_token(i32), )] type Counter = CustomCounter; @@ -219,12 +208,6 @@ mod delegated_parse_token { Value::scalar(v.0) } - fn from_input(v: &InputValue) -> prelude::Result { - v.as_int_value() - .map(CustomCounter) - .ok_or_else(|| format!("Expected `Counter`, found: {v}")) - } - struct QueryRoot; #[graphql_object] @@ -287,7 +270,8 @@ mod multiple_delegated_parse_token { Int(i32), } - #[graphql_scalar( + #[graphql_scalar] + #[graphql( to_output_with = to_output, from_input_with = from_input, parse_token(prelude::String, i32), @@ -301,13 +285,11 @@ mod multiple_delegated_parse_token { } } - fn from_input( - v: &InputValue, - ) -> prelude::Result { - v.as_string_value() - .map(|s| StringOrInt::String(s.to_owned())) - .or_else(|| v.as_int_value().map(StringOrInt::Int)) - .ok_or_else(|| format!("Expected `String` or `Int`, found: {v}")) + fn from_input(v: &Scalar) -> prelude::Result> { + 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()) } struct QueryRoot; @@ -349,7 +331,8 @@ mod where_attribute { struct CustomDateTimeScalar(DateTime); - #[graphql_scalar( + #[graphql_scalar] + #[graphql( to_output_with = to_output, from_input_with = from_input, parse_token(prelude::String), @@ -367,19 +350,14 @@ mod where_attribute { Value::scalar(v.0.to_rfc3339()) } - fn from_input(v: &InputValue) -> prelude::Result, prelude::String> + fn from_input(s: &str) -> prelude::Result, prelude::Box> where - S: ScalarValue, Tz: From + TimeZone, Tz::Offset: fmt::Display, { - v.as_string_value() - .ok_or_else(|| format!("Expected `String`, found: {v}")) - .and_then(|s| { - DateTime::parse_from_rfc3339(s) - .map(|dt| CustomDateTimeScalar(dt.with_timezone(&Tz::from(Utc)))) - .map_err(|e| format!("Failed to parse `CustomDateTime`: {e}")) - }) + DateTime::parse_from_rfc3339(s) + .map(|dt| CustomDateTimeScalar(dt.with_timezone(&Tz::from(Utc)))) + .map_err(|e| format!("Failed to parse `CustomDateTime`: {e}").into()) } struct QueryRoot; @@ -431,7 +409,8 @@ mod with_self { struct CustomCounter(i32); - #[graphql_scalar(with = Self)] + #[graphql_scalar] + #[graphql(with = Self)] type Counter = CustomCounter; impl Counter { @@ -439,10 +418,8 @@ mod with_self { Value::scalar(self.0) } - fn from_input(v: &InputValue) -> prelude::Result { - v.as_int_value() - .map(Self) - .ok_or_else(|| format!("Expected `Counter`, found: {v}")) + fn from_input(i: i32) -> Self { + Self(i) } fn parse_token(value: ScalarToken<'_>) -> ParseScalarResult { @@ -509,7 +486,8 @@ mod with_module { struct CustomDateTimeScalar(DateTime); - #[graphql_scalar( + #[graphql_scalar] + #[graphql( with = custom_date_time, parse_token(prelude::String), where(Tz: From + TimeZone, Tz::Offset: fmt::Display), @@ -529,21 +507,16 @@ mod with_module { Value::scalar(v.0.to_rfc3339()) } - pub(super) fn from_input( - v: &InputValue, - ) -> prelude::Result, prelude::String> + pub(super) fn from_input( + s: &str, + ) -> prelude::Result, prelude::Box> where - S: ScalarValue, Tz: From + TimeZone, Tz::Offset: fmt::Display, { - v.as_string_value() - .ok_or_else(|| format!("Expected `String`, found: {v}")) - .and_then(|s| { - DateTime::parse_from_rfc3339(s) - .map(|dt| CustomDateTimeScalar(dt.with_timezone(&Tz::from(Utc)))) - .map_err(|e| format!("Failed to parse `CustomDateTime`: {e}")) - }) + DateTime::parse_from_rfc3339(s) + .map(|dt| CustomDateTimeScalar(dt.with_timezone(&Tz::from(Utc)))) + .map_err(|e| format!("Failed to parse `CustomDateTime`: {e}").into()) } } @@ -597,7 +570,8 @@ mod description_from_doc_comment { struct CustomCounter(i32); /// Description - #[graphql_scalar(with = counter, parse_token(i32))] + #[graphql_scalar] + #[graphql(with = counter, parse_token(i32))] type Counter = CustomCounter; mod counter { @@ -607,12 +581,8 @@ mod description_from_doc_comment { Value::scalar(v.0) } - pub(super) fn from_input( - v: &InputValue, - ) -> prelude::Result { - v.as_int_value() - .map(CustomCounter) - .ok_or_else(|| format!("Expected `Counter`, found: {v}")) + pub(super) fn from_input(i: i32) -> Counter { + CustomCounter(i) } } @@ -679,7 +649,8 @@ mod description_from_attribute { struct CustomCounter(i32); /// Doc comment - #[graphql_scalar( + #[graphql_scalar] + #[graphql( description = "Description from attribute", with = counter, parse_token(i32), @@ -693,12 +664,8 @@ mod description_from_attribute { Value::scalar(v.0) } - pub(super) fn from_input( - v: &InputValue, - ) -> prelude::Result { - v.as_int_value() - .map(CustomCounter) - .ok_or_else(|| format!("Expected `Counter`, found: {v}")) + pub(super) fn from_input(i: i32) -> Counter { + CustomCounter(i) } } @@ -765,7 +732,8 @@ mod custom_scalar { struct CustomCounter(i32); /// Description - #[graphql_scalar( + #[graphql_scalar] + #[graphql( scalar = MyScalarValue, with = counter, parse_token(i32), @@ -779,12 +747,8 @@ mod custom_scalar { Value::scalar(v.0) } - pub(super) fn from_input( - v: &InputValue, - ) -> prelude::Result { - v.as_int_value() - .map(CustomCounter) - .ok_or_else(|| format!("Expected `Counter`, found: {v}")) + pub(super) fn from_input(i: i32) -> Counter { + CustomCounter(i) } } @@ -851,7 +815,8 @@ mod generic_scalar { struct CustomCounter(i32); /// Description - #[graphql_scalar( + #[graphql_scalar] + #[graphql( scalar = S: ScalarValue, with = counter, parse_token(i32), @@ -865,12 +830,8 @@ mod generic_scalar { Value::scalar(v.0) } - pub(super) fn from_input( - v: &InputValue, - ) -> prelude::Result { - v.as_int_value() - .map(CustomCounter) - .ok_or_else(|| format!("Expected `Counter`, found: {v}")) + pub(super) fn from_input(i: i32) -> Counter { + CustomCounter(i) } } @@ -937,7 +898,8 @@ mod bounded_generic_scalar { struct CustomCounter(i32); /// Description - #[graphql_scalar( + #[graphql_scalar] + #[graphql( scalar = S: ScalarValue + prelude::Clone, with = counter, parse_token(i32), @@ -951,12 +913,8 @@ mod bounded_generic_scalar { Value::scalar(v.0) } - pub(super) fn from_input( - v: &InputValue, - ) -> prelude::Result { - v.as_int_value() - .map(CustomCounter) - .ok_or_else(|| format!("Expected `Counter`, found: {v}")) + pub(super) fn from_input(i: i32) -> Counter { + CustomCounter(i) } } diff --git a/tests/integration/tests/codegen_scalar_derive.rs b/tests/integration/tests/codegen_scalar_derive.rs index c5f78f0a0..dfd0a5b96 100644 --- a/tests/integration/tests/codegen_scalar_derive.rs +++ b/tests/integration/tests/codegen_scalar_derive.rs @@ -6,8 +6,8 @@ use std::fmt; use chrono::{DateTime, TimeZone, Utc}; use juniper::{ - GraphQLScalar, InputValue, ParseScalarResult, ParseScalarValue, ScalarToken, ScalarValue, - Value, execute, graphql_object, graphql_value, graphql_vars, + GraphQLScalar, ParseScalarResult, ParseScalarValue, Scalar, ScalarToken, ScalarValue, Value, + execute, graphql_object, graphql_value, graphql_vars, }; use self::common::{ @@ -29,10 +29,8 @@ mod trivial { Value::scalar(self.0) } - fn from_input(v: &InputValue) -> prelude::Result { - v.as_int_value() - .map(Self) - .ok_or_else(|| format!("Expected `Counter`, found: {v}")) + fn from_input(i: i32) -> Self { + Self(i) } fn parse_token(t: ScalarToken<'_>) -> ParseScalarResult { @@ -228,7 +226,7 @@ mod all_custom_resolvers { #[derive(GraphQLScalar)] #[graphql( to_output_with = to_output, - from_input_with = from_input, + from_input_with = Counter, )] #[graphql(parse_token_with = parse_token)] struct Counter(i32); @@ -237,12 +235,6 @@ mod all_custom_resolvers { Value::scalar(v.0) } - fn from_input(v: &InputValue) -> prelude::Result { - v.as_int_value() - .map(Counter) - .ok_or_else(|| format!("Expected `Counter`, found: {v}")) - } - fn parse_token(value: ScalarToken<'_>) -> ParseScalarResult { >::from_str(value) } @@ -313,10 +305,8 @@ mod explicit_name { Value::scalar(self.0) } - fn from_input(v: &InputValue) -> prelude::Result { - v.as_int_value() - .map(Self) - .ok_or_else(|| format!("Expected `Counter`, found: {v}")) + fn from_input(i: i32) -> Self { + Self(i) } fn parse_token(value: ScalarToken<'_>) -> ParseScalarResult { @@ -390,10 +380,8 @@ mod delegated_parse_token { Value::scalar(self.0) } - fn from_input(v: &InputValue) -> prelude::Result { - v.as_int_value() - .map(Self) - .ok_or_else(|| format!("Expected `Counter`, found: {v}")) + fn from_input(i: i32) -> Self { + Self(i) } } @@ -469,11 +457,11 @@ mod multiple_delegated_parse_token { } } - fn from_input(v: &InputValue) -> prelude::Result { - v.as_string_value() - .map(|s| Self::String(s.to_owned())) - .or_else(|| v.as_int_value().map(Self::Int)) - .ok_or_else(|| format!("Expected `String` or `Int`, found: {v}")) + fn from_input(v: &Scalar) -> prelude::Result> { + 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()) } } @@ -533,19 +521,14 @@ mod where_attribute { Value::scalar(v.0.to_rfc3339()) } - fn from_input(v: &InputValue) -> prelude::Result, prelude::String> + fn from_input(s: &str) -> prelude::Result, prelude::Box> where - S: ScalarValue, Tz: From + TimeZone, Tz::Offset: fmt::Display, { - v.as_string_value() - .ok_or_else(|| format!("Expected `String`, found: {v}")) - .and_then(|s| { - DateTime::parse_from_rfc3339(s) - .map(|dt| CustomDateTime(dt.with_timezone(&Tz::from(Utc)))) - .map_err(|e| format!("Failed to parse `CustomDateTime`: {e}")) - }) + DateTime::parse_from_rfc3339(s) + .map(|dt| CustomDateTime(dt.with_timezone(&Tz::from(Utc)))) + .map_err(|e| format!("Failed to parse `CustomDateTime`: {e}").into()) } struct QueryRoot; @@ -604,10 +587,8 @@ mod with_self { Value::scalar(self.0) } - fn from_input(v: &InputValue) -> prelude::Result { - v.as_int_value() - .map(Self) - .ok_or_else(|| format!("Expected `Counter`, found: {v}")) + fn from_input(i: i32) -> Self { + Self(i) } fn parse_token(value: ScalarToken<'_>) -> ParseScalarResult { @@ -693,21 +674,16 @@ mod with_module { Value::scalar(v.0.to_rfc3339()) } - pub(super) fn from_input( - v: &InputValue, - ) -> prelude::Result, prelude::String> + pub(super) fn from_input( + s: &str, + ) -> prelude::Result, prelude::Box> where - S: ScalarValue, Tz: From + TimeZone, Tz::Offset: fmt::Display, { - v.as_string_value() - .ok_or_else(|| format!("Expected `String`, found: {v}")) - .and_then(|s| { - DateTime::parse_from_rfc3339(s) - .map(|dt| CustomDateTime(dt.with_timezone(&Tz::from(Utc)))) - .map_err(|e| format!("Failed to parse `CustomDateTime`: {e}")) - }) + DateTime::parse_from_rfc3339(s) + .map(|dt| CustomDateTime(dt.with_timezone(&Tz::from(Utc)))) + .map_err(|e| format!("Failed to parse `CustomDateTime`: {e}").into()) } } @@ -768,10 +744,8 @@ mod description_from_doc_comment { Value::scalar(self.0) } - fn from_input(v: &InputValue) -> prelude::Result { - v.as_int_value() - .map(Self) - .ok_or_else(|| format!("Expected `Counter`, found: {v}")) + fn from_input(i: i32) -> Self { + Self(i) } } @@ -845,10 +819,8 @@ mod description_from_attribute { Value::scalar(self.0) } - fn from_input(v: &InputValue) -> prelude::Result { - v.as_int_value() - .map(Self) - .ok_or_else(|| format!("Expected `Counter`, found: {v}")) + fn from_input(i: i32) -> Self { + Self(i) } } @@ -922,10 +894,8 @@ mod custom_scalar { Value::scalar(self.0) } - fn from_input(v: &InputValue) -> prelude::Result { - v.as_int_value() - .map(Self) - .ok_or_else(|| format!("Expected `Counter`, found: {v}")) + fn from_input(i: i32) -> Self { + Self(i) } } @@ -999,10 +969,8 @@ mod generic_scalar { Value::scalar(self.0) } - fn from_input(v: &InputValue) -> prelude::Result { - v.as_int_value() - .map(Self) - .ok_or_else(|| format!("Expected `Counter`, found: {v}")) + fn from_input(i: i32) -> Self { + Self(i) } } @@ -1075,10 +1043,8 @@ mod bounded_generic_scalar { Value::scalar(self.0) } - fn from_input(v: &InputValue) -> prelude::Result { - v.as_int_value() - .map(Self) - .ok_or_else(|| format!("Expected `String`, found: {v}")) + fn from_input(i: i32) -> Self { + Self(i) } } diff --git a/tests/integration/tests/codegen_scalar_value_derive.rs b/tests/integration/tests/codegen_scalar_value_derive.rs index 936318cbe..8b71d26a8 100644 --- a/tests/integration/tests/codegen_scalar_value_derive.rs +++ b/tests/integration/tests/codegen_scalar_value_derive.rs @@ -2,7 +2,8 @@ pub mod common; -use juniper::{DefaultScalarValue, ScalarValue}; +use derive_more::with_trait::{Display, From, TryInto}; +use juniper::{DefaultScalarValue, ScalarValue, TryScalarValueTo}; use serde::{Deserialize, Serialize}; // Override `std::prelude` items to check whether macros expand hygienically. @@ -11,16 +12,18 @@ use self::common::hygiene::*; mod trivial { use super::*; - #[derive(Clone, Debug, Deserialize, PartialEq, ScalarValue, Serialize)] + #[derive( + Clone, Debug, Deserialize, Display, From, PartialEq, ScalarValue, Serialize, TryInto, + )] #[serde(untagged)] pub enum CustomScalarValue { - #[value(as_float, as_int)] + #[value(to_float, to_int)] Int(i32), - #[value(as_float)] + #[value(to_float)] Float(f64), - #[value(as_str, as_string, into_string)] + #[value(as_str, to_string)] String(prelude::String), - #[value(as_bool)] + #[value(to_bool)] Boolean(bool), } @@ -52,16 +55,18 @@ mod trivial { mod named_fields { use super::*; - #[derive(Clone, Debug, Deserialize, PartialEq, ScalarValue, Serialize)] + #[derive( + Clone, Debug, Deserialize, Display, From, PartialEq, ScalarValue, Serialize, TryInto, + )] #[serde(untagged)] pub enum CustomScalarValue { - #[value(as_float, as_int)] + #[value(to_float, to_int)] Int { int: i32 }, - #[value(as_float)] + #[value(to_float)] Float(f64), - #[value(as_str, as_string, into_string)] + #[value(as_str, to_string)] String(prelude::String), - #[value(as_bool)] + #[value(to_bool)] Boolean { v: bool }, } @@ -93,20 +98,21 @@ mod named_fields { mod custom_fn { use super::*; - #[derive(Clone, Debug, Deserialize, PartialEq, ScalarValue, Serialize)] + #[derive( + Clone, Debug, Deserialize, Display, From, PartialEq, ScalarValue, Serialize, TryInto, + )] #[serde(untagged)] pub enum CustomScalarValue { - #[value(as_float, as_int)] + #[value(to_float, to_int)] Int(i32), - #[value(as_float)] + #[value(to_float)] Float(f64), #[value( as_str, - as_string = str::to_owned, - into_string = std::convert::identity, + to_string = str::to_owned, )] String(prelude::String), - #[value(as_bool)] + #[value(to_bool)] Boolean(bool), } @@ -135,25 +141,34 @@ mod custom_fn { } } -mod allow_missing_attributes { +mod missing_conv_attr { use super::*; - #[derive(Clone, Debug, Deserialize, PartialEq, ScalarValue, Serialize)] + #[derive( + Clone, Debug, Deserialize, Display, From, PartialEq, ScalarValue, Serialize, TryInto, + )] #[serde(untagged)] - #[value(allow_missing_attributes)] pub enum CustomScalarValue { Int(i32), - #[value(as_float)] + #[value(to_float)] Float(f64), - #[value(as_str, as_string, into_string)] + #[value(as_str, to_string)] String(prelude::String), - #[value(as_bool)] + #[value(to_bool)] Boolean(bool), } + impl<'me> TryScalarValueTo<'me, i32> for CustomScalarValue { + type Error = &'static str; + + fn try_scalar_value_to(&'me self) -> prelude::Result { + Err("Not `Int` definitely") + } + } + #[test] fn into_another() { - assert!(CustomScalarValue::Int(5).as_int().is_none()); + assert!(CustomScalarValue::Int(5).try_to_int().is_none()); assert!( CustomScalarValue::from(0.5_f64) .into_another::() diff --git a/tests/integration/tests/common/mod.rs b/tests/integration/tests/common/mod.rs index 3fdabb8d7..323a4cc07 100644 --- a/tests/integration/tests/common/mod.rs +++ b/tests/integration/tests/common/mod.rs @@ -1,5 +1,6 @@ use std::fmt; +use derive_more::with_trait::{Display, From, TryInto}; use juniper::{InputValue, IntoInputValue, IntoValue, ScalarValue, Value}; use serde::{Deserialize, Deserializer, Serialize, de}; use smartstring::alias::CompactString; @@ -75,17 +76,17 @@ pub mod util { } } -#[derive(Clone, Debug, PartialEq, ScalarValue, Serialize)] +#[derive(Clone, Debug, Display, From, PartialEq, ScalarValue, Serialize, TryInto)] #[serde(untagged)] pub enum MyScalarValue { - #[value(as_float, as_int)] + #[value(to_float, to_int)] Int(i32), Long(i64), - #[value(as_float)] + #[value(to_float)] Float(f64), - #[value(as_str, as_string, into_string)] + #[value(as_str, to_string)] String(String), - #[value(as_bool)] + #[value(to_bool)] Boolean(bool), } diff --git a/tests/integration/tests/custom_scalar.rs b/tests/integration/tests/custom_scalar.rs index da6f0d127..1f6b7c4d6 100644 --- a/tests/integration/tests/custom_scalar.rs +++ b/tests/integration/tests/custom_scalar.rs @@ -22,10 +22,12 @@ mod long { Value::scalar(*v) } - pub(super) fn from_input(v: &InputValue) -> Result { - v.as_scalar_value::() - .copied() - .ok_or_else(|| format!("Expected `MyScalarValue::Long`, found: {v}")) + pub(super) fn from_input(s: &MyScalarValue) -> Result> { + if let MyScalarValue::Long(i) = s { + Ok(*i) + } else { + Err(format!("Expected `MyScalarValue::Long`, found: {s}").into()) + } } pub(super) fn parse_token(value: ScalarToken<'_>) -> ParseScalarResult {