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:
parent
63198cdfcb
commit
0ebd19af5a
42 changed files with 1922 additions and 185 deletions
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
use juniper::graphql_scalar;
|
||||
|
||||
#[graphql_scalar(specified_by_url = "not an url", transparent)]
|
||||
struct ScalarSpecifiedByUrl(i32);
|
||||
|
||||
fn main() {}
|
|
@ -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)]
|
||||
| ^^^^^^^^^^^^
|
|
@ -0,0 +1,6 @@
|
|||
use juniper::graphql_scalar;
|
||||
|
||||
#[graphql_scalar(with = Self, transparent)]
|
||||
struct Scalar;
|
||||
|
||||
fn main() {}
|
|
@ -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)]
|
||||
| ^^^^
|
|
@ -0,0 +1,9 @@
|
|||
use juniper::graphql_scalar;
|
||||
|
||||
#[graphql_scalar(transparent)]
|
||||
struct Scalar {
|
||||
id: i32,
|
||||
another: i32,
|
||||
}
|
||||
|
||||
fn main() {}
|
|
@ -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 | | }
|
||||
| |_^
|
|
@ -0,0 +1,6 @@
|
|||
use juniper::graphql_scalar;
|
||||
|
||||
#[graphql_scalar(transparent)]
|
||||
struct Scalar(i32, i32);
|
||||
|
||||
fn main() {}
|
|
@ -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);
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^
|
|
@ -0,0 +1,6 @@
|
|||
use juniper::graphql_scalar;
|
||||
|
||||
#[graphql_scalar(transparent)]
|
||||
struct ScalarSpecifiedByUrl;
|
||||
|
||||
fn main() {}
|
|
@ -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;
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
@ -0,0 +1,7 @@
|
|||
use juniper::GraphQLScalar;
|
||||
|
||||
#[derive(GraphQLScalar)]
|
||||
#[graphql(specified_by_url = "not an url", transparent)]
|
||||
struct ScalarSpecifiedByUrl(i64);
|
||||
|
||||
fn main() {}
|
|
@ -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)]
|
||||
| ^^^^^^^^^^^^
|
|
@ -0,0 +1,7 @@
|
|||
use juniper::GraphQLScalar;
|
||||
|
||||
#[derive(GraphQLScalar)]
|
||||
#[graphql(with = Self, transparent)]
|
||||
struct Scalar;
|
||||
|
||||
fn main() {}
|
|
@ -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)]
|
||||
| ^^^^
|
|
@ -0,0 +1,10 @@
|
|||
use juniper::GraphQLScalar;
|
||||
|
||||
#[derive(GraphQLScalar)]
|
||||
#[graphql(transparent)]
|
||||
struct Scalar {
|
||||
id: i32,
|
||||
another: i32,
|
||||
}
|
||||
|
||||
fn main() {}
|
|
@ -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 | | }
|
||||
| |_^
|
|
@ -0,0 +1,7 @@
|
|||
use juniper::GraphQLScalar;
|
||||
|
||||
#[derive(GraphQLScalar)]
|
||||
#[graphql(transparent)]
|
||||
struct Scalar(i32, i32);
|
||||
|
||||
fn main() {}
|
|
@ -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);
|
||||
| |________________________^
|
|
@ -0,0 +1,7 @@
|
|||
use juniper::GraphQLScalar;
|
||||
|
||||
#[derive(GraphQLScalar)]
|
||||
#[graphql(transparent)]
|
||||
struct ScalarSpecifiedByUrl;
|
||||
|
||||
fn main() {}
|
|
@ -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;
|
||||
| |____________________________^
|
|
@ -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() {}
|
|
@ -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)
|
||||
| ^^^^
|
|
@ -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",
|
||||
| ^^^^^^^^^^^^
|
|
@ -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;
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
@ -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;
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
@ -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;
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
//! Tests for `#[graphql_scalar]` macro placed on a type alias.
|
||||
|
||||
use std::fmt;
|
||||
|
||||
use chrono::{DateTime, TimeZone, Utc};
|
||||
|
|
1114
integration_tests/juniper_tests/src/codegen/scalar_derive.rs
Normal file
1114
integration_tests/juniper_tests/src/codegen/scalar_derive.rs
Normal file
File diff suppressed because it is too large
Load diff
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
|
123
juniper_codegen/src/graphql_scalar/derive.rs
Normal file
123
juniper_codegen/src/graphql_scalar/derive.rs
Normal 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.",
|
||||
)),
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue