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>
This commit is contained in:
ilslv 2022-02-28 12:34:38 +03:00 committed by GitHub
parent 63198cdfcb
commit 0ebd19af5a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 1922 additions and 185 deletions

View file

@ -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))
}
}

View file

@ -0,0 +1,6 @@
use juniper::graphql_scalar;
#[graphql_scalar(specified_by_url = "not an url", transparent)]
struct ScalarSpecifiedByUrl(i32);
fn main() {}

View file

@ -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)]
| ^^^^^^^^^^^^

View file

@ -0,0 +1,6 @@
use juniper::graphql_scalar;
#[graphql_scalar(with = Self, transparent)]
struct Scalar;
fn main() {}

View file

@ -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)]
| ^^^^

View file

@ -0,0 +1,9 @@
use juniper::graphql_scalar;
#[graphql_scalar(transparent)]
struct Scalar {
id: i32,
another: i32,
}
fn main() {}

View file

@ -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 | | }
| |_^

View file

@ -0,0 +1,6 @@
use juniper::graphql_scalar;
#[graphql_scalar(transparent)]
struct Scalar(i32, i32);
fn main() {}

View file

@ -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);
| ^^^^^^^^^^^^^^^^^^^^^^^^

View file

@ -0,0 +1,6 @@
use juniper::graphql_scalar;
#[graphql_scalar(transparent)]
struct ScalarSpecifiedByUrl;
fn main() {}

View file

@ -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;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^

View file

@ -0,0 +1,7 @@
use juniper::GraphQLScalar;
#[derive(GraphQLScalar)]
#[graphql(specified_by_url = "not an url", transparent)]
struct ScalarSpecifiedByUrl(i64);
fn main() {}

View file

@ -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)]
| ^^^^^^^^^^^^

View file

@ -0,0 +1,7 @@
use juniper::GraphQLScalar;
#[derive(GraphQLScalar)]
#[graphql(with = Self, transparent)]
struct Scalar;
fn main() {}

View file

@ -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)]
| ^^^^

View file

@ -0,0 +1,10 @@
use juniper::GraphQLScalar;
#[derive(GraphQLScalar)]
#[graphql(transparent)]
struct Scalar {
id: i32,
another: i32,
}
fn main() {}

View file

@ -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 | | }
| |_^

View file

@ -0,0 +1,7 @@
use juniper::GraphQLScalar;
#[derive(GraphQLScalar)]
#[graphql(transparent)]
struct Scalar(i32, i32);
fn main() {}

View file

@ -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);
| |________________________^

View file

@ -0,0 +1,7 @@
use juniper::GraphQLScalar;
#[derive(GraphQLScalar)]
#[graphql(transparent)]
struct ScalarSpecifiedByUrl;
fn main() {}

View file

@ -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;
| |____________________________^

View file

@ -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() {}

View file

@ -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)
| ^^^^

View file

@ -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",
| ^^^^^^^^^^^^

View file

@ -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;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^

View file

@ -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;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^

View file

@ -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;

View file

@ -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))
}
}

View file

@ -1,3 +1,5 @@
//! Tests for `#[graphql_scalar]` macro placed on a type alias.
use std::fmt;
use chrono::{DateTime, TimeZone, Utc};

File diff suppressed because it is too large Load diff

View file

@ -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 {

View file

@ -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 {

View file

@ -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)]

View file

@ -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))
}

View file

@ -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",
)),
}
}

View file

@ -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.",
)),
}
}

View file

@ -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),

View file

@ -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

View file

@ -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,