diff --git a/book/src/types/scalars.md b/book/src/types/scalars.md index 244cec726..2d0ba81e5 100644 --- a/book/src/types/scalars.md +++ b/book/src/types/scalars.md @@ -85,7 +85,7 @@ pub struct UserId(String); In case we need to customize [resolving][7] of a [custom GraphQL scalar][2] value (change the way it gets executed), the `#[graphql(to_output_with = )]` attribute is the way to do so: ```rust # extern crate juniper; -# use juniper::{GraphQLScalar, ScalarValue, Value}; +# use juniper::{GraphQLScalar, IntoValue as _, ScalarValue, Value}; # #[derive(GraphQLScalar)] #[graphql(to_output_with = to_output, transparent)] @@ -93,8 +93,7 @@ struct Incremented(i32); /// Increments [`Incremented`] before converting into a [`Value`]. fn to_output(v: &Incremented) -> Value { - let inc = v.0 + 1; - Value::from(inc) + (v.0 + 1).into_value() } # # fn main() {} diff --git a/juniper/CHANGELOG.md b/juniper/CHANGELOG.md index d316e86c8..fc249f103 100644 --- a/juniper/CHANGELOG.md +++ b/juniper/CHANGELOG.md @@ -64,7 +64,9 @@ All user visible changes to `juniper` crate will be documented in this file. Thi - Made `name()` method returning `ArcStr`. - `GraphQLValue`: - Made `type_name()` method returning `ArcStr`. -- Switched `ParseError::UnexpectedToken` to `compact_str::CompactString` instead of `smartstring::SmartString`. ([todo]) +- 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]) ### Added @@ -78,7 +80,10 @@ All user visible changes to `juniper` crate will be documented in this file. Thi - `jiff::tz::Offset` as `UtcOffset` scalar. - `jiff::Span` as `Duration` scalar. - `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]) ### Changed @@ -103,6 +108,7 @@ All user visible changes to `juniper` crate will be documented in this file. Thi [#1293]: /../../pull/1293 [#1311]: /../../pull/1311 [#1318]: /../../pull/1318 +[#1324]: /../../pull/1324 [#1325]: /../../pull/1325 [1b1fc618]: /../../commit/1b1fc61879ffdd640d741e187dc20678bf7ab295 [20609366]: /../../commit/2060936635609b0186d46d8fbd06eb30fce660e3 diff --git a/juniper/Cargo.toml b/juniper/Cargo.toml index db1eb1dc2..552076d75 100644 --- a/juniper/Cargo.toml +++ b/juniper/Cargo.toml @@ -81,6 +81,7 @@ void = { version = "1.0.2", optional = true } [dev-dependencies] bencher = "0.1.2" chrono = { version = "0.4.30", features = ["alloc"], default-features = false } +compact_str = { version = "0.9", features = ["serde"] } jiff = { version = "0.2", features = ["tzdb-bundle-always"], default-features = false } pretty_assertions = "1.0.0" serde_json = "1.0.18" diff --git a/juniper/src/ast.rs b/juniper/src/ast.rs index a7ee27ef2..a505969b2 100644 --- a/juniper/src/ast.rs +++ b/juniper/src/ast.rs @@ -1,6 +1,7 @@ use std::{borrow::Cow, fmt, hash::Hash, slice, vec}; use arcstr::ArcStr; +use compact_str::CompactString; use indexmap::IndexMap; @@ -249,10 +250,7 @@ impl InputValue { } /// Construct a scalar value - pub fn scalar(v: T) -> Self - where - S: From, - { + pub fn scalar>(v: T) -> Self { Self::Scalar(v.into()) } @@ -509,51 +507,117 @@ impl fmt::Display for InputValue { } } -impl From> for InputValue +/// Conversion into an [`InputValue`]. +/// +/// This trait exists to work around [orphan rules] and allow to specify custom efficient +/// conversions whenever some custom [`ScalarValue`] is involved +/// (`impl IntoInputValue for ForeignType` would work, while +/// `impl From for InputValue` wound not). +/// +/// This trait is used inside [`graphql_input_value!`] macro expansion and implementing it allows to +/// put values of the implementor type there. +/// +/// [`graphql_input_value!`]: crate::graphql_input_value +/// [orphan rules]: https://doc.rust-lang.org/reference/items/implementations.html#orphan-rules +pub trait IntoInputValue { + /// Converts this value into an [`InputValue`]. + #[must_use] + fn into_input_value(self) -> InputValue; +} + +impl IntoInputValue for InputValue { + fn into_input_value(self) -> Self { + self + } +} + +impl IntoInputValue for Option where - Self: From, + T: IntoInputValue, { - fn from(v: Option) -> Self { - match v { - Some(v) => v.into(), - None => Self::Null, + fn into_input_value(self) -> InputValue { + match self { + Some(v) => v.into_input_value(), + None => InputValue::Null, } } } -impl<'a, S: From> From<&'a str> for InputValue { - fn from(s: &'a str) -> Self { - Self::scalar(s.to_owned()) +impl IntoInputValue for &str +where + String: Into, +{ + fn into_input_value(self) -> InputValue { + InputValue::scalar(self.to_owned()) + } +} + +impl IntoInputValue for Cow<'_, str> +where + String: Into, +{ + fn into_input_value(self) -> InputValue { + InputValue::scalar(self.into_owned()) + } +} + +impl IntoInputValue for String +where + String: Into, +{ + fn into_input_value(self) -> InputValue { + InputValue::scalar(self) + } +} + +impl IntoInputValue for &ArcStr { + fn into_input_value(self) -> InputValue { + InputValue::scalar(S::from_displayable(self)) } } -impl<'a, S: From> From> for InputValue { - fn from(s: Cow<'a, str>) -> Self { - Self::scalar(s.into_owned()) +impl IntoInputValue for ArcStr { + fn into_input_value(self) -> InputValue { + (&self).into_input_value() } } -impl> From for InputValue { - fn from(s: String) -> Self { - Self::scalar(s) +impl IntoInputValue for &CompactString { + fn into_input_value(self) -> InputValue { + InputValue::scalar(S::from_displayable(self)) } } -impl> From for InputValue { - fn from(i: i32) -> Self { - Self::scalar(i) +impl IntoInputValue for CompactString { + fn into_input_value(self) -> InputValue { + (&self).into_input_value() } } -impl> From for InputValue { - fn from(f: f64) -> Self { - Self::scalar(f) +impl IntoInputValue for i32 +where + i32: Into, +{ + fn into_input_value(self) -> InputValue { + InputValue::scalar(self) } } -impl> From for InputValue { - fn from(b: bool) -> Self { - Self::scalar(b) +impl IntoInputValue for f64 +where + f64: Into, +{ + fn into_input_value(self) -> InputValue { + InputValue::scalar(self) + } +} + +impl IntoInputValue for bool +where + bool: Into, +{ + fn into_input_value(self) -> InputValue { + InputValue::scalar(self) } } diff --git a/juniper/src/lib.rs b/juniper/src/lib.rs index 1b7687d83..c01c61cb5 100644 --- a/juniper/src/lib.rs +++ b/juniper/src/lib.rs @@ -74,8 +74,8 @@ use crate::{ pub use crate::{ ast::{ - Definition, Document, FromInputValue, InputValue, Operation, OperationType, Selection, - ToInputValue, Type, + Definition, Document, FromInputValue, InputValue, IntoInputValue, Operation, OperationType, + Selection, ToInputValue, Type, }, executor::{ Applies, Context, ExecutionError, ExecutionResult, Executor, FieldError, FieldResult, @@ -102,7 +102,10 @@ pub use crate::{ }, }, validation::RuleError, - value::{DefaultScalarValue, Object, ParseScalarResult, ParseScalarValue, ScalarValue, Value}, + value::{ + AnyExt, DefaultScalarValue, IntoValue, Object, ParseScalarResult, ParseScalarValue, + ScalarValue, Value, + }, }; /// An error that prevented query execution diff --git a/juniper/src/macros/graphql_input_value.rs b/juniper/src/macros/graphql_input_value.rs index 9bd7e0ee8..7991c12f1 100644 --- a/juniper/src/macros/graphql_input_value.rs +++ b/juniper/src/macros/graphql_input_value.rs @@ -368,17 +368,17 @@ macro_rules! graphql_input_value { (None$(,)?) => ($crate::InputValue::null()); - (true$(,)?) => ($crate::InputValue::from(true)); + (true$(,)?) => ($crate::IntoInputValue::into_input_value(true)); - (false$(,)?) => ($crate::InputValue::from(false)); + (false$(,)?) => ($crate::IntoInputValue::into_input_value(false)); (@$var:ident$(,)?) => ($crate::InputValue::variable(stringify!($var))); ($enum:ident$(,)?) => ($crate::InputValue::enum_value(stringify!($enum))); - (($e:expr)$(,)?) => ($crate::InputValue::from($e)); + (($e:expr)$(,)?) => ($crate::IntoInputValue::into_input_value($e)); - ($e:expr$(,)?) => ($crate::InputValue::from($e)); + ($e:expr$(,)?) => ($crate::IntoInputValue::into_input_value($e)); } #[cfg(test)] diff --git a/juniper/src/macros/graphql_value.rs b/juniper/src/macros/graphql_value.rs index cc4e34d38..e3e210a9b 100644 --- a/juniper/src/macros/graphql_value.rs +++ b/juniper/src/macros/graphql_value.rs @@ -268,7 +268,7 @@ macro_rules! graphql_value { (None$(,)?) => ($crate::Value::null()); - ($e:expr$(,)?) => ($crate::Value::from($e)); + ($e:expr$(,)?) => ($crate::IntoValue::into_value($e)); } #[cfg(test)] diff --git a/juniper/src/types/scalars.rs b/juniper/src/types/scalars.rs index 4be19356b..c15af3a13 100644 --- a/juniper/src/types/scalars.rs +++ b/juniper/src/types/scalars.rs @@ -176,12 +176,12 @@ where type ArcStr = arcstr::ArcStr; mod impl_arcstr_scalar { - use crate::{InputValue, ScalarValue, Value}; + use crate::{InputValue, IntoValue as _, ScalarValue, Value}; use super::ArcStr; pub(super) fn to_output(v: &ArcStr) -> Value { - Value::scalar(v.to_string()) + v.into_value() } pub(super) fn from_input(v: &InputValue) -> Result { @@ -196,12 +196,12 @@ mod impl_arcstr_scalar { type CompactString = compact_str::CompactString; mod impl_compactstring_scalar { - use crate::{InputValue, ScalarValue, Value}; + use crate::{InputValue, IntoValue as _, ScalarValue, Value}; use super::CompactString; pub(super) fn to_output(v: &CompactString) -> Value { - Value::scalar(v.to_string()) + v.into_value() } pub(super) fn from_input(v: &InputValue) -> Result { diff --git a/juniper/src/value/mod.rs b/juniper/src/value/mod.rs index 5867a35a1..08259f60b 100644 --- a/juniper/src/value/mod.rs +++ b/juniper/src/value/mod.rs @@ -3,7 +3,8 @@ mod scalar; use std::{any::TypeId, borrow::Cow, fmt, mem}; -use derive_more::with_trait::From; +use arcstr::ArcStr; +use compact_str::CompactString; use crate::{ ast::{InputValue, ToInputValue}, @@ -12,7 +13,7 @@ use crate::{ pub use self::{ object::Object, - scalar::{DefaultScalarValue, ParseScalarResult, ParseScalarValue, ScalarValue}, + scalar::{AnyExt, DefaultScalarValue, ParseScalarResult, ParseScalarValue, ScalarValue}, }; /// Serializable value returned from query and field execution. @@ -25,10 +26,9 @@ pub use self::{ /// information since they are generated by resolving fields and values rather /// than parsing a source query. #[expect(missing_docs, reason = "self-explanatory")] -#[derive(Clone, Debug, From, PartialEq)] +#[derive(Clone, Debug, PartialEq)] pub enum Value { Null, - #[from(ignore)] Scalar(S), List(Vec>), Object(Object), @@ -53,10 +53,7 @@ impl Value { } /// Construct a scalar value - pub fn scalar(s: T) -> Self - where - S: From, - { + pub fn scalar>(s: T) -> Self { Self::Scalar(s.into()) } @@ -233,51 +230,117 @@ impl fmt::Display for Value { } } -impl From> for Value +/// Conversion into a [`Value`]. +/// +/// This trait exists to work around [orphan rules] and allow to specify custom efficient +/// conversions whenever some custom [`ScalarValue`] is involved +/// (`impl IntoValue for ForeignType` would work, while +/// `impl From for Value` wound not). +/// +/// This trait is used inside [`graphql_value!`] macro expansion and implementing it allows to +/// put values of the implementor type there. +/// +/// [`graphql_value!`]: crate::graphql_value +/// [orphan rules]: https://doc.rust-lang.org/reference/items/implementations.html#orphan-rules +pub trait IntoValue { + /// Converts this value into a [`Value`]. + #[must_use] + fn into_value(self) -> Value; +} + +impl IntoValue for Value { + fn into_value(self) -> Self { + self + } +} + +impl IntoValue for Option where - Self: From, + T: IntoValue, { - fn from(v: Option) -> Self { - match v { - Some(v) => v.into(), - None => Self::Null, + fn into_value(self) -> Value { + match self { + Some(v) => v.into_value(), + None => Value::Null, } } } -impl<'a, S: From> From<&'a str> for Value { - fn from(s: &'a str) -> Self { - Self::scalar(s.to_owned()) +impl IntoValue for &str +where + String: Into, +{ + fn into_value(self) -> Value { + Value::scalar(self.to_owned()) } } -impl<'a, S: From> From> for Value { - fn from(s: Cow<'a, str>) -> Self { - Self::scalar(s.into_owned()) +impl IntoValue for Cow<'_, str> +where + String: Into, +{ + fn into_value(self) -> Value { + Value::scalar(self.into_owned()) } } -impl> From for Value { - fn from(s: String) -> Self { - Self::scalar(s) +impl IntoValue for String +where + String: Into, +{ + fn into_value(self) -> Value { + Value::scalar(self) } } -impl> From for Value { - fn from(i: i32) -> Self { - Self::scalar(i) +impl IntoValue for &ArcStr { + fn into_value(self) -> Value { + Value::scalar(S::from_displayable(self)) } } -impl> From for Value { - fn from(f: f64) -> Self { - Self::scalar(f) +impl IntoValue for ArcStr { + fn into_value(self) -> Value { + (&self).into_value() } } -impl> From for Value { - fn from(b: bool) -> Self { - Self::scalar(b) +impl IntoValue for &CompactString { + fn into_value(self) -> Value { + Value::scalar(S::from_displayable(self)) + } +} + +impl IntoValue for CompactString { + fn into_value(self) -> Value { + (&self).into_value() + } +} + +impl IntoValue for i32 +where + i32: Into, +{ + fn into_value(self) -> Value { + Value::scalar(self) + } +} + +impl IntoValue for f64 +where + f64: Into, +{ + fn into_value(self) -> Value { + Value::scalar(self) + } +} + +impl IntoValue for bool +where + bool: Into, +{ + fn into_value(self) -> Value { + Value::scalar(self) } } diff --git a/juniper/src/value/scalar.rs b/juniper/src/value/scalar.rs index a541de375..c3b6a8c32 100644 --- a/juniper/src/value/scalar.rs +++ b/juniper/src/value/scalar.rs @@ -1,9 +1,15 @@ -use std::{borrow::Cow, fmt}; +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}; +#[cfg(doc)] +use crate::{InputValue, Value}; pub use juniper_codegen::ScalarValue; @@ -16,32 +22,37 @@ pub trait ParseScalarValue { fn from_str(value: ScalarToken<'_>) -> ParseScalarResult; } -/// A trait marking a type that could be used as internal representation of -/// scalar values in juniper +/// Type that could be used as internal representation of scalar values (e.g. inside [`Value`] and +/// [`InputValue`]). /// -/// The main objective of this abstraction is to allow other libraries to -/// replace the default representation with something that better fits their -/// needs. -/// There is a custom derive (`#[derive(`[`ScalarValue`]`)]`) available that -/// implements most of the required traits automatically for a enum representing -/// a scalar value. However, [`Serialize`] and [`Deserialize`] implementations +/// This abstraction allows other libraries and user code to replace the default representation with +/// something that better fits their needs than [`DefaultScalarValue`]. +/// +/// # 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. /// -/// # Implementing a new scalar value representation -/// The preferred way to define a new scalar value representation is -/// defining a enum containing a variant for each type that needs to be -/// represented at the lowest level. -/// The following example introduces an new variant that is able to store 64 bit -/// integers. +/// # Example +/// +/// The preferred way to define a new [`ScalarValue`] representation is defining an enum containing +/// a variant for each type that needs to be represented at the lowest level. +/// +/// The following example introduces a new variant that is able to store 64-bit integers, and uses +/// a [`CompactString`] for a string representation. /// /// ```rust -/// # use std::fmt; +/// # use std::{any::Any, fmt}; /// # -/// # use serde::{de, Deserialize, Deserializer, Serialize}; +/// # use compact_str::CompactString; /// # use juniper::ScalarValue; +/// # use serde::{de, Deserialize, Deserializer, Serialize}; /// # /// #[derive(Clone, Debug, PartialEq, ScalarValue, Serialize)] /// #[serde(untagged)] +/// #[value(from_displayable_with = from_compact_str)] /// enum MyScalarValue { /// #[value(as_float, as_int)] /// Int(i32), @@ -49,11 +60,30 @@ pub trait ParseScalarValue { /// #[value(as_float)] /// Float(f64), /// #[value(as_str, as_string, into_string)] -/// String(String), +/// String(CompactString), /// #[value(as_bool)] /// Boolean(bool), /// } /// +/// // Custom implementation of `ScalarValue::from_displayable()` method +/// // for efficient conversions from `CompactString` into `MyScalarValue`. +/// fn from_compact_str(s: &Str) -> MyScalarValue { +/// use juniper::AnyExt as _; // allows downcasting directly on types without `dyn` +/// +/// if let Some(s) = s.downcast_ref::() { +/// MyScalarValue::String(s.clone()) +/// } else { +/// s.to_string().into() +/// } +/// } +/// +/// // 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()) +/// } +/// } +/// /// impl<'de> Deserialize<'de> for MyScalarValue { /// fn deserialize>(de: D) -> Result { /// struct Visitor; @@ -111,7 +141,7 @@ pub trait ParseScalarValue { /// } /// /// fn visit_string(self, s: String) -> Result { -/// Ok(MyScalarValue::String(s)) +/// Ok(MyScalarValue::String(s.into())) /// } /// } /// @@ -120,6 +150,7 @@ pub trait ParseScalarValue { /// } /// ``` /// +/// [`CompactString`]: compact_str::CompactString /// [`Deserialize`]: trait@serde::Deserialize /// [`Serialize`]: trait@serde::Serialize pub trait ScalarValue: @@ -223,8 +254,49 @@ pub trait ScalarValue: unreachable!("`ScalarValue` must represent at least one of the GraphQL spec types") } } + + /// Creates this [`ScalarValue`] from the provided [`fmt::Display`] type. + /// + /// 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`]`>`. + /// + /// # Example + /// + /// See the [example in trait documentation](ScalarValue#example) for how it can be used. + #[must_use] + fn from_displayable(s: &Str) -> Self { + s.to_string().into() + } } +/// 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`. + #[must_use] + fn is(&self) -> bool { + TypeId::of::() == self.type_id() + } + + /// Returns [`Some`] reference to this value if it's of type `T`, or [`None`] otherwise. + #[must_use] + fn downcast_ref(&self) -> Option<&T> { + self.is::() + .then(|| unsafe { &*(ptr::from_ref(self) as *const T) }) + } + + /// Returns [`Some`] mutable reference to this value if it's of type `T`, or [`None`] otherwise. + #[must_use] + fn downcast_mut(&mut self) -> Option<&mut T> { + // `self.is::()` produces a false positive here: borrowed data escapes outside of method + (TypeId::of::() == TypeId::of::()) + .then(|| unsafe { &mut *(ptr::from_mut(self) as *mut T) }) + } +} + +impl AnyExt for T {} + /// The default [`ScalarValue`] representation in [`juniper`]. /// /// These types closely follow the [GraphQL specification][0]. diff --git a/juniper_codegen/CHANGELOG.md b/juniper_codegen/CHANGELOG.md index 7c54e2795..d4d51a8c1 100644 --- a/juniper_codegen/CHANGELOG.md +++ b/juniper_codegen/CHANGELOG.md @@ -10,10 +10,15 @@ All user visible changes to `juniper_codegen` crate will be documented in this f ### BC Breaks -- Bumped up [MSRV] to 1.85. ([#1272], [todo]) +- Bumped up [MSRV] to 1.85. ([#1272], [1b1fc618]) + +### Added + +- Support of top-level `#[value(from_displayable_with = ...)]` attribute in `derive(ScalarValue)`. ([#1324]) [#1272]: /../../pull/1272 -[todo]: /../../commit/todo +[#1324]: /../../pull/1324 +[1b1fc618]: /../../commit/1b1fc61879ffdd640d741e187dc20678bf7ab295 diff --git a/juniper_codegen/src/lib.rs b/juniper_codegen/src/lib.rs index bc6060163..4100f6880 100644 --- a/juniper_codegen/src/lib.rs +++ b/juniper_codegen/src/lib.rs @@ -415,7 +415,7 @@ pub fn derive_enum(input: TokenStream) -> TokenStream { /// struct UserId(String); /// ``` /// -/// All of the methods inherited from `Newtype`'s field may also be overridden +/// All the methods inherited from `Newtype`'s field may also be overridden /// with the attributes described below. /// /// # Custom resolving @@ -423,7 +423,7 @@ pub fn derive_enum(input: TokenStream) -> TokenStream { /// Customization of a [GraphQL scalar][0] type resolving is possible via /// `#[graphql(to_output_with = )]` attribute: /// ```rust -/// # use juniper::{GraphQLScalar, ScalarValue, Value}; +/// # use juniper::{GraphQLScalar, IntoValue as _, ScalarValue, Value}; /// # /// #[derive(GraphQLScalar)] /// #[graphql(to_output_with = to_output, transparent)] @@ -431,8 +431,7 @@ pub fn derive_enum(input: TokenStream) -> TokenStream { /// /// /// Increments [`Incremented`] before converting into a [`Value`]. /// fn to_output(v: &Incremented) -> Value { -/// let inc = v.0 + 1; -/// Value::from(inc) +/// (v.0 + 1).into_value() /// } /// ``` /// @@ -772,22 +771,25 @@ pub fn graphql_scalar(attr: TokenStream, body: TokenStream) -> TokenStream { }) } -/// `#[derive(ScalarValue)]` macro for deriving a [`ScalarValue`] -/// implementation. +/// `#[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 argumentes (names correspond to [`ScalarValue`] required +/// `as_bool` attribute arguments (names correspond to [`ScalarValue`] required /// methods). /// +/// Additional `from_displayable_with` argument could be used to specify a custom +/// implementation to override the default `ScalarValue::from_displayable()` method. +/// /// ```rust -/// # use std::fmt; +/// # use std::{any::Any, fmt}; /// # /// # use serde::{de, Deserialize, Deserializer, Serialize}; /// # use juniper::ScalarValue; /// # /// #[derive(Clone, Debug, PartialEq, ScalarValue, Serialize)] /// #[serde(untagged)] +/// #[value(from_displayable_with = from_custom_str)] /// enum MyScalarValue { /// #[value(as_float, as_int)] /// Int(i32), @@ -805,6 +807,23 @@ pub fn graphql_scalar(attr: TokenStream, body: TokenStream) -> TokenStream { /// 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 { +/// use juniper::AnyExt as _; // allows downcasting directly on types without `dyn` +/// +/// // Imagine this is some custom optimized string type. +/// struct CustomString(String); +/// +/// // We specialize the conversion for this type without going through expensive +/// // `ToString` -> `From` conversion with allocation. +/// if let Some(s) = s.downcast_ref::() { +/// MyScalarValue::String(s.0.clone()) +/// } else { +/// s.to_string().into() +/// } +/// } +/// /// impl<'de> Deserialize<'de> for MyScalarValue { /// fn deserialize>(de: D) -> Result { /// struct Visitor; @@ -1361,7 +1380,7 @@ pub fn graphql_interface(attr: TokenStream, body: TokenStream) -> TokenStream { /// /// For more info and possibilities see [`#[graphql_interface]`][0] macro. /// -/// [0]: crate::graphql_interface +/// [0]: macro@crate::graphql_interface /// [1]: https://spec.graphql.org/October2021#sec-Interfaces #[proc_macro_derive(GraphQLInterface, attributes(graphql))] pub fn derive_interface(body: TokenStream) -> TokenStream { diff --git a/juniper_codegen/src/scalar_value/mod.rs b/juniper_codegen/src/scalar_value/mod.rs index 63ebe2381..4af754262 100644 --- a/juniper_codegen/src/scalar_value/mod.rs +++ b/juniper_codegen/src/scalar_value/mod.rs @@ -14,7 +14,10 @@ use syn::{ use crate::common::{ SpanContainer, diagnostic, filter_attrs, - parse::{ParseBufferExt as _, attr::err}, + parse::{ + ParseBufferExt as _, + attr::{OptionExt as _, err}, + }, }; /// [`diagnostic::Scope`] of errors for `#[derive(ScalarValue)]` macro. @@ -78,6 +81,7 @@ pub fn expand_derive(input: TokenStream) -> syn::Result { generics: ast.generics, variants: data_enum.variants.into_iter().collect(), methods, + from_displayable: attr.from_displayable.map(SpanContainer::into_inner), } .into_token_stream()) } @@ -88,6 +92,10 @@ pub fn expand_derive(input: TokenStream) -> syn::Result { struct Attr { /// Allows missing [`Method`]s. allow_missing_attrs: bool, + + /// Explicitly specified function to be used as `ScalarValue::from_displayable()` + /// implementation. + from_displayable: Option>, } impl Parse for Attr { @@ -99,6 +107,13 @@ impl Parse for Attr { "allow_missing_attributes" => { out.allow_missing_attrs = true; } + "from_displayable_with" => { + input.parse::()?; + let scl = input.parse::()?; + out.from_displayable + .replace(SpanContainer::new(ident.span(), Some(scl.span()), scl)) + .none_or_else(|_| err::dup_arg(&ident))? + } name => { return Err(err::unknown_arg(&ident, name)); } @@ -112,9 +127,11 @@ impl Parse for Attr { impl Attr { /// Tries to merge two [`Attr`]s into a single one, reporting about /// duplicates, if any. - fn try_merge(mut self, another: Self) -> syn::Result { - self.allow_missing_attrs |= another.allow_missing_attrs; - Ok(self) + 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), + }) } /// Parses [`Attr`] from the given multiple `name`d [`syn::Attribute`]s @@ -207,27 +224,24 @@ impl VariantAttr { } } -/// Definition of a [`ScalarValue`] for code generation. -/// -/// [`ScalarValue`]: juniper::ScalarValue +/// Definition of a `ScalarValue` for code generation. struct Definition { - /// [`syn::Ident`] of the enum representing this [`ScalarValue`]. - /// - /// [`ScalarValue`]: juniper::ScalarValue + /// [`syn::Ident`] of the enum representing this `ScalarValue`. ident: syn::Ident, - /// [`syn::Generics`] of the enum representing this [`ScalarValue`]. - /// - /// [`ScalarValue`]: juniper::ScalarValue + /// [`syn::Generics`] of the enum representing this `ScalarValue`. generics: syn::Generics, - /// [`syn::Variant`]s of the enum representing this [`ScalarValue`]. - /// - /// [`ScalarValue`]: juniper::ScalarValue + /// [`syn::Variant`]s of the enum representing this `ScalarValue`. variants: Vec, /// [`Variant`]s marked with a [`Method`] attribute. methods: HashMap>, + + /// Custom definition to call in `ScalarValue::from_displayable()` method. + /// + /// If [`None`] then `ScalarValue::from_displayable()` method is not generated. + from_displayable: Option, } impl ToTokens for Definition { @@ -239,9 +253,7 @@ impl ToTokens for Definition { } impl Definition { - /// Returns generated code implementing [`ScalarValue`]. - /// - /// [`ScalarValue`]: juniper::ScalarValue + /// Returns generated code implementing `ScalarValue`. fn impl_scalar_value_tokens(&self) -> TokenStream { let ident = &self.ident; let (impl_gens, ty_gens, where_clause) = self.generics.split_for_impl(); @@ -294,12 +306,23 @@ impl Definition { } }); + let from_displayable = self.from_displayable.as_ref().map(|expr| { + quote! { + fn from_displayable( + __s: &Str, + ) -> Self { + #expr(__s) + } + } + }); + quote! { #[automatically_derived] impl #impl_gens ::juniper::ScalarValue for #ident #ty_gens #where_clause { #( #methods )* + #from_displayable } } } diff --git a/tests/integration/Cargo.toml b/tests/integration/Cargo.toml index b9056b9a9..c977e0025 100644 --- a/tests/integration/Cargo.toml +++ b/tests/integration/Cargo.toml @@ -13,6 +13,7 @@ juniper_subscriptions = { path = "../../juniper_subscriptions" } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" tokio = { version = "1.0", features = ["rt", "macros", "time"] } +smartstring = "1.0" [lints.clippy] allow_attributes = "warn" diff --git a/tests/integration/tests/common/mod.rs b/tests/integration/tests/common/mod.rs index 16dae58e0..3fdabb8d7 100644 --- a/tests/integration/tests/common/mod.rs +++ b/tests/integration/tests/common/mod.rs @@ -1,7 +1,8 @@ use std::fmt; -use juniper::ScalarValue; +use juniper::{InputValue, IntoInputValue, IntoValue, ScalarValue, Value}; use serde::{Deserialize, Deserializer, Serialize, de}; +use smartstring::alias::CompactString; /// Common utilities used across tests. pub mod util { @@ -155,6 +156,22 @@ impl<'de> Deserialize<'de> for MyScalarValue { } } +/// Assert that [`IntoValue`] could be implemented for a foreign type when local [`MyScalarValue`] +/// is involved. +impl IntoValue for CompactString { + fn into_value(self) -> Value { + Value::Scalar(MyScalarValue::from_displayable(&self)) + } +} + +/// Assert that [`IntoInputValue`] could be implemented for a foreign type when local +/// [`MyScalarValue`] is involved. +impl IntoInputValue for CompactString { + fn into_input_value(self) -> InputValue { + InputValue::Scalar(MyScalarValue::from_displayable(&self)) + } +} + /// Definitions shadowing [`std::prelude`] items to check whether macro expansion is hygienic. pub mod hygiene { pub use std::prelude::rust_2021 as prelude;