From 0ebd19af5a7afa91d2d721808a12f07c5597aac2 Mon Sep 17 00:00:00 2001 From: ilslv <47687266+ilslv@users.noreply.github.com> Date: Mon, 28 Feb 2022 12:34:38 +0300 Subject: [PATCH] Redesign `#[derive(GraphQLScalar)]` and `#[graphql_scalar]` macros (#1017) - `#[derive(GraphQLScalar)]`: - support generic scalars - support structs with single named field - support for overriding resolvers - `#[graphql_scalar]`: - support `transparent` argument Co-authored-by: Kai Ren <tyranron@gmail.com> --- docs/book/content/types/scalars.md | 24 +- .../scalar/derive_input/attr_invalid_url.rs | 6 + .../derive_input/attr_invalid_url.stderr | 5 + .../derive_input/attr_transparent_and_with.rs | 6 + .../attr_transparent_and_with.stderr | 5 + .../attr_transparent_multiple_named_fields.rs | 9 + ...r_transparent_multiple_named_fields.stderr | 8 + ...ttr_transparent_multiple_unnamed_fields.rs | 6 + ...transparent_multiple_unnamed_fields.stderr | 5 + .../attr_transparent_unit_struct.rs | 6 + .../attr_transparent_unit_struct.stderr | 5 + .../scalar/derive_input/derive_invalid_url.rs | 7 + .../derive_input/derive_invalid_url.stderr | 5 + .../derive_transparent_and_with.rs | 7 + .../derive_transparent_and_with.stderr | 5 + ...erive_transparent_multiple_named_fields.rs | 10 + ...e_transparent_multiple_named_fields.stderr | 9 + ...ive_transparent_multiple_unnamed_fields.rs | 7 + ...transparent_multiple_unnamed_fields.stderr | 6 + .../derive_transparent_unit_struct.rs | 7 + .../derive_transparent_unit_struct.stderr | 6 + .../scalar/derive_input/impl_invalid_url.rs | 16 - .../derive_input/impl_invalid_url.stderr | 17 - ...mpl_invalid_url.rs => attr_invalid_url.rs} | 0 ...lid_url.stderr => attr_invalid_url.stderr} | 2 +- ...vers.rs => attr_with_not_all_resolvers.rs} | 0 ...err => attr_with_not_all_resolvers.stderr} | 4 +- ...resolvers.rs => attr_without_resolvers.rs} | 0 ...s.stderr => attr_without_resolvers.stderr} | 4 +- .../juniper_tests/src/codegen/mod.rs | 1 + .../src/codegen/scalar_attr_derive_input.rs | 135 +- .../src/codegen/scalar_attr_type_alias.rs | 2 + .../src/codegen/scalar_derive.rs | 1114 +++++++++++++++++ .../src/executor_tests/introspection/mod.rs | 20 +- juniper/src/executor_tests/variables.rs | 9 +- juniper/src/lib.rs | 2 +- juniper/src/types/scalars.rs | 8 +- juniper_codegen/src/graphql_scalar/attr.rs | 107 +- juniper_codegen/src/graphql_scalar/derive.rs | 123 ++ juniper_codegen/src/graphql_scalar/mod.rs | 38 +- juniper_codegen/src/lib.rs | 349 +++++- juniper_codegen/src/result.rs | 2 - 42 files changed, 1922 insertions(+), 185 deletions(-) create mode 100644 integration_tests/codegen_fail/fail/scalar/derive_input/attr_invalid_url.rs create mode 100644 integration_tests/codegen_fail/fail/scalar/derive_input/attr_invalid_url.stderr create mode 100644 integration_tests/codegen_fail/fail/scalar/derive_input/attr_transparent_and_with.rs create mode 100644 integration_tests/codegen_fail/fail/scalar/derive_input/attr_transparent_and_with.stderr create mode 100644 integration_tests/codegen_fail/fail/scalar/derive_input/attr_transparent_multiple_named_fields.rs create mode 100644 integration_tests/codegen_fail/fail/scalar/derive_input/attr_transparent_multiple_named_fields.stderr create mode 100644 integration_tests/codegen_fail/fail/scalar/derive_input/attr_transparent_multiple_unnamed_fields.rs create mode 100644 integration_tests/codegen_fail/fail/scalar/derive_input/attr_transparent_multiple_unnamed_fields.stderr create mode 100644 integration_tests/codegen_fail/fail/scalar/derive_input/attr_transparent_unit_struct.rs create mode 100644 integration_tests/codegen_fail/fail/scalar/derive_input/attr_transparent_unit_struct.stderr create mode 100644 integration_tests/codegen_fail/fail/scalar/derive_input/derive_invalid_url.rs create mode 100644 integration_tests/codegen_fail/fail/scalar/derive_input/derive_invalid_url.stderr create mode 100644 integration_tests/codegen_fail/fail/scalar/derive_input/derive_transparent_and_with.rs create mode 100644 integration_tests/codegen_fail/fail/scalar/derive_input/derive_transparent_and_with.stderr create mode 100644 integration_tests/codegen_fail/fail/scalar/derive_input/derive_transparent_multiple_named_fields.rs create mode 100644 integration_tests/codegen_fail/fail/scalar/derive_input/derive_transparent_multiple_named_fields.stderr create mode 100644 integration_tests/codegen_fail/fail/scalar/derive_input/derive_transparent_multiple_unnamed_fields.rs create mode 100644 integration_tests/codegen_fail/fail/scalar/derive_input/derive_transparent_multiple_unnamed_fields.stderr create mode 100644 integration_tests/codegen_fail/fail/scalar/derive_input/derive_transparent_unit_struct.rs create mode 100644 integration_tests/codegen_fail/fail/scalar/derive_input/derive_transparent_unit_struct.stderr delete mode 100644 integration_tests/codegen_fail/fail/scalar/derive_input/impl_invalid_url.rs delete mode 100644 integration_tests/codegen_fail/fail/scalar/derive_input/impl_invalid_url.stderr rename integration_tests/codegen_fail/fail/scalar/type_alias/{impl_invalid_url.rs => attr_invalid_url.rs} (100%) rename integration_tests/codegen_fail/fail/scalar/type_alias/{impl_invalid_url.stderr => attr_invalid_url.stderr} (71%) rename integration_tests/codegen_fail/fail/scalar/type_alias/{impl_with_not_all_resolvers.rs => attr_with_not_all_resolvers.rs} (100%) rename integration_tests/codegen_fail/fail/scalar/type_alias/{impl_with_not_all_resolvers.stderr => attr_with_not_all_resolvers.stderr} (65%) rename integration_tests/codegen_fail/fail/scalar/type_alias/{impl_without_resolvers.rs => attr_without_resolvers.rs} (100%) rename integration_tests/codegen_fail/fail/scalar/type_alias/{impl_without_resolvers.stderr => attr_without_resolvers.stderr} (66%) create mode 100644 integration_tests/juniper_tests/src/codegen/scalar_derive.rs create mode 100644 juniper_codegen/src/graphql_scalar/derive.rs diff --git a/docs/book/content/types/scalars.md b/docs/book/content/types/scalars.md index 66bb45cc..ccd35a6e 100644 --- a/docs/book/content/types/scalars.md +++ b/docs/book/content/types/scalars.md @@ -52,7 +52,7 @@ Often, you might need a custom scalar that just wraps an existing type. This can be done with the newtype pattern and a custom derive, similar to how serde supports this pattern with `#[serde(transparent)]`. -```rust,ignore +```rust # extern crate juniper; # #[derive(juniper::GraphQLScalar)] @@ -70,7 +70,7 @@ struct User { `#[derive(GraphQLScalar)]` is mostly interchangeable with `#[graphql_scalar]` attribute: -```rust,ignore +```rust # extern crate juniper; # use juniper::graphql_scalar; # @@ -91,7 +91,7 @@ That's it, you can now use `UserId` in your schema. The macro also allows for more customization: -```rust,ignore +```rust # extern crate juniper; /// You can use a doc comment to specify a description. #[derive(juniper::GraphQLScalar)] @@ -112,7 +112,7 @@ All the methods used from newtype's field can be replaced with attributes: ### `#[graphql(to_output_with = <fn>)]` attribute -```rust,ignore +```rust # use juniper::{GraphQLScalar, ScalarValue, Value}; # #[derive(GraphQLScalar)] @@ -129,7 +129,7 @@ fn to_output<S: ScalarValue>(v: &Incremented) -> Value<S> { ### `#[graphql(from_input_with = <fn>)]` attribute -```rust,ignore +```rust # use juniper::{GraphQLScalar, InputValue, ScalarValue}; # #[derive(GraphQLScalar)] @@ -164,7 +164,7 @@ impl UserId { ### `#[graphql(parse_token_with = <fn>]` or `#[graphql(parse_token(<types>)]` attributes -```rust,ignore +```rust # use juniper::{ # GraphQLScalar, InputValue, ParseScalarResult, ParseScalarValue, # ScalarValue, ScalarToken, Value @@ -226,7 +226,7 @@ Instead of providing all custom resolvers, you can provide path to the `to_outpu Path can be simply `with = Self` (default path where macro expects resolvers to be), in case there is an impl block with custom resolvers: -```rust,ignore +```rust # use juniper::{ # GraphQLScalar, InputValue, ParseScalarResult, ParseScalarValue, # ScalarValue, ScalarToken, Value @@ -253,7 +253,7 @@ impl StringOrInt { { v.as_string_value() .map(|s| Self::String(s.to_owned())) - .or_else(|| v.as_int_value().map(|i| Self::Int(i))) + .or_else(|| v.as_int_value().map(Self::Int)) .ok_or_else(|| format!("Expected `String` or `Int`, found: {}", v)) } @@ -271,7 +271,7 @@ impl StringOrInt { Or it can be path to a module, where custom resolvers are located. -```rust,ignore +```rust # use juniper::{ # GraphQLScalar, InputValue, ParseScalarResult, ParseScalarValue, # ScalarValue, ScalarToken, Value @@ -303,7 +303,7 @@ mod string_or_int { { v.as_string_value() .map(|s| StringOrInt::String(s.to_owned())) - .or_else(|| v.as_int_value().map(|i| StringOrInt::Int(i))) + .or_else(|| v.as_int_value().map(StringOrInt::Int)) .ok_or_else(|| format!("Expected `String` or `Int`, found: {}", v)) } @@ -321,7 +321,7 @@ mod string_or_int { Also, you can partially override `#[graphql(with)]` attribute with other custom scalars. -```rust,ignore +```rust # use juniper::{GraphQLScalar, InputValue, ParseScalarResult, ScalarValue, ScalarToken, Value}; # #[derive(GraphQLScalar)] @@ -348,7 +348,7 @@ impl StringOrInt { { v.as_string_value() .map(|s| Self::String(s.to_owned())) - .or_else(|| v.as_int_value().map(|i| Self::Int(i))) + .or_else(|| v.as_int_value().map(Self::Int)) .ok_or_else(|| format!("Expected `String` or `Int`, found: {}", v)) } } diff --git a/integration_tests/codegen_fail/fail/scalar/derive_input/attr_invalid_url.rs b/integration_tests/codegen_fail/fail/scalar/derive_input/attr_invalid_url.rs new file mode 100644 index 00000000..b28cb1fc --- /dev/null +++ b/integration_tests/codegen_fail/fail/scalar/derive_input/attr_invalid_url.rs @@ -0,0 +1,6 @@ +use juniper::graphql_scalar; + +#[graphql_scalar(specified_by_url = "not an url", transparent)] +struct ScalarSpecifiedByUrl(i32); + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/scalar/derive_input/attr_invalid_url.stderr b/integration_tests/codegen_fail/fail/scalar/derive_input/attr_invalid_url.stderr new file mode 100644 index 00000000..b1bd1210 --- /dev/null +++ b/integration_tests/codegen_fail/fail/scalar/derive_input/attr_invalid_url.stderr @@ -0,0 +1,5 @@ +error: Invalid URL: relative URL without a base + --> fail/scalar/derive_input/attr_invalid_url.rs:3:37 + | +3 | #[graphql_scalar(specified_by_url = "not an url", transparent)] + | ^^^^^^^^^^^^ diff --git a/integration_tests/codegen_fail/fail/scalar/derive_input/attr_transparent_and_with.rs b/integration_tests/codegen_fail/fail/scalar/derive_input/attr_transparent_and_with.rs new file mode 100644 index 00000000..3b319084 --- /dev/null +++ b/integration_tests/codegen_fail/fail/scalar/derive_input/attr_transparent_and_with.rs @@ -0,0 +1,6 @@ +use juniper::graphql_scalar; + +#[graphql_scalar(with = Self, transparent)] +struct Scalar; + +fn main() {} 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 new file mode 100644 index 00000000..829c42c7 --- /dev/null +++ b/integration_tests/codegen_fail/fail/scalar/derive_input/attr_transparent_and_with.stderr @@ -0,0 +1,5 @@ +error: GraphQL scalar `with = <path>` 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. + --> 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.rs b/integration_tests/codegen_fail/fail/scalar/derive_input/attr_transparent_multiple_named_fields.rs new file mode 100644 index 00000000..56658b54 --- /dev/null +++ b/integration_tests/codegen_fail/fail/scalar/derive_input/attr_transparent_multiple_named_fields.rs @@ -0,0 +1,9 @@ +use juniper::graphql_scalar; + +#[graphql_scalar(transparent)] +struct Scalar { + id: i32, + another: i32, +} + +fn main() {} 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 new file mode 100644 index 00000000..5a6f9406 --- /dev/null +++ b/integration_tests/codegen_fail/fail/scalar/derive_input/attr_transparent_multiple_named_fields.stderr @@ -0,0 +1,8 @@ +error: GraphQL scalar expected exactly 1 field, e.g., Test { test: i32 } because of `transparent` attribute + --> fail/scalar/derive_input/attr_transparent_multiple_named_fields.rs:4:1 + | +4 | / struct Scalar { +5 | | id: i32, +6 | | another: i32, +7 | | } + | |_^ diff --git a/integration_tests/codegen_fail/fail/scalar/derive_input/attr_transparent_multiple_unnamed_fields.rs b/integration_tests/codegen_fail/fail/scalar/derive_input/attr_transparent_multiple_unnamed_fields.rs new file mode 100644 index 00000000..bbf96936 --- /dev/null +++ b/integration_tests/codegen_fail/fail/scalar/derive_input/attr_transparent_multiple_unnamed_fields.rs @@ -0,0 +1,6 @@ +use juniper::graphql_scalar; + +#[graphql_scalar(transparent)] +struct Scalar(i32, i32); + +fn main() {} 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 new file mode 100644 index 00000000..dfdcff7c --- /dev/null +++ b/integration_tests/codegen_fail/fail/scalar/derive_input/attr_transparent_multiple_unnamed_fields.stderr @@ -0,0 +1,5 @@ +error: GraphQL scalar expected exactly 1 field, e.g., Test(i32) because of `transparent` attribute + --> 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.rs b/integration_tests/codegen_fail/fail/scalar/derive_input/attr_transparent_unit_struct.rs new file mode 100644 index 00000000..9f8d7568 --- /dev/null +++ b/integration_tests/codegen_fail/fail/scalar/derive_input/attr_transparent_unit_struct.rs @@ -0,0 +1,6 @@ +use juniper::graphql_scalar; + +#[graphql_scalar(transparent)] +struct ScalarSpecifiedByUrl; + +fn main() {} 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 new file mode 100644 index 00000000..11b661f9 --- /dev/null +++ b/integration_tests/codegen_fail/fail/scalar/derive_input/attr_transparent_unit_struct.stderr @@ -0,0 +1,5 @@ +error: GraphQL scalar expected exactly 1 field, e.g.: `Test(i32)`, `Test { test: i32 }` because of `transparent` attribute + --> 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_invalid_url.rs b/integration_tests/codegen_fail/fail/scalar/derive_input/derive_invalid_url.rs new file mode 100644 index 00000000..ae85bfd9 --- /dev/null +++ b/integration_tests/codegen_fail/fail/scalar/derive_input/derive_invalid_url.rs @@ -0,0 +1,7 @@ +use juniper::GraphQLScalar; + +#[derive(GraphQLScalar)] +#[graphql(specified_by_url = "not an url", transparent)] +struct ScalarSpecifiedByUrl(i64); + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/scalar/derive_input/derive_invalid_url.stderr b/integration_tests/codegen_fail/fail/scalar/derive_input/derive_invalid_url.stderr new file mode 100644 index 00000000..ee830d6a --- /dev/null +++ b/integration_tests/codegen_fail/fail/scalar/derive_input/derive_invalid_url.stderr @@ -0,0 +1,5 @@ +error: Invalid URL: relative URL without a base + --> fail/scalar/derive_input/derive_invalid_url.rs:4:30 + | +4 | #[graphql(specified_by_url = "not an url", transparent)] + | ^^^^^^^^^^^^ diff --git a/integration_tests/codegen_fail/fail/scalar/derive_input/derive_transparent_and_with.rs b/integration_tests/codegen_fail/fail/scalar/derive_input/derive_transparent_and_with.rs new file mode 100644 index 00000000..494ff668 --- /dev/null +++ b/integration_tests/codegen_fail/fail/scalar/derive_input/derive_transparent_and_with.rs @@ -0,0 +1,7 @@ +use juniper::GraphQLScalar; + +#[derive(GraphQLScalar)] +#[graphql(with = Self, transparent)] +struct Scalar; + +fn main() {} 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 new file mode 100644 index 00000000..860d5391 --- /dev/null +++ b/integration_tests/codegen_fail/fail/scalar/derive_input/derive_transparent_and_with.stderr @@ -0,0 +1,5 @@ +error: GraphQL scalar `with = <path>` 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. + --> 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.rs b/integration_tests/codegen_fail/fail/scalar/derive_input/derive_transparent_multiple_named_fields.rs new file mode 100644 index 00000000..3388e1fc --- /dev/null +++ b/integration_tests/codegen_fail/fail/scalar/derive_input/derive_transparent_multiple_named_fields.rs @@ -0,0 +1,10 @@ +use juniper::GraphQLScalar; + +#[derive(GraphQLScalar)] +#[graphql(transparent)] +struct Scalar { + id: i32, + another: i32, +} + +fn main() {} 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 new file mode 100644 index 00000000..d5258cef --- /dev/null +++ b/integration_tests/codegen_fail/fail/scalar/derive_input/derive_transparent_multiple_named_fields.stderr @@ -0,0 +1,9 @@ +error: GraphQL scalar expected exactly 1 field, e.g., Test { test: i32 } because of `transparent` attribute + --> fail/scalar/derive_input/derive_transparent_multiple_named_fields.rs:4:1 + | +4 | / #[graphql(transparent)] +5 | | struct Scalar { +6 | | id: i32, +7 | | another: i32, +8 | | } + | |_^ diff --git a/integration_tests/codegen_fail/fail/scalar/derive_input/derive_transparent_multiple_unnamed_fields.rs b/integration_tests/codegen_fail/fail/scalar/derive_input/derive_transparent_multiple_unnamed_fields.rs new file mode 100644 index 00000000..d7710fc8 --- /dev/null +++ b/integration_tests/codegen_fail/fail/scalar/derive_input/derive_transparent_multiple_unnamed_fields.rs @@ -0,0 +1,7 @@ +use juniper::GraphQLScalar; + +#[derive(GraphQLScalar)] +#[graphql(transparent)] +struct Scalar(i32, i32); + +fn main() {} 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 new file mode 100644 index 00000000..a832801b --- /dev/null +++ b/integration_tests/codegen_fail/fail/scalar/derive_input/derive_transparent_multiple_unnamed_fields.stderr @@ -0,0 +1,6 @@ +error: GraphQL scalar expected exactly 1 field, e.g., Test(i32) because of `transparent` attribute + --> fail/scalar/derive_input/derive_transparent_multiple_unnamed_fields.rs:4:1 + | +4 | / #[graphql(transparent)] +5 | | struct Scalar(i32, i32); + | |________________________^ diff --git a/integration_tests/codegen_fail/fail/scalar/derive_input/derive_transparent_unit_struct.rs b/integration_tests/codegen_fail/fail/scalar/derive_input/derive_transparent_unit_struct.rs new file mode 100644 index 00000000..09712baa --- /dev/null +++ b/integration_tests/codegen_fail/fail/scalar/derive_input/derive_transparent_unit_struct.rs @@ -0,0 +1,7 @@ +use juniper::GraphQLScalar; + +#[derive(GraphQLScalar)] +#[graphql(transparent)] +struct ScalarSpecifiedByUrl; + +fn main() {} 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 new file mode 100644 index 00000000..3fc185ed --- /dev/null +++ b/integration_tests/codegen_fail/fail/scalar/derive_input/derive_transparent_unit_struct.stderr @@ -0,0 +1,6 @@ +error: GraphQL scalar expected exactly 1 field, e.g.: `Test(i32)`, `Test { test: i32 }` because of `transparent` attribute + --> fail/scalar/derive_input/derive_transparent_unit_struct.rs:4:1 + | +4 | / #[graphql(transparent)] +5 | | struct ScalarSpecifiedByUrl; + | |____________________________^ diff --git a/integration_tests/codegen_fail/fail/scalar/derive_input/impl_invalid_url.rs b/integration_tests/codegen_fail/fail/scalar/derive_input/impl_invalid_url.rs deleted file mode 100644 index 4d93bc1f..00000000 --- a/integration_tests/codegen_fail/fail/scalar/derive_input/impl_invalid_url.rs +++ /dev/null @@ -1,16 +0,0 @@ -use juniper::{graphql_scalar, InputValue, ScalarValue, Value}; - -#[graphql_scalar(specified_by_url = "not an url", parse_token(i32))] -struct ScalarSpecifiedByUrl(i32); - -impl ScalarSpecifiedByUrl { - fn to_output<S: ScalarValue>(&self) -> Value<S> { - Value::scalar(0) - } - - fn from_input<S: ScalarValue>(_: &InputValue<S>) -> Result<Self, String> { - Ok(Self) - } -} - -fn main() {} diff --git a/integration_tests/codegen_fail/fail/scalar/derive_input/impl_invalid_url.stderr b/integration_tests/codegen_fail/fail/scalar/derive_input/impl_invalid_url.stderr deleted file mode 100644 index 1b6edaeb..00000000 --- a/integration_tests/codegen_fail/fail/scalar/derive_input/impl_invalid_url.stderr +++ /dev/null @@ -1,17 +0,0 @@ -error: Invalid URL: relative URL without a base - --> fail/scalar/derive_input/impl_invalid_url.rs:3:37 - | -3 | #[graphql_scalar(specified_by_url = "not an url", parse_token(i32))] - | ^^^^^^^^^^^^ - -error[E0412]: cannot find type `ScalarSpecifiedByUrl` in this scope - --> fail/scalar/derive_input/impl_invalid_url.rs:6:6 - | -6 | impl ScalarSpecifiedByUrl { - | ^^^^^^^^^^^^^^^^^^^^ not found in this scope - -error: the `Self` constructor can only be used with tuple or unit structs - --> fail/scalar/derive_input/impl_invalid_url.rs:12:12 - | -12 | Ok(Self) - | ^^^^ diff --git a/integration_tests/codegen_fail/fail/scalar/type_alias/impl_invalid_url.rs b/integration_tests/codegen_fail/fail/scalar/type_alias/attr_invalid_url.rs similarity index 100% rename from integration_tests/codegen_fail/fail/scalar/type_alias/impl_invalid_url.rs rename to integration_tests/codegen_fail/fail/scalar/type_alias/attr_invalid_url.rs diff --git a/integration_tests/codegen_fail/fail/scalar/type_alias/impl_invalid_url.stderr b/integration_tests/codegen_fail/fail/scalar/type_alias/attr_invalid_url.stderr similarity index 71% rename from integration_tests/codegen_fail/fail/scalar/type_alias/impl_invalid_url.stderr rename to integration_tests/codegen_fail/fail/scalar/type_alias/attr_invalid_url.stderr index b000693c..4ecefa21 100644 --- a/integration_tests/codegen_fail/fail/scalar/type_alias/impl_invalid_url.stderr +++ b/integration_tests/codegen_fail/fail/scalar/type_alias/attr_invalid_url.stderr @@ -1,5 +1,5 @@ error: Invalid URL: relative URL without a base - --> fail/scalar/type_alias/impl_invalid_url.rs:6:24 + --> fail/scalar/type_alias/attr_invalid_url.rs:6:24 | 6 | specified_by_url = "not an url", | ^^^^^^^^^^^^ diff --git a/integration_tests/codegen_fail/fail/scalar/type_alias/impl_with_not_all_resolvers.rs b/integration_tests/codegen_fail/fail/scalar/type_alias/attr_with_not_all_resolvers.rs similarity index 100% rename from integration_tests/codegen_fail/fail/scalar/type_alias/impl_with_not_all_resolvers.rs rename to integration_tests/codegen_fail/fail/scalar/type_alias/attr_with_not_all_resolvers.rs diff --git a/integration_tests/codegen_fail/fail/scalar/type_alias/impl_with_not_all_resolvers.stderr b/integration_tests/codegen_fail/fail/scalar/type_alias/attr_with_not_all_resolvers.stderr similarity index 65% rename from integration_tests/codegen_fail/fail/scalar/type_alias/impl_with_not_all_resolvers.stderr rename to integration_tests/codegen_fail/fail/scalar/type_alias/attr_with_not_all_resolvers.stderr index 9502bb06..2bb56a2b 100644 --- a/integration_tests/codegen_fail/fail/scalar/type_alias/impl_with_not_all_resolvers.stderr +++ b/integration_tests/codegen_fail/fail/scalar/type_alias/attr_with_not_all_resolvers.stderr @@ -1,5 +1,5 @@ -error: GraphQL scalar all custom resolvers have to be provided via `with` or combination of `to_output_with`, `from_input_with`, `parse_token_with` attributes - --> fail/scalar/type_alias/impl_with_not_all_resolvers.rs:6:1 +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 + --> 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/impl_without_resolvers.rs b/integration_tests/codegen_fail/fail/scalar/type_alias/attr_without_resolvers.rs similarity index 100% rename from integration_tests/codegen_fail/fail/scalar/type_alias/impl_without_resolvers.rs rename to integration_tests/codegen_fail/fail/scalar/type_alias/attr_without_resolvers.rs diff --git a/integration_tests/codegen_fail/fail/scalar/type_alias/impl_without_resolvers.stderr b/integration_tests/codegen_fail/fail/scalar/type_alias/attr_without_resolvers.stderr similarity index 66% rename from integration_tests/codegen_fail/fail/scalar/type_alias/impl_without_resolvers.stderr rename to integration_tests/codegen_fail/fail/scalar/type_alias/attr_without_resolvers.stderr index 93f6eb7a..626f34a3 100644 --- a/integration_tests/codegen_fail/fail/scalar/type_alias/impl_without_resolvers.stderr +++ b/integration_tests/codegen_fail/fail/scalar/type_alias/attr_without_resolvers.stderr @@ -1,5 +1,5 @@ -error: GraphQL scalar all custom resolvers have to be provided via `with` or combination of `to_output_with`, `from_input_with`, `parse_token_with` attributes - --> fail/scalar/type_alias/impl_without_resolvers.rs:6:1 +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 + --> fail/scalar/type_alias/attr_without_resolvers.rs:6:1 | 6 | type CustomScalar = Scalar; | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/integration_tests/juniper_tests/src/codegen/mod.rs b/integration_tests/juniper_tests/src/codegen/mod.rs index f87b4a3c..2c617724 100644 --- a/integration_tests/juniper_tests/src/codegen/mod.rs +++ b/integration_tests/juniper_tests/src/codegen/mod.rs @@ -6,6 +6,7 @@ mod object_attr; mod object_derive; mod scalar_attr_derive_input; mod scalar_attr_type_alias; +mod scalar_derive; mod subscription_attr; mod union_attr; mod union_derive; diff --git a/integration_tests/juniper_tests/src/codegen/scalar_attr_derive_input.rs b/integration_tests/juniper_tests/src/codegen/scalar_attr_derive_input.rs index d50cc58c..ccfa3575 100644 --- a/integration_tests/juniper_tests/src/codegen/scalar_attr_derive_input.rs +++ b/integration_tests/juniper_tests/src/codegen/scalar_attr_derive_input.rs @@ -1,3 +1,7 @@ +//! Tests for `#[graphql_scalar]` macro placed on [`DeriveInput`]. +//! +//! [`DeriveInput`]: syn::DeriveInput + use std::fmt; use chrono::{DateTime, TimeZone, Utc}; @@ -87,6 +91,135 @@ mod trivial { } } +mod transparent { + use super::*; + + #[graphql_scalar(transparent)] + struct Counter(i32); + + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + fn counter(value: Counter) -> Counter { + value + } + } + + #[tokio::test] + async fn is_graphql_scalar() { + const DOC: &str = r#"{ + __type(name: "Counter") { + kind + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"__type": {"kind": "SCALAR"}}), vec![])), + ); + } + + #[tokio::test] + async fn resolves_counter() { + const DOC: &str = r#"{ counter(value: 0) }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"counter": 0}), vec![])), + ); + } + + #[tokio::test] + async fn has_no_description() { + const DOC: &str = r#"{ + __type(name: "Counter") { + description + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"__type": {"description": null}}), vec![])), + ); + } +} + +mod transparent_with_resolver { + use super::*; + + #[graphql_scalar( + transparent, + to_output_with = Self::to_output, + )] + struct Counter(i32); + + impl Counter { + fn to_output<S: ScalarValue>(&self) -> Value<S> { + Value::scalar(self.0 + 1) + } + } + + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + fn counter(value: Counter) -> Counter { + value + } + } + + #[tokio::test] + async fn is_graphql_scalar() { + const DOC: &str = r#"{ + __type(name: "Counter") { + kind + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"__type": {"kind": "SCALAR"}}), vec![])), + ); + } + + #[tokio::test] + async fn resolves_counter() { + const DOC: &str = r#"{ counter(value: 0) }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"counter": 1}), vec![])), + ); + } + + #[tokio::test] + async fn has_no_description() { + const DOC: &str = r#"{ + __type(name: "Counter") { + description + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"__type": {"description": null}}), vec![])), + ); + } +} + mod all_custom_resolvers { use super::*; @@ -333,7 +466,7 @@ mod multiple_delegated_parse_token { fn from_input<S: ScalarValue>(v: &InputValue<S>) -> Result<Self, String> { v.as_string_value() .map(|s| Self::String(s.to_owned())) - .or_else(|| v.as_int_value().map(|i| Self::Int(i))) + .or_else(|| v.as_int_value().map(Self::Int)) .ok_or_else(|| format!("Expected `String` or `Int`, found: {}", v)) } } diff --git a/integration_tests/juniper_tests/src/codegen/scalar_attr_type_alias.rs b/integration_tests/juniper_tests/src/codegen/scalar_attr_type_alias.rs index 3f8854c3..9dfd8f6c 100644 --- a/integration_tests/juniper_tests/src/codegen/scalar_attr_type_alias.rs +++ b/integration_tests/juniper_tests/src/codegen/scalar_attr_type_alias.rs @@ -1,3 +1,5 @@ +//! Tests for `#[graphql_scalar]` macro placed on a type alias. + use std::fmt; use chrono::{DateTime, TimeZone, Utc}; diff --git a/integration_tests/juniper_tests/src/codegen/scalar_derive.rs b/integration_tests/juniper_tests/src/codegen/scalar_derive.rs new file mode 100644 index 00000000..a538095f --- /dev/null +++ b/integration_tests/juniper_tests/src/codegen/scalar_derive.rs @@ -0,0 +1,1114 @@ +//! Tests for `#[derive(GraphQLScalar)]` macro. + +use std::fmt; + +use chrono::{DateTime, TimeZone, Utc}; +use juniper::{ + execute, graphql_object, graphql_value, graphql_vars, GraphQLScalar, InputValue, + ParseScalarResult, ParseScalarValue, ScalarToken, ScalarValue, Value, +}; + +use crate::{ + custom_scalar::MyScalarValue, + util::{schema, schema_with_scalar}, +}; + +mod trivial { + use super::*; + + #[derive(GraphQLScalar)] + struct Counter(i32); + + impl Counter { + fn to_output<S: ScalarValue>(&self) -> Value<S> { + Value::scalar(self.0) + } + + fn from_input<S: ScalarValue>(v: &InputValue<S>) -> Result<Self, String> { + v.as_int_value() + .map(Self) + .ok_or_else(|| format!("Expected `Counter`, found: {}", v)) + } + + fn parse_token<S: ScalarValue>(t: ScalarToken<'_>) -> ParseScalarResult<'_, S> { + <i32 as ParseScalarValue<S>>::from_str(t) + } + } + + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + fn counter(value: Counter) -> Counter { + value + } + } + + #[tokio::test] + async fn is_graphql_scalar() { + const DOC: &str = r#"{ + __type(name: "Counter") { + kind + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"__type": {"kind": "SCALAR"}}), vec![])), + ); + } + + #[tokio::test] + async fn resolves_counter() { + const DOC: &str = r#"{ counter(value: 0) }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"counter": 0}), vec![])), + ); + } + + #[tokio::test] + async fn has_no_description() { + const DOC: &str = r#"{ + __type(name: "Counter") { + description + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"__type": {"description": null}}), vec![])), + ); + } +} + +mod transparent { + use super::*; + + #[derive(GraphQLScalar)] + #[graphql(transparent)] + struct Counter(i32); + + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + fn counter(value: Counter) -> Counter { + value + } + } + + #[tokio::test] + async fn is_graphql_scalar() { + const DOC: &str = r#"{ + __type(name: "Counter") { + kind + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"__type": {"kind": "SCALAR"}}), vec![])), + ); + } + + #[tokio::test] + async fn resolves_counter() { + const DOC: &str = r#"{ counter(value: 0) }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"counter": 0}), vec![])), + ); + } + + #[tokio::test] + async fn has_no_description() { + const DOC: &str = r#"{ + __type(name: "Counter") { + description + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"__type": {"description": null}}), vec![])), + ); + } +} + +mod transparent_with_resolver { + use super::*; + + #[derive(GraphQLScalar)] + #[graphql(transparent, to_output_with = Self::to_output)] + struct Counter(i32); + + impl Counter { + fn to_output<S: ScalarValue>(&self) -> Value<S> { + Value::scalar(self.0 + 1) + } + } + + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + fn counter(value: Counter) -> Counter { + value + } + } + + #[tokio::test] + async fn is_graphql_scalar() { + const DOC: &str = r#"{ + __type(name: "Counter") { + kind + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"__type": {"kind": "SCALAR"}}), vec![])), + ); + } + + #[tokio::test] + async fn resolves_counter() { + const DOC: &str = r#"{ counter(value: 0) }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"counter": 1}), vec![])), + ); + } + + #[tokio::test] + async fn has_no_description() { + const DOC: &str = r#"{ + __type(name: "Counter") { + description + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"__type": {"description": null}}), vec![])), + ); + } +} + +mod all_custom_resolvers { + use super::*; + + #[derive(GraphQLScalar)] + #[graphql( + to_output_with = to_output, + from_input_with = from_input, + )] + #[graphql(parse_token_with = parse_token)] + struct Counter(i32); + + fn to_output<S: ScalarValue>(v: &Counter) -> Value<S> { + Value::scalar(v.0) + } + + fn from_input<S: ScalarValue>(v: &InputValue<S>) -> Result<Counter, String> { + v.as_int_value() + .map(Counter) + .ok_or_else(|| format!("Expected `Counter`, found: {}", v)) + } + + fn parse_token<S: ScalarValue>(value: ScalarToken<'_>) -> ParseScalarResult<'_, S> { + <i32 as ParseScalarValue<S>>::from_str(value) + } + + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + fn counter(value: Counter) -> Counter { + value + } + } + + #[tokio::test] + async fn is_graphql_scalar() { + const DOC: &str = r#"{ + __type(name: "Counter") { + kind + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"__type": {"kind": "SCALAR"}}), vec![])), + ); + } + + #[tokio::test] + async fn resolves_counter() { + const DOC: &str = r#"{ counter(value: 0) }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"counter": 0}), vec![])), + ); + } + + #[tokio::test] + async fn has_no_description() { + const DOC: &str = r#"{ + __type(name: "Counter") { + description + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"__type": {"description": null}}), vec![])), + ); + } +} + +mod explicit_name { + use super::*; + + #[derive(GraphQLScalar)] + #[graphql(name = "Counter")] + struct CustomCounter(i32); + + impl CustomCounter { + fn to_output<S: ScalarValue>(&self) -> Value<S> { + Value::scalar(self.0) + } + + fn from_input<S: ScalarValue>(v: &InputValue<S>) -> Result<Self, String> { + v.as_int_value() + .map(Self) + .ok_or_else(|| format!("Expected `Counter`, found: {}", v)) + } + + fn parse_token<S: ScalarValue>(value: ScalarToken<'_>) -> ParseScalarResult<'_, S> { + <i32 as ParseScalarValue<S>>::from_str(value) + } + } + + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + fn counter(value: CustomCounter) -> CustomCounter { + value + } + } + + #[tokio::test] + async fn is_graphql_scalar() { + const DOC: &str = r#"{ + __type(name: "Counter") { + kind + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"__type": {"kind": "SCALAR"}}), vec![])), + ); + } + + #[tokio::test] + async fn resolves_counter() { + const DOC: &str = r#"{ counter(value: 0) }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"counter": 0}), vec![])), + ); + } + + #[tokio::test] + async fn has_no_description() { + const DOC: &str = r#"{ + __type(name: "Counter") { + description + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"__type": {"description": null}}), vec![])), + ); + } +} + +mod delegated_parse_token { + use super::*; + + #[derive(GraphQLScalar)] + #[graphql(parse_token(i32))] + struct Counter(i32); + + impl Counter { + fn to_output<S: ScalarValue>(&self) -> Value<S> { + Value::scalar(self.0) + } + + fn from_input<S: ScalarValue>(v: &InputValue<S>) -> Result<Self, String> { + v.as_int_value() + .map(Self) + .ok_or_else(|| format!("Expected `Counter`, found: {}", v)) + } + } + + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + fn counter(value: Counter) -> Counter { + value + } + } + + #[tokio::test] + async fn is_graphql_scalar() { + const DOC: &str = r#"{ + __type(name: "Counter") { + kind + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"__type": {"kind": "SCALAR"}}), vec![])), + ); + } + + #[tokio::test] + async fn resolves_counter() { + const DOC: &str = r#"{ counter(value: 0) }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"counter": 0}), vec![])), + ); + } + + #[tokio::test] + async fn has_no_description() { + const DOC: &str = r#"{ + __type(name: "Counter") { + description + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"__type": {"description": null}}), vec![])), + ); + } +} + +mod multiple_delegated_parse_token { + use super::*; + + #[derive(GraphQLScalar)] + #[graphql(parse_token(String, i32))] + enum StringOrInt { + String(String), + Int(i32), + } + + impl StringOrInt { + fn to_output<S: ScalarValue>(&self) -> Value<S> { + match self { + Self::String(str) => Value::scalar(str.to_owned()), + Self::Int(i) => Value::scalar(*i), + } + } + + fn from_input<S: ScalarValue>(v: &InputValue<S>) -> Result<Self, String> { + v.as_string_value() + .map(|s| Self::String(s.to_owned())) + .or_else(|| v.as_int_value().map(Self::Int)) + .ok_or_else(|| format!("Expected `String` or `Int`, found: {}", v)) + } + } + + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + fn string_or_int(value: StringOrInt) -> StringOrInt { + value + } + } + + #[tokio::test] + async fn resolves_string() { + const DOC: &str = r#"{ stringOrInt(value: "test") }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"stringOrInt": "test"}), vec![])), + ); + } + + #[tokio::test] + async fn resolves_int() { + const DOC: &str = r#"{ stringOrInt(value: 0) }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"stringOrInt": 0}), vec![])), + ); + } +} + +mod where_attribute { + use super::*; + + #[derive(GraphQLScalar)] + #[graphql( + to_output_with = to_output, + from_input_with = from_input, + parse_token(String), + where(Tz: From<Utc>, Tz::Offset: fmt::Display), + specified_by_url = "https://tools.ietf.org/html/rfc3339", + )] + struct CustomDateTime<Tz: TimeZone>(DateTime<Tz>); + + fn to_output<S, Tz>(v: &CustomDateTime<Tz>) -> Value<S> + where + S: ScalarValue, + Tz: From<Utc> + TimeZone, + Tz::Offset: fmt::Display, + { + Value::scalar(v.0.to_rfc3339()) + } + + fn from_input<S, Tz>(v: &InputValue<S>) -> Result<CustomDateTime<Tz>, String> + where + S: ScalarValue, + Tz: From<Utc> + TimeZone, + Tz::Offset: fmt::Display, + { + v.as_string_value() + .ok_or_else(|| format!("Expected `String`, found: {}", v)) + .and_then(|s| { + DateTime::parse_from_rfc3339(s) + .map(|dt| CustomDateTime(dt.with_timezone(&Tz::from(Utc)))) + .map_err(|e| format!("Failed to parse `CustomDateTime`: {}", e)) + }) + } + + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + fn date_time(value: CustomDateTime<Utc>) -> CustomDateTime<Utc> { + value + } + } + + #[tokio::test] + async fn resolves_custom_date_time() { + const DOC: &str = r#"{ dateTime(value: "1996-12-19T16:39:57-08:00") }"#; + + let schema = schema_with_scalar::<MyScalarValue, _, _>(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok(( + graphql_value!({"dateTime": "1996-12-20T00:39:57+00:00"}), + vec![], + )), + ); + } + + #[tokio::test] + async fn has_specified_by_url() { + const DOC: &str = r#"{ + __type(name: "CustomDateTime") { + specifiedByUrl + } + }"#; + + let schema = schema_with_scalar::<MyScalarValue, _, _>(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok(( + graphql_value!({"__type": {"specifiedByUrl": "https://tools.ietf.org/html/rfc3339"}}), + vec![], + )), + ); + } +} + +mod with_self { + use super::*; + + #[derive(GraphQLScalar)] + #[graphql(with = Self)] + struct Counter(i32); + + impl Counter { + fn to_output<S: ScalarValue>(&self) -> Value<S> { + Value::scalar(self.0) + } + + fn from_input<S: ScalarValue>(v: &InputValue<S>) -> Result<Self, String> { + v.as_int_value() + .map(Self) + .ok_or_else(|| format!("Expected `Counter`, found: {}", v)) + } + + fn parse_token<S: ScalarValue>(value: ScalarToken<'_>) -> ParseScalarResult<'_, S> { + <i32 as ParseScalarValue<S>>::from_str(value) + } + } + + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + fn counter(value: Counter) -> Counter { + value + } + } + + #[tokio::test] + async fn is_graphql_scalar() { + const DOC: &str = r#"{ + __type(name: "Counter") { + kind + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"__type": {"kind": "SCALAR"}}), vec![])), + ); + } + + #[tokio::test] + async fn resolves_counter() { + const DOC: &str = r#"{ counter(value: 0) }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"counter": 0}), vec![])), + ); + } + + #[tokio::test] + async fn has_no_description() { + const DOC: &str = r#"{ + __type(name: "Counter") { + description + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"__type": {"description": null}}), vec![])), + ); + } +} + +mod with_module { + use super::*; + + #[derive(GraphQLScalar)] + #[graphql( + with = custom_date_time, + parse_token(String), + where(Tz: From<Utc>, Tz::Offset: fmt::Display), + specified_by_url = "https://tools.ietf.org/html/rfc3339", + )] + struct CustomDateTime<Tz: TimeZone>(DateTime<Tz>); + + mod custom_date_time { + use super::*; + + pub(super) fn to_output<S, Tz>(v: &CustomDateTime<Tz>) -> Value<S> + where + S: ScalarValue, + Tz: From<Utc> + TimeZone, + Tz::Offset: fmt::Display, + { + Value::scalar(v.0.to_rfc3339()) + } + + pub(super) fn from_input<S, Tz>(v: &InputValue<S>) -> Result<CustomDateTime<Tz>, String> + where + S: ScalarValue, + Tz: From<Utc> + TimeZone, + Tz::Offset: fmt::Display, + { + v.as_string_value() + .ok_or_else(|| format!("Expected `String`, found: {}", v)) + .and_then(|s| { + DateTime::parse_from_rfc3339(s) + .map(|dt| CustomDateTime(dt.with_timezone(&Tz::from(Utc)))) + .map_err(|e| format!("Failed to parse `CustomDateTime`: {}", e)) + }) + } + } + + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + fn date_time(value: CustomDateTime<Utc>) -> CustomDateTime<Utc> { + value + } + } + + #[tokio::test] + async fn resolves_custom_date_time() { + const DOC: &str = r#"{ dateTime(value: "1996-12-19T16:39:57-08:00") }"#; + + let schema = schema_with_scalar::<MyScalarValue, _, _>(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok(( + graphql_value!({"dateTime": "1996-12-20T00:39:57+00:00"}), + vec![], + )), + ); + } + + #[tokio::test] + async fn has_specified_by_url() { + const DOC: &str = r#"{ + __type(name: "CustomDateTime") { + specifiedByUrl + } + }"#; + + let schema = schema_with_scalar::<MyScalarValue, _, _>(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok(( + graphql_value!({"__type": {"specifiedByUrl": "https://tools.ietf.org/html/rfc3339"}}), + vec![], + )), + ); + } +} + +mod description_from_doc_comment { + use super::*; + + /// Description + #[derive(GraphQLScalar)] + #[graphql(parse_token(i32))] + struct Counter(i32); + + impl Counter { + fn to_output<S: ScalarValue>(&self) -> Value<S> { + Value::scalar(self.0) + } + + fn from_input<S: ScalarValue>(v: &InputValue<S>) -> Result<Self, String> { + v.as_int_value() + .map(Self) + .ok_or_else(|| format!("Expected `Counter`, found: {}", v)) + } + } + + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + fn counter(value: Counter) -> Counter { + value + } + } + + #[tokio::test] + async fn is_graphql_scalar() { + const DOC: &str = r#"{ + __type(name: "Counter") { + kind + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"__type": {"kind": "SCALAR"}}), vec![])), + ); + } + + #[tokio::test] + async fn resolves_counter() { + const DOC: &str = r#"{ counter(value: 0) }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"counter": 0}), vec![])), + ); + } + + #[tokio::test] + async fn has_description() { + const DOC: &str = r#"{ + __type(name: "Counter") { + description + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok(( + graphql_value!({"__type": {"description": "Description"}}), + vec![], + )), + ); + } +} + +mod description_from_attribute { + use super::*; + + /// Doc comment + #[derive(GraphQLScalar)] + #[graphql(description = "Description from attribute", parse_token(i32))] + struct Counter(i32); + + impl Counter { + fn to_output<S: ScalarValue>(&self) -> Value<S> { + Value::scalar(self.0) + } + + fn from_input<S: ScalarValue>(v: &InputValue<S>) -> Result<Self, String> { + v.as_int_value() + .map(Self) + .ok_or_else(|| format!("Expected `Counter`, found: {}", v)) + } + } + + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + fn counter(value: Counter) -> Counter { + value + } + } + + #[tokio::test] + async fn is_graphql_scalar() { + const DOC: &str = r#"{ + __type(name: "Counter") { + kind + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"__type": {"kind": "SCALAR"}}), vec![])), + ); + } + + #[tokio::test] + async fn resolves_counter() { + const DOC: &str = r#"{ counter(value: 0) }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"counter": 0}), vec![])), + ); + } + + #[tokio::test] + async fn has_description() { + const DOC: &str = r#"{ + __type(name: "Counter") { + description + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok(( + graphql_value!({"__type": {"description": "Description from attribute"}}), + vec![], + )), + ); + } +} + +mod custom_scalar { + use super::*; + + /// Description + #[derive(GraphQLScalar)] + #[graphql(scalar = MyScalarValue, parse_token(i32))] + struct Counter(i32); + + impl Counter { + fn to_output<S: ScalarValue>(&self) -> Value<S> { + Value::scalar(self.0) + } + + fn from_input<S: ScalarValue>(v: &InputValue<S>) -> Result<Self, String> { + v.as_int_value() + .map(Self) + .ok_or_else(|| format!("Expected `Counter`, found: {}", v)) + } + } + + struct QueryRoot; + + #[graphql_object(scalar = MyScalarValue)] + impl QueryRoot { + fn counter(value: Counter) -> Counter { + value + } + } + + #[tokio::test] + async fn is_graphql_scalar() { + const DOC: &str = r#"{ + __type(name: "Counter") { + kind + } + }"#; + + let schema = schema_with_scalar::<MyScalarValue, _, _>(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"__type": {"kind": "SCALAR"}}), vec![])), + ); + } + + #[tokio::test] + async fn resolves_counter() { + const DOC: &str = r#"{ counter(value: 0) }"#; + + let schema = schema_with_scalar::<MyScalarValue, _, _>(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"counter": 0}), vec![])), + ); + } + + #[tokio::test] + async fn has_description() { + const DOC: &str = r#"{ + __type(name: "Counter") { + description + } + }"#; + + let schema = schema_with_scalar::<MyScalarValue, _, _>(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok(( + graphql_value!({"__type": {"description": "Description"}}), + vec![], + )), + ); + } +} + +mod generic_scalar { + use super::*; + + /// Description + #[derive(GraphQLScalar)] + #[graphql(scalar = S: ScalarValue, parse_token(i32))] + struct Counter(i32); + + impl Counter { + fn to_output<S: ScalarValue>(&self) -> Value<S> { + Value::scalar(self.0) + } + + fn from_input<S: ScalarValue>(v: &InputValue<S>) -> Result<Self, String> { + v.as_int_value() + .map(Self) + .ok_or_else(|| format!("Expected `Counter`, found: {}", v)) + } + } + + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + fn counter(value: Counter) -> Counter { + value + } + } + + #[tokio::test] + async fn is_graphql_scalar() { + const DOC: &str = r#"{ + __type(name: "Counter") { + kind + } + }"#; + + let schema = schema_with_scalar::<MyScalarValue, _, _>(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"__type": {"kind": "SCALAR"}}), vec![])), + ); + } + + #[tokio::test] + async fn resolves_counter() { + const DOC: &str = r#"{ counter(value: 0) }"#; + + let schema = schema_with_scalar::<MyScalarValue, _, _>(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"counter": 0}), vec![])), + ); + } + + #[tokio::test] + async fn has_description() { + const DOC: &str = r#"{ + __type(name: "Counter") { + description + } + }"#; + + let schema = schema_with_scalar::<MyScalarValue, _, _>(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok(( + graphql_value!({"__type": {"description": "Description"}}), + vec![], + )), + ); + } +} + +mod bounded_generic_scalar { + use super::*; + + #[derive(GraphQLScalar)] + #[graphql(scalar = S: ScalarValue + Clone, parse_token(i32))] + struct Counter(i32); + + impl Counter { + fn to_output<S: ScalarValue>(&self) -> Value<S> { + Value::scalar(self.0) + } + + fn from_input<S: ScalarValue>(v: &InputValue<S>) -> Result<Self, String> { + v.as_int_value() + .map(Self) + .ok_or_else(|| format!("Expected `String`, found: {}", v)) + } + } + + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + fn counter(value: Counter) -> Counter { + value + } + } + + #[tokio::test] + async fn is_graphql_scalar() { + const DOC: &str = r#"{ + __type(name: "Counter") { + kind + } + }"#; + + let schema = schema_with_scalar::<MyScalarValue, _, _>(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"__type": {"kind": "SCALAR"}}), vec![])), + ); + } + + #[tokio::test] + async fn resolves_counter() { + const DOC: &str = r#"{ counter(value: 0) }"#; + + let schema = schema_with_scalar::<MyScalarValue, _, _>(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"counter": 0}), vec![])), + ); + } +} diff --git a/juniper/src/executor_tests/introspection/mod.rs b/juniper/src/executor_tests/introspection/mod.rs index 8f140cf7..25eff845 100644 --- a/juniper/src/executor_tests/introspection/mod.rs +++ b/juniper/src/executor_tests/introspection/mod.rs @@ -6,10 +6,10 @@ mod input_object; use self::input_object::{NamedPublic, NamedPublicWithDescription}; use crate::{ - graphql_interface, graphql_object, graphql_scalar, graphql_value, graphql_vars, + graphql_interface, graphql_object, graphql_value, graphql_vars, schema::model::RootNode, types::scalars::{EmptyMutation, EmptySubscription}, - GraphQLEnum, InputValue, ScalarValue, Value, + GraphQLEnum, GraphQLScalar, }; #[derive(GraphQLEnum)] @@ -19,22 +19,10 @@ enum Sample { Two, } -// TODO: Use `#[derive(GraphQLScalar)]` once implemented. -#[graphql_scalar(name = "SampleScalar", parse_token(i32))] +#[derive(GraphQLScalar)] +#[graphql(name = "SampleScalar", transparent)] struct Scalar(i32); -impl Scalar { - fn to_output<S: ScalarValue>(&self) -> Value<S> { - Value::scalar(self.0) - } - - fn from_input<S: ScalarValue>(v: &InputValue<S>) -> Result<Self, String> { - v.as_int_value() - .map(Self) - .ok_or_else(|| format!("Expected `Int`, found: {}", v)) - } -} - /// A sample interface #[graphql_interface(name = "SampleInterface", for = Root)] trait Interface { diff --git a/juniper/src/executor_tests/variables.rs b/juniper/src/executor_tests/variables.rs index 22bb8b49..257f26c4 100644 --- a/juniper/src/executor_tests/variables.rs +++ b/juniper/src/executor_tests/variables.rs @@ -1,18 +1,17 @@ use crate::{ executor::Variables, - graphql_object, graphql_scalar, graphql_value, graphql_vars, + graphql_object, graphql_value, graphql_vars, parser::SourcePosition, schema::model::RootNode, types::scalars::{EmptyMutation, EmptySubscription}, validation::RuleError, value::{DefaultScalarValue, Object}, GraphQLError::ValidationError, - GraphQLInputObject, InputValue, ScalarValue, Value, + GraphQLInputObject, GraphQLScalar, InputValue, ScalarValue, Value, }; -// TODO: Use `#[derive(GraphQLScalar)]` once implemented. -#[derive(Debug)] -#[graphql_scalar(parse_token(String))] +#[derive(Debug, GraphQLScalar)] +#[graphql(parse_token(String))] struct TestComplexScalar; impl TestComplexScalar { diff --git a/juniper/src/lib.rs b/juniper/src/lib.rs index bd3dadd0..ff430e32 100644 --- a/juniper/src/lib.rs +++ b/juniper/src/lib.rs @@ -115,7 +115,7 @@ pub use futures::future::{BoxFuture, LocalBoxFuture}; // functionality automatically. pub use juniper_codegen::{ graphql_interface, graphql_object, graphql_scalar, graphql_subscription, graphql_union, - GraphQLEnum, GraphQLInputObject, GraphQLObject, GraphQLUnion, + GraphQLEnum, GraphQLInputObject, GraphQLObject, GraphQLScalar, GraphQLUnion, }; #[doc(hidden)] diff --git a/juniper/src/types/scalars.rs b/juniper/src/types/scalars.rs index 52a21dc0..2ba5e5ea 100644 --- a/juniper/src/types/scalars.rs +++ b/juniper/src/types/scalars.rs @@ -17,14 +17,14 @@ use crate::{ subscriptions::GraphQLSubscriptionValue, }, value::{ParseScalarResult, ScalarValue, Value}, + GraphQLScalar, }; /// An ID as defined by the GraphQL specification /// /// Represented as a string, but can be converted _to_ from an integer as well. -// TODO: Use `#[derive(GraphQLScalar)]` once implemented. -#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] -#[graphql_scalar(parse_token(String, i32))] +#[derive(Clone, Debug, Deserialize, Eq, GraphQLScalar, PartialEq, Serialize)] +#[graphql(parse_token(String, i32))] pub struct ID(String); impl ID { @@ -35,7 +35,7 @@ impl ID { fn from_input<S: ScalarValue>(v: &InputValue<S>) -> Result<Self, String> { v.as_string_value() .map(str::to_owned) - .or_else(|| v.as_int_value().map(|i| i.to_string())) + .or_else(|| v.as_int_value().as_ref().map(ToString::to_string)) .map(Self) .ok_or_else(|| format!("Expected `String` or `Int`, found: {}", v)) } diff --git a/juniper_codegen/src/graphql_scalar/attr.rs b/juniper_codegen/src/graphql_scalar/attr.rs index 1405b236..b9ff5b58 100644 --- a/juniper_codegen/src/graphql_scalar/attr.rs +++ b/juniper_codegen/src/graphql_scalar/attr.rs @@ -1,7 +1,7 @@ //! Code generation for `#[graphql_scalar]` macro. use proc_macro2::{Span, TokenStream}; -use quote::{quote, ToTokens}; +use quote::quote; use syn::{parse_quote, spanned::Spanned}; use crate::{ @@ -10,7 +10,7 @@ use crate::{ GraphQLScope, }; -use super::{Attr, Definition, GraphQLScalarMethods, ParseToken}; +use super::{derive::parse_derived_methods, Attr, Definition, Methods, ParseToken}; const ERR: GraphQLScope = GraphQLScope::ScalarAttr; @@ -39,36 +39,14 @@ fn expand_on_type_alias( ast: syn::ItemType, ) -> syn::Result<TokenStream> { let attr = Attr::from_attrs("graphql_scalar", &attrs)?; + if attr.transparent { + return Err(ERR.custom_error( + ast.span(), + "`transparent` attribute argument isn't applicable to type aliases", + )); + } - let field = match ( - attr.to_output.as_deref().cloned(), - attr.from_input.as_deref().cloned(), - attr.parse_token.as_deref().cloned(), - attr.with.as_deref().cloned(), - ) { - (Some(to_output), Some(from_input), Some(parse_token), None) => { - GraphQLScalarMethods::Custom { - to_output, - from_input, - parse_token, - } - } - (to_output, from_input, parse_token, Some(module)) => GraphQLScalarMethods::Custom { - to_output: to_output.unwrap_or_else(|| parse_quote! { #module::to_output }), - from_input: from_input.unwrap_or_else(|| parse_quote! { #module::from_input }), - parse_token: parse_token - .unwrap_or_else(|| ParseToken::Custom(parse_quote! { #module::parse_token })), - }, - _ => { - return Err(ERR.custom_error( - ast.span(), - "all custom resolvers have to be provided via `with` or \ - combination of `to_output_with`, `from_input_with`, \ - `parse_token_with` attribute arguments", - )); - } - }; - + let methods = parse_type_alias_methods(&ast, &attr)?; let scalar = scalar::Type::parse(attr.scalar.as_deref(), &ast.generics); let def = Definition { @@ -77,7 +55,7 @@ fn expand_on_type_alias( .where_clause .map_or_else(Vec::new, |cl| cl.into_inner()), generics: ast.generics.clone(), - methods: field, + methods, name: attr .name .as_deref() @@ -86,8 +64,7 @@ fn expand_on_type_alias( description: attr.description.as_deref().cloned(), specified_by_url: attr.specified_by_url.as_deref().cloned(), scalar, - } - .to_token_stream(); + }; Ok(quote! { #ast @@ -95,38 +72,13 @@ fn expand_on_type_alias( }) } -// TODO: Support `#[graphql(transparent)]`. -/// Expands `#[graphql_scalar]` macro placed on a struct/enum/union. +/// Expands `#[graphql_scalar]` macro placed on a struct, enum or union. fn expand_on_derive_input( attrs: Vec<syn::Attribute>, ast: syn::DeriveInput, ) -> syn::Result<TokenStream> { let attr = Attr::from_attrs("graphql_scalar", &attrs)?; - - let field = match ( - attr.to_output.as_deref().cloned(), - attr.from_input.as_deref().cloned(), - attr.parse_token.as_deref().cloned(), - attr.with.as_deref().cloned(), - ) { - (Some(to_output), Some(from_input), Some(parse_token), None) => { - GraphQLScalarMethods::Custom { - to_output, - from_input, - parse_token, - } - } - (to_output, from_input, parse_token, module) => { - let module = module.unwrap_or_else(|| parse_quote! { Self }); - GraphQLScalarMethods::Custom { - to_output: to_output.unwrap_or_else(|| parse_quote! { #module::to_output }), - from_input: from_input.unwrap_or_else(|| parse_quote! { #module::from_input }), - parse_token: parse_token - .unwrap_or_else(|| ParseToken::Custom(parse_quote! { #module::parse_token })), - } - } - }; - + let methods = parse_derived_methods(&ast, &attr)?; let scalar = scalar::Type::parse(attr.scalar.as_deref(), &ast.generics); let def = Definition { @@ -135,7 +87,7 @@ fn expand_on_derive_input( .where_clause .map_or_else(Vec::new, |cl| cl.into_inner()), generics: ast.generics.clone(), - methods: field, + methods, name: attr .name .as_deref() @@ -144,11 +96,38 @@ fn expand_on_derive_input( description: attr.description.as_deref().cloned(), specified_by_url: attr.specified_by_url.as_deref().cloned(), scalar, - } - .to_token_stream(); + }; Ok(quote! { #ast #def }) } + +/// Parses [`Methods`] from the provided [`Attr`] for the specified type alias. +fn parse_type_alias_methods(ast: &syn::ItemType, attr: &Attr) -> syn::Result<Methods> { + match ( + attr.to_output.as_deref().cloned(), + attr.from_input.as_deref().cloned(), + attr.parse_token.as_deref().cloned(), + attr.with.as_deref().cloned(), + ) { + (Some(to_output), Some(from_input), Some(parse_token), None) => Ok(Methods::Custom { + to_output, + from_input, + parse_token, + }), + (to_output, from_input, parse_token, Some(module)) => Ok(Methods::Custom { + to_output: to_output.unwrap_or_else(|| parse_quote! { #module::to_output }), + from_input: from_input.unwrap_or_else(|| parse_quote! { #module::from_input }), + parse_token: parse_token + .unwrap_or_else(|| ParseToken::Custom(parse_quote! { #module::parse_token })), + }), + _ => Err(ERR.custom_error( + ast.span(), + "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", + )), + } +} diff --git a/juniper_codegen/src/graphql_scalar/derive.rs b/juniper_codegen/src/graphql_scalar/derive.rs new file mode 100644 index 00000000..e16bcd7d --- /dev/null +++ b/juniper_codegen/src/graphql_scalar/derive.rs @@ -0,0 +1,123 @@ +//! Code generation for `#[derive(GraphQLScalar)]` macro. + +use proc_macro2::TokenStream; +use quote::ToTokens; +use syn::{parse_quote, spanned::Spanned}; + +use crate::{common::scalar, result::GraphQLScope}; + +use super::{Attr, Definition, Field, Methods, ParseToken, TypeOrIdent}; + +/// [`GraphQLScope`] of errors for `#[derive(GraphQLScalar)]` macro. +const ERR: GraphQLScope = GraphQLScope::ScalarDerive; + +/// Expands `#[derive(GraphQLScalar)]` macro into generated code. +pub fn expand(input: TokenStream) -> syn::Result<TokenStream> { + let ast = syn::parse2::<syn::DeriveInput>(input)?; + let attr = Attr::from_attrs("graphql", &ast.attrs)?; + let methods = parse_derived_methods(&ast, &attr)?; + let scalar = scalar::Type::parse(attr.scalar.as_deref(), &ast.generics); + + Ok(Definition { + ty: TypeOrIdent::Ident(ast.ident.clone()), + where_clause: attr + .where_clause + .map_or_else(Vec::new, |cl| cl.into_inner()), + generics: ast.generics.clone(), + methods, + name: attr + .name + .as_deref() + .cloned() + .unwrap_or_else(|| ast.ident.to_string()), + description: attr.description.as_deref().cloned(), + specified_by_url: attr.specified_by_url.as_deref().cloned(), + scalar, + } + .to_token_stream()) +} + +/// Parses [`Methods`] from the provided [`Attr`] for the specified +/// [`syn::DeriveInput`]. +pub(super) fn parse_derived_methods(ast: &syn::DeriveInput, attr: &Attr) -> syn::Result<Methods> { + match ( + attr.to_output.as_deref().cloned(), + attr.from_input.as_deref().cloned(), + attr.parse_token.as_deref().cloned(), + attr.with.as_deref().cloned(), + attr.transparent, + ) { + (Some(to_output), Some(from_input), Some(parse_token), None, false) => { + Ok(Methods::Custom { + to_output, + from_input, + parse_token, + }) + } + (to_output, from_input, parse_token, module, false) => { + let module = module.unwrap_or_else(|| parse_quote! { Self }); + Ok(Methods::Custom { + to_output: to_output.unwrap_or_else(|| parse_quote! { #module::to_output }), + from_input: from_input.unwrap_or_else(|| parse_quote! { #module::from_input }), + parse_token: parse_token + .unwrap_or_else(|| ParseToken::Custom(parse_quote! { #module::parse_token })), + }) + } + (to_output, from_input, parse_token, None, true) => { + let data = if let syn::Data::Struct(data) = &ast.data { + data + } else { + return Err(ERR.custom_error( + ast.span(), + "`transparent` attribute argument requires exactly 1 field", + )); + }; + let field = match &data.fields { + syn::Fields::Unit => Err(ERR.custom_error( + ast.span(), + "`transparent` attribute argument requires exactly 1 field", + )), + syn::Fields::Unnamed(fields) => fields + .unnamed + .first() + .filter(|_| fields.unnamed.len() == 1) + .cloned() + .map(Field::Unnamed) + .ok_or_else(|| { + ERR.custom_error( + ast.span(), + "`transparent` attribute argument requires \ + exactly 1 field", + ) + }), + syn::Fields::Named(fields) => fields + .named + .first() + .filter(|_| fields.named.len() == 1) + .cloned() + .map(Field::Named) + .ok_or_else(|| { + ERR.custom_error( + ast.span(), + "`transparent` attribute argument requires \ + exactly 1 field", + ) + }), + }?; + Ok(Methods::Delegated { + to_output, + from_input, + parse_token, + field: Box::new(field), + }) + } + (_, _, _, Some(module), true) => Err(ERR.custom_error( + module.span(), + "`with = <path>` 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.", + )), + } +} diff --git a/juniper_codegen/src/graphql_scalar/mod.rs b/juniper_codegen/src/graphql_scalar/mod.rs index 08dad125..a7de21bc 100644 --- a/juniper_codegen/src/graphql_scalar/mod.rs +++ b/juniper_codegen/src/graphql_scalar/mod.rs @@ -26,6 +26,7 @@ use crate::{ }; pub mod attr; +pub mod derive; /// Available arguments behind `#[graphql]`/`#[graphql_scalar]` attributes when /// generating code for [GraphQL scalar][1]. @@ -86,6 +87,10 @@ struct Attr { /// Explicit where clause added to [`syn::WhereClause`]. where_clause: Option<SpanContainer<Vec<syn::WherePredicate>>>, + + /// Indicator for single-field structs allowing to delegate implmemntations + /// of non-provided resolvers to that field. + transparent: bool, } impl Parse for Attr { @@ -184,10 +189,7 @@ impl Parse for Attr { .none_or_else(|_| err::dup_arg(&ident))? } "where" => { - let (span, parsed_predicates) = if input.parse::<token::Eq>().is_ok() { - let pred = input.parse::<syn::WherePredicate>()?; - (pred.span(), vec![pred]) - } else { + let (span, parsed_predicates) = { let predicates; let _ = syn::parenthesized!(predicates in input); let parsed_predicates = predicates @@ -196,7 +198,7 @@ impl Parse for Attr { if parsed_predicates.is_empty() { return Err(syn::Error::new( ident.span(), - "expected at least 1 where predicate.", + "expected at least 1 where predicate", )); } @@ -214,6 +216,9 @@ impl Parse for Attr { )) .none_or_else(|_| err::dup_arg(&ident))? } + "transparent" => { + out.transparent = true; + } name => { return Err(err::unknown_arg(&ident, name)); } @@ -238,6 +243,7 @@ impl Attr { parse_token: try_merge_opt!(parse_token: self, another), with: try_merge_opt!(with: self, another), where_clause: try_merge_opt!(where_clause: self, another), + transparent: self.transparent || another.transparent, }) } @@ -293,7 +299,7 @@ struct Definition { /// [`GraphQLScalarMethods`] representing [GraphQL scalar][1]. /// /// [1]: https://spec.graphql.org/October2021#sec-Scalars - methods: GraphQLScalarMethods, + methods: Methods, /// Description of this [GraphQL scalar][1] to put into GraphQL schema. /// @@ -535,10 +541,10 @@ impl Definition { #[automatically_derived] impl#impl_gens ::juniper::ParseScalarValue<#scalar> for #ty #where_clause - { - fn from_str( + { + fn from_str( token: ::juniper::parser::ScalarToken<'_>, - ) -> ::juniper::ParseScalarResult<'_, #scalar> { + ) -> ::juniper::ParseScalarResult<'_, #scalar> { #from_str } } @@ -676,8 +682,7 @@ impl VisitMut for ModifyLifetimes { /// Methods representing [GraphQL scalar][1]. /// /// [1]: https://spec.graphql.org/October2021#sec-Scalars -#[allow(dead_code)] -enum GraphQLScalarMethods { +enum Methods { /// [GraphQL scalar][1] represented with only custom resolvers. /// /// [1]: https://spec.graphql.org/October2021#sec-Scalars @@ -713,7 +718,7 @@ enum GraphQLScalarMethods { }, } -impl GraphQLScalarMethods { +impl Methods { /// Expands [`GraphQLValue::resolve`] method. /// /// [`GraphQLValue::resolve`]: juniper::GraphQLValue::resolve @@ -755,7 +760,9 @@ impl GraphQLScalarMethods { } } Self::Delegated { field, .. } => { - quote! { ::juniper::ToInputValue::<#scalar>::to_input_value(&self.#field) } + quote! { + ::juniper::ToInputValue::<#scalar>::to_input_value(&self.#field) + } } } } @@ -798,7 +805,9 @@ impl GraphQLScalarMethods { } Self::Delegated { field, .. } => { let field_ty = field.ty(); - quote! { <#field_ty as ::juniper::ParseScalarValue<#scalar>>::from_str(token) } + quote! { + <#field_ty as ::juniper::ParseScalarValue<#scalar>>::from_str(token) + } } } } @@ -848,7 +857,6 @@ impl ParseToken { } /// Struct field to resolve not provided methods. -#[allow(dead_code)] enum Field { /// Named [`Field`]. Named(syn::Field), diff --git a/juniper_codegen/src/lib.rs b/juniper_codegen/src/lib.rs index 2be16385..6208a9a3 100644 --- a/juniper_codegen/src/lib.rs +++ b/juniper_codegen/src/lib.rs @@ -142,19 +142,48 @@ pub fn derive_input_object(input: TokenStream) -> TokenStream { } } -/// `#[graphql_scalar]` is interchangeable with `#[derive(`[`GraphQLScalar`]`)]` -/// macro: +/// `#[derive(GraphQLScalar)]` macro for deriving a [GraphQL scalar][0] +/// implementation. /// -/// ```rust,ignore +/// # Transparent delegation +/// +/// Sometimes, you want to create a custom [GraphQL scalar][0] type by just +/// wrapping an existing one, inheriting all its behavior. In Rust, this is +/// often called as ["`Newtype` pattern"][1]. This may be achieved by providing +/// a `#[graphql(transparent)]` attribute to the definition: +/// ```rust +/// # use juniper::{GraphQLObject, GraphQLScalar}; +/// # +/// #[derive(GraphQLScalar)] +/// #[graphql(transparent)] +/// struct UserId(String); +/// +/// #[derive(GraphQLScalar)] +/// #[graphql(transparent)] +/// struct DroidId { +/// value: String, +/// } +/// +/// #[derive(GraphQLObject)] +/// struct Pair { +/// user_id: UserId, +/// droid_id: DroidId, +/// } +/// ``` +/// +/// The inherited behaviour may also be customized: +/// ```rust +/// # use juniper::GraphQLScalar; +/// # /// /// Doc comments are used for the GraphQL type description. -/// #[derive(juniper::GraphQLScalar)] +/// #[derive(GraphQLScalar)] /// #[graphql( -/// // Set a custom GraphQL name. +/// // Custom GraphQL name. /// name = "MyUserId", -/// // A description can also specified in the attribute. +/// // Description can also specified in the attribute. /// // This will the doc comment, if one exists. /// description = "...", -/// // A specification URL. +/// // Optional specification URL. /// specified_by_url = "https://tools.ietf.org/html/rfc4122", /// // Explicit generic scalar. /// scalar = S: juniper::ScalarValue, @@ -163,17 +192,284 @@ pub fn derive_input_object(input: TokenStream) -> TokenStream { /// struct UserId(String); /// ``` /// -/// Is transformed into: +/// All of the methods inherited from `Newtype`'s field may also be overridden +/// with the attributes described below. /// -/// ```rust,ignore +/// # Custom resolving +/// +/// Customization of a [GraphQL scalar][0] type resolving is possible via +/// `#[graphql(to_output_with = <fn path>)]` attribute: +/// ```rust +/// # use juniper::{GraphQLScalar, ScalarValue, Value}; +/// # +/// #[derive(GraphQLScalar)] +/// #[graphql(to_output_with = to_output, transparent)] +/// struct Incremented(i32); +/// +/// /// Increments [`Incremented`] before converting into a [`Value`]. +/// fn to_output<S: ScalarValue>(v: &Incremented) -> Value<S> { +/// let inc = v.0 + 1; +/// Value::from(inc) +/// } +/// ``` +/// +/// # Custom parsing +/// +/// Customization of a [GraphQL scalar][0] type parsing is possible via +/// `#[graphql(from_input_with = <fn path>)]` attribute: +/// ```rust +/// # use juniper::{DefaultScalarValue, GraphQLScalar, InputValue, ScalarValue}; +/// # +/// #[derive(GraphQLScalar)] +/// #[graphql(from_input_with = Self::from_input, transparent)] +/// struct UserId(String); +/// +/// impl UserId { +/// /// Checks whether [`InputValue`] is `String` beginning with `id: ` and +/// /// strips it. +/// fn from_input<S: ScalarValue>( +/// input: &InputValue<S>, +/// ) -> Result<Self, String> { +/// // ^^^^^^ must implement `IntoFieldError` +/// input.as_string_value() +/// .ok_or_else(|| format!("Expected `String`, found: {}", input)) +/// .and_then(|str| { +/// str.strip_prefix("id: ") +/// .ok_or_else(|| { +/// format!( +/// "Expected `UserId` to begin with `id: `, \ +/// found: {}", +/// input, +/// ) +/// }) +/// }) +/// .map(|id| Self(id.to_owned())) +/// } +/// } +/// ``` +/// +/// # Custom token parsing +/// +/// Customization of which tokens a [GraphQL scalar][0] type should be parsed is +/// possible via `#[graphql(parse_token_with = <fn path>)]` or +/// `#[graphql(parse_token(<types>)]` attributes: +/// ```rust +/// # use juniper::{ +/// # GraphQLScalar, InputValue, ParseScalarResult, ParseScalarValue, +/// # ScalarValue, ScalarToken, Value, +/// # }; +/// # +/// #[derive(GraphQLScalar)] +/// #[graphql( +/// to_output_with = to_output, +/// from_input_with = from_input, +/// parse_token_with = parse_token, +/// )] +/// // ^^^^^^^^^^^^^^^^ Can be replaced with `parse_token(String, i32)`, which +/// // tries to parse as `String` first, and then as `i32` if +/// // prior fails. +/// enum StringOrInt { +/// String(String), +/// Int(i32), +/// } +/// +/// fn to_output<S: ScalarValue>(v: &StringOrInt) -> Value<S> { +/// match v { +/// StringOrInt::String(str) => Value::scalar(str.to_owned()), +/// StringOrInt::Int(i) => Value::scalar(*i), +/// } +/// } +/// +/// fn from_input<S: ScalarValue>(v: &InputValue<S>) -> Result<StringOrInt, String> { +/// v.as_string_value() +/// .map(|s| StringOrInt::String(s.to_owned())) +/// .or_else(|| v.as_int_value().map(StringOrInt::Int)) +/// .ok_or_else(|| format!("Expected `String` or `Int`, found: {}", v)) +/// } +/// +/// fn parse_token<S: ScalarValue>(value: ScalarToken<'_>) -> ParseScalarResult<'_, S> { +/// <String as ParseScalarValue<S>>::from_str(value) +/// .or_else(|_| <i32 as ParseScalarValue<S>>::from_str(value)) +/// } +/// ``` +/// > __NOTE:__ Once we provide all 3 custom functions, there is no sense to +/// > follow [`Newtype` pattern][1] anymore. +/// +/// # All at once +/// +/// Instead of providing all custom functions separately, it's possible to +/// provide a module holding the appropriate `to_output()`, `from_input()` and +/// `parse_token()` functions: +/// ```rust +/// # use juniper::{ +/// # GraphQLScalar, InputValue, ParseScalarResult, ParseScalarValue, +/// # ScalarValue, ScalarToken, Value, +/// # }; +/// # +/// #[derive(GraphQLScalar)] +/// #[graphql(with = string_or_int)] +/// enum StringOrInt { +/// String(String), +/// Int(i32), +/// } +/// +/// mod string_or_int { +/// use super::*; +/// +/// pub(super) fn to_output<S: ScalarValue>(v: &StringOrInt) -> Value<S> { +/// match v { +/// StringOrInt::String(str) => Value::scalar(str.to_owned()), +/// StringOrInt::Int(i) => Value::scalar(*i), +/// } +/// } +/// +/// pub(super) fn from_input<S: ScalarValue>(v: &InputValue<S>) -> Result<StringOrInt, String> { +/// v.as_string_value() +/// .map(|s| StringOrInt::String(s.to_owned())) +/// .or_else(|| v.as_int_value().map(StringOrInt::Int)) +/// .ok_or_else(|| format!("Expected `String` or `Int`, found: {}", v)) +/// } +/// +/// pub(super) fn parse_token<S: ScalarValue>(t: ScalarToken<'_>) -> ParseScalarResult<'_, S> { +/// <String as ParseScalarValue<S>>::from_str(t) +/// .or_else(|_| <i32 as ParseScalarValue<S>>::from_str(t)) +/// } +/// } +/// # +/// # fn main() {} +/// ``` +/// +/// A regular `impl` block is also suitable for that: +/// ```rust +/// # use juniper::{ +/// # GraphQLScalar, InputValue, ParseScalarResult, ParseScalarValue, +/// # ScalarValue, ScalarToken, Value, +/// # }; +/// # +/// #[derive(GraphQLScalar)] +/// // #[graphql(with = Self)] <- default behaviour, so can be omitted +/// enum StringOrInt { +/// String(String), +/// Int(i32), +/// } +/// +/// impl StringOrInt { +/// fn to_output<S: ScalarValue>(&self) -> Value<S> { +/// match self { +/// Self::String(str) => Value::scalar(str.to_owned()), +/// Self::Int(i) => Value::scalar(*i), +/// } +/// } +/// +/// fn from_input<S>(v: &InputValue<S>) -> Result<Self, String> +/// where +/// S: ScalarValue +/// { +/// v.as_string_value() +/// .map(|s| Self::String(s.to_owned())) +/// .or_else(|| v.as_int_value().map(Self::Int)) +/// .ok_or_else(|| format!("Expected `String` or `Int`, found: {}", v)) +/// } +/// +/// fn parse_token<S>(value: ScalarToken<'_>) -> ParseScalarResult<'_, S> +/// where +/// S: ScalarValue +/// { +/// <String as ParseScalarValue<S>>::from_str(value) +/// .or_else(|_| <i32 as ParseScalarValue<S>>::from_str(value)) +/// } +/// } +/// # +/// # fn main() {} +/// ``` +/// +/// At the same time, any custom function still may be specified separately: +/// ```rust +/// # use juniper::{ +/// # GraphQLScalar, InputValue, ParseScalarResult, ScalarValue, +/// # ScalarToken, Value +/// # }; +/// # +/// #[derive(GraphQLScalar)] +/// #[graphql( +/// with = string_or_int, +/// parse_token(String, i32) +/// )] +/// enum StringOrInt { +/// String(String), +/// Int(i32), +/// } +/// +/// mod string_or_int { +/// use super::*; +/// +/// pub(super) fn to_output<S>(v: &StringOrInt) -> Value<S> +/// where +/// S: ScalarValue, +/// { +/// match v { +/// StringOrInt::String(str) => Value::scalar(str.to_owned()), +/// StringOrInt::Int(i) => Value::scalar(*i), +/// } +/// } +/// +/// pub(super) fn from_input<S>(v: &InputValue<S>) -> Result<StringOrInt, String> +/// where +/// S: ScalarValue, +/// { +/// v.as_string_value() +/// .map(|s| StringOrInt::String(s.to_owned())) +/// .or_else(|| v.as_int_value().map(StringOrInt::Int)) +/// .ok_or_else(|| format!("Expected `String` or `Int`, found: {}", v)) +/// } +/// +/// // No need in `parse_token()` function. +/// } +/// # +/// # fn main() {} +/// ``` +/// +/// # Custom `ScalarValue` +/// +/// By default, this macro generates code, which is generic over a +/// [`ScalarValue`] type. Concrete [`ScalarValue`] type may be specified via +/// `#[graphql(scalar = <type>)]` attribute. +/// +/// It also may be used to provide additional bounds to the [`ScalarValue`] +/// generic, like the following: `#[graphql(scalar = S: Trait)]`. +/// +/// # Additional arbitrary trait bounds +/// +/// [GraphQL scalar][0] type implementation may be bound with any additional +/// trait bounds via `#[graphql(where(<bounds>))]` attribute, like the +/// following: `#[graphql(where(S: Trait, Self: fmt::Debug + fmt::Display))]`. +/// +/// [0]: https://spec.graphql.org/October2021#sec-Scalars +/// [1]: https://rust-unofficial.github.io/patterns/patterns/behavioural/newtype.html +/// [`ScalarValue`]: juniper::ScalarValue +#[proc_macro_error] +#[proc_macro_derive(GraphQLScalar, attributes(graphql))] +pub fn derive_scalar(input: TokenStream) -> TokenStream { + graphql_scalar::derive::expand(input.into()) + .unwrap_or_abort() + .into() +} + +/// `#[graphql_scalar]` macro.is interchangeable with +/// `#[derive(`[`GraphQLScalar`]`)]` macro, and is used for deriving a +/// [GraphQL scalar][0] implementation. +/// +/// ```rust +/// # use juniper::graphql_scalar; +/// # /// /// Doc comments are used for the GraphQL type description. -/// #[juniper::graphql_scalar( -/// // Set a custom GraphQL name. +/// #[graphql_scalar( +/// // Custom GraphQL name. /// name = "MyUserId", -/// // A description can also specified in the attribute. +/// // Description can also specified in the attribute. /// // This will the doc comment, if one exists. /// description = "...", -/// // A specification URL. +/// // Optional specification URL. /// specified_by_url = "https://tools.ietf.org/html/rfc4122", /// // Explicit generic scalar. /// scalar = S: juniper::ScalarValue, @@ -182,30 +478,30 @@ pub fn derive_input_object(input: TokenStream) -> TokenStream { /// struct UserId(String); /// ``` /// -/// In addition to that `#[graphql_scalar]` can be used in case -/// [`GraphQLScalar`] isn't applicable because type located in other crate and -/// you don't want to wrap it in a newtype. This is done by placing -/// `#[graphql_scalar]` on a type alias. +/// # Foreign types /// -/// All attributes are mirroring [`GraphQLScalar`] derive macro. +/// Additionally, `#[graphql_scalar]` can be used directly on foreign types via +/// type alias, without using [`Newtype` pattern][1]. /// /// > __NOTE:__ To satisfy [orphan rules] you should provide local /// > [`ScalarValue`] implementation. /// /// ```rust /// # mod date { +/// # use std::{fmt, str::FromStr}; +/// # /// # pub struct Date; /// # -/// # impl std::str::FromStr for Date { +/// # impl FromStr for Date { /// # type Err = String; /// # -/// # fn from_str(_value: &str) -> Result<Self, Self::Err> { +/// # fn from_str(_: &str) -> Result<Self, Self::Err> { /// # unimplemented!() /// # } /// # } /// # -/// # impl std::fmt::Display for Date { -/// # fn fmt(&self, _f: &mut std::fmt::Formatter) -> std::fmt::Result { +/// # impl fmt::Display for Date { +/// # fn fmt(&self, _: &mut fmt::Formatter<'_>) -> fmt::Result { /// # unimplemented!() /// # } /// # } @@ -218,21 +514,18 @@ pub fn derive_input_object(input: TokenStream) -> TokenStream { /// with = date_scalar, /// parse_token(String), /// scalar = CustomScalarValue, -/// // ^^^^^^^^^^^^^^^^^ Local `ScalarValue` implementation. /// )] +/// // ^^^^^^^^^^^^^^^^^ local `ScalarValue` implementation /// type Date = date::Date; -/// // ^^^^^^^^^^ Type from another crate. +/// // ^^^^^^^^^^ type from another crate /// /// mod date_scalar { /// use super::*; /// -/// // Define how to convert your custom scalar into a primitive type. /// pub(super) fn to_output(v: &Date) -> Value<CustomScalarValue> { /// Value::scalar(v.to_string()) /// } /// -/// // Define how to parse a primitive type into your custom scalar. -/// // NOTE: The error type should implement `IntoFieldError<S>`. /// pub(super) fn from_input(v: &InputValue<CustomScalarValue>) -> Result<Date, String> { /// v.as_string_value() /// .ok_or_else(|| format!("Expected `String`, found: {}", v)) @@ -243,6 +536,8 @@ pub fn derive_input_object(input: TokenStream) -> TokenStream { /// # fn main() { } /// ``` /// +/// [0]: https://spec.graphql.org/October2021#sec-Scalars +/// [1]: https://rust-unofficial.github.io/patterns/patterns/behavioural/newtype.html /// [orphan rules]: https://bit.ly/3glAGC2 /// [`GraphQLScalar`]: juniper::GraphQLScalar /// [`ScalarValue`]: juniper::ScalarValue diff --git a/juniper_codegen/src/result.rs b/juniper_codegen/src/result.rs index 93db3e4a..811daf57 100644 --- a/juniper_codegen/src/result.rs +++ b/juniper_codegen/src/result.rs @@ -8,13 +8,11 @@ use std::fmt; /// URL of the GraphQL specification (June 2018 Edition). pub const SPEC_URL: &str = "https://spec.graphql.org/June2018/"; -#[allow(unused_variables)] pub enum GraphQLScope { InterfaceAttr, ObjectAttr, ObjectDerive, ScalarAttr, - #[allow(dead_code)] ScalarDerive, UnionAttr, UnionDerive,