From 7ee8d6af235ff1d04082cae9bacd76d09dd91954 Mon Sep 17 00:00:00 2001 From: tyranron Date: Wed, 28 May 2025 20:44:04 +0200 Subject: [PATCH 1/9] Bootstrap solution --- juniper/src/ast.rs | 6 ++++++ juniper/src/lib.rs | 4 +++- juniper/src/types/scalars.rs | 4 ++-- juniper/src/value/mod.rs | 9 +++++++- juniper/src/value/scalar.rs | 42 +++++++++++++++++++++++++++++++++++- 5 files changed, 60 insertions(+), 5 deletions(-) diff --git a/juniper/src/ast.rs b/juniper/src/ast.rs index a7ee27ef2..eb11302b1 100644 --- a/juniper/src/ast.rs +++ b/juniper/src/ast.rs @@ -527,6 +527,12 @@ impl<'a, S: From> From<&'a str> for InputValue { } } +impl From<&ArcStr> for InputValue { + fn from(value: &ArcStr) -> Self { + Self::scalar(S::from_custom_string(value)) + } +} + impl<'a, S: From> From> for InputValue { fn from(s: Cow<'a, str>) -> Self { Self::scalar(s.into_owned()) diff --git a/juniper/src/lib.rs b/juniper/src/lib.rs index 1b7687d83..b2d4c0dc7 100644 --- a/juniper/src/lib.rs +++ b/juniper/src/lib.rs @@ -102,7 +102,9 @@ pub use crate::{ }, }, validation::RuleError, - value::{DefaultScalarValue, Object, ParseScalarResult, ParseScalarValue, ScalarValue, Value}, + value::{ + AnyExt, DefaultScalarValue, Object, ParseScalarResult, ParseScalarValue, ScalarValue, Value, + }, }; /// An error that prevented query execution diff --git a/juniper/src/types/scalars.rs b/juniper/src/types/scalars.rs index 4be19356b..5dd4cbebb 100644 --- a/juniper/src/types/scalars.rs +++ b/juniper/src/types/scalars.rs @@ -181,7 +181,7 @@ mod impl_arcstr_scalar { use super::ArcStr; pub(super) fn to_output(v: &ArcStr) -> Value { - Value::scalar(v.to_string()) + Value::scalar(S::from_custom_string(v)) } pub(super) fn from_input(v: &InputValue) -> Result { @@ -201,7 +201,7 @@ mod impl_compactstring_scalar { use super::CompactString; pub(super) fn to_output(v: &CompactString) -> Value { - Value::scalar(v.to_string()) + Value::scalar(S::from_custom_string(v)) } pub(super) fn from_input(v: &InputValue) -> Result { diff --git a/juniper/src/value/mod.rs b/juniper/src/value/mod.rs index 5867a35a1..d82f074cb 100644 --- a/juniper/src/value/mod.rs +++ b/juniper/src/value/mod.rs @@ -3,6 +3,7 @@ mod scalar; use std::{any::TypeId, borrow::Cow, fmt, mem}; +use arcstr::ArcStr; use derive_more::with_trait::From; use crate::{ @@ -12,7 +13,7 @@ use crate::{ pub use self::{ object::Object, - scalar::{DefaultScalarValue, ParseScalarResult, ParseScalarValue, ScalarValue}, + scalar::{DefaultScalarValue, ParseScalarResult, ParseScalarValue, ScalarValue, AnyExt}, }; /// Serializable value returned from query and field execution. @@ -251,6 +252,12 @@ impl<'a, S: From> From<&'a str> for Value { } } +impl From<&ArcStr> for Value { + fn from(value: &ArcStr) -> Self { + Self::scalar(S::from_custom_string(value)) + } +} + impl<'a, S: From> From> for Value { fn from(s: Cow<'a, str>) -> Self { Self::scalar(s.into_owned()) diff --git a/juniper/src/value/scalar.rs b/juniper/src/value/scalar.rs index a541de375..d428bbce5 100644 --- a/juniper/src/value/scalar.rs +++ b/juniper/src/value/scalar.rs @@ -1,4 +1,8 @@ -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}; @@ -223,8 +227,44 @@ pub trait ScalarValue: unreachable!("`ScalarValue` must represent at least one of the GraphQL spec types") } } + + /// Creates this [`ScalarValue`] from the provided custom string 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`]`>`. + fn from_custom_string(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]. From ac0726dfe8f7a9ac1bbfe5ccf394879a406db74a Mon Sep 17 00:00:00 2001 From: tyranron Date: Thu, 29 May 2025 17:49:14 +0200 Subject: [PATCH 2/9] Bootstrap conv --- juniper/src/value/mod.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/juniper/src/value/mod.rs b/juniper/src/value/mod.rs index d82f074cb..201b7446c 100644 --- a/juniper/src/value/mod.rs +++ b/juniper/src/value/mod.rs @@ -288,6 +288,22 @@ impl> From for Value { } } +pub trait IntoValue { + fn into_value(self) -> Value; +} + +impl>, S> IntoValue for T { + fn into_value(self) -> Value { + self.into() + } +} + +impl IntoValue for compact_str::CompactString { + fn into_value(self) -> Value { + todo!() + } +} + #[cfg(test)] mod tests { use crate::graphql_value; From 4ce1cec4a6194a5356f759ec6954513ee38ea0b7 Mon Sep 17 00:00:00 2001 From: tyranron Date: Thu, 29 May 2025 19:21:58 +0200 Subject: [PATCH 3/9] Impl `IntoValue` --- juniper/src/lib.rs | 3 +- juniper/src/macros/graphql_value.rs | 2 +- juniper/src/value/mod.rs | 120 +++++++++++++++++--------- juniper/src/value/scalar.rs | 3 +- tests/integration/Cargo.toml | 1 + tests/integration/tests/common/mod.rs | 10 ++- 6 files changed, 96 insertions(+), 43 deletions(-) diff --git a/juniper/src/lib.rs b/juniper/src/lib.rs index b2d4c0dc7..04d8efe3b 100644 --- a/juniper/src/lib.rs +++ b/juniper/src/lib.rs @@ -103,7 +103,8 @@ pub use crate::{ }, validation::RuleError, value::{ - AnyExt, DefaultScalarValue, Object, ParseScalarResult, ParseScalarValue, ScalarValue, Value, + AnyExt, DefaultScalarValue, IntoValue, Object, ParseScalarResult, ParseScalarValue, + ScalarValue, Value, }, }; 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/value/mod.rs b/juniper/src/value/mod.rs index 201b7446c..ea8899c83 100644 --- a/juniper/src/value/mod.rs +++ b/juniper/src/value/mod.rs @@ -4,6 +4,7 @@ mod scalar; use std::{any::TypeId, borrow::Cow, fmt, mem}; use arcstr::ArcStr; +use compact_str::CompactString; use derive_more::with_trait::From; use crate::{ @@ -13,7 +14,7 @@ use crate::{ pub use self::{ object::Object, - scalar::{DefaultScalarValue, ParseScalarResult, ParseScalarValue, ScalarValue, AnyExt}, + scalar::{AnyExt, DefaultScalarValue, ParseScalarResult, ParseScalarValue, ScalarValue}, }; /// Serializable value returned from query and field execution. @@ -54,10 +55,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()) } @@ -234,73 +232,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 From<&ArcStr> for Value { - fn from(value: &ArcStr) -> Self { - Self::scalar(S::from_custom_string(value)) +impl IntoValue for Cow<'_, str> +where + String: Into, +{ + fn into_value(self) -> Value { + Value::scalar(self.into_owned()) } } -impl<'a, S: From> From> for Value { - fn from(s: Cow<'a, str>) -> Self { - Self::scalar(s.into_owned()) +impl IntoValue for String +where + String: Into, +{ + fn into_value(self) -> Value { + Value::scalar(self) } } -impl> From for Value { - fn from(s: String) -> Self { - Self::scalar(s) +impl IntoValue for &ArcStr { + fn into_value(self) -> Value { + Value::scalar(S::from_custom_string(self)) } } -impl> From for Value { - fn from(i: i32) -> Self { - Self::scalar(i) +impl IntoValue for ArcStr { + fn into_value(self) -> Value { + (&self).into_value() } } -impl> From for Value { - fn from(f: f64) -> Self { - Self::scalar(f) +impl IntoValue for &CompactString { + fn into_value(self) -> Value { + Value::scalar(S::from_custom_string(self)) } } -impl> From for Value { - fn from(b: bool) -> Self { - Self::scalar(b) +impl IntoValue for CompactString { + fn into_value(self) -> Value { + (&self).into_value() } } -pub trait IntoValue { - fn into_value(self) -> Value; +impl IntoValue for i32 +where + i32: Into, +{ + fn into_value(self) -> Value { + Value::scalar(self) + } } -impl>, S> IntoValue for T { +impl IntoValue for f64 +where + f64: Into, +{ fn into_value(self) -> Value { - self.into() + Value::scalar(self) } } -impl IntoValue for compact_str::CompactString { - fn into_value(self) -> Value { - todo!() +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 d428bbce5..925e38c07 100644 --- a/juniper/src/value/scalar.rs +++ b/juniper/src/value/scalar.rs @@ -230,10 +230,11 @@ pub trait ScalarValue: /// Creates this [`ScalarValue`] from the provided custom string type. /// - /// This method should be implemented if [`ScalarValue`] implementation uses some custom string + /// 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`]`>`. + #[must_use] fn from_custom_string(s: &Str) -> Self { s.to_string().into() } 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..b11ff4ec1 100644 --- a/tests/integration/tests/common/mod.rs +++ b/tests/integration/tests/common/mod.rs @@ -1,6 +1,6 @@ use std::fmt; -use juniper::ScalarValue; +use juniper::{IntoValue, ScalarValue, Value}; use serde::{Deserialize, Deserializer, Serialize, de}; /// Common utilities used across tests. @@ -155,6 +155,14 @@ impl<'de> Deserialize<'de> for MyScalarValue { } } +/// Assert that we can implement [`IntoValue`] for a foreign type when local [`MyScalarValue`] is +/// involved. +impl IntoValue for smartstring::alias::CompactString { + fn into_value(self) -> Value { + Value::Scalar(MyScalarValue::from_custom_string(&self)) + } +} + /// Definitions shadowing [`std::prelude`] items to check whether macro expansion is hygienic. pub mod hygiene { pub use std::prelude::rust_2021 as prelude; From cb431887279c4299cb7bbf2fc6fcfc71ada5d9d4 Mon Sep 17 00:00:00 2001 From: tyranron Date: Thu, 29 May 2025 19:31:05 +0200 Subject: [PATCH 4/9] Impl `IntoInputValue` --- juniper/src/ast.rs | 120 ++++++++++++++++------ juniper/src/lib.rs | 4 +- juniper/src/macros/graphql_input_value.rs | 8 +- tests/integration/tests/common/mod.rs | 17 ++- 4 files changed, 108 insertions(+), 41 deletions(-) diff --git a/juniper/src/ast.rs b/juniper/src/ast.rs index eb11302b1..f55f660cb 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,57 +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 From<&ArcStr> for InputValue { - fn from(value: &ArcStr) -> Self { - Self::scalar(S::from_custom_string(value)) +impl IntoInputValue for Cow<'_, str> +where + String: Into, +{ + fn into_input_value(self) -> InputValue { + InputValue::scalar(self.into_owned()) } } -impl<'a, S: From> From> for InputValue { - fn from(s: Cow<'a, str>) -> Self { - Self::scalar(s.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_custom_string(self)) + } +} + +impl IntoInputValue for ArcStr { + fn into_input_value(self) -> InputValue { + (&self).into_input_value() + } +} + +impl IntoInputValue for &CompactString { + fn into_input_value(self) -> InputValue { + InputValue::scalar(S::from_custom_string(self)) } } -impl> From for InputValue { - fn from(s: String) -> Self { - Self::scalar(s) +impl IntoInputValue for CompactString { + fn into_input_value(self) -> InputValue { + (&self).into_input_value() } } -impl> From for InputValue { - fn from(i: i32) -> Self { - Self::scalar(i) +impl IntoInputValue for i32 +where + i32: Into, +{ + fn into_input_value(self) -> InputValue { + InputValue::scalar(self) } } -impl> From for InputValue { - fn from(f: f64) -> Self { - Self::scalar(f) +impl IntoInputValue for f64 +where + f64: 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 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 04d8efe3b..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, 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/tests/integration/tests/common/mod.rs b/tests/integration/tests/common/mod.rs index b11ff4ec1..a2d1f8bc4 100644 --- a/tests/integration/tests/common/mod.rs +++ b/tests/integration/tests/common/mod.rs @@ -1,7 +1,8 @@ use std::fmt; -use juniper::{IntoValue, ScalarValue, Value}; +use juniper::{IntoValue, ScalarValue, Value, IntoInputValue, InputValue}; use serde::{Deserialize, Deserializer, Serialize, de}; +use smartstring::alias::CompactString; /// Common utilities used across tests. pub mod util { @@ -155,14 +156,22 @@ impl<'de> Deserialize<'de> for MyScalarValue { } } -/// Assert that we can implement [`IntoValue`] for a foreign type when local [`MyScalarValue`] is -/// involved. -impl IntoValue for smartstring::alias::CompactString { +/// 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_custom_string(&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_custom_string(&self)) + } +} + /// Definitions shadowing [`std::prelude`] items to check whether macro expansion is hygienic. pub mod hygiene { pub use std::prelude::rust_2021 as prelude; From 23f9c6a0062865372a1208e421a498159ec0d2a9 Mon Sep 17 00:00:00 2001 From: tyranron Date: Thu, 29 May 2025 20:40:32 +0200 Subject: [PATCH 5/9] Support in macro --- juniper/Cargo.toml | 1 + juniper/src/ast.rs | 4 +- juniper/src/types/scalars.rs | 4 +- juniper/src/value/mod.rs | 8 ++- juniper/src/value/scalar.rs | 71 ++++++++++++++++++------- juniper_codegen/src/lib.rs | 37 +++++++++---- juniper_codegen/src/scalar_value/mod.rs | 61 ++++++++++++++------- tests/integration/tests/common/mod.rs | 10 ++-- 8 files changed, 134 insertions(+), 62 deletions(-) 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 f55f660cb..a505969b2 100644 --- a/juniper/src/ast.rs +++ b/juniper/src/ast.rs @@ -572,7 +572,7 @@ where impl IntoInputValue for &ArcStr { fn into_input_value(self) -> InputValue { - InputValue::scalar(S::from_custom_string(self)) + InputValue::scalar(S::from_displayable(self)) } } @@ -584,7 +584,7 @@ impl IntoInputValue for ArcStr { impl IntoInputValue for &CompactString { fn into_input_value(self) -> InputValue { - InputValue::scalar(S::from_custom_string(self)) + InputValue::scalar(S::from_displayable(self)) } } diff --git a/juniper/src/types/scalars.rs b/juniper/src/types/scalars.rs index 5dd4cbebb..ff5339280 100644 --- a/juniper/src/types/scalars.rs +++ b/juniper/src/types/scalars.rs @@ -181,7 +181,7 @@ mod impl_arcstr_scalar { use super::ArcStr; pub(super) fn to_output(v: &ArcStr) -> Value { - Value::scalar(S::from_custom_string(v)) + Value::scalar(S::from_displayable(v)) } pub(super) fn from_input(v: &InputValue) -> Result { @@ -201,7 +201,7 @@ mod impl_compactstring_scalar { use super::CompactString; pub(super) fn to_output(v: &CompactString) -> Value { - Value::scalar(S::from_custom_string(v)) + Value::scalar(S::from_displayable(v)) } pub(super) fn from_input(v: &InputValue) -> Result { diff --git a/juniper/src/value/mod.rs b/juniper/src/value/mod.rs index ea8899c83..08259f60b 100644 --- a/juniper/src/value/mod.rs +++ b/juniper/src/value/mod.rs @@ -5,7 +5,6 @@ use std::{any::TypeId, borrow::Cow, fmt, mem}; use arcstr::ArcStr; use compact_str::CompactString; -use derive_more::with_trait::From; use crate::{ ast::{InputValue, ToInputValue}, @@ -27,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), @@ -297,7 +295,7 @@ where impl IntoValue for &ArcStr { fn into_value(self) -> Value { - Value::scalar(S::from_custom_string(self)) + Value::scalar(S::from_displayable(self)) } } @@ -309,7 +307,7 @@ impl IntoValue for ArcStr { impl IntoValue for &CompactString { fn into_value(self) -> Value { - Value::scalar(S::from_custom_string(self)) + Value::scalar(S::from_displayable(self)) } } diff --git a/juniper/src/value/scalar.rs b/juniper/src/value/scalar.rs index 925e38c07..f5f6767fa 100644 --- a/juniper/src/value/scalar.rs +++ b/juniper/src/value/scalar.rs @@ -8,6 +8,8 @@ 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; @@ -20,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), @@ -53,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; @@ -115,7 +141,7 @@ pub trait ParseScalarValue { /// } /// /// fn visit_string(self, s: String) -> Result { -/// Ok(MyScalarValue::String(s)) +/// Ok(MyScalarValue::String(s.into())) /// } /// } /// @@ -124,6 +150,7 @@ pub trait ParseScalarValue { /// } /// ``` /// +/// [`CompactString`]: compact_str::CompactString /// [`Deserialize`]: trait@serde::Deserialize /// [`Serialize`]: trait@serde::Serialize pub trait ScalarValue: @@ -228,14 +255,18 @@ pub trait ScalarValue: } } - /// Creates this [`ScalarValue`] from the provided custom string type. + /// 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_custom_string(s: &Str) -> Self { + fn from_displayable(s: &Str) -> Self { s.to_string().into() } } diff --git a/juniper_codegen/src/lib.rs b/juniper_codegen/src/lib.rs index bc6060163..d3e8893bb 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), @@ -804,6 +806,23 @@ pub fn graphql_scalar(attr: TokenStream, body: TokenStream) -> TokenStream { /// #[value(as_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 { +/// 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 { @@ -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..00c71da39 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/tests/common/mod.rs b/tests/integration/tests/common/mod.rs index a2d1f8bc4..3fdabb8d7 100644 --- a/tests/integration/tests/common/mod.rs +++ b/tests/integration/tests/common/mod.rs @@ -1,6 +1,6 @@ use std::fmt; -use juniper::{IntoValue, ScalarValue, Value, IntoInputValue, InputValue}; +use juniper::{InputValue, IntoInputValue, IntoValue, ScalarValue, Value}; use serde::{Deserialize, Deserializer, Serialize, de}; use smartstring::alias::CompactString; @@ -156,19 +156,19 @@ impl<'de> Deserialize<'de> for MyScalarValue { } } -/// Assert that [`IntoValue`] could be implemented for a foreign type when local [`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_custom_string(&self)) + Value::Scalar(MyScalarValue::from_displayable(&self)) } } -/// Assert that [`IntoInputValue`] could be implemented for a foreign type when local +/// 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_custom_string(&self)) + InputValue::Scalar(MyScalarValue::from_displayable(&self)) } } From d8382cba0ca602ff85d552b2afa99ab02a095fa4 Mon Sep 17 00:00:00 2001 From: tyranron Date: Thu, 29 May 2025 20:41:59 +0200 Subject: [PATCH 6/9] Dat fmt --- juniper/src/value/scalar.rs | 4 ++-- juniper_codegen/src/lib.rs | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/juniper/src/value/scalar.rs b/juniper/src/value/scalar.rs index f5f6767fa..c3b6a8c32 100644 --- a/juniper/src/value/scalar.rs +++ b/juniper/src/value/scalar.rs @@ -76,8 +76,8 @@ pub trait ParseScalarValue { /// s.to_string().into() /// } /// } -/// -/// // Macro cannot infer and generate this impl if a custom string type is used. +/// +/// // 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()) diff --git a/juniper_codegen/src/lib.rs b/juniper_codegen/src/lib.rs index d3e8893bb..4100f6880 100644 --- a/juniper_codegen/src/lib.rs +++ b/juniper_codegen/src/lib.rs @@ -806,12 +806,12 @@ pub fn graphql_scalar(attr: TokenStream, body: TokenStream) -> TokenStream { /// #[value(as_bool)] /// Boolean(bool), /// } -/// -/// // Custom implementation of `ScalarValue::from_displayable()` method for +/// +/// // 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); /// From 93ffb18baa916336bb0bc40eff0e692809b4996b Mon Sep 17 00:00:00 2001 From: tyranron Date: Fri, 30 May 2025 13:24:34 +0200 Subject: [PATCH 7/9] Polish --- book/src/types/scalars.md | 5 ++--- juniper/src/types/scalars.rs | 8 ++++---- 2 files changed, 6 insertions(+), 7 deletions(-) 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/src/types/scalars.rs b/juniper/src/types/scalars.rs index ff5339280..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(S::from_displayable(v)) + 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(S::from_displayable(v)) + v.into_value() } pub(super) fn from_input(v: &InputValue) -> Result { From 5f82b53a7a81b9a641d608477684f6d0354b02c6 Mon Sep 17 00:00:00 2001 From: tyranron Date: Fri, 30 May 2025 16:01:57 +0200 Subject: [PATCH 8/9] Mention in CHANGELOG --- juniper/CHANGELOG.md | 8 +++++++- juniper_codegen/src/scalar_value/mod.rs | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) 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_codegen/src/scalar_value/mod.rs b/juniper_codegen/src/scalar_value/mod.rs index 00c71da39..4af754262 100644 --- a/juniper_codegen/src/scalar_value/mod.rs +++ b/juniper_codegen/src/scalar_value/mod.rs @@ -309,7 +309,7 @@ impl Definition { let from_displayable = self.from_displayable.as_ref().map(|expr| { quote! { fn from_displayable( - __s: &Str + __s: &Str, ) -> Self { #expr(__s) } From 962c8bee9d1f9f48cfe57d330639b61b000ae1d5 Mon Sep 17 00:00:00 2001 From: tyranron Date: Fri, 30 May 2025 17:04:14 +0200 Subject: [PATCH 9/9] Upd CHANGELOG for codegen too --- juniper_codegen/CHANGELOG.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) 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