diff --git a/integration_tests/codegen_fail/fail/interface/argument_wrong_default_array.stderr b/integration_tests/codegen_fail/fail/interface/argument_wrong_default_array.stderr index b745a82d..c134913f 100644 --- a/integration_tests/codegen_fail/fail/interface/argument_wrong_default_array.stderr +++ b/integration_tests/codegen_fail/fail/interface/argument_wrong_default_array.stderr @@ -5,6 +5,9 @@ error[E0277]: the trait bound `[bool; 2]: From<[bool; 3]>` is not satisfied | ^^^^^^^^^^^^^^^^^^^^ the trait `From<[bool; 3]>` is not implemented for `[bool; 2]` | = help: the following implementations were found: + <&'a [ascii::ascii_char::AsciiChar] as From<&'a ascii::ascii_str::AsciiStr>> + <&'a [u8] as From<&'a ascii::ascii_str::AsciiStr>> + <&'a mut [ascii::ascii_char::AsciiChar] as From<&'a mut ascii::ascii_str::AsciiStr>> <[T; LANES] as From>> <[bool; LANES] as From>> = note: required because of the requirements on the impl of `Into<[bool; 2]>` for `[bool; 3]` diff --git a/integration_tests/codegen_fail/fail/object/argument_wrong_default_array.stderr b/integration_tests/codegen_fail/fail/object/argument_wrong_default_array.stderr index 27332cfe..6992876c 100644 --- a/integration_tests/codegen_fail/fail/object/argument_wrong_default_array.stderr +++ b/integration_tests/codegen_fail/fail/object/argument_wrong_default_array.stderr @@ -5,6 +5,9 @@ error[E0277]: the trait bound `[bool; 2]: From<[bool; 3]>` is not satisfied | ^^^^^^^^^^^^^^^^^ the trait `From<[bool; 3]>` is not implemented for `[bool; 2]` | = help: the following implementations were found: + <&'a [ascii::ascii_char::AsciiChar] as From<&'a ascii::ascii_str::AsciiStr>> + <&'a [u8] as From<&'a ascii::ascii_str::AsciiStr>> + <&'a mut [ascii::ascii_char::AsciiChar] as From<&'a mut ascii::ascii_str::AsciiStr>> <[T; LANES] as From>> <[bool; LANES] as From>> = note: required because of the requirements on the impl of `Into<[bool; 2]>` for `[bool; 3]` diff --git a/integration_tests/codegen_fail/fail/scalar/derive_input/attr_transparent_and_with.stderr b/integration_tests/codegen_fail/fail/scalar/derive_input/attr_transparent_and_with.stderr index 829c42c7..ce9f808d 100644 --- a/integration_tests/codegen_fail/fail/scalar/derive_input/attr_transparent_and_with.stderr +++ b/integration_tests/codegen_fail/fail/scalar/derive_input/attr_transparent_and_with.stderr @@ -1,4 +1,4 @@ -error: GraphQL scalar `with = ` attribute can\'t be combined with `transparent`. You can specify custom resolvers with `to_output`, `from_input`, `parse_token` attributes and still use `transparent` for unspecified ones. +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 | 3 | #[graphql_scalar(with = Self, transparent)] diff --git a/integration_tests/codegen_fail/fail/scalar/derive_input/attr_transparent_multiple_named_fields.stderr b/integration_tests/codegen_fail/fail/scalar/derive_input/attr_transparent_multiple_named_fields.stderr index 5a6f9406..a101fdb6 100644 --- a/integration_tests/codegen_fail/fail/scalar/derive_input/attr_transparent_multiple_named_fields.stderr +++ b/integration_tests/codegen_fail/fail/scalar/derive_input/attr_transparent_multiple_named_fields.stderr @@ -1,4 +1,4 @@ -error: GraphQL scalar expected exactly 1 field, e.g., Test { test: i32 } because of `transparent` attribute +error: GraphQL scalar `transparent` attribute argument requires exactly 1 field --> fail/scalar/derive_input/attr_transparent_multiple_named_fields.rs:4:1 | 4 | / struct Scalar { diff --git a/integration_tests/codegen_fail/fail/scalar/derive_input/attr_transparent_multiple_unnamed_fields.stderr b/integration_tests/codegen_fail/fail/scalar/derive_input/attr_transparent_multiple_unnamed_fields.stderr index dfdcff7c..6ff2eb84 100644 --- a/integration_tests/codegen_fail/fail/scalar/derive_input/attr_transparent_multiple_unnamed_fields.stderr +++ b/integration_tests/codegen_fail/fail/scalar/derive_input/attr_transparent_multiple_unnamed_fields.stderr @@ -1,4 +1,4 @@ -error: GraphQL scalar expected exactly 1 field, e.g., Test(i32) because of `transparent` attribute +error: GraphQL scalar `transparent` attribute argument requires exactly 1 field --> fail/scalar/derive_input/attr_transparent_multiple_unnamed_fields.rs:4:1 | 4 | struct Scalar(i32, i32); diff --git a/integration_tests/codegen_fail/fail/scalar/derive_input/attr_transparent_unit_struct.stderr b/integration_tests/codegen_fail/fail/scalar/derive_input/attr_transparent_unit_struct.stderr index 11b661f9..8882861f 100644 --- a/integration_tests/codegen_fail/fail/scalar/derive_input/attr_transparent_unit_struct.stderr +++ b/integration_tests/codegen_fail/fail/scalar/derive_input/attr_transparent_unit_struct.stderr @@ -1,4 +1,4 @@ -error: GraphQL scalar expected exactly 1 field, e.g.: `Test(i32)`, `Test { test: i32 }` because of `transparent` attribute +error: GraphQL scalar `transparent` attribute argument requires exactly 1 field --> fail/scalar/derive_input/attr_transparent_unit_struct.rs:4:1 | 4 | struct ScalarSpecifiedByUrl; diff --git a/integration_tests/codegen_fail/fail/scalar/derive_input/derive_transparent_and_with.stderr b/integration_tests/codegen_fail/fail/scalar/derive_input/derive_transparent_and_with.stderr index 860d5391..74a2fd96 100644 --- a/integration_tests/codegen_fail/fail/scalar/derive_input/derive_transparent_and_with.stderr +++ b/integration_tests/codegen_fail/fail/scalar/derive_input/derive_transparent_and_with.stderr @@ -1,4 +1,4 @@ -error: GraphQL scalar `with = ` attribute can\'t be combined with `transparent`. You can specify custom resolvers with `to_output`, `from_input`, `parse_token` attributes and still use `transparent` for unspecified ones. +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/derive_transparent_and_with.rs:4:18 | 4 | #[graphql(with = Self, transparent)] diff --git a/integration_tests/codegen_fail/fail/scalar/derive_input/derive_transparent_multiple_named_fields.stderr b/integration_tests/codegen_fail/fail/scalar/derive_input/derive_transparent_multiple_named_fields.stderr index d5258cef..1d42b4c1 100644 --- a/integration_tests/codegen_fail/fail/scalar/derive_input/derive_transparent_multiple_named_fields.stderr +++ b/integration_tests/codegen_fail/fail/scalar/derive_input/derive_transparent_multiple_named_fields.stderr @@ -1,4 +1,4 @@ -error: GraphQL scalar expected exactly 1 field, e.g., Test { test: i32 } because of `transparent` attribute +error: GraphQL scalar `transparent` attribute argument requires exactly 1 field --> fail/scalar/derive_input/derive_transparent_multiple_named_fields.rs:4:1 | 4 | / #[graphql(transparent)] diff --git a/integration_tests/codegen_fail/fail/scalar/derive_input/derive_transparent_multiple_unnamed_fields.stderr b/integration_tests/codegen_fail/fail/scalar/derive_input/derive_transparent_multiple_unnamed_fields.stderr index a832801b..e7c5f5a8 100644 --- a/integration_tests/codegen_fail/fail/scalar/derive_input/derive_transparent_multiple_unnamed_fields.stderr +++ b/integration_tests/codegen_fail/fail/scalar/derive_input/derive_transparent_multiple_unnamed_fields.stderr @@ -1,4 +1,4 @@ -error: GraphQL scalar expected exactly 1 field, e.g., Test(i32) because of `transparent` attribute +error: GraphQL scalar `transparent` attribute argument requires exactly 1 field --> fail/scalar/derive_input/derive_transparent_multiple_unnamed_fields.rs:4:1 | 4 | / #[graphql(transparent)] diff --git a/integration_tests/codegen_fail/fail/scalar/derive_input/derive_transparent_unit_struct.stderr b/integration_tests/codegen_fail/fail/scalar/derive_input/derive_transparent_unit_struct.stderr index 3fc185ed..21156b67 100644 --- a/integration_tests/codegen_fail/fail/scalar/derive_input/derive_transparent_unit_struct.stderr +++ b/integration_tests/codegen_fail/fail/scalar/derive_input/derive_transparent_unit_struct.stderr @@ -1,4 +1,4 @@ -error: GraphQL scalar expected exactly 1 field, e.g.: `Test(i32)`, `Test { test: i32 }` because of `transparent` attribute +error: GraphQL scalar `transparent` attribute argument requires exactly 1 field --> fail/scalar/derive_input/derive_transparent_unit_struct.rs:4:1 | 4 | / #[graphql(transparent)] diff --git a/integration_tests/codegen_fail/fail/scalar/type_alias/attr_with_not_all_resolvers.stderr b/integration_tests/codegen_fail/fail/scalar/type_alias/attr_with_not_all_resolvers.stderr index 2bb56a2b..879dfbd0 100644 --- a/integration_tests/codegen_fail/fail/scalar/type_alias/attr_with_not_all_resolvers.stderr +++ b/integration_tests/codegen_fail/fail/scalar/type_alias/attr_with_not_all_resolvers.stderr @@ -1,4 +1,4 @@ -error: GraphQL scalar all custom resolvers have to be provided via `with` or combination of `to_output_with`, `from_input_with`, `parse_token_with` attribute arguments +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 | 6 | type CustomScalar = Scalar; diff --git a/integration_tests/codegen_fail/fail/scalar/type_alias/attr_without_resolvers.stderr b/integration_tests/codegen_fail/fail/scalar/type_alias/attr_without_resolvers.stderr index 626f34a3..6f77be79 100644 --- a/integration_tests/codegen_fail/fail/scalar/type_alias/attr_without_resolvers.stderr +++ b/integration_tests/codegen_fail/fail/scalar/type_alias/attr_without_resolvers.stderr @@ -1,4 +1,4 @@ -error: GraphQL scalar all custom resolvers have to be provided via `with` or combination of `to_output_with`, `from_input_with`, `parse_token_with` attribute arguments +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_without_resolvers.rs:6:1 | 6 | type CustomScalar = Scalar; diff --git a/integration_tests/codegen_fail/fail/scalar_value/missing_attributes.rs b/integration_tests/codegen_fail/fail/scalar_value/missing_attributes.rs new file mode 100644 index 00000000..fb8f9c9b --- /dev/null +++ b/integration_tests/codegen_fail/fail/scalar_value/missing_attributes.rs @@ -0,0 +1,13 @@ +use juniper::ScalarValue; + +#[derive(Clone, Debug, PartialEq, ScalarValue)] +pub enum DefaultScalarValue { + Int(i32), + Float(f64), + #[value(as_str, as_string, into_string)] + String(String), + #[value(as_bool)] + Boolean(bool), +} + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/scalar_value/missing_attributes.stderr b/integration_tests/codegen_fail/fail/scalar_value/missing_attributes.stderr new file mode 100644 index 00000000..1e0ff344 --- /dev/null +++ b/integration_tests/codegen_fail/fail/scalar_value/missing_attributes.stderr @@ -0,0 +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 + | +4 | / pub enum DefaultScalarValue { +5 | | Int(i32), +6 | | Float(f64), +7 | | #[value(as_str, as_string, into_string)] +... | +10 | | Boolean(bool), +11 | | } + | |_^ diff --git a/integration_tests/codegen_fail/fail/scalar_value/multiple_named_fields.rs b/integration_tests/codegen_fail/fail/scalar_value/multiple_named_fields.rs new file mode 100644 index 00000000..f8c5a5e5 --- /dev/null +++ b/integration_tests/codegen_fail/fail/scalar_value/multiple_named_fields.rs @@ -0,0 +1,6 @@ +#[derive(juniper::ScalarValue)] +enum ScalarValue { + Variant { first: i32, second: u64 }, +} + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/scalar_value/multiple_named_fields.stderr b/integration_tests/codegen_fail/fail/scalar_value/multiple_named_fields.stderr new file mode 100644 index 00000000..b328b593 --- /dev/null +++ b/integration_tests/codegen_fail/fail/scalar_value/multiple_named_fields.stderr @@ -0,0 +1,5 @@ +error: GraphQL built-in scalars expected exactly 1 field + --> fail/scalar_value/multiple_named_fields.rs:3:13 + | +3 | Variant { first: i32, second: u64 }, + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/integration_tests/codegen_fail/fail/scalar_value/multiple_unnamed_fields.rs b/integration_tests/codegen_fail/fail/scalar_value/multiple_unnamed_fields.rs new file mode 100644 index 00000000..cb159225 --- /dev/null +++ b/integration_tests/codegen_fail/fail/scalar_value/multiple_unnamed_fields.rs @@ -0,0 +1,6 @@ +#[derive(juniper::ScalarValue)] +enum ScalarValue { + Variant(u32, i64), +} + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/scalar_value/multiple_unnamed_fields.stderr b/integration_tests/codegen_fail/fail/scalar_value/multiple_unnamed_fields.stderr new file mode 100644 index 00000000..b3735784 --- /dev/null +++ b/integration_tests/codegen_fail/fail/scalar_value/multiple_unnamed_fields.stderr @@ -0,0 +1,5 @@ +error: GraphQL built-in scalars expected exactly 1 field + --> fail/scalar_value/multiple_unnamed_fields.rs:3:12 + | +3 | Variant(u32, i64), + | ^^^^^^^^^^ diff --git a/integration_tests/codegen_fail/fail/scalar_value/not_enum.rs b/integration_tests/codegen_fail/fail/scalar_value/not_enum.rs new file mode 100644 index 00000000..812903dc --- /dev/null +++ b/integration_tests/codegen_fail/fail/scalar_value/not_enum.rs @@ -0,0 +1,4 @@ +#[derive(juniper::ScalarValue)] +struct ScalarValue; + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/scalar_value/not_enum.stderr b/integration_tests/codegen_fail/fail/scalar_value/not_enum.stderr new file mode 100644 index 00000000..f8783c99 --- /dev/null +++ b/integration_tests/codegen_fail/fail/scalar_value/not_enum.stderr @@ -0,0 +1,5 @@ +error: GraphQL built-in scalars can only be derived for enums + --> fail/scalar_value/not_enum.rs:2:1 + | +2 | struct ScalarValue; + | ^^^^^^^^^^^^^^^^^^^ diff --git a/integration_tests/codegen_fail/fail/subscription/argument_wrong_default_array.stderr b/integration_tests/codegen_fail/fail/subscription/argument_wrong_default_array.stderr index 4d71e80d..d6416583 100644 --- a/integration_tests/codegen_fail/fail/subscription/argument_wrong_default_array.stderr +++ b/integration_tests/codegen_fail/fail/subscription/argument_wrong_default_array.stderr @@ -5,6 +5,9 @@ error[E0277]: the trait bound `[bool; 2]: From<[bool; 3]>` is not satisfied | ^^^^^^^^^^^^^^^^^^^^^^^ the trait `From<[bool; 3]>` is not implemented for `[bool; 2]` | = help: the following implementations were found: + <&'a [ascii::ascii_char::AsciiChar] as From<&'a ascii::ascii_str::AsciiStr>> + <&'a [u8] as From<&'a ascii::ascii_str::AsciiStr>> + <&'a mut [ascii::ascii_char::AsciiChar] as From<&'a mut ascii::ascii_str::AsciiStr>> <[T; LANES] as From>> <[bool; LANES] as From>> = note: required because of the requirements on the impl of `Into<[bool; 2]>` for `[bool; 3]` diff --git a/integration_tests/juniper_tests/src/codegen/mod.rs b/integration_tests/juniper_tests/src/codegen/mod.rs index 2c617724..ba6bfd72 100644 --- a/integration_tests/juniper_tests/src/codegen/mod.rs +++ b/integration_tests/juniper_tests/src/codegen/mod.rs @@ -7,6 +7,7 @@ mod object_derive; mod scalar_attr_derive_input; mod scalar_attr_type_alias; mod scalar_derive; +mod scalar_value_derive; mod subscription_attr; mod union_attr; mod union_derive; diff --git a/integration_tests/juniper_tests/src/codegen/scalar_value_derive.rs b/integration_tests/juniper_tests/src/codegen/scalar_value_derive.rs new file mode 100644 index 00000000..1648ec42 --- /dev/null +++ b/integration_tests/juniper_tests/src/codegen/scalar_value_derive.rs @@ -0,0 +1,138 @@ +//! Tests for `#[derive(ScalarValue)]` macro. + +use juniper::{DefaultScalarValue, ScalarValue}; +use serde::{Deserialize, Serialize}; + +mod trivial { + use super::*; + + #[derive(Clone, Debug, Deserialize, PartialEq, ScalarValue, Serialize)] + #[serde(untagged)] + pub enum CustomScalarValue { + #[value(as_float, as_int)] + Int(i32), + #[value(as_float)] + Float(f64), + #[value(as_str, as_string, into_string)] + String(String), + #[value(as_bool)] + Boolean(bool), + } + + #[test] + fn into_another() { + assert!(CustomScalarValue::from(5) + .into_another::() + .is_type::()); + assert!(CustomScalarValue::from(0.5_f64) + .into_another::() + .is_type::()); + assert!(CustomScalarValue::from("str".to_owned()) + .into_another::() + .is_type::()); + assert!(CustomScalarValue::from(true) + .into_another::() + .is_type::()); + } +} + +mod named_fields { + use super::*; + + #[derive(Clone, Debug, Deserialize, PartialEq, ScalarValue, Serialize)] + #[serde(untagged)] + pub enum CustomScalarValue { + #[value(as_float, as_int)] + Int { int: i32 }, + #[value(as_float)] + Float(f64), + #[value(as_str, as_string, into_string)] + String(String), + #[value(as_bool)] + Boolean { v: bool }, + } + + #[test] + fn into_another() { + assert!(CustomScalarValue::from(5) + .into_another::() + .is_type::()); + assert!(CustomScalarValue::from(0.5_f64) + .into_another::() + .is_type::()); + assert!(CustomScalarValue::from("str".to_owned()) + .into_another::() + .is_type::()); + assert!(CustomScalarValue::from(true) + .into_another::() + .is_type::()); + } +} + +mod custom_fn { + use super::*; + + #[derive(Clone, Debug, Deserialize, PartialEq, ScalarValue, Serialize)] + #[serde(untagged)] + pub enum CustomScalarValue { + #[value(as_float, as_int)] + Int(i32), + #[value(as_float)] + Float(f64), + #[value( + as_str, + as_string = str::to_owned, + into_string = std::convert::identity, + )] + String(String), + #[value(as_bool)] + Boolean(bool), + } + + #[test] + fn into_another() { + assert!(CustomScalarValue::from(5) + .into_another::() + .is_type::()); + assert!(CustomScalarValue::from(0.5_f64) + .into_another::() + .is_type::()); + assert!(CustomScalarValue::from("str".to_owned()) + .into_another::() + .is_type::()); + assert!(CustomScalarValue::from(true) + .into_another::() + .is_type::()); + } +} + +mod allow_missing_attributes { + use super::*; + + #[derive(Clone, Debug, Deserialize, PartialEq, ScalarValue, Serialize)] + #[serde(untagged)] + #[value(allow_missing_attributes)] + pub enum CustomScalarValue { + Int(i32), + #[value(as_float)] + Float(f64), + #[value(as_str, as_string, into_string)] + String(String), + #[value(as_bool)] + Boolean(bool), + } + + #[test] + fn into_another() { + assert!(CustomScalarValue::Int(5).as_int().is_none()); + assert!(CustomScalarValue::from(0.5_f64) + .into_another::() + .is_type::()); + assert!(CustomScalarValue::from("str".to_owned()) + .into_another::() + .is_type::()); + assert!(CustomScalarValue::from(true) + .into_another::() + .is_type::()); + } +} diff --git a/integration_tests/juniper_tests/src/custom_scalar.rs b/integration_tests/juniper_tests/src/custom_scalar.rs index 3e060623..68f4be4c 100644 --- a/integration_tests/juniper_tests/src/custom_scalar.rs +++ b/integration_tests/juniper_tests/src/custom_scalar.rs @@ -10,204 +10,20 @@ use juniper::{ Value, Variables, }; -#[derive(Clone, Debug, PartialEq, Serialize)] +#[derive(Clone, Debug, PartialEq, ScalarValue, Serialize)] #[serde(untagged)] pub(crate) enum MyScalarValue { + #[value(as_float, as_int)] Int(i32), Long(i64), + #[value(as_float)] Float(f64), + #[value(as_str, as_string, into_string)] String(String), + #[value(as_bool)] Boolean(bool), } -// TODO: replace all underlying `From` impls with `GraphQLScalarValue` macro. -impl From for MyScalarValue { - fn from(v: i32) -> Self { - Self::Int(v) - } -} - -impl From for Option { - fn from(v: MyScalarValue) -> Self { - if let MyScalarValue::Int(v) = v { - Some(v) - } else { - None - } - } -} - -impl<'a> From<&'a MyScalarValue> for Option<&'a i32> { - fn from(v: &'a MyScalarValue) -> Self { - if let MyScalarValue::Int(v) = v { - Some(v) - } else { - None - } - } -} - -impl From for MyScalarValue { - fn from(v: i64) -> Self { - Self::Long(v) - } -} - -impl From for Option { - fn from(v: MyScalarValue) -> Self { - if let MyScalarValue::Long(v) = v { - Some(v) - } else { - None - } - } -} - -impl<'a> From<&'a MyScalarValue> for Option<&'a i64> { - fn from(v: &'a MyScalarValue) -> Self { - if let MyScalarValue::Long(v) = v { - Some(v) - } else { - None - } - } -} - -impl From for MyScalarValue { - fn from(v: f64) -> Self { - Self::Float(v) - } -} - -impl From for Option { - fn from(v: MyScalarValue) -> Self { - if let MyScalarValue::Float(v) = v { - Some(v) - } else { - None - } - } -} - -impl<'a> From<&'a MyScalarValue> for Option<&'a f64> { - fn from(v: &'a MyScalarValue) -> Self { - if let MyScalarValue::Float(v) = v { - Some(v) - } else { - None - } - } -} - -impl From for MyScalarValue { - fn from(v: String) -> Self { - Self::String(v) - } -} - -impl From for Option { - fn from(v: MyScalarValue) -> Self { - if let MyScalarValue::String(v) = v { - Some(v) - } else { - None - } - } -} - -impl<'a> From<&'a MyScalarValue> for Option<&'a String> { - fn from(v: &'a MyScalarValue) -> Self { - if let MyScalarValue::String(v) = v { - Some(v) - } else { - None - } - } -} - -impl From for MyScalarValue { - fn from(v: bool) -> Self { - Self::Boolean(v) - } -} - -impl From for Option { - fn from(v: MyScalarValue) -> Self { - if let MyScalarValue::Boolean(v) = v { - Some(v) - } else { - None - } - } -} - -impl<'a> From<&'a MyScalarValue> for Option<&'a bool> { - fn from(v: &'a MyScalarValue) -> Self { - if let MyScalarValue::Boolean(v) = v { - Some(v) - } else { - None - } - } -} - -impl fmt::Display for MyScalarValue { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Int(v) => v.fmt(f), - Self::Long(v) => v.fmt(f), - Self::Float(v) => v.fmt(f), - Self::String(v) => v.fmt(f), - Self::Boolean(v) => v.fmt(f), - } - } -} - -impl ScalarValue for MyScalarValue { - fn as_int(&self) -> Option { - match self { - Self::Int(i) => Some(*i), - _ => None, - } - } - - fn as_string(&self) -> Option { - match self { - Self::String(s) => Some(s.clone()), - _ => None, - } - } - - fn into_string(self) -> Option { - match self { - Self::String(s) => Some(s), - _ => None, - } - } - - fn as_str(&self) -> Option<&str> { - match self { - Self::String(s) => Some(s.as_str()), - _ => None, - } - } - - fn as_float(&self) -> Option { - match self { - Self::Int(i) => Some(f64::from(*i)), - Self::Float(f) => Some(*f), - _ => None, - } - } - - fn as_boolean(&self) -> Option { - match self { - Self::Boolean(b) => Some(*b), - _ => None, - } - } -} - impl<'de> Deserialize<'de> for MyScalarValue { fn deserialize>(de: D) -> Result { struct Visitor; diff --git a/juniper/CHANGELOG.md b/juniper/CHANGELOG.md index 56da38c6..dc411719 100644 --- a/juniper/CHANGELOG.md +++ b/juniper/CHANGELOG.md @@ -28,6 +28,7 @@ - Redesign `#[graphql_scalar]` macro: ([#1014](https://github.com/graphql-rust/juniper/pull/1014)) - Mirror `#[derive(GraphQLScalar)]` macro. - Support usage on type aliases in case `#[derive(GraphQLScalar)]` isn't applicable because of [orphan rules](https://doc.rust-lang.org/reference/items/implementations.html#orphan-rules). +- Rename `ScalarValue::as_boolean` to `ScalarValue::as_bool`. ([#1025](https://github.com/graphql-rust/juniper/pull/1025)) ## Features @@ -42,6 +43,7 @@ - Support `isRepeatable` field on directives. ([#1003](https://github.com/graphql-rust/juniper/pull/1003), [#1000](https://github.com/graphql-rust/juniper/pull/1000)) - Support `__Schema.description`, `__Type.specifiedByURL` and `__Directive.isRepeatable` fields in introspection. ([#1003](https://github.com/graphql-rust/juniper/pull/1003), [#1000](https://github.com/graphql-rust/juniper/pull/1000)) - Support directives on variables definitions. ([#1005](https://github.com/graphql-rust/juniper/pull/1005)) +- Implement `#[derive(ScalarValue)]` macro to derive `ScalarValue` on enums. ([#1025](https://github.com/graphql-rust/juniper/pull/1025)) ## Fixes diff --git a/juniper/src/executor/look_ahead.rs b/juniper/src/executor/look_ahead.rs index d4664287..41bae7ee 100644 --- a/juniper/src/executor/look_ahead.rs +++ b/juniper/src/executor/look_ahead.rs @@ -153,7 +153,7 @@ where if let LookAheadValue::Scalar(s) = LookAheadValue::from_input_value(&v.item, vars) { - s.as_boolean().unwrap_or(false) + s.as_bool().unwrap_or(false) } else { false } @@ -168,7 +168,7 @@ where if let LookAheadValue::Scalar(b) = LookAheadValue::from_input_value(&v.item, vars) { - b.as_boolean().map(::std::ops::Not::not).unwrap_or(false) + b.as_bool().map(::std::ops::Not::not).unwrap_or(false) } else { false } diff --git a/juniper/src/schema/translate/graphql_parser.rs b/juniper/src/schema/translate/graphql_parser.rs index 859bf942..dc0ab05d 100644 --- a/juniper/src/schema/translate/graphql_parser.rs +++ b/juniper/src/schema/translate/graphql_parser.rs @@ -108,7 +108,7 @@ impl GraphQLParserTranslator { ExternalValue::Int(ExternalNumber::from(v)) } else if let Some(v) = x.as_float() { ExternalValue::Float(v) - } else if let Some(v) = x.as_boolean() { + } else if let Some(v) = x.as_bool() { ExternalValue::Boolean(v) } else { panic!("unknown argument type") diff --git a/juniper/src/types/scalars.rs b/juniper/src/types/scalars.rs index 2ba5e5ea..66e26e74 100644 --- a/juniper/src/types/scalars.rs +++ b/juniper/src/types/scalars.rs @@ -281,7 +281,7 @@ mod impl_boolean_scalar { pub(super) fn from_input(v: &InputValue) -> Result { v.as_scalar_value() - .and_then(ScalarValue::as_boolean) + .and_then(ScalarValue::as_bool) .ok_or_else(|| format!("Expected `Boolean`, found: {}", v)) } diff --git a/juniper/src/value/scalar.rs b/juniper/src/value/scalar.rs index 1359312f..d14e8a6a 100644 --- a/juniper/src/value/scalar.rs +++ b/juniper/src/value/scalar.rs @@ -4,6 +4,8 @@ use serde::{de::DeserializeOwned, Serialize}; use crate::parser::{ParseError, ScalarToken}; +pub use juniper_codegen::ScalarValue; + /// The result of converting a string into a scalar value pub type ParseScalarResult<'a, S = DefaultScalarValue> = Result>; @@ -13,226 +15,44 @@ pub trait ParseScalarValue { fn from_str(value: ScalarToken<'_>) -> ParseScalarResult<'_, S>; } -// TODO: Revisit this doc, once `GraphQLScalarValue` macro is re-implemented. /// A trait marking a type that could be used as internal representation of /// scalar values in juniper /// /// 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(juniper::GraphQLScalarValue)]`) available that implements -/// most of the required traits automatically for a enum representing a scalar value. -/// However, [`Serialize`](trait@serde::Serialize) and [`Deserialize`](trait@serde::Deserialize) -/// implementations are expected to be provided. +/// 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 +/// 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. +/// 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. /// /// ```rust /// # use std::{fmt, convert::TryInto as _}; +/// # /// # use serde::{de, Deserialize, Deserializer, Serialize}; /// # use juniper::ScalarValue; /// # -/// #[derive(Clone, Debug, PartialEq, Serialize)] +/// #[derive(Clone, Debug, PartialEq, ScalarValue, Serialize)] /// #[serde(untagged)] /// enum MyScalarValue { +/// #[value(as_float, as_int)] /// Int(i32), /// Long(i64), +/// #[value(as_float)] /// Float(f64), +/// #[value(as_str, as_string, into_string)] /// String(String), +/// #[value(as_bool)] /// Boolean(bool), /// } /// -/// impl From for MyScalarValue { -/// fn from(v: i32) -> Self { -/// Self::Int(v) -/// } -/// } -/// -/// impl From for Option { -/// fn from(v: MyScalarValue) -> Self { -/// if let MyScalarValue::Int(v) = v { -/// Some(v) -/// } else { -/// None -/// } -/// } -/// } -/// -/// impl<'a> From<&'a MyScalarValue> for Option<&'a i32> { -/// fn from(v: &'a MyScalarValue) -> Self { -/// if let MyScalarValue::Int(v) = v { -/// Some(v) -/// } else { -/// None -/// } -/// } -/// } -/// -/// impl From for MyScalarValue { -/// fn from(v: i64) -> Self { -/// Self::Long(v) -/// } -/// } -/// -/// impl From for Option { -/// fn from(v: MyScalarValue) -> Self { -/// if let MyScalarValue::Long(v) = v { -/// Some(v) -/// } else { -/// None -/// } -/// } -/// } -/// -/// impl<'a> From<&'a MyScalarValue> for Option<&'a i64> { -/// fn from(v: &'a MyScalarValue) -> Self { -/// if let MyScalarValue::Long(v) = v { -/// Some(v) -/// } else { -/// None -/// } -/// } -/// } -/// -/// impl From for MyScalarValue { -/// fn from(v: f64) -> Self { -/// Self::Float(v) -/// } -/// } -/// -/// impl From for Option { -/// fn from(v: MyScalarValue) -> Self { -/// if let MyScalarValue::Float(v) = v { -/// Some(v) -/// } else { -/// None -/// } -/// } -/// } -/// -/// impl<'a> From<&'a MyScalarValue> for Option<&'a f64> { -/// fn from(v: &'a MyScalarValue) -> Self { -/// if let MyScalarValue::Float(v) = v { -/// Some(v) -/// } else { -/// None -/// } -/// } -/// } -/// -/// impl From for MyScalarValue { -/// fn from(v: String) -> Self { -/// Self::String(v) -/// } -/// } -/// -/// impl From for Option { -/// fn from(v: MyScalarValue) -> Self { -/// if let MyScalarValue::String(v) = v { -/// Some(v) -/// } else { -/// None -/// } -/// } -/// } -/// -/// impl<'a> From<&'a MyScalarValue> for Option<&'a String> { -/// fn from(v: &'a MyScalarValue) -> Self { -/// if let MyScalarValue::String(v) = v { -/// Some(v) -/// } else { -/// None -/// } -/// } -/// } -/// -/// impl From for MyScalarValue { -/// fn from(v: bool) -> Self { -/// Self::Boolean(v) -/// } -/// } -/// -/// impl From for Option { -/// fn from(v: MyScalarValue) -> Self { -/// if let MyScalarValue::Boolean(v) = v { -/// Some(v) -/// } else { -/// None -/// } -/// } -/// } -/// -/// impl<'a> From<&'a MyScalarValue> for Option<&'a bool> { -/// fn from(v: &'a MyScalarValue) -> Self { -/// if let MyScalarValue::Boolean(v) = v { -/// Some(v) -/// } else { -/// None -/// } -/// } -/// } -/// -/// impl fmt::Display for MyScalarValue { -/// fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { -/// match self { -/// Self::Int(v) => v.fmt(f), -/// Self::Long(v) => v.fmt(f), -/// Self::Float(v) => v.fmt(f), -/// Self::String(v) => v.fmt(f), -/// Self::Boolean(v) => v.fmt(f), -/// } -/// } -/// } -/// -/// impl ScalarValue for MyScalarValue { -/// fn as_int(&self) -> Option { -/// match self { -/// Self::Int(i) => Some(*i), -/// _ => None, -/// } -/// } -/// -/// fn as_string(&self) -> Option { -/// match self { -/// Self::String(s) => Some(s.clone()), -/// _ => None, -/// } -/// } -/// -/// fn into_string(self) -> Option { -/// match self { -/// Self::String(s) => Some(s), -/// _ => None, -/// } -/// } -/// -/// fn as_str(&self) -> Option<&str> { -/// match self { -/// Self::String(s) => Some(s.as_str()), -/// _ => None, -/// } -/// } -/// -/// fn as_float(&self) -> Option { -/// match self { -/// Self::Int(i) => Some(f64::from(*i)), -/// Self::Float(f) => Some(*f), -/// _ => None, -/// } -/// } -/// -/// fn as_boolean(&self) -> Option { -/// match self { -/// Self::Boolean(b) => Some(*b), -/// _ => None, -/// } -/// } -/// } -/// /// impl<'de> Deserialize<'de> for MyScalarValue { /// fn deserialize>(de: D) -> Result { /// struct Visitor; @@ -298,6 +118,9 @@ pub trait ParseScalarValue { /// } /// } /// ``` +/// +/// [`Deserialize`]: trait@serde::Deserialize +/// [`Serialize`]: trait@serde::Serialize pub trait ScalarValue: fmt::Debug + fmt::Display @@ -383,7 +206,7 @@ pub trait ScalarValue: /// all possible [`ScalarValue`]s. /// /// [`GraphQLValue`]: crate::GraphQLValue - fn as_boolean(&self) -> Option; + fn as_bool(&self) -> Option; /// Converts this [`ScalarValue`] into another one. fn into_another(self) -> S { @@ -391,7 +214,7 @@ pub trait ScalarValue: S::from(i) } else if let Some(f) = self.as_float() { S::from(f) - } else if let Some(b) = self.as_boolean() { + } else if let Some(b) = self.as_bool() { S::from(b) } else if let Some(s) = self.into_string() { S::from(s) @@ -406,12 +229,13 @@ pub trait ScalarValue: /// These types closely follow the [GraphQL specification][0]. /// /// [0]: https://spec.graphql.org/June2018 -#[derive(Clone, Debug, PartialEq, Serialize)] +#[derive(Clone, Debug, PartialEq, ScalarValue, Serialize)] #[serde(untagged)] pub enum DefaultScalarValue { /// [`Int` scalar][0] as a signed 32‐bit numeric non‐fractional value. /// /// [0]: https://spec.graphql.org/June2018/#sec-Int + #[value(as_float, as_int)] Int(i32), /// [`Float` scalar][0] as a signed double‐precision fractional values as @@ -419,190 +243,23 @@ pub enum DefaultScalarValue { /// /// [0]: https://spec.graphql.org/June2018/#sec-Float /// [IEEE 754]: https://en.wikipedia.org/wiki/IEEE_floating_point + #[value(as_float)] Float(f64), /// [`String` scalar][0] as a textual data, represented as UTF‐8 character /// sequences. /// /// [0]: https://spec.graphql.org/June2018/#sec-String + #[value(as_str, as_string, into_string)] String(String), /// [`Boolean` scalar][0] as a `true` or `false` value. /// /// [0]: https://spec.graphql.org/June2018/#sec-Boolean + #[value(as_bool)] Boolean(bool), } -// TODO: Revisit these impls, once `GraphQLScalarValue` macro is re-implemented. -impl From for DefaultScalarValue { - fn from(v: i32) -> Self { - Self::Int(v) - } -} - -impl From for Option { - fn from(v: DefaultScalarValue) -> Self { - if let DefaultScalarValue::Int(v) = v { - Some(v) - } else { - None - } - } -} - -impl<'a> From<&'a DefaultScalarValue> for Option<&'a i32> { - fn from(v: &'a DefaultScalarValue) -> Self { - if let DefaultScalarValue::Int(v) = v { - Some(v) - } else { - None - } - } -} - -impl From for DefaultScalarValue { - fn from(v: f64) -> Self { - Self::Float(v) - } -} - -impl From for Option { - fn from(v: DefaultScalarValue) -> Self { - if let DefaultScalarValue::Float(v) = v { - Some(v) - } else { - None - } - } -} - -impl<'a> From<&'a DefaultScalarValue> for Option<&'a f64> { - fn from(v: &'a DefaultScalarValue) -> Self { - if let DefaultScalarValue::Float(v) = v { - Some(v) - } else { - None - } - } -} - -impl From for DefaultScalarValue { - fn from(v: String) -> Self { - Self::String(v) - } -} - -impl From for Option { - fn from(v: DefaultScalarValue) -> Self { - if let DefaultScalarValue::String(v) = v { - Some(v) - } else { - None - } - } -} - -impl<'a> From<&'a DefaultScalarValue> for Option<&'a String> { - fn from(v: &'a DefaultScalarValue) -> Self { - if let DefaultScalarValue::String(v) = v { - Some(v) - } else { - None - } - } -} - -impl From for DefaultScalarValue { - fn from(v: bool) -> Self { - Self::Boolean(v) - } -} - -impl From for Option { - fn from(v: DefaultScalarValue) -> Self { - if let DefaultScalarValue::Boolean(v) = v { - Some(v) - } else { - None - } - } -} - -impl<'a> From<&'a DefaultScalarValue> for Option<&'a bool> { - fn from(v: &'a DefaultScalarValue) -> Self { - if let DefaultScalarValue::Boolean(v) = v { - Some(v) - } else { - None - } - } -} - -impl fmt::Display for DefaultScalarValue { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Int(v) => v.fmt(f), - Self::Float(v) => v.fmt(f), - Self::String(v) => v.fmt(f), - Self::Boolean(v) => v.fmt(f), - } - } -} - -impl ScalarValue for DefaultScalarValue { - fn as_int(&self) -> Option { - match self { - Self::Int(i) => Some(*i), - _ => None, - } - } - - fn as_float(&self) -> Option { - match self { - Self::Int(i) => Some(f64::from(*i)), - Self::Float(f) => Some(*f), - _ => None, - } - } - - fn as_str(&self) -> Option<&str> { - match self { - Self::String(s) => Some(s.as_str()), - _ => None, - } - } - - fn as_string(&self) -> Option { - match self { - Self::String(s) => Some(s.clone()), - _ => None, - } - } - - fn into_string(self) -> Option { - match self { - Self::String(s) => Some(s), - _ => None, - } - } - - fn as_boolean(&self) -> Option { - match self { - Self::Boolean(b) => Some(*b), - _ => None, - } - } - - fn into_another(self) -> S { - match self { - Self::Int(i) => S::from(i), - Self::Float(f) => S::from(f), - Self::String(s) => S::from(s), - Self::Boolean(b) => S::from(b), - } - } -} - impl<'a> From<&'a str> for DefaultScalarValue { fn from(s: &'a str) -> Self { Self::String(s.into()) diff --git a/juniper_codegen/Cargo.toml b/juniper_codegen/Cargo.toml index 4f8ad1c9..07bd7787 100644 --- a/juniper_codegen/Cargo.toml +++ b/juniper_codegen/Cargo.toml @@ -28,3 +28,4 @@ url = "2.0" derive_more = "0.99.7" futures = "0.3" juniper = { version = "0.16.0-dev", path = "../juniper" } +serde = "1.0" diff --git a/juniper_codegen/src/lib.rs b/juniper_codegen/src/lib.rs index 6208a9a3..cb9a0ba4 100644 --- a/juniper_codegen/src/lib.rs +++ b/juniper_codegen/src/lib.rs @@ -115,6 +115,7 @@ mod graphql_object; mod graphql_scalar; mod graphql_subscription; mod graphql_union; +mod scalar_value; use proc_macro::TokenStream; use proc_macro_error::{proc_macro_error, ResultExt as _}; @@ -549,6 +550,114 @@ pub fn graphql_scalar(attr: TokenStream, body: TokenStream) -> TokenStream { .into() } +/// `#[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 +/// methods). +/// +/// ```rust +/// # use std::{fmt, convert::TryInto as _}; +/// # +/// # use serde::{de, Deserialize, Deserializer, Serialize}; +/// # use juniper::ScalarValue; +/// # +/// #[derive(Clone, Debug, PartialEq, ScalarValue, Serialize)] +/// #[serde(untagged)] +/// enum MyScalarValue { +/// #[value(as_float, as_int)] +/// Int(i32), +/// Long(i64), +/// #[value(as_float)] +/// Float(f64), +/// #[value( +/// into_string, +/// as_str, +/// as_string = String::clone, +/// )] +/// // ^^^^^^^^^^^^^ custom resolvers may be provided +/// String(String), +/// #[value(as_bool)] +/// Boolean(bool), +/// } +/// +/// impl<'de> Deserialize<'de> for MyScalarValue { +/// fn deserialize>(de: D) -> Result { +/// struct Visitor; +/// +/// impl<'de> de::Visitor<'de> for Visitor { +/// type Value = MyScalarValue; +/// +/// fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { +/// f.write_str("a valid input value") +/// } +/// +/// fn visit_bool(self, b: bool) -> Result { +/// Ok(MyScalarValue::Boolean(b)) +/// } +/// +/// fn visit_i32(self, n: i32) -> Result { +/// Ok(MyScalarValue::Int(n)) +/// } +/// +/// fn visit_i64(self, n: i64) -> Result { +/// if n <= i64::from(i32::MAX) { +/// self.visit_i32(n.try_into().unwrap()) +/// } else { +/// Ok(MyScalarValue::Long(n)) +/// } +/// } +/// +/// fn visit_u32(self, n: u32) -> Result { +/// if n <= i32::MAX as u32 { +/// self.visit_i32(n.try_into().unwrap()) +/// } else { +/// self.visit_u64(n.into()) +/// } +/// } +/// +/// fn visit_u64(self, n: u64) -> Result { +/// if n <= i64::MAX as u64 { +/// self.visit_i64(n.try_into().unwrap()) +/// } else { +/// // Browser's `JSON.stringify()` serialize all numbers +/// // having no fractional part as integers (no decimal +/// // point), so we must parse large integers as floating +/// // point, otherwise we would error on transferring large +/// // floating point numbers. +/// Ok(MyScalarValue::Float(n as f64)) +/// } +/// } +/// +/// fn visit_f64(self, f: f64) -> Result { +/// Ok(MyScalarValue::Float(f)) +/// } +/// +/// fn visit_str(self, s: &str) -> Result { +/// self.visit_string(s.into()) +/// } +/// +/// fn visit_string(self, s: String) -> Result { +/// Ok(MyScalarValue::String(s)) +/// } +/// } +/// +/// de.deserialize_any(Visitor) +/// } +/// } +/// ``` +/// +/// [`ScalarValue`]: juniper::ScalarValue +#[proc_macro_error] +#[proc_macro_derive(ScalarValue, attributes(value))] +pub fn derive_scalar_value(input: TokenStream) -> TokenStream { + scalar_value::expand_derive(input.into()) + .unwrap_or_abort() + .into() +} + /// `#[graphql_interface]` macro for generating a [GraphQL interface][1] /// implementation for traits and its implementers. /// diff --git a/juniper_codegen/src/result.rs b/juniper_codegen/src/result.rs index 811daf57..f3a06f42 100644 --- a/juniper_codegen/src/result.rs +++ b/juniper_codegen/src/result.rs @@ -14,6 +14,7 @@ pub enum GraphQLScope { ObjectDerive, ScalarAttr, ScalarDerive, + ScalarValueDerive, UnionAttr, UnionDerive, DeriveInputObject, @@ -26,6 +27,7 @@ impl GraphQLScope { Self::InterfaceAttr => "#sec-Interfaces", Self::ObjectAttr | Self::ObjectDerive => "#sec-Objects", Self::ScalarAttr | Self::ScalarDerive => "#sec-Scalars", + Self::ScalarValueDerive => "#sec-Scalars.Built-in-Scalars", Self::UnionAttr | Self::UnionDerive => "#sec-Unions", Self::DeriveInputObject => "#sec-Input-Objects", Self::DeriveEnum => "#sec-Enums", @@ -39,6 +41,7 @@ impl fmt::Display for GraphQLScope { Self::InterfaceAttr => "interface", Self::ObjectAttr | Self::ObjectDerive => "object", Self::ScalarAttr | Self::ScalarDerive => "scalar", + Self::ScalarValueDerive => "built-in scalars", Self::UnionAttr | Self::UnionDerive => "union", Self::DeriveInputObject => "input object", Self::DeriveEnum => "enum", diff --git a/juniper_codegen/src/scalar_value/mod.rs b/juniper_codegen/src/scalar_value/mod.rs new file mode 100644 index 00000000..023ba148 --- /dev/null +++ b/juniper_codegen/src/scalar_value/mod.rs @@ -0,0 +1,531 @@ +//! Code generation for `#[derive(ScalarValue)]` macro. + +use std::{collections::HashMap, convert::TryFrom}; + +use proc_macro2::{Literal, TokenStream}; +use quote::{quote, ToTokens, TokenStreamExt as _}; +use syn::{ + parse::{Parse, ParseStream}, + parse_quote, + spanned::Spanned as _, + token, + visit::Visit, +}; + +use crate::{ + common::parse::{attr::err, ParseBufferExt as _}, + util::{filter_attrs, span_container::SpanContainer}, + GraphQLScope, +}; + +/// [`GraphQLScope`] of errors for `#[derive(ScalarValue)]` macro. +const ERR: GraphQLScope = GraphQLScope::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, + _ => return Err(ERR.custom_error(ast.span(), "can only be derived for enums")), + }; + + let attr = Attr::from_attrs("value", &ast.attrs)?; + + let mut methods = HashMap::>::new(); + for var in data_enum.variants.clone() { + let (ident, field) = (var.ident, Field::try_from(var.fields)?); + for attr in VariantAttr::from_attrs("value", &var.attrs)?.0 { + let (method, expr) = attr.into_inner(); + methods.entry(method).or_default().push(Variant { + ident: ident.clone(), + field: field.clone(), + expr, + }); + } + } + + 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(|| err)) + .fold(None, |acc, &method| { + Some( + acc.map(|acc| format!("{}, {}", acc, method)) + .unwrap_or_else(|| method.to_owned()), + ) + }) + .filter(|_| !attr.allow_missing_attrs); + if let Some(missing_methods) = missing_methods { + return Err(ERR.custom_error( + span, + format!( + "missing `#[value({})]` attributes. In case you are sure \ + that it's ok, use `#[value(allow_missing_attributes)]` to \ + suppress this error.", + missing_methods, + ), + )); + } + + Ok(Definition { + ident: ast.ident, + generics: ast.generics, + variants: data_enum.variants.into_iter().collect(), + methods, + } + .into_token_stream()) +} + +/// Available arguments behind `#[value]` attribute when generating code for +/// an enum definition. +#[derive(Default)] +struct Attr { + /// Allows missing [`Method`]s. + allow_missing_attrs: bool, +} + +impl Parse for Attr { + fn parse(input: ParseStream<'_>) -> syn::Result { + let mut out = Attr::default(); + while !input.is_empty() { + let ident = input.parse::()?; + match ident.to_string().as_str() { + "allow_missing_attributes" => { + out.allow_missing_attrs = true; + } + name => { + return Err(err::unknown_arg(&ident, name)); + } + }; + input.try_parse::()?; + } + Ok(out) + } +} + +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) + } + + /// Parses [`Attr`] from the given multiple `name`d [`syn::Attribute`]s + /// placed on a enum variant. + fn from_attrs(name: &str, attrs: &[syn::Attribute]) -> syn::Result { + filter_attrs(name, attrs) + .map(|attr| attr.parse_args()) + .try_fold(Self::default(), |prev, curr| prev.try_merge(curr?)) + } +} + +/// Possible attribute names of the `#[derive(ScalarValue)]`. +#[derive(Eq, Hash, PartialEq)] +enum Method { + /// `#[value(as_int)]`. + AsInt, + + /// `#[value(as_float)]`. + AsFloat, + + /// `#[value(as_str)]`. + AsStr, + + /// `#[value(as_string)]`. + AsString, + + /// `#[value(into_string)]`. + IntoString, + + /// `#[value(as_bool)]`. + AsBool, +} + +/// Available arguments behind `#[value]` attribute when generating code for an +/// enum variant. +#[derive(Default)] +struct VariantAttr(Vec)>>); + +impl Parse for VariantAttr { + fn parse(input: ParseStream<'_>) -> syn::Result { + let mut out = Vec::new(); + while !input.is_empty() { + let ident = input.parse::()?; + let method = match ident.to_string().as_str() { + "as_int" => Method::AsInt, + "as_float" => Method::AsFloat, + "as_str" => Method::AsStr, + "as_string" => Method::AsString, + "into_string" => Method::IntoString, + "as_bool" => Method::AsBool, + name => { + return Err(err::unknown_arg(&ident, name)); + } + }; + let expr = input + .parse::() + .ok() + .map(|_| input.parse::()) + .transpose()?; + out.push(SpanContainer::new( + ident.span(), + expr.as_ref().map(|e| e.span()), + (method, expr), + )); + input.try_parse::()?; + } + Ok(VariantAttr(out)) + } +} + +impl VariantAttr { + /// Tries to merge two [`VariantAttr`]s into a single one, reporting about + /// duplicates, if any. + fn try_merge(mut self, mut another: Self) -> syn::Result { + let dup = another.0.iter().find(|m| self.0.contains(m)); + if let Some(dup) = dup { + Err(err::dup_arg(dup.span_ident())) + } else { + self.0.append(&mut another.0); + Ok(self) + } + } + + /// Parses [`VariantAttr`] from the given multiple `name`d + /// [`syn::Attribute`]s placed on a enum variant. + fn from_attrs(name: &str, attrs: &[syn::Attribute]) -> syn::Result { + filter_attrs(name, attrs) + .map(|attr| attr.parse_args()) + .try_fold(Self::default(), |prev, curr| prev.try_merge(curr?)) + } +} + +/// Definition of a [`ScalarValue`] for code generation. +/// +/// [`ScalarValue`]: juniper::ScalarValue +struct Definition { + /// [`syn::Ident`] of the enum representing this [`ScalarValue`]. + /// + /// [`ScalarValue`]: juniper::ScalarValue + ident: syn::Ident, + + /// [`syn::Generics`] of the enum representing this [`ScalarValue`]. + /// + /// [`ScalarValue`]: juniper::ScalarValue + generics: syn::Generics, + + /// [`syn::Variant`]s of the enum representing this [`ScalarValue`]. + /// + /// [`ScalarValue`]: juniper::ScalarValue + variants: Vec, + + /// [`Variant`]s marked with a [`Method`] attribute. + methods: HashMap>, +} + +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); + } +} + +impl Definition { + /// Returns generated code implementing [`ScalarValue`]. + /// + /// [`ScalarValue`]: juniper::ScalarValue + fn impl_scalar_value_tokens(&self) -> TokenStream { + let ident = &self.ident; + let (impl_gens, ty_gens, where_clause) = self.generics.split_for_impl(); + + let methods = [ + ( + Method::AsInt, + quote! { fn as_int(&self) -> Option }, + quote! { i32::from(*v) }, + ), + ( + Method::AsFloat, + quote! { fn as_float(&self) -> Option }, + quote! { f64::from(*v) }, + ), + ( + Method::AsStr, + quote! { fn as_str(&self) -> Option<&str> }, + quote! { ::std::convert::AsRef::as_ref(v) }, + ), + ( + Method::AsString, + quote! { fn as_string(&self) -> Option }, + quote! { ::std::string::ToString::to_string(v) }, + ), + ( + Method::IntoString, + quote! { fn into_string(self) -> Option }, + quote! { ::std::string::String::from(v) }, + ), + ( + Method::AsBool, + quote! { fn as_bool(&self) -> Option }, + quote! { 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 => Some(#call), } + }); + quote! { + #sig { + match self { + #(#arms)* + _ => None, + } + } + } + }); + + quote! { + #[automatically_derived] + impl#impl_gens ::juniper::ScalarValue for #ident#ty_gens + #where_clause + { + #(#methods)* + } + } + } + + /// 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 { + let ty_ident = &self.ident; + let (impl_gen, ty_gen, where_clause) = self.generics.split_for_impl(); + + // 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 } }); + + quote! { + #[automatically_derived] + impl#impl_gen ::std::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 ::std::convert::From<#ty_ident#ty_gen> for Option<#var_ty> + #where_clause + { + fn from(ty: #ty_ident#ty_gen) -> Self { + if let #ty_ident::#var_ident#var_field = ty { + Some(v) + } else { + None + } + } + } + + #[automatically_derived] + impl#lf_impl_gen ::std::convert::From<&'___a #ty_ident#ty_gen> for + Option<&'___a #var_ty> + #where_clause + { + fn from(ty: &'___a #ty_ident#ty_gen) -> Self { + if let #ty_ident::#var_ident#var_field = ty { + Some(v) + } else { + None + } + } + } + } + }) + .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: ::std::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 => ::std::fmt::Display::fmt(v, f), } + }); + + quote! { + #[automatically_derived] + impl#impl_gen ::std::fmt::Display for #ident#ty_gen + #where_clause + { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + match self { + #(#arms)* + } + } + } + } + } +} + +/// Single-[`Field`] enum variant. +#[derive(Clone)] +struct Variant { + /// [`Variant`] [`syn::Ident`]. + ident: syn::Ident, + + /// Single [`Variant`] [`Field`]. + field: Field, + + /// Optional resolver provided by [`VariantAttr`]. + expr: Option, +} + +impl Variant { + /// Returns generated code for matching over this [`Variant`]. + fn match_arm(&self) -> TokenStream { + let (ident, field) = (&self.ident, &self.field.match_arg()); + quote! { + Self::#ident#field + } + } +} + +/// Enum [`Variant`] field. +#[derive(Clone)] +enum Field { + /// Named [`Field`]. + Named(syn::Field), + + /// Unnamed [`Field`]. + Unnamed(syn::Field), +} + +impl ToTokens for Field { + fn to_tokens(&self, tokens: &mut TokenStream) { + match self { + Self::Named(f) => f.ident.to_tokens(tokens), + Self::Unnamed(_) => tokens.append(Literal::u8_unsuffixed(0)), + } + } +} + +impl TryFrom for Field { + type Error = syn::Error; + + fn try_from(value: syn::Fields) -> Result { + match value { + syn::Fields::Named(mut f) if f.named.len() == 1 => { + Ok(Self::Named(f.named.pop().unwrap().into_value())) + } + syn::Fields::Unnamed(mut f) if f.unnamed.len() == 1 => { + Ok(Self::Unnamed(f.unnamed.pop().unwrap().into_value())) + } + _ => Err(ERR.custom_error(value.span(), "expected exactly 1 field")), + } + } +} + +impl Field { + /// Returns a [`Field`] for constructing or matching over a [`Variant`]. + fn match_arg(&self) -> TokenStream { + match self { + Self::Named(_) => quote! { { #self: v } }, + Self::Unnamed(_) => quote! { (v) }, + } + } +} + +/// [`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, 'gen> Visit<'ast> for IsVariantGeneric<'gen> { + 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); + } + } + } +}