Redesign #[graphql_scalar] macro (#1014, #1000)

- support generic scalars
- make it applicable to type aliases and struct/enums/unions
This commit is contained in:
ilslv 2022-02-24 18:12:17 +03:00 committed by GitHub
parent 3a70403aba
commit 63198cdfcb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
47 changed files with 4298 additions and 2042 deletions

View file

@ -40,34 +40,61 @@ crates. They are enabled via features that are on by default.
* url::Url
* bson::oid::ObjectId
## newtype pattern
## Custom scalars
### `#[graphql(transparent)]` attribute
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
```rust,ignore
# extern crate juniper;
#[derive(juniper::GraphQLScalarValue)]
#
#[derive(juniper::GraphQLScalar)]
#[graphql(transparent)]
pub struct UserId(i32);
#[derive(juniper::GraphQLObject)]
struct User {
id: UserId,
}
#
# fn main() {}
```
That's it, you can now user `UserId` in your schema.
`#[derive(GraphQLScalar)]` is mostly interchangeable with `#[graphql_scalar]`
attribute:
```rust,ignore
# extern crate juniper;
# use juniper::graphql_scalar;
#
#[graphql_scalar(transparent)]
pub struct UserId {
value: i32,
}
#[derive(juniper::GraphQLObject)]
struct User {
id: UserId,
}
#
# fn main() {}
```
That's it, you can now use `UserId` in your schema.
The macro also allows for more customization:
```rust
```rust,ignore
# extern crate juniper;
/// You can use a doc comment to specify a description.
#[derive(juniper::GraphQLScalarValue)]
#[derive(juniper::GraphQLScalar)]
#[graphql(
transparent,
// Overwrite the GraphQL type name.
@ -77,37 +104,276 @@ The macro also allows for more customization:
description = "My user id description",
)]
pub struct UserId(i32);
#
# fn main() {}
```
## Custom scalars
All the methods used from newtype's field can be replaced with attributes:
For more complex situations where you also need custom parsing or validation,
you can use the `graphql_scalar` proc macro.
### `#[graphql(to_output_with = <fn>)]` attribute
Typically, you represent your custom scalars as strings.
```rust,ignore
# use juniper::{GraphQLScalar, ScalarValue, Value};
#
#[derive(GraphQLScalar)]
#[graphql(to_output_with = to_output, transparent)]
struct Incremented(i32);
The example below implements a custom scalar for a custom `Date` type.
/// Increments [`Incremented`] before converting into a [`Value`].
fn to_output<S: ScalarValue>(v: &Incremented) -> Value<S> {
Value::from(v.0 + 1)
}
#
# fn main() {}
```
Note: juniper already has built-in support for the `chrono::DateTime` type
via `chrono` feature, which is enabled by default and should be used for this
purpose.
### `#[graphql(from_input_with = <fn>)]` attribute
The example below is used just for illustration.
```rust,ignore
# use juniper::{GraphQLScalar, InputValue, ScalarValue};
#
#[derive(GraphQLScalar)]
#[graphql(from_input_with = Self::from_input, transparent)]
struct UserId(String);
**Note**: the example assumes that the `Date` type implements
`std::fmt::Display` and `std::str::FromStr`.
impl UserId {
/// Checks whether [`InputValue`] is `String` beginning with `id: ` and
/// strips it.
fn from_input<S>(input: &InputValue<S>) -> Result<Self, String>
where
S: ScalarValue
{
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()))
}
}
#
# fn main() {}
```
### `#[graphql(parse_token_with = <fn>]` or `#[graphql(parse_token(<types>)]` attributes
```rust,ignore
# 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` and then as `i32`
// if prior fails.
)]
enum StringOrInt {
String(String),
Int(i32),
}
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),
}
}
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(|i| StringOrInt::Int(i)))
.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() {}
```
> __NOTE:__ As you can see, once you provide all 3 custom resolvers, there
> is no need to follow `newtype` pattern.
### `#[graphql(with = <path>)]` attribute
Instead of providing all custom resolvers, you can provide path to the `to_output`,
`from_input`, `parse_token` functions.
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
# use juniper::{
# GraphQLScalar, InputValue, ParseScalarResult, ParseScalarValue,
# ScalarValue, ScalarToken, Value
# };
#
#[derive(GraphQLScalar)]
// #[graphql(with = Self)] <- default behaviour
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(|i| Self::Int(i)))
.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() {}
```
Or it can be path to a module, where custom resolvers are located.
```rust,ignore
# 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>(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(|i| StringOrInt::Int(i)))
.ok_or_else(|| format!("Expected `String` or `Int`, found: {}", v))
}
pub(super) 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() {}
```
Also, you can partially override `#[graphql(with)]` attribute with other custom scalars.
```rust,ignore
# use juniper::{GraphQLScalar, InputValue, ParseScalarResult, ScalarValue, ScalarToken, Value};
#
#[derive(GraphQLScalar)]
#[graphql(parse_token(String, i32))]
enum StringOrInt {
String(String),
Int(i32),
}
impl StringOrInt {
fn to_output<S>(&self) -> Value<S>
where
S: ScalarValue,
{
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(|i| Self::Int(i)))
.ok_or_else(|| format!("Expected `String` or `Int`, found: {}", v))
}
}
#
# fn main() {}
```
### Using foreign types as scalars
For implementing custom scalars on foreign types there is `#[graphql_scalar]` attribute macro.
> __NOTE:__ To satisfy [orphan rules] you should provide local [`ScalarValue`] implementation.
```rust
# extern crate juniper;
# mod date {
# pub struct Date;
# impl std::str::FromStr for Date {
# type Err = String; fn from_str(_value: &str) -> Result<Self, Self::Err> { unimplemented!() }
# type Err = String;
#
# fn from_str(_value: &str) -> Result<Self, Self::Err> {
# unimplemented!()
# }
# }
# // And we define how to represent date as a string.
#
# impl std::fmt::Display for Date {
# fn fmt(&self, _f: &mut std::fmt::Formatter) -> std::fmt::Result {
# unimplemented!()
@ -115,32 +381,34 @@ The example below is used just for illustration.
# }
# }
#
use juniper::{Value, ParseScalarResult, ParseScalarValue};
use date::Date;
# use juniper::DefaultScalarValue as CustomScalarValue;
use juniper::{graphql_scalar, InputValue, ScalarValue, Value};
#[juniper::graphql_scalar(description = "Date")]
impl<S> GraphQLScalar for Date
where
S: ScalarValue
{
// Define how to convert your custom scalar into a primitive type.
fn resolve(&self) -> Value {
Value::scalar(self.to_string())
#[graphql_scalar(
with = date_scalar,
parse_token(String),
scalar = CustomScalarValue,
// ^^^^^^^^^^^^^^^^^ Local `ScalarValue` implementation.
)]
type Date = date::Date;
// ^^^^^^^^^^ Type from another crate.
mod date_scalar {
use super::*;
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>`.
fn from_input_value(v: &InputValue) -> Result<Date, String> {
v.as_string_value()
.ok_or_else(|| format!("Expected `String`, found: {}", v))
.and_then(|s| s.parse().map_err(|e| format!("Failed to parse `Date`: {}", e)))
}
// Define how to parse a string value.
fn from_str<'a>(value: ScalarToken<'a>) -> ParseScalarResult<'a, S> {
<String as ParseScalarValue<S>>::from_str(value)
pub(super) fn from_input(v: &InputValue<CustomScalarValue>) -> Result<Date, String> {
v.as_string_value()
.ok_or_else(|| format!("Expected `String`, found: {}", v))
.and_then(|s| s.parse().map_err(|e| format!("Failed to parse `Date`: {}", e)))
}
}
#
# fn main() {}
```
[orphan rules]: https://doc.rust-lang.org/reference/items/implementations.html#orphan-rules
[`ScalarValue`]: https://docs.rs/juniper/latest/juniper/trait.ScalarValue.html

View file

@ -1,14 +1,3 @@
error[E0119]: conflicting implementations of trait `<CharacterValueEnum<ObjA, ObjA> as juniper::GraphQLInterface<__S>>::mark::_::{closure#0}::MutuallyExclusive` for type `ObjA`
--> fail/interface/implementers_duplicate_ugly.rs:11:1
|
11 | #[graphql_interface(for = [ObjA, ObjAlias])]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
| |
| first implementation here
| conflicting implementation for `ObjA`
|
= note: this error originates in the macro `::juniper::sa::assert_type_ne_all` (in Nightly builds, run with -Z macro-backtrace for more info)
error[E0119]: conflicting implementations of trait `std::convert::From<ObjA>` for type `CharacterValueEnum<ObjA, ObjA>`
--> fail/interface/implementers_duplicate_ugly.rs:11:1
|
@ -19,3 +8,14 @@ error[E0119]: conflicting implementations of trait `std::convert::From<ObjA>` fo
| conflicting implementation for `CharacterValueEnum<ObjA, ObjA>`
|
= note: this error originates in the attribute macro `graphql_interface` (in Nightly builds, run with -Z macro-backtrace for more info)
error[E0119]: conflicting implementations of trait `<CharacterValueEnum<ObjA, ObjA> as juniper::GraphQLInterface<__S>>::mark::_::{closure#0}::MutuallyExclusive` for type `ObjA`
--> fail/interface/implementers_duplicate_ugly.rs:11:1
|
11 | #[graphql_interface(for = [ObjA, ObjAlias])]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
| |
| first implementation here
| conflicting implementation for `ObjA`
|
= note: this error originates in the macro `::juniper::sa::assert_type_ne_all` (in Nightly builds, run with -Z macro-backtrace for more info)

View file

@ -0,0 +1,16 @@
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

@ -0,0 +1,17 @@
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,7 +0,0 @@
use juniper::GraphQLScalarValue;
#[derive(GraphQLScalarValue)]
#[graphql(specified_by_url = "not an url")]
struct ScalarSpecifiedByUrl(i64);
fn main() {}

View file

@ -1,5 +0,0 @@
error: Invalid URL: relative URL without a base
--> fail/scalar/derive_invalid_url.rs:4:30
|
4 | #[graphql(specified_by_url = "not an url")]
| ^^^^^^^^^^^^

View file

@ -1,22 +0,0 @@
use juniper::graphql_scalar;
struct ScalarSpecifiedByUrl(i32);
#[graphql_scalar(specified_by_url = "not an url")]
impl GraphQLScalar for ScalarSpecifiedByUrl {
fn resolve(&self) -> Value {
Value::scalar(self.0)
}
fn from_input_value(v: &InputValue) -> Result<ScalarSpecifiedByUrl, String> {
v.as_int_value()
.map(ScalarSpecifiedByUrl)
.ok_or_else(|| format!("Expected `Int`, found: {}", v))
}
fn from_str<'a>(value: ScalarToken<'a>) -> ParseScalarResult<'a, DefaultScalarValue> {
<i32 as ParseScalarValue>::from_str(value)
}
}
fn main() {}

View file

@ -1,5 +0,0 @@
error: Invalid URL: relative URL without a base
--> fail/scalar/impl_invalid_url.rs:5:18
|
5 | #[graphql_scalar(specified_by_url = "not an url")]
| ^^^^^^^^^^^^^^^^

View file

@ -0,0 +1,26 @@
use juniper::{graphql_scalar, InputValue, ScalarValue, Value};
struct ScalarSpecifiedByUrl;
#[graphql_scalar(
specified_by_url = "not an url",
with = scalar,
parse_token(i32),
)]
type Scalar = ScalarSpecifiedByUrl;
mod scalar {
use super::*;
pub(super) fn to_output<S: ScalarValue>(_: &ScalarSpecifiedByUrl) -> Value<S> {
Value::scalar(0)
}
pub(super) fn from_input<S: ScalarValue>(
_: &InputValue<S>,
) -> Result<ScalarSpecifiedByUrl, String> {
Ok(ScalarSpecifiedByUrl)
}
}
fn main() {}

View file

@ -0,0 +1,5 @@
error: Invalid URL: relative URL without a base
--> fail/scalar/type_alias/impl_invalid_url.rs:6:24
|
6 | specified_by_url = "not an url",
| ^^^^^^^^^^^^

View file

@ -0,0 +1,14 @@
use juniper::{graphql_scalar, Value};
struct Scalar;
#[graphql_scalar(to_output_with = Scalar::to_output)]
type CustomScalar = Scalar;
impl Scalar {
fn to_output(&self) -> Value {
Value::scalar(0)
}
}
fn main() {}

View file

@ -0,0 +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
|
6 | type CustomScalar = Scalar;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^

View file

@ -0,0 +1,8 @@
use juniper::graphql_scalar;
struct Scalar;
#[graphql_scalar]
type CustomScalar = Scalar;
fn main() {}

View file

@ -0,0 +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
|
6 | type CustomScalar = Scalar;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^

View file

@ -5,6 +5,7 @@ edition = "2018"
publish = false
[dependencies]
chrono = "0.4"
derive_more = "0.99"
futures = "0.3"
juniper = { path = "../../juniper" }

View file

@ -1,125 +0,0 @@
use juniper::{
execute, graphql_value, EmptyMutation, EmptySubscription, FromInputValue, InputValue, RootNode,
ToInputValue, Value, Variables,
};
use crate::custom_scalar::MyScalarValue;
#[derive(Debug, PartialEq, Eq, Hash, juniper::GraphQLScalarValue)]
#[graphql(
transparent,
scalar = MyScalarValue,
specified_by_url = "https://tools.ietf.org/html/rfc4122",
)]
pub struct LargeId(i64);
#[derive(juniper::GraphQLObject)]
#[graphql(scalar = MyScalarValue)]
struct User {
id: LargeId,
}
struct Query;
#[juniper::graphql_object(scalar = MyScalarValue)]
impl Query {
fn user() -> User {
User { id: LargeId(0) }
}
}
struct Mutation;
#[juniper::graphql_object(scalar = MyScalarValue)]
impl Mutation {
fn change_user(id: LargeId) -> User {
User { id }
}
}
#[test]
fn test_scalar_value_large_id() {
let num: i64 = 4294967297;
let input_integer: InputValue<MyScalarValue> =
serde_json::from_value(serde_json::json!(num)).unwrap();
let output: LargeId =
FromInputValue::<MyScalarValue>::from_input_value(&input_integer).unwrap();
assert_eq!(output, LargeId(num));
let id = LargeId(num);
let output = ToInputValue::<MyScalarValue>::to_input_value(&id);
assert_eq!(output, InputValue::scalar(num));
}
#[tokio::test]
async fn test_scalar_value_large_specified_url() {
let schema = RootNode::<'_, _, _, _, MyScalarValue>::new_with_scalar_value(
Query,
EmptyMutation::<()>::new(),
EmptySubscription::<()>::new(),
);
let doc = r#"{
__type(name: "LargeId") {
specifiedByUrl
}
}"#;
assert_eq!(
execute(doc, None, &schema, &Variables::<MyScalarValue>::new(), &()).await,
Ok((
graphql_value!({"__type": {"specifiedByUrl": "https://tools.ietf.org/html/rfc4122"}}),
vec![],
)),
);
}
#[tokio::test]
async fn test_scalar_value_large_query() {
let schema = RootNode::<'_, _, _, _, MyScalarValue>::new_with_scalar_value(
Query,
EmptyMutation::<()>::new(),
EmptySubscription::<()>::new(),
);
let doc = r#"{
user { id }
}"#;
let val = Value::<MyScalarValue>::scalar(0_i64);
assert_eq!(
execute(doc, None, &schema, &Variables::<MyScalarValue>::new(), &()).await,
Ok((graphql_value!({"user": {"id": val}}), vec![])),
);
}
#[tokio::test]
async fn test_scalar_value_large_mutation() {
let schema = RootNode::<'_, _, _, _, MyScalarValue>::new_with_scalar_value(
Query,
Mutation,
EmptySubscription::<()>::new(),
);
let doc = r#"mutation {
changeUser(id: 1) { id }
}"#;
let val = Value::<MyScalarValue>::scalar(1_i64);
assert_eq!(
execute(doc, None, &schema, &Variables::<MyScalarValue>::new(), &()).await,
Ok((graphql_value!({"changeUser": {"id": val}}), vec![])),
);
let doc = r#"mutation {
changeUser(id: 4294967297) { id }
}"#;
let val = Value::<MyScalarValue>::scalar(4294967297_i64);
assert_eq!(
execute(doc, None, &schema, &Variables::<MyScalarValue>::new(), &()).await,
Ok((graphql_value!({"changeUser": {"id": val}}), vec![])),
);
}

View file

@ -1,405 +0,0 @@
use juniper::{
execute, graphql_object, graphql_scalar, graphql_value, graphql_vars, DefaultScalarValue,
EmptyMutation, EmptySubscription, Object, ParseScalarResult, ParseScalarValue, RootNode, Value,
};
use crate::custom_scalar::MyScalarValue;
struct DefaultName(i32);
struct OtherOrder(i32);
struct Named(i32);
struct ScalarDescription(i32);
struct ScalarSpecifiedByUrl(i32);
struct Generated(String);
struct Root;
/*
Syntax to validate:
* Default name vs. custom name
* Description vs. no description on the scalar
*/
#[graphql_scalar]
impl<S> GraphQLScalar for DefaultName
where
S: ScalarValue,
{
fn resolve(&self) -> Value {
Value::scalar(self.0)
}
fn from_input_value(v: &InputValue) -> Result<DefaultName, String> {
v.as_int_value()
.map(DefaultName)
.ok_or_else(|| format!("Expected `Int`, found: {}", v))
}
fn from_str<'a>(value: ScalarToken<'a>) -> ParseScalarResult<'a, S> {
<i32 as ParseScalarValue<S>>::from_str(value)
}
}
#[graphql_scalar]
impl GraphQLScalar for OtherOrder {
fn resolve(&self) -> Value {
Value::scalar(self.0)
}
fn from_input_value(v: &InputValue) -> Result<OtherOrder, String> {
v.as_int_value()
.map(OtherOrder)
.ok_or_else(|| format!("Expected `Int`, found: {}", v))
}
fn from_str<'a>(value: ScalarToken<'a>) -> ParseScalarResult<'a, DefaultScalarValue> {
<i32 as ParseScalarValue>::from_str(value)
}
}
#[graphql_scalar(name = "ANamedScalar")]
impl GraphQLScalar for Named {
fn resolve(&self) -> Value {
Value::scalar(self.0)
}
fn from_input_value(v: &InputValue) -> Result<Named, String> {
v.as_int_value()
.map(Named)
.ok_or_else(|| format!("Expected `Int`, found: {}", v))
}
fn from_str<'a>(value: ScalarToken<'a>) -> ParseScalarResult<'a, DefaultScalarValue> {
<i32 as ParseScalarValue>::from_str(value)
}
}
#[graphql_scalar(description = "A sample scalar, represented as an integer")]
impl GraphQLScalar for ScalarDescription {
fn resolve(&self) -> Value {
Value::scalar(self.0)
}
fn from_input_value(v: &InputValue) -> Result<ScalarDescription, String> {
v.as_int_value()
.map(ScalarDescription)
.ok_or_else(|| format!("Expected `Int`, found: {}", v))
}
fn from_str<'a>(value: ScalarToken<'a>) -> ParseScalarResult<'a, DefaultScalarValue> {
<i32 as ParseScalarValue>::from_str(value)
}
}
#[graphql_scalar(specified_by_url = "https://tools.ietf.org/html/rfc4122")]
impl GraphQLScalar for ScalarSpecifiedByUrl {
fn resolve(&self) -> Value {
Value::scalar(self.0)
}
fn from_input_value(v: &InputValue) -> Result<ScalarSpecifiedByUrl, String> {
v.as_int_value()
.map(ScalarSpecifiedByUrl)
.ok_or_else(|| format!("Expected `Int`, found: {}", v))
}
fn from_str<'a>(value: ScalarToken<'a>) -> ParseScalarResult<'a, DefaultScalarValue> {
<i32 as ParseScalarValue>::from_str(value)
}
}
macro_rules! impl_scalar {
($name: ident) => {
#[graphql_scalar]
impl<S> GraphQLScalar for $name
where
S: ScalarValue,
{
fn resolve(&self) -> Value {
Value::scalar(self.0.clone())
}
fn from_input_value(v: &InputValue) -> Result<Self, &'static str> {
v.as_scalar_value()
.and_then(|v| v.as_str())
.and_then(|s| Some(Self(s.to_owned())))
.ok_or_else(|| "Expected `String`")
}
fn from_str<'a>(value: ScalarToken<'a>) -> ParseScalarResult<'a, S> {
<String as ParseScalarValue<S>>::from_str(value)
}
}
};
}
impl_scalar!(Generated);
#[graphql_object(scalar = DefaultScalarValue)]
impl Root {
fn default_name() -> DefaultName {
DefaultName(0)
}
fn other_order() -> OtherOrder {
OtherOrder(0)
}
fn named() -> Named {
Named(0)
}
fn scalar_description() -> ScalarDescription {
ScalarDescription(0)
}
fn scalar_specified_by_url() -> ScalarSpecifiedByUrl {
ScalarSpecifiedByUrl(0)
}
fn generated() -> Generated {
Generated("foo".to_owned())
}
}
struct WithCustomScalarValue(i32);
#[graphql_scalar]
impl GraphQLScalar for WithCustomScalarValue {
fn resolve(&self) -> Value<MyScalarValue> {
Value::scalar(self.0)
}
fn from_input_value(v: &InputValue<MyScalarValue>) -> Result<WithCustomScalarValue, String> {
v.as_int_value()
.map(WithCustomScalarValue)
.ok_or_else(|| format!("Expected Int, found: {}", v))
}
fn from_str<'a>(value: ScalarToken<'a>) -> ParseScalarResult<'a, MyScalarValue> {
<i32 as ParseScalarValue<MyScalarValue>>::from_str(value)
}
}
struct RootWithCustomScalarValue;
#[graphql_object(scalar = MyScalarValue)]
impl RootWithCustomScalarValue {
fn with_custom_scalar_value() -> WithCustomScalarValue {
WithCustomScalarValue(0)
}
}
async fn run_type_info_query<F>(doc: &str, f: F)
where
F: Fn(&Object<DefaultScalarValue>) -> (),
{
let schema = RootNode::new(
Root {},
EmptyMutation::<()>::new(),
EmptySubscription::<()>::new(),
);
let (result, errs) = execute(doc, None, &schema, &graphql_vars! {}, &())
.await
.expect("Execution failed");
assert_eq!(errs, []);
println!("Result: {:#?}", result);
let type_info = result
.as_object_value()
.expect("Result is not an object")
.get_field_value("__type")
.expect("__type field missing")
.as_object_value()
.expect("__type field not an object value");
f(type_info);
}
#[test]
fn path_in_resolve_return_type() {
struct ResolvePath(i32);
#[graphql_scalar]
impl GraphQLScalar for ResolvePath {
fn resolve(&self) -> self::Value {
Value::scalar(self.0)
}
fn from_input_value(v: &InputValue) -> Result<ResolvePath, String> {
v.as_int_value()
.map(ResolvePath)
.ok_or_else(|| format!("Expected `Int`, found: {}", v))
}
fn from_str<'a>(value: ScalarToken<'a>) -> ParseScalarResult<'a, DefaultScalarValue> {
<i32 as ParseScalarValue>::from_str(value)
}
}
}
#[tokio::test]
async fn default_name_introspection() {
let doc = r#"
{
__type(name: "DefaultName") {
name
description
}
}
"#;
run_type_info_query(doc, |type_info| {
assert_eq!(
type_info.get_field_value("name"),
Some(&graphql_value!("DefaultName")),
);
assert_eq!(
type_info.get_field_value("description"),
Some(&graphql_value!(null)),
);
})
.await;
}
#[tokio::test]
async fn other_order_introspection() {
let doc = r#"
{
__type(name: "OtherOrder") {
name
description
}
}
"#;
run_type_info_query(doc, |type_info| {
assert_eq!(
type_info.get_field_value("name"),
Some(&graphql_value!("OtherOrder")),
);
assert_eq!(
type_info.get_field_value("description"),
Some(&graphql_value!(null)),
);
})
.await;
}
#[tokio::test]
async fn named_introspection() {
let doc = r#"
{
__type(name: "ANamedScalar") {
name
description
}
}
"#;
run_type_info_query(doc, |type_info| {
assert_eq!(
type_info.get_field_value("name"),
Some(&graphql_value!("ANamedScalar")),
);
assert_eq!(
type_info.get_field_value("description"),
Some(&graphql_value!(null)),
);
})
.await;
}
#[tokio::test]
async fn scalar_description_introspection() {
let doc = r#"
{
__type(name: "ScalarDescription") {
name
description
specifiedByUrl
}
}
"#;
run_type_info_query(doc, |type_info| {
assert_eq!(
type_info.get_field_value("name"),
Some(&graphql_value!("ScalarDescription")),
);
assert_eq!(
type_info.get_field_value("description"),
Some(&graphql_value!(
"A sample scalar, represented as an integer",
)),
);
assert_eq!(
type_info.get_field_value("specifiedByUrl"),
Some(&graphql_value!(null)),
);
})
.await;
}
#[tokio::test]
async fn scalar_specified_by_url_introspection() {
let doc = r#"{
__type(name: "ScalarSpecifiedByUrl") {
name
specifiedByUrl
}
}"#;
run_type_info_query(doc, |type_info| {
assert_eq!(
type_info.get_field_value("name"),
Some(&graphql_value!("ScalarSpecifiedByUrl")),
);
assert_eq!(
type_info.get_field_value("specifiedByUrl"),
Some(&graphql_value!("https://tools.ietf.org/html/rfc4122")),
);
})
.await;
}
#[tokio::test]
async fn generated_scalar_introspection() {
let doc = r#"
{
__type(name: "Generated") {
name
description
}
}
"#;
run_type_info_query(doc, |type_info| {
assert_eq!(
type_info.get_field_value("name"),
Some(&graphql_value!("Generated")),
);
assert_eq!(
type_info.get_field_value("description"),
Some(&graphql_value!(null)),
);
})
.await;
}
#[tokio::test]
async fn resolves_with_custom_scalar_value() {
const DOC: &str = r#"{ withCustomScalarValue }"#;
let schema = RootNode::<_, _, _, MyScalarValue>::new_with_scalar_value(
RootWithCustomScalarValue,
EmptyMutation::<()>::new(),
EmptySubscription::<()>::new(),
);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((graphql_value!({"withCustomScalarValue": 0}), vec![])),
);
}

View file

@ -2,34 +2,11 @@
use juniper::{
execute, graphql_interface, graphql_object, graphql_value, graphql_vars, DefaultScalarValue,
EmptyMutation, EmptySubscription, Executor, FieldError, FieldResult, GraphQLInputObject,
GraphQLObject, GraphQLType, GraphQLUnion, IntoFieldError, RootNode, ScalarValue,
Executor, FieldError, FieldResult, GraphQLInputObject, GraphQLObject, GraphQLUnion,
IntoFieldError, ScalarValue,
};
fn schema<'q, C, Q>(query_root: Q) -> RootNode<'q, Q, EmptyMutation<C>, EmptySubscription<C>>
where
Q: GraphQLType<DefaultScalarValue, Context = C, TypeInfo = ()> + 'q,
{
RootNode::new(
query_root,
EmptyMutation::<C>::new(),
EmptySubscription::<C>::new(),
)
}
fn schema_with_scalar<'q, S, C, Q>(
query_root: Q,
) -> RootNode<'q, Q, EmptyMutation<C>, EmptySubscription<C>, S>
where
Q: GraphQLType<S, Context = C, TypeInfo = ()> + 'q,
S: ScalarValue + 'q,
{
RootNode::new_with_scalar_value(
query_root,
EmptyMutation::<C>::new(),
EmptySubscription::<C>::new(),
)
}
use crate::util::{schema, schema_with_scalar};
mod no_implers {
use super::*;

View file

@ -1,12 +1,11 @@
mod derive_enum;
mod derive_input_object;
mod derive_object_with_raw_idents;
mod derive_scalar;
mod impl_scalar;
mod interface_attr;
mod object_attr;
mod object_derive;
mod scalar_value_transparent;
mod scalar_attr_derive_input;
mod scalar_attr_type_alias;
mod subscription_attr;
mod union_attr;
mod union_derive;

View file

@ -0,0 +1,972 @@
use std::fmt;
use chrono::{DateTime, TimeZone, Utc};
use juniper::{
execute, graphql_object, graphql_scalar, graphql_value, graphql_vars, InputValue,
ParseScalarResult, ParseScalarValue, ScalarToken, ScalarValue, Value,
};
use crate::{
custom_scalar::MyScalarValue,
util::{schema, schema_with_scalar},
};
mod trivial {
use super::*;
#[graphql_scalar]
struct Counter(i32);
impl Counter {
fn to_output<S: ScalarValue>(&self) -> Value<S> {
Value::scalar(self.0)
}
fn from_input<S: ScalarValue>(v: &InputValue<S>) -> Result<Self, String> {
v.as_int_value()
.map(Self)
.ok_or_else(|| format!("Expected `Counter`, found: {}", v))
}
fn parse_token<S: ScalarValue>(t: ScalarToken<'_>) -> ParseScalarResult<'_, S> {
<i32 as ParseScalarValue<S>>::from_str(t)
}
}
struct QueryRoot;
#[graphql_object]
impl QueryRoot {
fn counter(value: Counter) -> Counter {
value
}
}
#[tokio::test]
async fn is_graphql_scalar() {
const DOC: &str = r#"{
__type(name: "Counter") {
kind
}
}"#;
let schema = schema(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((graphql_value!({"__type": {"kind": "SCALAR"}}), vec![])),
);
}
#[tokio::test]
async fn resolves_counter() {
const DOC: &str = r#"{ counter(value: 0) }"#;
let schema = schema(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((graphql_value!({"counter": 0}), vec![])),
);
}
#[tokio::test]
async fn has_no_description() {
const DOC: &str = r#"{
__type(name: "Counter") {
description
}
}"#;
let schema = schema(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((graphql_value!({"__type": {"description": null}}), vec![])),
);
}
}
mod all_custom_resolvers {
use super::*;
#[graphql_scalar(
to_output_with = to_output,
from_input_with = from_input,
)]
#[graphql_scalar(parse_token_with = parse_token)]
struct Counter(i32);
fn to_output<S: ScalarValue>(v: &Counter) -> Value<S> {
Value::scalar(v.0)
}
fn from_input<S: ScalarValue>(v: &InputValue<S>) -> Result<Counter, String> {
v.as_int_value()
.map(Counter)
.ok_or_else(|| format!("Expected `Counter`, found: {}", v))
}
fn parse_token<S: ScalarValue>(value: ScalarToken<'_>) -> ParseScalarResult<'_, S> {
<i32 as ParseScalarValue<S>>::from_str(value)
}
struct QueryRoot;
#[graphql_object]
impl QueryRoot {
fn counter(value: Counter) -> Counter {
value
}
}
#[tokio::test]
async fn is_graphql_scalar() {
const DOC: &str = r#"{
__type(name: "Counter") {
kind
}
}"#;
let schema = schema(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((graphql_value!({"__type": {"kind": "SCALAR"}}), vec![])),
);
}
#[tokio::test]
async fn resolves_counter() {
const DOC: &str = r#"{ counter(value: 0) }"#;
let schema = schema(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((graphql_value!({"counter": 0}), vec![])),
);
}
#[tokio::test]
async fn has_no_description() {
const DOC: &str = r#"{
__type(name: "Counter") {
description
}
}"#;
let schema = schema(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((graphql_value!({"__type": {"description": null}}), vec![])),
);
}
}
mod explicit_name {
use super::*;
#[graphql_scalar(name = "Counter")]
struct CustomCounter(i32);
impl CustomCounter {
fn to_output<S: ScalarValue>(&self) -> Value<S> {
Value::scalar(self.0)
}
fn from_input<S: ScalarValue>(v: &InputValue<S>) -> Result<Self, String> {
v.as_int_value()
.map(Self)
.ok_or_else(|| format!("Expected `Counter`, found: {}", v))
}
fn parse_token<S: ScalarValue>(value: ScalarToken<'_>) -> ParseScalarResult<'_, S> {
<i32 as ParseScalarValue<S>>::from_str(value)
}
}
struct QueryRoot;
#[graphql_object]
impl QueryRoot {
fn counter(value: CustomCounter) -> CustomCounter {
value
}
}
#[tokio::test]
async fn is_graphql_scalar() {
const DOC: &str = r#"{
__type(name: "Counter") {
kind
}
}"#;
let schema = schema(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((graphql_value!({"__type": {"kind": "SCALAR"}}), vec![])),
);
}
#[tokio::test]
async fn resolves_counter() {
const DOC: &str = r#"{ counter(value: 0) }"#;
let schema = schema(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((graphql_value!({"counter": 0}), vec![])),
);
}
#[tokio::test]
async fn has_no_description() {
const DOC: &str = r#"{
__type(name: "Counter") {
description
}
}"#;
let schema = schema(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((graphql_value!({"__type": {"description": null}}), vec![])),
);
}
}
mod delegated_parse_token {
use super::*;
#[graphql_scalar(parse_token(i32))]
struct Counter(i32);
impl Counter {
fn to_output<S: ScalarValue>(&self) -> Value<S> {
Value::scalar(self.0)
}
fn from_input<S: ScalarValue>(v: &InputValue<S>) -> Result<Self, String> {
v.as_int_value()
.map(Self)
.ok_or_else(|| format!("Expected `Counter`, found: {}", v))
}
}
struct QueryRoot;
#[graphql_object]
impl QueryRoot {
fn counter(value: Counter) -> Counter {
value
}
}
#[tokio::test]
async fn is_graphql_scalar() {
const DOC: &str = r#"{
__type(name: "Counter") {
kind
}
}"#;
let schema = schema(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((graphql_value!({"__type": {"kind": "SCALAR"}}), vec![])),
);
}
#[tokio::test]
async fn resolves_counter() {
const DOC: &str = r#"{ counter(value: 0) }"#;
let schema = schema(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((graphql_value!({"counter": 0}), vec![])),
);
}
#[tokio::test]
async fn has_no_description() {
const DOC: &str = r#"{
__type(name: "Counter") {
description
}
}"#;
let schema = schema(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((graphql_value!({"__type": {"description": null}}), vec![])),
);
}
}
mod multiple_delegated_parse_token {
use super::*;
#[graphql_scalar(parse_token(String, i32))]
enum StringOrInt {
String(String),
Int(i32),
}
impl StringOrInt {
fn to_output<S: ScalarValue>(&self) -> Value<S> {
match self {
Self::String(str) => Value::scalar(str.to_owned()),
Self::Int(i) => Value::scalar(*i),
}
}
fn from_input<S: ScalarValue>(v: &InputValue<S>) -> Result<Self, String> {
v.as_string_value()
.map(|s| Self::String(s.to_owned()))
.or_else(|| v.as_int_value().map(|i| Self::Int(i)))
.ok_or_else(|| format!("Expected `String` or `Int`, found: {}", v))
}
}
struct QueryRoot;
#[graphql_object]
impl QueryRoot {
fn string_or_int(value: StringOrInt) -> StringOrInt {
value
}
}
#[tokio::test]
async fn resolves_string() {
const DOC: &str = r#"{ stringOrInt(value: "test") }"#;
let schema = schema(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((graphql_value!({"stringOrInt": "test"}), vec![])),
);
}
#[tokio::test]
async fn resolves_int() {
const DOC: &str = r#"{ stringOrInt(value: 0) }"#;
let schema = schema(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((graphql_value!({"stringOrInt": 0}), vec![])),
);
}
}
mod where_attribute {
use super::*;
#[graphql_scalar(
to_output_with = to_output,
from_input_with = from_input,
parse_token(String),
where(Tz: From<Utc>, Tz::Offset: fmt::Display),
specified_by_url = "https://tools.ietf.org/html/rfc3339",
)]
struct CustomDateTime<Tz: TimeZone>(DateTime<Tz>);
fn to_output<S, Tz>(v: &CustomDateTime<Tz>) -> Value<S>
where
S: ScalarValue,
Tz: From<Utc> + TimeZone,
Tz::Offset: fmt::Display,
{
Value::scalar(v.0.to_rfc3339())
}
fn from_input<S, Tz>(v: &InputValue<S>) -> Result<CustomDateTime<Tz>, String>
where
S: ScalarValue,
Tz: From<Utc> + TimeZone,
Tz::Offset: fmt::Display,
{
v.as_string_value()
.ok_or_else(|| format!("Expected `String`, found: {}", v))
.and_then(|s| {
DateTime::parse_from_rfc3339(s)
.map(|dt| CustomDateTime(dt.with_timezone(&Tz::from(Utc))))
.map_err(|e| format!("Failed to parse `CustomDateTime`: {}", e))
})
}
struct QueryRoot;
#[graphql_object]
impl QueryRoot {
fn date_time(value: CustomDateTime<Utc>) -> CustomDateTime<Utc> {
value
}
}
#[tokio::test]
async fn resolves_custom_date_time() {
const DOC: &str = r#"{ dateTime(value: "1996-12-19T16:39:57-08:00") }"#;
let schema = schema_with_scalar::<MyScalarValue, _, _>(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((
graphql_value!({"dateTime": "1996-12-20T00:39:57+00:00"}),
vec![],
)),
);
}
#[tokio::test]
async fn has_specified_by_url() {
const DOC: &str = r#"{
__type(name: "CustomDateTime") {
specifiedByUrl
}
}"#;
let schema = schema_with_scalar::<MyScalarValue, _, _>(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((
graphql_value!({"__type": {"specifiedByUrl": "https://tools.ietf.org/html/rfc3339"}}),
vec![],
)),
);
}
}
mod with_self {
use super::*;
#[graphql_scalar(with = Self)]
struct Counter(i32);
impl Counter {
fn to_output<S: ScalarValue>(&self) -> Value<S> {
Value::scalar(self.0)
}
fn from_input<S: ScalarValue>(v: &InputValue<S>) -> Result<Self, String> {
v.as_int_value()
.map(Self)
.ok_or_else(|| format!("Expected `Counter`, found: {}", v))
}
fn parse_token<S: ScalarValue>(value: ScalarToken<'_>) -> ParseScalarResult<'_, S> {
<i32 as ParseScalarValue<S>>::from_str(value)
}
}
struct QueryRoot;
#[graphql_object]
impl QueryRoot {
fn counter(value: Counter) -> Counter {
value
}
}
#[tokio::test]
async fn is_graphql_scalar() {
const DOC: &str = r#"{
__type(name: "Counter") {
kind
}
}"#;
let schema = schema(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((graphql_value!({"__type": {"kind": "SCALAR"}}), vec![])),
);
}
#[tokio::test]
async fn resolves_counter() {
const DOC: &str = r#"{ counter(value: 0) }"#;
let schema = schema(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((graphql_value!({"counter": 0}), vec![])),
);
}
#[tokio::test]
async fn has_no_description() {
const DOC: &str = r#"{
__type(name: "Counter") {
description
}
}"#;
let schema = schema(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((graphql_value!({"__type": {"description": null}}), vec![])),
);
}
}
mod with_module {
use super::*;
#[graphql_scalar(
with = custom_date_time,
parse_token(String),
where(Tz: From<Utc>, Tz::Offset: fmt::Display),
specified_by_url = "https://tools.ietf.org/html/rfc3339",
)]
struct CustomDateTime<Tz: TimeZone>(DateTime<Tz>);
mod custom_date_time {
use super::*;
pub(super) fn to_output<S, Tz>(v: &CustomDateTime<Tz>) -> Value<S>
where
S: ScalarValue,
Tz: From<Utc> + TimeZone,
Tz::Offset: fmt::Display,
{
Value::scalar(v.0.to_rfc3339())
}
pub(super) fn from_input<S, Tz>(v: &InputValue<S>) -> Result<CustomDateTime<Tz>, String>
where
S: ScalarValue,
Tz: From<Utc> + TimeZone,
Tz::Offset: fmt::Display,
{
v.as_string_value()
.ok_or_else(|| format!("Expected `String`, found: {}", v))
.and_then(|s| {
DateTime::parse_from_rfc3339(s)
.map(|dt| CustomDateTime(dt.with_timezone(&Tz::from(Utc))))
.map_err(|e| format!("Failed to parse `CustomDateTime`: {}", e))
})
}
}
struct QueryRoot;
#[graphql_object]
impl QueryRoot {
fn date_time(value: CustomDateTime<Utc>) -> CustomDateTime<Utc> {
value
}
}
#[tokio::test]
async fn resolves_custom_date_time() {
const DOC: &str = r#"{ dateTime(value: "1996-12-19T16:39:57-08:00") }"#;
let schema = schema_with_scalar::<MyScalarValue, _, _>(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((
graphql_value!({"dateTime": "1996-12-20T00:39:57+00:00"}),
vec![],
)),
);
}
#[tokio::test]
async fn has_specified_by_url() {
const DOC: &str = r#"{
__type(name: "CustomDateTime") {
specifiedByUrl
}
}"#;
let schema = schema_with_scalar::<MyScalarValue, _, _>(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((
graphql_value!({"__type": {"specifiedByUrl": "https://tools.ietf.org/html/rfc3339"}}),
vec![],
)),
);
}
}
mod description_from_doc_comment {
use super::*;
/// Description
#[graphql_scalar(parse_token(i32))]
struct Counter(i32);
impl Counter {
fn to_output<S: ScalarValue>(&self) -> Value<S> {
Value::scalar(self.0)
}
fn from_input<S: ScalarValue>(v: &InputValue<S>) -> Result<Self, String> {
v.as_int_value()
.map(Self)
.ok_or_else(|| format!("Expected `Counter`, found: {}", v))
}
}
struct QueryRoot;
#[graphql_object]
impl QueryRoot {
fn counter(value: Counter) -> Counter {
value
}
}
#[tokio::test]
async fn is_graphql_scalar() {
const DOC: &str = r#"{
__type(name: "Counter") {
kind
}
}"#;
let schema = schema(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((graphql_value!({"__type": {"kind": "SCALAR"}}), vec![])),
);
}
#[tokio::test]
async fn resolves_counter() {
const DOC: &str = r#"{ counter(value: 0) }"#;
let schema = schema(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((graphql_value!({"counter": 0}), vec![])),
);
}
#[tokio::test]
async fn has_description() {
const DOC: &str = r#"{
__type(name: "Counter") {
description
}
}"#;
let schema = schema(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((
graphql_value!({"__type": {"description": "Description"}}),
vec![],
)),
);
}
}
mod description_from_attribute {
use super::*;
/// Doc comment
#[graphql_scalar(description = "Description from attribute", parse_token(i32))]
struct Counter(i32);
impl Counter {
fn to_output<S: ScalarValue>(&self) -> Value<S> {
Value::scalar(self.0)
}
fn from_input<S: ScalarValue>(v: &InputValue<S>) -> Result<Self, String> {
v.as_int_value()
.map(Self)
.ok_or_else(|| format!("Expected `Counter`, found: {}", v))
}
}
struct QueryRoot;
#[graphql_object]
impl QueryRoot {
fn counter(value: Counter) -> Counter {
value
}
}
#[tokio::test]
async fn is_graphql_scalar() {
const DOC: &str = r#"{
__type(name: "Counter") {
kind
}
}"#;
let schema = schema(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((graphql_value!({"__type": {"kind": "SCALAR"}}), vec![])),
);
}
#[tokio::test]
async fn resolves_counter() {
const DOC: &str = r#"{ counter(value: 0) }"#;
let schema = schema(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((graphql_value!({"counter": 0}), vec![])),
);
}
#[tokio::test]
async fn has_description() {
const DOC: &str = r#"{
__type(name: "Counter") {
description
}
}"#;
let schema = schema(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((
graphql_value!({"__type": {"description": "Description from attribute"}}),
vec![],
)),
);
}
}
mod custom_scalar {
use super::*;
/// Description
#[graphql_scalar(scalar = MyScalarValue, parse_token(i32))]
struct Counter(i32);
impl Counter {
fn to_output<S: ScalarValue>(&self) -> Value<S> {
Value::scalar(self.0)
}
fn from_input<S: ScalarValue>(v: &InputValue<S>) -> Result<Self, String> {
v.as_int_value()
.map(Self)
.ok_or_else(|| format!("Expected `Counter`, found: {}", v))
}
}
struct QueryRoot;
#[graphql_object(scalar = MyScalarValue)]
impl QueryRoot {
fn counter(value: Counter) -> Counter {
value
}
}
#[tokio::test]
async fn is_graphql_scalar() {
const DOC: &str = r#"{
__type(name: "Counter") {
kind
}
}"#;
let schema = schema_with_scalar::<MyScalarValue, _, _>(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((graphql_value!({"__type": {"kind": "SCALAR"}}), vec![])),
);
}
#[tokio::test]
async fn resolves_counter() {
const DOC: &str = r#"{ counter(value: 0) }"#;
let schema = schema_with_scalar::<MyScalarValue, _, _>(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((graphql_value!({"counter": 0}), vec![])),
);
}
#[tokio::test]
async fn has_description() {
const DOC: &str = r#"{
__type(name: "Counter") {
description
}
}"#;
let schema = schema_with_scalar::<MyScalarValue, _, _>(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((
graphql_value!({"__type": {"description": "Description"}}),
vec![],
)),
);
}
}
mod generic_scalar {
use super::*;
/// Description
#[graphql_scalar(scalar = S: ScalarValue, parse_token(i32))]
struct Counter(i32);
impl Counter {
fn to_output<S: ScalarValue>(&self) -> Value<S> {
Value::scalar(self.0)
}
fn from_input<S: ScalarValue>(v: &InputValue<S>) -> Result<Self, String> {
v.as_int_value()
.map(Self)
.ok_or_else(|| format!("Expected `Counter`, found: {}", v))
}
}
struct QueryRoot;
#[graphql_object]
impl QueryRoot {
fn counter(value: Counter) -> Counter {
value
}
}
#[tokio::test]
async fn is_graphql_scalar() {
const DOC: &str = r#"{
__type(name: "Counter") {
kind
}
}"#;
let schema = schema_with_scalar::<MyScalarValue, _, _>(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((graphql_value!({"__type": {"kind": "SCALAR"}}), vec![])),
);
}
#[tokio::test]
async fn resolves_counter() {
const DOC: &str = r#"{ counter(value: 0) }"#;
let schema = schema_with_scalar::<MyScalarValue, _, _>(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((graphql_value!({"counter": 0}), vec![])),
);
}
#[tokio::test]
async fn has_description() {
const DOC: &str = r#"{
__type(name: "Counter") {
description
}
}"#;
let schema = schema_with_scalar::<MyScalarValue, _, _>(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((
graphql_value!({"__type": {"description": "Description"}}),
vec![]
)),
);
}
}
mod bounded_generic_scalar {
use super::*;
#[graphql_scalar(scalar = S: ScalarValue + Clone, parse_token(i32))]
struct Counter(i32);
impl Counter {
fn to_output<S: ScalarValue>(&self) -> Value<S> {
Value::scalar(self.0)
}
fn from_input<S: ScalarValue>(v: &InputValue<S>) -> Result<Self, String> {
v.as_int_value()
.map(Self)
.ok_or_else(|| format!("Expected `Counter`, found: {}", v))
}
}
struct QueryRoot;
#[graphql_object]
impl QueryRoot {
fn counter(value: Counter) -> Counter {
value
}
}
#[tokio::test]
async fn is_graphql_scalar() {
const DOC: &str = r#"{
__type(name: "Counter") {
kind
}
}"#;
let schema = schema_with_scalar::<MyScalarValue, _, _>(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((graphql_value!({"__type": {"kind": "SCALAR"}}), vec![])),
);
}
#[tokio::test]
async fn resolves_counter() {
const DOC: &str = r#"{ counter(value: 0) }"#;
let schema = schema_with_scalar::<MyScalarValue, _, _>(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((graphql_value!({"counter": 0}), vec![])),
);
}
}

View file

@ -0,0 +1,996 @@
use std::fmt;
use chrono::{DateTime, TimeZone, Utc};
use juniper::{
execute, graphql_object, graphql_scalar, graphql_value, graphql_vars, InputValue,
ParseScalarResult, ParseScalarValue, ScalarToken, ScalarValue, Value,
};
use crate::{
custom_scalar::MyScalarValue,
util::{schema, schema_with_scalar},
};
mod all_custom_resolvers {
use super::*;
struct CustomCounter(i32);
#[graphql_scalar(
to_output_with = to_output,
from_input_with = from_input,
)]
#[graphql_scalar(
parse_token_with = parse_token,
)]
type Counter = CustomCounter;
fn to_output<S: ScalarValue>(v: &Counter) -> Value<S> {
Value::scalar(v.0)
}
fn from_input<S: ScalarValue>(v: &InputValue<S>) -> Result<Counter, String> {
v.as_int_value()
.map(CustomCounter)
.ok_or_else(|| format!("Expected `Counter`, found: {}", v))
}
fn parse_token<S: ScalarValue>(value: ScalarToken<'_>) -> ParseScalarResult<'_, S> {
<i32 as ParseScalarValue<S>>::from_str(value)
}
struct QueryRoot;
#[graphql_object]
impl QueryRoot {
fn counter(value: Counter) -> Counter {
value
}
}
#[tokio::test]
async fn is_graphql_scalar() {
const DOC: &str = r#"{
__type(name: "Counter") {
kind
}
}"#;
let schema = schema(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((graphql_value!({"__type": {"kind": "SCALAR"}}), vec![])),
);
}
#[tokio::test]
async fn resolves_counter() {
const DOC: &str = r#"{ counter(value: 0) }"#;
let schema = schema(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((graphql_value!({"counter": 0}), vec![])),
);
}
#[tokio::test]
async fn has_no_description() {
const DOC: &str = r#"{
__type(name: "Counter") {
description
}
}"#;
let schema = schema(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((graphql_value!({"__type": {"description": null}}), vec![])),
);
}
}
mod explicit_name {
use super::*;
struct CustomCounter(i32);
#[graphql_scalar(
name = "Counter",
to_output_with = to_output,
from_input_with = from_input,
parse_token_with = parse_token,
)]
type CounterScalar = CustomCounter;
fn to_output<S: ScalarValue>(v: &CounterScalar) -> Value<S> {
Value::scalar(v.0)
}
fn from_input<S: ScalarValue>(v: &InputValue<S>) -> Result<CounterScalar, String> {
v.as_int_value()
.map(CustomCounter)
.ok_or_else(|| format!("Expected `Counter`, found: {}", v))
}
fn parse_token<S: ScalarValue>(value: ScalarToken<'_>) -> ParseScalarResult<'_, S> {
<i32 as ParseScalarValue<S>>::from_str(value)
}
struct QueryRoot;
#[graphql_object]
impl QueryRoot {
fn counter(value: CounterScalar) -> CounterScalar {
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 no_custom_counter() {
for name in ["CustomCounter", "CustomScalar"] {
let doc = format!(
r#"{{
__type(name: "{}") {{
kind
}}
}}"#,
name,
);
let schema = schema(QueryRoot);
assert_eq!(
execute(&doc, None, &schema, &graphql_vars! {}, &()).await,
Ok((graphql_value!(null), vec![])),
);
}
}
#[tokio::test]
async fn resolves_counter() {
const DOC: &str = r#"{ counter(value: 0) }"#;
let schema = schema(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((graphql_value!({"counter": 0}), vec![])),
);
}
#[tokio::test]
async fn has_no_description() {
const DOC: &str = r#"{
__type(name: "Counter") {
description
}
}"#;
let schema = schema(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((graphql_value!({"__type": {"description": null}}), vec![])),
);
}
}
mod delegated_parse_token {
use super::*;
struct CustomCounter(i32);
#[graphql_scalar(
to_output_with = to_output,
from_input_with = from_input,
parse_token(i32),
)]
type Counter = CustomCounter;
fn to_output<S: ScalarValue>(v: &Counter) -> Value<S> {
Value::scalar(v.0)
}
fn from_input<S: ScalarValue>(v: &InputValue<S>) -> Result<Counter, String> {
v.as_int_value()
.map(CustomCounter)
.ok_or_else(|| format!("Expected `Counter`, found: {}", v))
}
struct QueryRoot;
#[graphql_object]
impl QueryRoot {
fn counter(value: Counter) -> Counter {
value
}
}
#[tokio::test]
async fn is_graphql_scalar() {
const DOC: &str = r#"{
__type(name: "Counter") {
kind
}
}"#;
let schema = schema(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((graphql_value!({"__type": {"kind": "SCALAR"}}), vec![])),
);
}
#[tokio::test]
async fn resolves_counter() {
const DOC: &str = r#"{ counter(value: 0) }"#;
let schema = schema(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((graphql_value!({"counter": 0}), vec![])),
);
}
#[tokio::test]
async fn has_no_description() {
const DOC: &str = r#"{
__type(name: "Counter") {
description
}
}"#;
let schema = schema(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((graphql_value!({"__type": {"description": null}}), vec![])),
);
}
}
mod multiple_delegated_parse_token {
use super::*;
enum StringOrIntScalar {
String(String),
Int(i32),
}
#[graphql_scalar(
to_output_with = to_output,
from_input_with = from_input,
parse_token(String, i32),
)]
type StringOrInt = StringOrIntScalar;
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(|i| StringOrInt::Int(i)))
.ok_or_else(|| format!("Expected `String` or `Int`, found: {}", v))
}
struct QueryRoot;
#[graphql_object]
impl QueryRoot {
fn string_or_int(value: StringOrInt) -> StringOrInt {
value
}
}
#[tokio::test]
async fn resolves_string() {
const DOC: &str = r#"{ stringOrInt(value: "test") }"#;
let schema = schema(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((graphql_value!({"stringOrInt": "test"}), vec![])),
);
}
#[tokio::test]
async fn resolves_int() {
const DOC: &str = r#"{ stringOrInt(value: 0) }"#;
let schema = schema(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((graphql_value!({"stringOrInt": 0}), vec![])),
);
}
}
mod where_attribute {
use super::*;
struct CustomDateTimeScalar<Tz: TimeZone>(DateTime<Tz>);
#[graphql_scalar(
to_output_with = to_output,
from_input_with = from_input,
parse_token(String),
where(Tz: From<Utc> + TimeZone, Tz::Offset: fmt::Display),
specified_by_url = "https://tools.ietf.org/html/rfc3339",
)]
type CustomDateTime<Tz> = CustomDateTimeScalar<Tz>;
fn to_output<S, Tz>(v: &CustomDateTime<Tz>) -> Value<S>
where
S: ScalarValue,
Tz: From<Utc> + TimeZone,
Tz::Offset: fmt::Display,
{
Value::scalar(v.0.to_rfc3339())
}
fn from_input<S, Tz>(v: &InputValue<S>) -> Result<CustomDateTime<Tz>, String>
where
S: ScalarValue,
Tz: From<Utc> + TimeZone,
Tz::Offset: fmt::Display,
{
v.as_string_value()
.ok_or_else(|| format!("Expected `String`, found: {}", v))
.and_then(|s| {
DateTime::parse_from_rfc3339(s)
.map(|dt| CustomDateTimeScalar(dt.with_timezone(&Tz::from(Utc))))
.map_err(|e| format!("Failed to parse `CustomDateTime`: {}", e))
})
}
struct QueryRoot;
#[graphql_object]
impl QueryRoot {
fn date_time(value: CustomDateTime<Utc>) -> CustomDateTime<Utc> {
value
}
}
#[tokio::test]
async fn resolves_custom_date_time() {
const DOC: &str = r#"{ dateTime(value: "1996-12-19T16:39:57-08:00") }"#;
let schema = schema_with_scalar::<MyScalarValue, _, _>(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((
graphql_value!({"dateTime": "1996-12-20T00:39:57+00:00"}),
vec![],
)),
);
}
#[tokio::test]
async fn has_specified_by_url() {
const DOC: &str = r#"{
__type(name: "CustomDateTime") {
specifiedByUrl
}
}"#;
let schema = schema_with_scalar::<MyScalarValue, _, _>(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((
graphql_value!({"__type": {"specifiedByUrl": "https://tools.ietf.org/html/rfc3339"}}),
vec![],
)),
);
}
}
mod with_self {
use super::*;
struct CustomCounter(i32);
#[graphql_scalar(with = Self)]
type Counter = CustomCounter;
impl Counter {
fn to_output<S: ScalarValue>(&self) -> Value<S> {
Value::scalar(self.0)
}
fn from_input<S: ScalarValue>(v: &InputValue<S>) -> Result<Self, String> {
v.as_int_value()
.map(Self)
.ok_or_else(|| format!("Expected `Counter`, found: {}", v))
}
fn parse_token<S: ScalarValue>(value: ScalarToken<'_>) -> ParseScalarResult<'_, S> {
<i32 as ParseScalarValue<S>>::from_str(value)
}
}
struct QueryRoot;
#[graphql_object]
impl QueryRoot {
fn counter(value: Counter) -> Counter {
value
}
}
#[tokio::test]
async fn is_graphql_scalar() {
const DOC: &str = r#"{
__type(name: "Counter") {
kind
}
}"#;
let schema = schema(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((graphql_value!({"__type": {"kind": "SCALAR"}}), vec![])),
);
}
#[tokio::test]
async fn resolves_counter() {
const DOC: &str = r#"{ counter(value: 0) }"#;
let schema = schema(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((graphql_value!({"counter": 0}), vec![])),
);
}
#[tokio::test]
async fn has_no_description() {
const DOC: &str = r#"{
__type(name: "Counter") {
description
}
}"#;
let schema = schema(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((graphql_value!({"__type": {"description": null}}), vec![])),
);
}
}
mod with_module {
use super::*;
struct CustomDateTimeScalar<Tz: TimeZone>(DateTime<Tz>);
#[graphql_scalar(
with = custom_date_time,
parse_token(String),
where(Tz: From<Utc> + TimeZone, Tz::Offset: fmt::Display),
specified_by_url = "https://tools.ietf.org/html/rfc3339",
)]
type CustomDateTime<Tz> = CustomDateTimeScalar<Tz>;
mod custom_date_time {
use super::*;
pub(super) fn to_output<S, Tz>(v: &CustomDateTime<Tz>) -> Value<S>
where
S: ScalarValue,
Tz: From<Utc> + TimeZone,
Tz::Offset: fmt::Display,
{
Value::scalar(v.0.to_rfc3339())
}
pub(super) fn from_input<S, Tz>(v: &InputValue<S>) -> Result<CustomDateTime<Tz>, String>
where
S: ScalarValue,
Tz: From<Utc> + TimeZone,
Tz::Offset: fmt::Display,
{
v.as_string_value()
.ok_or_else(|| format!("Expected `String`, found: {}", v))
.and_then(|s| {
DateTime::parse_from_rfc3339(s)
.map(|dt| CustomDateTimeScalar(dt.with_timezone(&Tz::from(Utc))))
.map_err(|e| format!("Failed to parse `CustomDateTime`: {}", e))
})
}
}
struct QueryRoot;
#[graphql_object]
impl QueryRoot {
fn date_time(value: CustomDateTime<Utc>) -> CustomDateTime<Utc> {
value
}
}
#[tokio::test]
async fn resolves_custom_date_time() {
const DOC: &str = r#"{ dateTime(value: "1996-12-19T16:39:57-08:00") }"#;
let schema = schema_with_scalar::<MyScalarValue, _, _>(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((
graphql_value!({"dateTime": "1996-12-20T00:39:57+00:00"}),
vec![],
)),
);
}
#[tokio::test]
async fn has_specified_by_url() {
const DOC: &str = r#"{
__type(name: "CustomDateTime") {
specifiedByUrl
}
}"#;
let schema = schema_with_scalar::<MyScalarValue, _, _>(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((
graphql_value!({"__type": {"specifiedByUrl": "https://tools.ietf.org/html/rfc3339"}}),
vec![],
)),
);
}
}
mod description_from_doc_comment {
use super::*;
struct CustomCounter(i32);
/// Description
#[graphql_scalar(with = counter, parse_token(i32))]
type Counter = CustomCounter;
mod counter {
use super::*;
pub(super) fn to_output<S: ScalarValue>(v: &Counter) -> Value<S> {
Value::scalar(v.0)
}
pub(super) fn from_input<S: ScalarValue>(v: &InputValue<S>) -> Result<Counter, String> {
v.as_int_value()
.map(CustomCounter)
.ok_or_else(|| format!("Expected `Counter`, found: {}", v))
}
}
struct QueryRoot;
#[graphql_object]
impl QueryRoot {
fn counter(value: Counter) -> Counter {
value
}
}
#[tokio::test]
async fn is_graphql_scalar() {
const DOC: &str = r#"{
__type(name: "Counter") {
kind
}
}"#;
let schema = schema(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((graphql_value!({"__type": {"kind": "SCALAR"}}), vec![])),
);
}
#[tokio::test]
async fn resolves_counter() {
const DOC: &str = r#"{ counter(value: 0) }"#;
let schema = schema(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((graphql_value!({"counter": 0}), vec![])),
);
}
#[tokio::test]
async fn has_description() {
const DOC: &str = r#"{
__type(name: "Counter") {
description
}
}"#;
let schema = schema(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((
graphql_value!({"__type": {"description": "Description"}}),
vec![],
)),
);
}
}
mod description_from_attribute {
use super::*;
struct CustomCounter(i32);
/// Doc comment
#[graphql_scalar(
description = "Description from attribute",
with = counter,
parse_token(i32),
)]
type Counter = CustomCounter;
mod counter {
use super::*;
pub(super) fn to_output<S: ScalarValue>(v: &Counter) -> Value<S> {
Value::scalar(v.0)
}
pub(super) fn from_input<S: ScalarValue>(v: &InputValue<S>) -> Result<Counter, String> {
v.as_int_value()
.map(CustomCounter)
.ok_or_else(|| format!("Expected `Counter`, found: {}", v))
}
}
struct QueryRoot;
#[graphql_object]
impl QueryRoot {
fn counter(value: Counter) -> Counter {
value
}
}
#[tokio::test]
async fn is_graphql_scalar() {
const DOC: &str = r#"{
__type(name: "Counter") {
kind
}
}"#;
let schema = schema(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((graphql_value!({"__type": {"kind": "SCALAR"}}), vec![])),
);
}
#[tokio::test]
async fn resolves_counter() {
const DOC: &str = r#"{ counter(value: 0) }"#;
let schema = schema(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((graphql_value!({"counter": 0}), vec![])),
);
}
#[tokio::test]
async fn has_description() {
const DOC: &str = r#"{
__type(name: "Counter") {
description
}
}"#;
let schema = schema(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((
graphql_value!({"__type": {"description": "Description from attribute"}}),
vec![],
)),
);
}
}
mod custom_scalar {
use super::*;
struct CustomCounter(i32);
/// Description
#[graphql_scalar(
scalar = MyScalarValue,
with = counter,
parse_token(i32),
)]
type Counter = CustomCounter;
mod counter {
use super::*;
pub(super) fn to_output<S: ScalarValue>(v: &Counter) -> Value<S> {
Value::scalar(v.0)
}
pub(super) fn from_input<S: ScalarValue>(v: &InputValue<S>) -> Result<Counter, String> {
v.as_int_value()
.map(CustomCounter)
.ok_or_else(|| format!("Expected `Counter`, found: {}", v))
}
}
struct QueryRoot;
#[graphql_object(scalar = MyScalarValue)]
impl QueryRoot {
fn counter(value: Counter) -> Counter {
value
}
}
#[tokio::test]
async fn is_graphql_scalar() {
const DOC: &str = r#"{
__type(name: "Counter") {
kind
}
}"#;
let schema = schema_with_scalar::<MyScalarValue, _, _>(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((graphql_value!({"__type": {"kind": "SCALAR"}}), vec![])),
);
}
#[tokio::test]
async fn resolves_counter() {
const DOC: &str = r#"{ counter(value: 0) }"#;
let schema = schema_with_scalar::<MyScalarValue, _, _>(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((graphql_value!({"counter": 0}), vec![])),
);
}
#[tokio::test]
async fn has_description() {
const DOC: &str = r#"{
__type(name: "Counter") {
description
}
}"#;
let schema = schema_with_scalar::<MyScalarValue, _, _>(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((
graphql_value!({"__type": {"description": "Description"}}),
vec![],
)),
);
}
}
mod generic_scalar {
use super::*;
struct CustomCounter(i32);
/// Description
#[graphql_scalar(
scalar = S: ScalarValue,
with = counter,
parse_token(i32),
)]
type Counter = CustomCounter;
mod counter {
use super::*;
pub(super) fn to_output<S: ScalarValue>(v: &Counter) -> Value<S> {
Value::scalar(v.0)
}
pub(super) fn from_input<S: ScalarValue>(v: &InputValue<S>) -> Result<Counter, String> {
v.as_int_value()
.map(CustomCounter)
.ok_or_else(|| format!("Expected `Counter`, found: {}", v))
}
}
struct QueryRoot;
#[graphql_object]
impl QueryRoot {
fn counter(value: Counter) -> Counter {
value
}
}
#[tokio::test]
async fn is_graphql_scalar() {
const DOC: &str = r#"{
__type(name: "Counter") {
kind
}
}"#;
let schema = schema_with_scalar::<MyScalarValue, _, _>(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((graphql_value!({"__type": {"kind": "SCALAR"}}), vec![])),
);
}
#[tokio::test]
async fn resolves_counter() {
const DOC: &str = r#"{ counter(value: 0) }"#;
let schema = schema_with_scalar::<MyScalarValue, _, _>(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((graphql_value!({"counter": 0}), vec![])),
);
}
#[tokio::test]
async fn has_description() {
const DOC: &str = r#"{
__type(name: "Counter") {
description
}
}"#;
let schema = schema_with_scalar::<MyScalarValue, _, _>(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((
graphql_value!({"__type": {"description": "Description"}}),
vec![],
)),
);
}
}
mod bounded_generic_scalar {
use super::*;
struct CustomCounter(i32);
/// Description
#[graphql_scalar(
scalar = S: ScalarValue + Clone,
with = counter,
parse_token(i32),
)]
type Counter = CustomCounter;
mod counter {
use super::*;
pub(super) fn to_output<S: ScalarValue>(v: &Counter) -> Value<S> {
Value::scalar(v.0)
}
pub(super) fn from_input<S: ScalarValue>(v: &InputValue<S>) -> Result<Counter, String> {
v.as_int_value()
.map(CustomCounter)
.ok_or_else(|| format!("Expected `Counter`, found: {}", v))
}
}
struct QueryRoot;
#[graphql_object]
impl QueryRoot {
fn counter(value: Counter) -> Counter {
value
}
}
#[tokio::test]
async fn is_graphql_scalar() {
const DOC: &str = r#"{
__type(name: "Counter") {
kind
}
}"#;
let schema = schema_with_scalar::<MyScalarValue, _, _>(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((graphql_value!({"__type": {"kind": "SCALAR"}}), vec![])),
);
}
#[tokio::test]
async fn resolves_counter() {
const DOC: &str = r#"{ counter(value: 0) }"#;
let schema = schema_with_scalar::<MyScalarValue, _, _>(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((graphql_value!({"counter": 0}), vec![])),
);
}
#[tokio::test]
async fn has_description() {
const DOC: &str = r#"{
__type(name: "Counter") {
description
}
}"#;
let schema = schema_with_scalar::<MyScalarValue, _, _>(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((
graphql_value!({"__type": {"description": "Description"}}),
vec![],
)),
);
}
}

View file

@ -1,87 +0,0 @@
use fnv::FnvHashMap;
use juniper::{
graphql_input_value, graphql_object, DefaultScalarValue, FromInputValue, GraphQLObject,
GraphQLScalarValue, GraphQLType, InputValue, Registry, ToInputValue,
};
#[derive(GraphQLScalarValue, Debug, Eq, PartialEq)]
#[graphql(transparent)]
struct UserId(String);
#[derive(GraphQLScalarValue, Debug, Eq, PartialEq)]
#[graphql(transparent, name = "MyUserId", description = "custom description...")]
struct CustomUserId(String);
/// The doc comment...
#[derive(GraphQLScalarValue, Debug, Eq, PartialEq)]
#[graphql(transparent, specified_by_url = "https://tools.ietf.org/html/rfc4122")]
struct IdWithDocComment(i32);
#[derive(GraphQLObject)]
struct User {
id: UserId,
id_custom: CustomUserId,
}
struct User2;
#[graphql_object]
impl User2 {
fn id(&self) -> UserId {
UserId("id".to_string())
}
}
#[test]
fn test_scalar_value_simple() {
assert_eq!(
<UserId as GraphQLType<DefaultScalarValue>>::name(&()),
Some("UserId")
);
let mut registry: Registry = Registry::new(FnvHashMap::default());
let meta = UserId::meta(&(), &mut registry);
assert_eq!(meta.name(), Some("UserId"));
assert_eq!(meta.description(), None);
let input: InputValue = serde_json::from_value(serde_json::json!("userId1")).unwrap();
let output: UserId = FromInputValue::from_input_value(&input).unwrap();
assert_eq!(output, UserId("userId1".into()),);
let id = UserId("111".into());
let output = ToInputValue::<DefaultScalarValue>::to_input_value(&id);
assert_eq!(output, graphql_input_value!("111"));
}
#[test]
fn test_scalar_value_custom() {
assert_eq!(
<CustomUserId as GraphQLType<DefaultScalarValue>>::name(&()),
Some("MyUserId")
);
let mut registry: Registry = Registry::new(FnvHashMap::default());
let meta = CustomUserId::meta(&(), &mut registry);
assert_eq!(meta.name(), Some("MyUserId"));
assert_eq!(meta.description(), Some("custom description..."));
assert_eq!(meta.specified_by_url(), None);
let input: InputValue = serde_json::from_value(serde_json::json!("userId1")).unwrap();
let output: CustomUserId = FromInputValue::from_input_value(&input).unwrap();
assert_eq!(output, CustomUserId("userId1".into()),);
let id = CustomUserId("111".into());
let output = ToInputValue::<DefaultScalarValue>::to_input_value(&id);
assert_eq!(output, graphql_input_value!("111"));
}
#[test]
fn test_scalar_value_doc_comment() {
let mut registry: Registry = Registry::new(FnvHashMap::default());
let meta = IdWithDocComment::meta(&(), &mut registry);
assert_eq!(meta.description(), Some("The doc comment..."));
assert_eq!(
meta.specified_by_url(),
Some("https://tools.ietf.org/html/rfc4122"),
);
}

View file

@ -6,11 +6,11 @@ use juniper::{
graphql_vars,
parser::{ParseError, ScalarToken, Token},
serde::{de, Deserialize, Deserializer, Serialize},
EmptyMutation, FieldResult, GraphQLScalarValue, InputValue, Object, ParseScalarResult,
RootNode, ScalarValue, Value, Variables,
EmptyMutation, FieldResult, InputValue, Object, ParseScalarResult, RootNode, ScalarValue,
Value, Variables,
};
#[derive(GraphQLScalarValue, Clone, Debug, PartialEq, Serialize)]
#[derive(Clone, Debug, PartialEq, Serialize)]
#[serde(untagged)]
pub(crate) enum MyScalarValue {
Int(i32),
@ -20,6 +20,149 @@ pub(crate) enum MyScalarValue {
Boolean(bool),
}
// TODO: replace all underlying `From` impls with `GraphQLScalarValue` macro.
impl From<i32> for MyScalarValue {
fn from(v: i32) -> Self {
Self::Int(v)
}
}
impl From<MyScalarValue> for Option<i32> {
fn from(v: MyScalarValue) -> Self {
if let MyScalarValue::Int(v) = v {
Some(v)
} else {
None
}
}
}
impl<'a> From<&'a MyScalarValue> for Option<&'a i32> {
fn from(v: &'a MyScalarValue) -> Self {
if let MyScalarValue::Int(v) = v {
Some(v)
} else {
None
}
}
}
impl From<i64> for MyScalarValue {
fn from(v: i64) -> Self {
Self::Long(v)
}
}
impl From<MyScalarValue> for Option<i64> {
fn from(v: MyScalarValue) -> Self {
if let MyScalarValue::Long(v) = v {
Some(v)
} else {
None
}
}
}
impl<'a> From<&'a MyScalarValue> for Option<&'a i64> {
fn from(v: &'a MyScalarValue) -> Self {
if let MyScalarValue::Long(v) = v {
Some(v)
} else {
None
}
}
}
impl From<f64> for MyScalarValue {
fn from(v: f64) -> Self {
Self::Float(v)
}
}
impl From<MyScalarValue> for Option<f64> {
fn from(v: MyScalarValue) -> Self {
if let MyScalarValue::Float(v) = v {
Some(v)
} else {
None
}
}
}
impl<'a> From<&'a MyScalarValue> for Option<&'a f64> {
fn from(v: &'a MyScalarValue) -> Self {
if let MyScalarValue::Float(v) = v {
Some(v)
} else {
None
}
}
}
impl From<String> for MyScalarValue {
fn from(v: String) -> Self {
Self::String(v)
}
}
impl From<MyScalarValue> for Option<String> {
fn from(v: MyScalarValue) -> Self {
if let MyScalarValue::String(v) = v {
Some(v)
} else {
None
}
}
}
impl<'a> From<&'a MyScalarValue> for Option<&'a String> {
fn from(v: &'a MyScalarValue) -> Self {
if let MyScalarValue::String(v) = v {
Some(v)
} else {
None
}
}
}
impl From<bool> for MyScalarValue {
fn from(v: bool) -> Self {
Self::Boolean(v)
}
}
impl From<MyScalarValue> for Option<bool> {
fn from(v: MyScalarValue) -> Self {
if let MyScalarValue::Boolean(v) = v {
Some(v)
} else {
None
}
}
}
impl<'a> From<&'a MyScalarValue> for Option<&'a bool> {
fn from(v: &'a MyScalarValue) -> Self {
if let MyScalarValue::Boolean(v) = v {
Some(v)
} else {
None
}
}
}
impl fmt::Display for MyScalarValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Int(v) => v.fmt(f),
Self::Long(v) => v.fmt(f),
Self::Float(v) => v.fmt(f),
Self::String(v) => v.fmt(f),
Self::Boolean(v) => v.fmt(f),
}
}
}
impl ScalarValue for MyScalarValue {
fn as_int(&self) -> Option<i32> {
match self {
@ -132,19 +275,23 @@ impl<'de> Deserialize<'de> for MyScalarValue {
}
}
#[graphql_scalar(name = "Long")]
impl GraphQLScalar for i64 {
fn resolve(&self) -> Value {
Value::scalar(*self)
#[graphql_scalar(with = long, scalar = MyScalarValue)]
type Long = i64;
mod long {
use super::*;
pub(super) fn to_output(v: &Long) -> Value<MyScalarValue> {
Value::scalar(*v)
}
fn from_input_value(v: &InputValue) -> Result<i64, String> {
pub(super) fn from_input(v: &InputValue<MyScalarValue>) -> Result<Long, String> {
v.as_scalar_value::<i64>()
.copied()
.ok_or_else(|| format!("Expected `MyScalarValue::Long`, found: {}", v))
}
fn from_str<'a>(value: ScalarToken<'a>) -> ParseScalarResult<'a, MyScalarValue> {
pub(super) fn parse_token(value: ScalarToken<'_>) -> ParseScalarResult<'_, MyScalarValue> {
if let ScalarToken::Int(v) = value {
v.parse()
.map_err(|_| ParseError::UnexpectedToken(Token::Scalar(value)))

View file

@ -37,7 +37,37 @@ mod pre_parse;
/// Common utilities used across tests.
pub(crate) mod util {
use futures::StreamExt as _;
use juniper::{graphql_value, ExecutionError, GraphQLError, ScalarValue, Value, ValuesStream};
use juniper::{
graphql_value, DefaultScalarValue, EmptyMutation, EmptySubscription, ExecutionError,
GraphQLError, GraphQLType, RootNode, ScalarValue, Value, ValuesStream,
};
pub(crate) fn schema<'q, C, Q>(
query_root: Q,
) -> RootNode<'q, Q, EmptyMutation<C>, EmptySubscription<C>>
where
Q: GraphQLType<DefaultScalarValue, Context = C, TypeInfo = ()> + 'q,
{
RootNode::new(
query_root,
EmptyMutation::<C>::new(),
EmptySubscription::<C>::new(),
)
}
pub(crate) fn schema_with_scalar<'q, S, C, Q>(
query_root: Q,
) -> RootNode<'q, Q, EmptyMutation<C>, EmptySubscription<C>, S>
where
Q: GraphQLType<S, Context = C, TypeInfo = ()> + 'q,
S: ScalarValue + 'q,
{
RootNode::new_with_scalar_value(
query_root,
EmptyMutation::<C>::new(),
EmptySubscription::<C>::new(),
)
}
/// Extracts a single next value from the result returned by
/// [`juniper::resolve_into_stream()`] and transforms it into a regular

View file

@ -21,6 +21,13 @@
- Remove necessity of writing `impl Trait for Type` blocks (interfaces are implemented just by matching its fields).
- Forbid default impls on non-ignored trait methods.
- Support coercion of additional nullable arguments and return sub-typing on implementer.
- Redesign `#[derive(GraphQLScalar)]` macro: ([#1017](https://github.com/graphql-rust/juniper/pull/1017))
- Support generic scalars.
- Support structs with single named field.
- Support overriding resolvers.
- Redesign `#[graphql_scalar]` macro: ([#1014](https://github.com/graphql-rust/juniper/pull/1014))
- Mirror `#[derive(GraphQLScalar)]` macro.
- Support usage on type aliases in case `#[derive(GraphQLScalar)]` isn't applicable because of [orphan rules](https://doc.rust-lang.org/reference/items/implementations.html#orphan-rules).
## Features

View file

@ -1252,7 +1252,7 @@ impl<'r, S: 'r> Registry<'r, S> {
/// Creates a [`ScalarMeta`] type.
pub fn build_scalar_type<T>(&mut self, info: &T::TypeInfo) -> ScalarMeta<'r, S>
where
T: GraphQLType<S> + FromInputValue<S> + ParseScalarValue<S> + 'r,
T: GraphQLType<S> + FromInputValue<S> + ParseScalarValue<S>,
T::Error: IntoFieldError<S>,
S: ScalarValue,
{

View file

@ -9,8 +9,7 @@ use crate::{
graphql_interface, graphql_object, graphql_scalar, graphql_value, graphql_vars,
schema::model::RootNode,
types::scalars::{EmptyMutation, EmptySubscription},
value::{ParseScalarResult, ParseScalarValue, Value},
GraphQLEnum,
GraphQLEnum, InputValue, ScalarValue, Value,
};
#[derive(GraphQLEnum)]
@ -20,23 +19,20 @@ enum Sample {
Two,
}
// TODO: Use `#[derive(GraphQLScalar)]` once implemented.
#[graphql_scalar(name = "SampleScalar", parse_token(i32))]
struct Scalar(i32);
#[graphql_scalar(name = "SampleScalar")]
impl<S: ScalarValue> GraphQLScalar for Scalar {
fn resolve(&self) -> Value {
impl Scalar {
fn to_output<S: ScalarValue>(&self) -> Value<S> {
Value::scalar(self.0)
}
fn from_input_value(v: &InputValue) -> Result<Scalar, String> {
fn from_input<S: ScalarValue>(v: &InputValue<S>) -> Result<Self, String> {
v.as_int_value()
.map(Scalar)
.map(Self)
.ok_or_else(|| format!("Expected `Int`, found: {}", v))
}
fn from_str<'a>(value: ScalarToken<'a>) -> ParseScalarResult<'a, S> {
<i32 as ParseScalarValue<S>>::from_str(value)
}
}
/// A sample interface

View file

@ -5,30 +5,27 @@ use crate::{
schema::model::RootNode,
types::scalars::{EmptyMutation, EmptySubscription},
validation::RuleError,
value::{DefaultScalarValue, Object, ParseScalarResult, ParseScalarValue},
value::{DefaultScalarValue, Object},
GraphQLError::ValidationError,
GraphQLInputObject,
GraphQLInputObject, InputValue, ScalarValue, Value,
};
// TODO: Use `#[derive(GraphQLScalar)]` once implemented.
#[derive(Debug)]
#[graphql_scalar(parse_token(String))]
struct TestComplexScalar;
#[graphql_scalar]
impl<S: ScalarValue> GraphQLScalar for TestComplexScalar {
fn resolve(&self) -> Value {
impl TestComplexScalar {
fn to_output<S: ScalarValue>(&self) -> Value<S> {
graphql_value!("SerializedValue")
}
fn from_input_value(v: &InputValue) -> Result<TestComplexScalar, String> {
fn from_input<S: ScalarValue>(v: &InputValue<S>) -> Result<Self, String> {
v.as_string_value()
.filter(|s| *s == "SerializedValue")
.map(|_| TestComplexScalar)
.map(|_| Self)
.ok_or_else(|| format!(r#"Expected "SerializedValue" string, found: {}"#, v))
}
fn from_str<'a>(value: ScalarToken<'a>) -> ParseScalarResult<'a, S> {
<String as ParseScalarValue<S>>::from_str(value)
}
}
#[derive(GraphQLInputObject, Debug)]

View file

@ -1,66 +1,46 @@
//! GraphQL support for [bson](https://github.com/mongodb/bson-rust) types.
use bson::{oid::ObjectId, DateTime as UtcDateTime};
use chrono::prelude::*;
use crate::{
graphql_scalar,
parser::{ParseError, ScalarToken, Token},
value::ParseScalarResult,
Value,
};
use crate::{graphql_scalar, InputValue, ScalarValue, Value};
#[graphql_scalar(description = "ObjectId")]
impl<S> GraphQLScalar for ObjectId
where
S: ScalarValue,
{
fn resolve(&self) -> Value {
Value::scalar(self.to_hex())
#[graphql_scalar(with = object_id, parse_token(String))]
type ObjectId = bson::oid::ObjectId;
mod object_id {
use super::*;
pub(super) fn to_output<S: ScalarValue>(v: &ObjectId) -> Value<S> {
Value::scalar(v.to_hex())
}
fn from_input_value(v: &InputValue) -> Result<ObjectId, String> {
pub(super) fn from_input<S: ScalarValue>(v: &InputValue<S>) -> Result<ObjectId, String> {
v.as_string_value()
.ok_or_else(|| format!("Expected `String`, found: {}", v))
.and_then(|s| {
Self::parse_str(s).map_err(|e| format!("Failed to parse `ObjectId`: {}", e))
ObjectId::parse_str(s).map_err(|e| format!("Failed to parse `ObjectId`: {}", e))
})
}
fn from_str<'a>(value: ScalarToken<'a>) -> ParseScalarResult<'a, S> {
if let ScalarToken::String(val) = value {
Ok(S::from(val.to_owned()))
} else {
Err(ParseError::UnexpectedToken(Token::Scalar(value)))
}
}
}
#[graphql_scalar(description = "UtcDateTime")]
impl<S> GraphQLScalar for UtcDateTime
where
S: ScalarValue,
{
fn resolve(&self) -> Value {
Value::scalar((*self).to_chrono().to_rfc3339())
#[graphql_scalar(with = utc_date_time, parse_token(String))]
type UtcDateTime = bson::DateTime;
mod utc_date_time {
use super::*;
pub(super) fn to_output<S: ScalarValue>(v: &UtcDateTime) -> Value<S> {
Value::scalar((*v).to_chrono().to_rfc3339())
}
fn from_input_value(v: &InputValue) -> Result<UtcDateTime, String> {
pub(super) fn from_input<S: ScalarValue>(v: &InputValue<S>) -> Result<UtcDateTime, String> {
v.as_string_value()
.ok_or_else(|| format!("Expected `String`, found: {}", v))
.and_then(|s| {
s.parse::<DateTime<Utc>>()
.map_err(|e| format!("Failed to parse `UtcDateTime`: {}", e))
})
.map(Self::from_chrono)
}
fn from_str<'a>(value: ScalarToken<'a>) -> ParseScalarResult<'a, S> {
if let ScalarToken::String(val) = value {
Ok(S::from(val.to_owned()))
} else {
Err(ParseError::UnexpectedToken(Token::Scalar(value)))
}
.map(UtcDateTime::from_chrono)
}
}
@ -72,7 +52,7 @@ mod test {
use crate::{graphql_input_value, FromInputValue, InputValue};
#[test]
fn objectid_from_input_value() {
fn objectid_from_input() {
let raw = "53e37d08776f724e42000000";
let input: InputValue = graphql_input_value!((raw));
@ -83,7 +63,7 @@ mod test {
}
#[test]
fn utcdatetime_from_input_value() {
fn utcdatetime_from_input() {
let raw = "2020-03-23T17:38:32.446+00:00";
let input: InputValue = graphql_input_value!((raw));

View file

@ -16,66 +16,48 @@
*/
#![allow(clippy::needless_lifetimes)]
use chrono::prelude::*;
use crate::{graphql_scalar, InputValue, ScalarValue, Value};
use crate::{
parser::{ParseError, ScalarToken, Token},
value::{ParseScalarResult, ParseScalarValue},
Value,
};
#[graphql_scalar(with = date_time_fixed_offset, parse_token(String))]
type DateTimeFixedOffset = chrono::DateTime<chrono::FixedOffset>;
#[crate::graphql_scalar(name = "DateTimeFixedOffset", description = "DateTime")]
impl<S> GraphQLScalar for DateTime<FixedOffset>
where
S: ScalarValue,
{
fn resolve(&self) -> Value {
Value::scalar(self.to_rfc3339())
mod date_time_fixed_offset {
use super::*;
pub(super) fn to_output<S: ScalarValue>(v: &DateTimeFixedOffset) -> Value<S> {
Value::scalar(v.to_rfc3339())
}
fn from_input_value(v: &InputValue) -> Result<DateTime<FixedOffset>, String> {
pub(super) fn from_input<S: ScalarValue>(
v: &InputValue<S>,
) -> Result<DateTimeFixedOffset, String> {
v.as_string_value()
.ok_or_else(|| format!("Expected `String`, found: {}", v))
.and_then(|s| {
DateTime::parse_from_rfc3339(s)
DateTimeFixedOffset::parse_from_rfc3339(s)
.map_err(|e| format!("Failed to parse `DateTimeFixedOffset`: {}", e))
})
}
fn from_str<'a>(value: ScalarToken<'a>) -> ParseScalarResult<'a, S> {
if let ScalarToken::String(value) = value {
Ok(S::from(value.to_owned()))
} else {
Err(ParseError::UnexpectedToken(Token::Scalar(value)))
}
}
}
#[crate::graphql_scalar(name = "DateTimeUtc", description = "DateTime")]
impl<S> GraphQLScalar for DateTime<Utc>
where
S: ScalarValue,
{
fn resolve(&self) -> Value {
Value::scalar(self.to_rfc3339())
#[graphql_scalar(with = date_time_utc, parse_token(String))]
type DateTimeUtc = chrono::DateTime<chrono::Utc>;
mod date_time_utc {
use super::*;
pub(super) fn to_output<S: ScalarValue>(v: &DateTimeUtc) -> Value<S> {
Value::scalar(v.to_rfc3339())
}
fn from_input_value(v: &InputValue) -> Result<DateTime<Utc>, String> {
pub(super) fn from_input<S: ScalarValue>(v: &InputValue<S>) -> Result<DateTimeUtc, String> {
v.as_string_value()
.ok_or_else(|| format!("Expected `String`, found: {}", v))
.and_then(|s| {
s.parse::<DateTime<Utc>>()
s.parse::<DateTimeUtc>()
.map_err(|e| format!("Failed to parse `DateTimeUtc`: {}", e))
})
}
fn from_str<'a>(value: ScalarToken<'a>) -> ParseScalarResult<'a, S> {
if let ScalarToken::String(value) = value {
Ok(S::from(value.to_owned()))
} else {
Err(ParseError::UnexpectedToken(Token::Scalar(value)))
}
}
}
// Don't use `Date` as the docs say:
@ -83,16 +65,17 @@ where
// inherent lack of precision required for the time zone resolution.
// For serialization and deserialization uses, it is best to use
// `NaiveDate` instead."
#[crate::graphql_scalar(description = "NaiveDate")]
impl<S> GraphQLScalar for NaiveDate
where
S: ScalarValue,
{
fn resolve(&self) -> Value {
Value::scalar(self.format("%Y-%m-%d").to_string())
#[graphql_scalar(with = naive_date, parse_token(String))]
type NaiveDate = chrono::NaiveDate;
mod naive_date {
use super::*;
pub(super) fn to_output<S: ScalarValue>(v: &NaiveDate) -> Value<S> {
Value::scalar(v.format("%Y-%m-%d").to_string())
}
fn from_input_value(v: &InputValue) -> Result<NaiveDate, String> {
pub(super) fn from_input<S: ScalarValue>(v: &InputValue<S>) -> Result<NaiveDate, String> {
v.as_string_value()
.ok_or_else(|| format!("Expected `String`, found: {}", v))
.and_then(|s| {
@ -100,27 +83,21 @@ where
.map_err(|e| format!("Failed to parse `NaiveDate`: {}", e))
})
}
fn from_str<'a>(value: ScalarToken<'a>) -> ParseScalarResult<'a, S> {
if let ScalarToken::String(value) = value {
Ok(S::from(value.to_owned()))
} else {
Err(ParseError::UnexpectedToken(Token::Scalar(value)))
}
}
}
#[cfg(feature = "scalar-naivetime")]
#[crate::graphql_scalar(description = "NaiveTime")]
impl<S> GraphQLScalar for NaiveTime
where
S: ScalarValue,
{
fn resolve(&self) -> Value {
Value::scalar(self.format("%H:%M:%S").to_string())
#[graphql_scalar(with = naive_time, parse_token(String))]
type NaiveTime = chrono::NaiveTime;
#[cfg(feature = "scalar-naivetime")]
mod naive_time {
use super::*;
pub(super) fn to_output<S: ScalarValue>(v: &NaiveTime) -> Value<S> {
Value::scalar(v.format("%H:%M:%S").to_string())
}
fn from_input_value(v: &InputValue) -> Result<NaiveTime, String> {
pub(super) fn from_input<S: ScalarValue>(v: &InputValue<S>) -> Result<NaiveTime, String> {
v.as_string_value()
.ok_or_else(|| format!("Expected `String`, found: {}", v))
.and_then(|s| {
@ -128,28 +105,21 @@ where
.map_err(|e| format!("Failed to parse `NaiveTime`: {}", e))
})
}
fn from_str<'a>(value: ScalarToken<'a>) -> ParseScalarResult<'a, S> {
if let ScalarToken::String(value) = value {
Ok(S::from(value.to_owned()))
} else {
Err(ParseError::UnexpectedToken(Token::Scalar(value)))
}
}
}
// JSON numbers (i.e. IEEE doubles) are not precise enough for nanosecond
// datetimes. Values will be truncated to microsecond resolution.
#[crate::graphql_scalar(description = "NaiveDateTime")]
impl<S> GraphQLScalar for NaiveDateTime
where
S: ScalarValue,
{
fn resolve(&self) -> Value {
Value::scalar(self.timestamp() as f64)
#[graphql_scalar(with = naive_date_time, parse_token(f64))]
type NaiveDateTime = chrono::NaiveDateTime;
mod naive_date_time {
use super::*;
pub(super) fn to_output<S: ScalarValue>(v: &NaiveDateTime) -> Value<S> {
Value::scalar(v.timestamp() as f64)
}
fn from_input_value(v: &InputValue) -> Result<NaiveDateTime, String> {
pub(super) fn from_input<S: ScalarValue>(v: &InputValue<S>) -> Result<NaiveDateTime, String> {
v.as_float_value()
.ok_or_else(|| format!("Expected `Float`, found: {}", v))
.and_then(|f| {
@ -158,10 +128,6 @@ where
.ok_or_else(|| format!("Out-of-range number of seconds: {}", secs))
})
}
fn from_str<'a>(value: ScalarToken<'a>) -> ParseScalarResult<'a, S> {
<f64 as ParseScalarValue<S>>::from_str(value)
}
}
#[cfg(test)]
@ -180,17 +146,17 @@ mod test {
}
#[test]
fn datetime_fixedoffset_from_input_value() {
fn datetime_fixedoffset_from_input() {
datetime_fixedoffset_test("2014-11-28T21:00:09+09:00");
}
#[test]
fn datetime_fixedoffset_from_input_value_with_z_timezone() {
fn datetime_fixedoffset_from_input_with_z_timezone() {
datetime_fixedoffset_test("2014-11-28T21:00:09Z");
}
#[test]
fn datetime_fixedoffset_from_input_value_with_fractional_seconds() {
fn datetime_fixedoffset_from_input_with_fractional_seconds() {
datetime_fixedoffset_test("2014-11-28T21:00:09.05+09:00");
}
@ -206,22 +172,22 @@ mod test {
}
#[test]
fn datetime_utc_from_input_value() {
fn datetime_utc_from_input() {
datetime_utc_test("2014-11-28T21:00:09+09:00")
}
#[test]
fn datetime_utc_from_input_value_with_z_timezone() {
fn datetime_utc_from_input_with_z_timezone() {
datetime_utc_test("2014-11-28T21:00:09Z")
}
#[test]
fn datetime_utc_from_input_value_with_fractional_seconds() {
fn datetime_utc_from_input_with_fractional_seconds() {
datetime_utc_test("2014-11-28T21:00:09.005+09:00");
}
#[test]
fn naivedate_from_input_value() {
fn naivedate_from_input() {
let input: InputValue = graphql_input_value!("1996-12-19");
let y = 1996;
let m = 12;
@ -239,7 +205,7 @@ mod test {
#[test]
#[cfg(feature = "scalar-naivetime")]
fn naivetime_from_input_value() {
fn naivetime_from_input() {
let input: InputValue = graphql_input_value!("21:12:19");
let [h, m, s] = [21, 12, 19];
let parsed: NaiveTime = FromInputValue::from_input_value(&input).unwrap();
@ -251,7 +217,7 @@ mod test {
}
#[test]
fn naivedatetime_from_input_value() {
fn naivedatetime_from_input() {
let raw = 1_000_000_000_f64;
let input: InputValue = graphql_input_value!((raw));

View file

@ -3,25 +3,19 @@
//! [`Tz`]: chrono_tz::Tz
//! [1]: http://www.iana.org/time-zones
use chrono_tz::Tz;
use crate::{graphql_scalar, InputValue, ScalarValue, Value};
use crate::{
graphql_scalar,
parser::{ParseError, ScalarToken, Token},
value::ParseScalarResult,
Value,
};
#[graphql_scalar(with = tz, parse_token(String))]
type Tz = chrono_tz::Tz;
#[graphql_scalar(name = "Tz", description = "Timezone")]
impl<S> GraphQLScalar for Tz
where
S: ScalarValue,
{
fn resolve(&self) -> Value {
Value::scalar(self.name().to_owned())
mod tz {
use super::*;
pub(super) fn to_output<S: ScalarValue>(v: &Tz) -> Value<S> {
Value::scalar(v.name().to_owned())
}
fn from_input_value(v: &InputValue) -> Result<Tz, String> {
pub(super) fn from_input<S: ScalarValue>(v: &InputValue<S>) -> Result<Tz, String> {
v.as_string_value()
.ok_or_else(|| format!("Expected `String`, found: {}", v))
.and_then(|s| {
@ -29,32 +23,22 @@ where
.map_err(|e| format!("Failed to parse `Tz`: {}", e))
})
}
fn from_str<'a>(val: ScalarToken<'a>) -> ParseScalarResult<'a, S> {
if let ScalarToken::String(s) = val {
Ok(S::from(s.to_owned()))
} else {
Err(ParseError::UnexpectedToken(Token::Scalar(val)))
}
}
}
#[cfg(test)]
mod test {
mod from_input_value {
use std::ops::Deref;
mod from_input {
use chrono_tz::Tz;
use crate::{graphql_input_value, FromInputValue, InputValue};
use crate::{graphql_input_value, FromInputValue, InputValue, IntoFieldError};
fn tz_input_test(raw: &'static str, expected: Result<Tz, &str>) {
let input: InputValue = graphql_input_value!((raw));
let parsed = FromInputValue::from_input_value(&input);
assert_eq!(
parsed.as_ref().map_err(Deref::deref),
expected.as_ref().map_err(Deref::deref),
parsed.as_ref(),
expected.map_err(IntoFieldError::into_field_error).as_ref(),
);
}

View file

@ -26,59 +26,45 @@ use time::{
macros::format_description,
};
use crate::{
graphql_scalar,
parser::{ParseError, ScalarToken, Token},
value::ParseScalarResult,
Value,
};
pub use time::{
Date, OffsetDateTime as DateTime, PrimitiveDateTime as LocalDateTime, Time as LocalTime,
UtcOffset,
};
use crate::{graphql_scalar, InputValue, ScalarValue, Value};
/// Format of a [`Date` scalar][1].
///
/// [1]: https://graphql-scalars.dev/docs/scalars/date
const DATE_FORMAT: &[FormatItem<'_>] = format_description!("[year]-[month]-[day]");
/// Date in the proleptic Gregorian calendar (without time zone).
///
/// Represents a description of the date (as used for birthdays, for example).
/// It cannot represent an instant on the time-line.
///
/// [`Date` scalar][1] compliant.
///
/// See also [`time::Date`][2] for details.
///
/// [1]: https://graphql-scalars.dev/docs/scalars/date
/// [2]: https://docs.rs/time/*/time/struct.Date.html
#[graphql_scalar(
description = "Date in the proleptic Gregorian calendar (without time \
zone).\
\n\n\
Represents a description of the date (as used for birthdays,
for example). It cannot represent an instant on the \
time-line.\
\n\n\
[`Date` scalar][1] compliant.\
\n\n\
See also [`time::Date`][2] for details.\
\n\n\
[1]: https://graphql-scalars.dev/docs/scalars/date\n\
[2]: https://docs.rs/time/*/time/struct.Date.html",
specified_by_url = "https://graphql-scalars.dev/docs/scalars/date"
with = date,
parse_token(String),
specified_by_url = "https://graphql-scalars.dev/docs/scalars/date",
)]
impl<S: ScalarValue> GraphQLScalar for Date {
fn resolve(&self) -> Value {
pub type Date = time::Date;
mod date {
use super::*;
pub(super) fn to_output<S: ScalarValue>(v: &Date) -> Value<S> {
Value::scalar(
self.format(DATE_FORMAT)
v.format(DATE_FORMAT)
.unwrap_or_else(|e| panic!("Failed to format `Date`: {}", e)),
)
}
fn from_input_value(v: &InputValue) -> Result<Self, String> {
pub(super) fn from_input<S: ScalarValue>(v: &InputValue<S>) -> Result<Date, String> {
v.as_string_value()
.ok_or_else(|| format!("Expected `String`, found: {}", v))
.and_then(|s| Self::parse(s, DATE_FORMAT).map_err(|e| format!("Invalid `Date`: {}", e)))
}
fn from_str<'a>(value: ScalarToken<'a>) -> ParseScalarResult<'a, S> {
if let ScalarToken::String(s) = value {
Ok(S::from(s.to_owned()))
} else {
Err(ParseError::UnexpectedToken(Token::Scalar(value)))
}
.and_then(|s| Date::parse(s, DATE_FORMAT).map_err(|e| format!("Invalid `Date`: {}", e)))
}
}
@ -99,137 +85,120 @@ const LOCAL_TIME_FORMAT_NO_MILLIS: &[FormatItem<'_>] =
/// [1]: https://graphql-scalars.dev/docs/scalars/local-time
const LOCAL_TIME_FORMAT_NO_SECS: &[FormatItem<'_>] = format_description!("[hour]:[minute]");
#[graphql_scalar(
description = "Clock time within a given date (without time zone) in \
`HH:mm[:ss[.SSS]]` format.\
\n\n\
All minutes are assumed to have exactly 60 seconds; no \
attempt is made to handle leap seconds (either positive or \
negative).\
\n\n\
[`LocalTime` scalar][1] compliant.\
\n\n\
See also [`time::Time`][2] for details.\
\n\n\
[1]: https://graphql-scalars.dev/docs/scalars/local-time\n\
[2]: https://docs.rs/time/*/time/struct.Time.html",
specified_by_url = "https://graphql-scalars.dev/docs/scalars/local-time"
)]
impl<S: ScalarValue> GraphQLScalar for LocalTime {
fn resolve(&self) -> Value {
/// Clock time within a given date (without time zone) in `HH:mm[:ss[.SSS]]`
/// format.
///
/// All minutes are assumed to have exactly 60 seconds; no attempt is made to
/// handle leap seconds (either positive or negative).
///
/// [`LocalTime` scalar][1] compliant.
///
/// See also [`time::Time`][2] for details.
///
/// [1]: https://graphql-scalars.dev/docs/scalars/local-time
/// [2]: https://docs.rs/time/*/time/struct.Time.html
#[graphql_scalar(with = local_time, parse_token(String))]
pub type LocalTime = time::Time;
mod local_time {
use super::*;
pub(super) fn to_output<S: ScalarValue>(v: &LocalTime) -> Value<S> {
Value::scalar(
if self.millisecond() == 0 {
self.format(LOCAL_TIME_FORMAT_NO_MILLIS)
if v.millisecond() == 0 {
v.format(LOCAL_TIME_FORMAT_NO_MILLIS)
} else {
self.format(LOCAL_TIME_FORMAT)
v.format(LOCAL_TIME_FORMAT)
}
.unwrap_or_else(|e| panic!("Failed to format `LocalTime`: {}", e)),
)
}
fn from_input_value(v: &InputValue) -> Result<Self, String> {
pub(super) fn from_input<S: ScalarValue>(v: &InputValue<S>) -> Result<LocalTime, String> {
v.as_string_value()
.ok_or_else(|| format!("Expected `String`, found: {}", v))
.and_then(|s| {
// First, try to parse the most used format.
// At the end, try to parse the full format for the parsing
// error to be most informative.
Self::parse(s, LOCAL_TIME_FORMAT_NO_MILLIS)
.or_else(|_| Self::parse(s, LOCAL_TIME_FORMAT_NO_SECS))
.or_else(|_| Self::parse(s, LOCAL_TIME_FORMAT))
LocalTime::parse(s, LOCAL_TIME_FORMAT_NO_MILLIS)
.or_else(|_| LocalTime::parse(s, LOCAL_TIME_FORMAT_NO_SECS))
.or_else(|_| LocalTime::parse(s, LOCAL_TIME_FORMAT))
.map_err(|e| format!("Invalid `LocalTime`: {}", e))
})
}
fn from_str<'a>(value: ScalarToken<'a>) -> ParseScalarResult<'a, S> {
if let ScalarToken::String(s) = value {
Ok(S::from(s.to_owned()))
} else {
Err(ParseError::UnexpectedToken(Token::Scalar(value)))
}
}
}
/// Format of a [`LocalDateTime`] scalar.
const LOCAL_DATE_TIME_FORMAT: &[FormatItem<'_>] =
format_description!("[year]-[month]-[day] [hour]:[minute]:[second]");
#[graphql_scalar(
description = "Combined date and time (without time zone) in `yyyy-MM-dd \
HH:mm:ss` format.\
\n\n\
See also [`time::PrimitiveDateTime`][2] for details.\
\n\n\
[2]: https://docs.rs/time/*/time/struct.PrimitiveDateTime.html"
)]
impl<S: ScalarValue> GraphQLScalar for LocalDateTime {
fn resolve(&self) -> Value {
/// Combined date and time (without time zone) in `yyyy-MM-dd HH:mm:ss` format.
///
/// See also [`time::PrimitiveDateTime`][2] for details.
///
/// [2]: https://docs.rs/time/*/time/struct.PrimitiveDateTime.html
#[graphql_scalar(with = local_date_time, parse_token(String))]
pub type LocalDateTime = time::PrimitiveDateTime;
mod local_date_time {
use super::*;
pub(super) fn to_output<S: ScalarValue>(v: &LocalDateTime) -> Value<S> {
Value::scalar(
self.format(LOCAL_DATE_TIME_FORMAT)
v.format(LOCAL_DATE_TIME_FORMAT)
.unwrap_or_else(|e| panic!("Failed to format `LocalDateTime`: {}", e)),
)
}
fn from_input_value(v: &InputValue) -> Result<Self, String> {
pub(super) fn from_input<S: ScalarValue>(v: &InputValue<S>) -> Result<LocalDateTime, String> {
v.as_string_value()
.ok_or_else(|| format!("Expected `String`, found: {}", v))
.and_then(|s| {
Self::parse(s, LOCAL_DATE_TIME_FORMAT)
LocalDateTime::parse(s, LOCAL_DATE_TIME_FORMAT)
.map_err(|e| format!("Invalid `LocalDateTime`: {}", e))
})
}
fn from_str<'a>(value: ScalarToken<'a>) -> ParseScalarResult<'a, S> {
if let ScalarToken::String(s) = value {
Ok(S::from(s.to_owned()))
} else {
Err(ParseError::UnexpectedToken(Token::Scalar(value)))
}
}
}
/// Combined date and time (with time zone) in [RFC 3339][0] format.
///
/// Represents a description of an exact instant on the time-line (such as the
/// instant that a user account was created).
///
/// [`DateTime` scalar][1] compliant.
///
/// See also [`time::OffsetDateTime`][2] for details.
///
/// [0]: https://datatracker.ietf.org/doc/html/rfc3339#section-5.6
/// [1]: https://graphql-scalars.dev/docs/scalars/date-time
/// [2]: https://docs.rs/time/*/time/struct.OffsetDateTime.html
#[graphql_scalar(
description = "Combined date and time (with time zone) in [RFC 3339][0] \
format.\
\n\n\
Represents a description of an exact instant on the \
time-line (such as the instant that a user account was \
created).\
\n\n\
[`DateTime` scalar][1] compliant.\
\n\n\
See also [`time::OffsetDateTime`][2] for details.\
\n\n\
[0]: https://datatracker.ietf.org/doc/html/rfc3339#section-5.6\n\
[1]: https://graphql-scalars.dev/docs/scalars/date-time\n\
[2]: https://docs.rs/time/*/time/struct.OffsetDateTime.html",
specified_by_url = "https://graphql-scalars.dev/docs/scalars/date-time"
with = date_time,
parse_token(String),
specified_by_url = "https://graphql-scalars.dev/docs/scalars/date-time",
)]
impl<S: ScalarValue> GraphQLScalar for DateTime {
fn resolve(&self) -> Value {
pub type DateTime = time::OffsetDateTime;
mod date_time {
use super::*;
pub(super) fn to_output<S: ScalarValue>(v: &DateTime) -> Value<S> {
Value::scalar(
self.to_offset(UtcOffset::UTC)
v.to_offset(UtcOffset::UTC)
.format(&Rfc3339)
.unwrap_or_else(|e| panic!("Failed to format `DateTime`: {}", e)),
)
}
fn from_input_value(v: &InputValue) -> Result<Self, String> {
pub(super) fn from_input<S: ScalarValue>(v: &InputValue<S>) -> Result<DateTime, String> {
v.as_string_value()
.ok_or_else(|| format!("Expected `String`, found: {}", v))
.and_then(|s| {
Self::parse(s, &Rfc3339).map_err(|e| format!("Invalid `DateTime`: {}", e))
DateTime::parse(s, &Rfc3339).map_err(|e| format!("Invalid `DateTime`: {}", e))
})
.map(|dt| dt.to_offset(UtcOffset::UTC))
}
fn from_str<'a>(value: ScalarToken<'a>) -> ParseScalarResult<'a, S> {
if let ScalarToken::String(s) = value {
Ok(S::from(s.to_owned()))
} else {
Err(ParseError::UnexpectedToken(Token::Scalar(value)))
}
}
}
/// Format of a [`UtcOffset` scalar][1].
@ -238,42 +207,40 @@ impl<S: ScalarValue> GraphQLScalar for DateTime {
const UTC_OFFSET_FORMAT: &[FormatItem<'_>] =
format_description!("[offset_hour sign:mandatory]:[offset_minute]");
/// Offset from UTC in `±hh:mm` format. See [list of database time zones][0].
///
/// [`UtcOffset` scalar][1] compliant.
///
/// See also [`time::UtcOffset`][2] for details.
///
/// [0]: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
/// [1]: https://graphql-scalars.dev/docs/scalars/utc-offset
/// [2]: https://docs.rs/time/*/time/struct.UtcOffset.html
#[graphql_scalar(
description = "Offset from UTC in `±hh:mm` format. See [list of database \
time zones][0].\
\n\n\
[`UtcOffset` scalar][1] compliant.\
\n\n\
See also [`time::UtcOffset`][2] for details.\
\n\n\
[0]: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones\n\
[1]: https://graphql-scalars.dev/docs/scalars/utc-offset\n\
[2]: https://docs.rs/time/*/time/struct.UtcOffset.html",
specified_by_url = "https://graphql-scalars.dev/docs/scalars/utc-offset"
with = utc_offset,
parse_token(String),
specified_by_url = "https://graphql-scalars.dev/docs/scalars/utc-offset",
)]
impl<S: ScalarValue> GraphQLScalar for UtcOffset {
fn resolve(&self) -> Value {
pub type UtcOffset = time::UtcOffset;
mod utc_offset {
use super::*;
pub(super) fn to_output<S: ScalarValue>(v: &UtcOffset) -> Value<S> {
Value::scalar(
self.format(UTC_OFFSET_FORMAT)
v.format(UTC_OFFSET_FORMAT)
.unwrap_or_else(|e| panic!("Failed to format `UtcOffset`: {}", e)),
)
}
fn from_input_value(v: &InputValue) -> Result<Self, String> {
pub(super) fn from_input<S: ScalarValue>(v: &InputValue<S>) -> Result<UtcOffset, String> {
v.as_string_value()
.ok_or_else(|| format!("Expected `String`, found: {}", v))
.and_then(|s| {
Self::parse(s, UTC_OFFSET_FORMAT).map_err(|e| format!("Invalid `UtcOffset`: {}", e))
UtcOffset::parse(s, UTC_OFFSET_FORMAT)
.map_err(|e| format!("Invalid `UtcOffset`: {}", e))
})
}
fn from_str<'a>(value: ScalarToken<'a>) -> ParseScalarResult<'a, S> {
if let ScalarToken::String(s) = value {
Ok(S::from(s.to_owned()))
} else {
Err(ParseError::UnexpectedToken(Token::Scalar(value)))
}
}
}
#[cfg(test)]
@ -295,7 +262,7 @@ mod date_test {
assert!(
parsed.is_ok(),
"failed to parse `{}`: {}",
"failed to parse `{}`: {:?}",
raw,
parsed.unwrap_err(),
);
@ -364,7 +331,7 @@ mod local_time_test {
assert!(
parsed.is_ok(),
"failed to parse `{}`: {}",
"failed to parse `{}`: {:?}",
raw,
parsed.unwrap_err(),
);
@ -436,7 +403,7 @@ mod local_date_time_test {
assert!(
parsed.is_ok(),
"failed to parse `{}`: {}",
"failed to parse `{}`: {:?}",
raw,
parsed.unwrap_err(),
);
@ -524,7 +491,7 @@ mod date_time_test {
assert!(
parsed.is_ok(),
"failed to parse `{}`: {}",
"failed to parse `{}`: {:?}",
raw,
parsed.unwrap_err(),
);
@ -608,7 +575,7 @@ mod utc_offset_test {
assert!(
parsed.is_ok(),
"failed to parse `{}`: {}",
"failed to parse `{}`: {:?}",
raw,
parsed.unwrap_err(),
);

View file

@ -1,30 +1,22 @@
//! GraphQL support for [url](https://github.com/servo/rust-url) types.
use url::Url;
use crate::{graphql_scalar, InputValue, ScalarValue, Value};
use crate::{
value::{ParseScalarResult, ParseScalarValue},
Value,
};
#[graphql_scalar(with = url_scalar, parse_token(String))]
type Url = url::Url;
#[crate::graphql_scalar(description = "Url")]
impl<S> GraphQLScalar for Url
where
S: ScalarValue,
{
fn resolve(&self) -> Value {
Value::scalar(self.as_str().to_owned())
mod url_scalar {
use super::*;
pub(super) fn to_output<S: ScalarValue>(v: &Url) -> Value<S> {
Value::scalar(v.as_str().to_owned())
}
fn from_input_value(v: &InputValue) -> Result<Url, String> {
pub(super) fn from_input<S: ScalarValue>(v: &InputValue<S>) -> Result<Url, String> {
v.as_string_value()
.ok_or_else(|| format!("Expected `String`, found: {}", v))
.and_then(|s| Url::parse(s).map_err(|e| format!("Failed to parse `Url`: {}", e)))
}
fn from_str<'a>(value: ScalarToken<'a>) -> ParseScalarResult<'a, S> {
<String as ParseScalarValue<S>>::from_str(value)
}
}
#[cfg(test)]
@ -34,7 +26,7 @@ mod test {
use crate::{graphql_input_value, InputValue};
#[test]
fn url_from_input_value() {
fn url_from_input() {
let raw = "https://example.net/";
let input: InputValue = graphql_input_value!((raw));

View file

@ -2,36 +2,23 @@
#![allow(clippy::needless_lifetimes)]
use uuid::Uuid;
use crate::{graphql_scalar, InputValue, ScalarValue, Value};
use crate::{
parser::{ParseError, ScalarToken, Token},
value::ParseScalarResult,
Value,
};
#[graphql_scalar(with = uuid_scalar, parse_token(String))]
type Uuid = uuid::Uuid;
#[crate::graphql_scalar(description = "Uuid")]
impl<S> GraphQLScalar for Uuid
where
S: ScalarValue,
{
fn resolve(&self) -> Value {
Value::scalar(self.to_string())
mod uuid_scalar {
use super::*;
pub(super) fn to_output<S: ScalarValue>(v: &Uuid) -> Value<S> {
Value::scalar(v.to_string())
}
fn from_input_value(v: &InputValue) -> Result<Uuid, String> {
pub(super) fn from_input<S: ScalarValue>(v: &InputValue<S>) -> Result<Uuid, String> {
v.as_string_value()
.ok_or_else(|| format!("Expected `String`, found: {}", v))
.and_then(|s| Uuid::parse_str(s).map_err(|e| format!("Failed to parse `Uuid`: {}", e)))
}
fn from_str<'a>(value: ScalarToken<'a>) -> ParseScalarResult<'a, S> {
if let ScalarToken::String(value) = value {
Ok(S::from(value.to_owned()))
} else {
Err(ParseError::UnexpectedToken(Token::Scalar(value)))
}
}
}
#[cfg(test)]
@ -41,7 +28,7 @@ mod test {
use crate::{graphql_input_value, FromInputValue, InputValue};
#[test]
fn uuid_from_input_value() {
fn uuid_from_input() {
let raw = "123e4567-e89b-12d3-a456-426655440000";
let input: InputValue = graphql_input_value!((raw));

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, GraphQLScalarValue, GraphQLUnion,
GraphQLEnum, GraphQLInputObject, GraphQLObject, GraphQLUnion,
};
#[doc(hidden)]
@ -168,7 +168,7 @@ pub use crate::{
subscription::{ExtractTypeFromStream, IntoFieldResult},
AsDynGraphQLValue,
},
parser::{ParseError, Spanning},
parser::{ParseError, ScalarToken, Spanning},
schema::{
meta,
model::{RootNode, SchemaType},

View file

@ -599,7 +599,7 @@ where
#[cfg(test)]
mod coercion {
use crate::{graphql_input_value, FromInputValue as _, InputValue};
use crate::{graphql_input_value, FromInputValue as _, InputValue, IntoFieldError as _};
use super::{FromInputValueArrayError, FromInputValueVecError};
@ -685,13 +685,13 @@ mod coercion {
assert_eq!(
<Vec<i32>>::from_input_value(&v),
Err(FromInputValueVecError::Item(
"Expected `Int`, found: null".to_owned(),
"Expected `Int`, found: null".into_field_error(),
)),
);
assert_eq!(
<Option<Vec<i32>>>::from_input_value(&v),
Err(FromInputValueVecError::Item(
"Expected `Int`, found: null".to_owned(),
"Expected `Int`, found: null".into_field_error(),
)),
);
assert_eq!(
@ -795,13 +795,13 @@ mod coercion {
assert_eq!(
<[i32; 3]>::from_input_value(&v),
Err(FromInputValueArrayError::Item(
"Expected `Int`, found: null".to_owned(),
"Expected `Int`, found: null".into_field_error(),
)),
);
assert_eq!(
<Option<[i32; 3]>>::from_input_value(&v),
Err(FromInputValueArrayError::Item(
"Expected `Int`, found: null".to_owned(),
"Expected `Int`, found: null".into_field_error(),
)),
);
assert_eq!(

View file

@ -7,6 +7,7 @@ use serde::{Deserialize, Serialize};
use crate::{
ast::{InputValue, Selection, ToInputValue},
executor::{ExecutionResult, Executor, Registry},
graphql_scalar,
macros::reflect,
parser::{LexerError, ParseError, ScalarToken, Token},
schema::meta::MetaType,
@ -21,9 +22,25 @@ use crate::{
/// 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))]
pub struct ID(String);
impl ID {
fn to_output<S: ScalarValue>(&self) -> Value<S> {
Value::scalar(self.0.clone())
}
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()))
.map(Self)
.ok_or_else(|| format!("Expected `String` or `Int`, found: {}", v))
}
}
impl From<String> for ID {
fn from(s: String) -> ID {
ID(s)
@ -51,47 +68,23 @@ impl fmt::Display for ID {
}
}
#[crate::graphql_scalar(name = "ID")]
impl<S> GraphQLScalar for ID
where
S: ScalarValue,
{
fn resolve(&self) -> Value {
Value::scalar(self.0.clone())
#[graphql_scalar(with = impl_string_scalar)]
type String = std::string::String;
mod impl_string_scalar {
use super::*;
pub(super) fn to_output<S: ScalarValue>(v: &str) -> Value<S> {
Value::scalar(v.to_owned())
}
fn from_input_value(v: &InputValue) -> Result<ID, String> {
v.as_string_value()
.map(str::to_owned)
.or_else(|| v.as_int_value().map(|i| i.to_string()))
.map(ID)
.ok_or_else(|| format!("Expected `String` or `Int`, found: {}", v))
}
fn from_str<'a>(value: ScalarToken<'a>) -> ParseScalarResult<'a, S> {
match value {
ScalarToken::String(value) | ScalarToken::Int(value) => Ok(S::from(value.to_owned())),
_ => Err(ParseError::UnexpectedToken(Token::Scalar(value))),
}
}
}
#[crate::graphql_scalar(name = "String")]
impl<S> GraphQLScalar for String
where
S: ScalarValue,
{
fn resolve(&self) -> Value {
Value::scalar(self.clone())
}
fn from_input_value(v: &InputValue) -> Result<String, String> {
pub(super) fn from_input<S: ScalarValue>(v: &InputValue<S>) -> Result<String, String> {
v.as_string_value()
.map(str::to_owned)
.ok_or_else(|| format!("Expected `String`, found: {}", v))
}
fn from_str<'a>(value: ScalarToken<'a>) -> ParseScalarResult<'a, S> {
pub(super) fn parse_token<S: ScalarValue>(value: ScalarToken<'_>) -> ParseScalarResult<'_, S> {
if let ScalarToken::String(value) = value {
let mut ret = String::with_capacity(value.len());
let mut char_iter = value.chars();
@ -276,42 +269,44 @@ where
}
}
#[crate::graphql_scalar(name = "Boolean")]
impl<S> GraphQLScalar for bool
where
S: ScalarValue,
{
fn resolve(&self) -> Value {
Value::scalar(*self)
#[graphql_scalar(with = impl_boolean_scalar)]
type Boolean = bool;
mod impl_boolean_scalar {
use super::*;
pub(super) fn to_output<S: ScalarValue>(v: &Boolean) -> Value<S> {
Value::scalar(*v)
}
fn from_input_value(v: &InputValue) -> Result<bool, String> {
pub(super) fn from_input<S: ScalarValue>(v: &InputValue<S>) -> Result<Boolean, String> {
v.as_scalar_value()
.and_then(ScalarValue::as_boolean)
.ok_or_else(|| format!("Expected `Boolean`, found: {}", v))
}
fn from_str<'a>(value: ScalarToken<'a>) -> ParseScalarResult<'a, S> {
// Bools are parsed separately - they shouldn't reach this code path
pub(super) fn parse_token<S: ScalarValue>(value: ScalarToken<'_>) -> ParseScalarResult<'_, S> {
// `Boolean`s are parsed separately, they shouldn't reach this code path.
Err(ParseError::UnexpectedToken(Token::Scalar(value)))
}
}
#[crate::graphql_scalar(name = "Int")]
impl<S> GraphQLScalar for i32
where
S: ScalarValue,
{
fn resolve(&self) -> Value {
Value::scalar(*self)
#[graphql_scalar(with = impl_int_scalar)]
type Int = i32;
mod impl_int_scalar {
use super::*;
pub(super) fn to_output<S: ScalarValue>(v: &Int) -> Value<S> {
Value::scalar(*v)
}
fn from_input_value(v: &InputValue) -> Result<i32, String> {
pub(super) fn from_input<S: ScalarValue>(v: &InputValue<S>) -> Result<Int, String> {
v.as_int_value()
.ok_or_else(|| format!("Expected `Int`, found: {}", v))
}
fn from_str<'a>(value: ScalarToken<'a>) -> ParseScalarResult<'a, S> {
pub(super) fn parse_token<S: ScalarValue>(value: ScalarToken<'_>) -> ParseScalarResult<'_, S> {
if let ScalarToken::Int(v) = value {
v.parse()
.map_err(|_| ParseError::UnexpectedToken(Token::Scalar(value)))
@ -322,21 +317,22 @@ where
}
}
#[crate::graphql_scalar(name = "Float")]
impl<S> GraphQLScalar for f64
where
S: ScalarValue,
{
fn resolve(&self) -> Value {
Value::scalar(*self)
#[graphql_scalar(with = impl_float_scalar)]
type Float = f64;
mod impl_float_scalar {
use super::*;
pub(super) fn to_output<S: ScalarValue>(v: &Float) -> Value<S> {
Value::scalar(*v)
}
fn from_input_value(v: &InputValue) -> Result<f64, String> {
pub(super) fn from_input<S: ScalarValue>(v: &InputValue<S>) -> Result<Float, String> {
v.as_float_value()
.ok_or_else(|| format!("Expected `Float`, found: {}", v))
}
fn from_str<'a>(value: ScalarToken<'a>) -> ParseScalarResult<'a, S> {
pub(super) fn parse_token<S: ScalarValue>(value: ScalarToken<'_>) -> ParseScalarResult<'_, S> {
match value {
ScalarToken::Int(v) => v
.parse()

View file

@ -2,10 +2,7 @@ use std::{borrow::Cow, fmt};
use serde::{de::DeserializeOwned, Serialize};
use crate::{
parser::{ParseError, ScalarToken},
GraphQLScalarValue,
};
use crate::parser::{ParseError, ScalarToken};
/// The result of converting a string into a scalar value
pub type ParseScalarResult<'a, S = DefaultScalarValue> = Result<S, ParseError<'a>>;
@ -16,6 +13,7 @@ pub trait ParseScalarValue<S = DefaultScalarValue> {
fn from_str(value: ScalarToken<'_>) -> ParseScalarResult<'_, S>;
}
// TODO: Revisit this doc, once `GraphQLScalarValue` macro is re-implemented.
/// A trait marking a type that could be used as internal representation of
/// scalar values in juniper
///
@ -36,9 +34,9 @@ pub trait ParseScalarValue<S = DefaultScalarValue> {
/// ```rust
/// # use std::{fmt, convert::TryInto as _};
/// # use serde::{de, Deserialize, Deserializer, Serialize};
/// # use juniper::{GraphQLScalarValue, ScalarValue};
/// # use juniper::ScalarValue;
/// #
/// #[derive(Clone, Debug, GraphQLScalarValue, PartialEq, Serialize)]
/// #[derive(Clone, Debug, PartialEq, Serialize)]
/// #[serde(untagged)]
/// enum MyScalarValue {
/// Int(i32),
@ -48,6 +46,148 @@ pub trait ParseScalarValue<S = DefaultScalarValue> {
/// Boolean(bool),
/// }
///
/// impl From<i32> for MyScalarValue {
/// fn from(v: i32) -> Self {
/// Self::Int(v)
/// }
/// }
///
/// impl From<MyScalarValue> for Option<i32> {
/// fn from(v: MyScalarValue) -> Self {
/// if let MyScalarValue::Int(v) = v {
/// Some(v)
/// } else {
/// None
/// }
/// }
/// }
///
/// impl<'a> From<&'a MyScalarValue> for Option<&'a i32> {
/// fn from(v: &'a MyScalarValue) -> Self {
/// if let MyScalarValue::Int(v) = v {
/// Some(v)
/// } else {
/// None
/// }
/// }
/// }
///
/// impl From<i64> for MyScalarValue {
/// fn from(v: i64) -> Self {
/// Self::Long(v)
/// }
/// }
///
/// impl From<MyScalarValue> for Option<i64> {
/// fn from(v: MyScalarValue) -> Self {
/// if let MyScalarValue::Long(v) = v {
/// Some(v)
/// } else {
/// None
/// }
/// }
/// }
///
/// impl<'a> From<&'a MyScalarValue> for Option<&'a i64> {
/// fn from(v: &'a MyScalarValue) -> Self {
/// if let MyScalarValue::Long(v) = v {
/// Some(v)
/// } else {
/// None
/// }
/// }
/// }
///
/// impl From<f64> for MyScalarValue {
/// fn from(v: f64) -> Self {
/// Self::Float(v)
/// }
/// }
///
/// impl From<MyScalarValue> for Option<f64> {
/// fn from(v: MyScalarValue) -> Self {
/// if let MyScalarValue::Float(v) = v {
/// Some(v)
/// } else {
/// None
/// }
/// }
/// }
///
/// impl<'a> From<&'a MyScalarValue> for Option<&'a f64> {
/// fn from(v: &'a MyScalarValue) -> Self {
/// if let MyScalarValue::Float(v) = v {
/// Some(v)
/// } else {
/// None
/// }
/// }
/// }
///
/// impl From<String> for MyScalarValue {
/// fn from(v: String) -> Self {
/// Self::String(v)
/// }
/// }
///
/// impl From<MyScalarValue> for Option<String> {
/// fn from(v: MyScalarValue) -> Self {
/// if let MyScalarValue::String(v) = v {
/// Some(v)
/// } else {
/// None
/// }
/// }
/// }
///
/// impl<'a> From<&'a MyScalarValue> for Option<&'a String> {
/// fn from(v: &'a MyScalarValue) -> Self {
/// if let MyScalarValue::String(v) = v {
/// Some(v)
/// } else {
/// None
/// }
/// }
/// }
///
/// impl From<bool> for MyScalarValue {
/// fn from(v: bool) -> Self {
/// Self::Boolean(v)
/// }
/// }
///
/// impl From<MyScalarValue> for Option<bool> {
/// fn from(v: MyScalarValue) -> Self {
/// if let MyScalarValue::Boolean(v) = v {
/// Some(v)
/// } else {
/// None
/// }
/// }
/// }
///
/// impl<'a> From<&'a MyScalarValue> for Option<&'a bool> {
/// fn from(v: &'a MyScalarValue) -> Self {
/// if let MyScalarValue::Boolean(v) = v {
/// Some(v)
/// } else {
/// None
/// }
/// }
/// }
///
/// impl fmt::Display for MyScalarValue {
/// fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
/// match self {
/// Self::Int(v) => v.fmt(f),
/// Self::Long(v) => v.fmt(f),
/// Self::Float(v) => v.fmt(f),
/// Self::String(v) => v.fmt(f),
/// Self::Boolean(v) => v.fmt(f),
/// }
/// }
/// }
///
/// impl ScalarValue for MyScalarValue {
/// fn as_int(&self) -> Option<i32> {
/// match self {
@ -266,7 +406,7 @@ pub trait ScalarValue:
/// These types closely follow the [GraphQL specification][0].
///
/// [0]: https://spec.graphql.org/June2018
#[derive(Clone, Debug, GraphQLScalarValue, PartialEq, Serialize)]
#[derive(Clone, Debug, PartialEq, Serialize)]
#[serde(untagged)]
pub enum DefaultScalarValue {
/// [`Int` scalar][0] as a signed 32bit numeric nonfractional value.
@ -293,6 +433,122 @@ pub enum DefaultScalarValue {
Boolean(bool),
}
// TODO: Revisit these impls, once `GraphQLScalarValue` macro is re-implemented.
impl From<i32> for DefaultScalarValue {
fn from(v: i32) -> Self {
Self::Int(v)
}
}
impl From<DefaultScalarValue> for Option<i32> {
fn from(v: DefaultScalarValue) -> Self {
if let DefaultScalarValue::Int(v) = v {
Some(v)
} else {
None
}
}
}
impl<'a> From<&'a DefaultScalarValue> for Option<&'a i32> {
fn from(v: &'a DefaultScalarValue) -> Self {
if let DefaultScalarValue::Int(v) = v {
Some(v)
} else {
None
}
}
}
impl From<f64> for DefaultScalarValue {
fn from(v: f64) -> Self {
Self::Float(v)
}
}
impl From<DefaultScalarValue> for Option<f64> {
fn from(v: DefaultScalarValue) -> Self {
if let DefaultScalarValue::Float(v) = v {
Some(v)
} else {
None
}
}
}
impl<'a> From<&'a DefaultScalarValue> for Option<&'a f64> {
fn from(v: &'a DefaultScalarValue) -> Self {
if let DefaultScalarValue::Float(v) = v {
Some(v)
} else {
None
}
}
}
impl From<String> for DefaultScalarValue {
fn from(v: String) -> Self {
Self::String(v)
}
}
impl From<DefaultScalarValue> for Option<String> {
fn from(v: DefaultScalarValue) -> Self {
if let DefaultScalarValue::String(v) = v {
Some(v)
} else {
None
}
}
}
impl<'a> From<&'a DefaultScalarValue> for Option<&'a String> {
fn from(v: &'a DefaultScalarValue) -> Self {
if let DefaultScalarValue::String(v) = v {
Some(v)
} else {
None
}
}
}
impl From<bool> for DefaultScalarValue {
fn from(v: bool) -> Self {
Self::Boolean(v)
}
}
impl From<DefaultScalarValue> for Option<bool> {
fn from(v: DefaultScalarValue) -> Self {
if let DefaultScalarValue::Boolean(v) = v {
Some(v)
} else {
None
}
}
}
impl<'a> From<&'a DefaultScalarValue> for Option<&'a bool> {
fn from(v: &'a DefaultScalarValue) -> Self {
if let DefaultScalarValue::Boolean(v) = v {
Some(v)
} else {
None
}
}
}
impl fmt::Display for DefaultScalarValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Int(v) => v.fmt(f),
Self::Float(v) => v.fmt(f),
Self::String(v) => v.fmt(f),
Self::Boolean(v) => v.fmt(f),
}
}
}
impl ScalarValue for DefaultScalarValue {
fn as_int(&self) -> Option<i32> {
match self {

View file

@ -159,7 +159,7 @@ impl TypeExt for syn::Type {
ty.lifetimes_iter_mut(func)
}
if let syn::ReturnType::Type(_, ty) = &mut args.output {
(&mut *ty).lifetimes_iter_mut(func)
(*ty).lifetimes_iter_mut(func)
}
}
syn::PathArguments::None => {}
@ -172,7 +172,7 @@ impl TypeExt for syn::Type {
| T::Group(syn::TypeGroup { elem, .. })
| T::Paren(syn::TypeParen { elem, .. })
| T::Ptr(syn::TypePtr { elem, .. })
| T::Slice(syn::TypeSlice { elem, .. }) => (&mut *elem).lifetimes_iter_mut(func),
| T::Slice(syn::TypeSlice { elem, .. }) => (*elem).lifetimes_iter_mut(func),
T::Tuple(syn::TypeTuple { elems, .. }) => {
for ty in elems.iter_mut() {
@ -199,7 +199,7 @@ impl TypeExt for syn::Type {
if let Some(lt) = ref_ty.lifetime.as_mut() {
func(lt)
}
(&mut *ref_ty.elem).lifetimes_iter_mut(func)
(*ref_ty.elem).lifetimes_iter_mut(func)
}
T::Path(ty) => iter_path(&mut ty.path, func),
@ -228,7 +228,7 @@ impl TypeExt for syn::Type {
fn topmost_ident(&self) -> Option<&syn::Ident> {
match self.unparenthesized() {
syn::Type::Path(p) => Some(&p.path),
syn::Type::Reference(r) => match (&*r.elem).unparenthesized() {
syn::Type::Reference(r) => match (*r.elem).unparenthesized() {
syn::Type::Path(p) => Some(&p.path),
syn::Type::TraitObject(o) => match o.bounds.iter().next().unwrap() {
syn::TypeParamBound::Trait(b) => Some(&b.path),

View file

@ -1,347 +0,0 @@
use crate::{
common::parse::ParseBufferExt as _,
result::GraphQLScope,
util::{self, span_container::SpanContainer},
};
use proc_macro2::TokenStream;
use quote::quote;
use syn::{spanned::Spanned, token, Data, Fields, Ident, Variant};
use url::Url;
#[derive(Debug, Default)]
struct TransparentAttributes {
transparent: Option<bool>,
name: Option<String>,
description: Option<String>,
specified_by_url: Option<Url>,
scalar: Option<syn::Type>,
}
impl syn::parse::Parse for TransparentAttributes {
fn parse(input: syn::parse::ParseStream) -> syn::parse::Result<Self> {
let mut output = Self {
transparent: None,
name: None,
description: None,
specified_by_url: None,
scalar: None,
};
while !input.is_empty() {
let ident: syn::Ident = input.parse()?;
match ident.to_string().as_str() {
"name" => {
input.parse::<token::Eq>()?;
let val = input.parse::<syn::LitStr>()?;
output.name = Some(val.value());
}
"description" => {
input.parse::<token::Eq>()?;
let val = input.parse::<syn::LitStr>()?;
output.description = Some(val.value());
}
"specified_by_url" => {
input.parse::<token::Eq>()?;
let val: syn::LitStr = input.parse::<syn::LitStr>()?;
output.specified_by_url =
Some(val.value().parse().map_err(|e| {
syn::Error::new(val.span(), format!("Invalid URL: {}", e))
})?);
}
"transparent" => {
output.transparent = Some(true);
}
"scalar" | "Scalar" => {
input.parse::<token::Eq>()?;
let val = input.parse::<syn::Type>()?;
output.scalar = Some(val);
}
_ => return Err(syn::Error::new(ident.span(), "unknown attribute")),
}
input.try_parse::<token::Comma>()?;
}
Ok(output)
}
}
impl TransparentAttributes {
fn from_attrs(attrs: &[syn::Attribute]) -> syn::parse::Result<Self> {
match util::find_graphql_attr(attrs) {
Some(attr) => {
let mut parsed: TransparentAttributes = attr.parse_args()?;
if parsed.description.is_none() {
parsed.description =
util::get_doc_comment(attrs).map(SpanContainer::into_inner);
}
Ok(parsed)
}
None => Ok(Default::default()),
}
}
}
pub fn impl_scalar_value(ast: &syn::DeriveInput, error: GraphQLScope) -> syn::Result<TokenStream> {
let ident = &ast.ident;
match ast.data {
Data::Enum(ref enum_data) => impl_scalar_enum(ident, enum_data, error),
Data::Struct(ref struct_data) => impl_scalar_struct(ast, struct_data, error),
Data::Union(_) => Err(error.custom_error(ast.span(), "may not be applied to unions")),
}
}
fn impl_scalar_struct(
ast: &syn::DeriveInput,
data: &syn::DataStruct,
error: GraphQLScope,
) -> syn::Result<TokenStream> {
let field = match data.fields {
syn::Fields::Unnamed(ref fields) if fields.unnamed.len() == 1 => {
fields.unnamed.first().unwrap()
}
_ => {
return Err(error.custom_error(
data.fields.span(),
"requires exact one field, e.g., Test(i32)",
))
}
};
let ident = &ast.ident;
let attrs = TransparentAttributes::from_attrs(&ast.attrs)?;
let inner_ty = &field.ty;
let name = attrs.name.unwrap_or_else(|| ident.to_string());
let description = attrs.description.map(|val| quote!(.description(#val)));
let specified_by_url = attrs.specified_by_url.map(|url| {
let url_lit = url.as_str();
quote!(.specified_by_url(#url_lit))
});
let scalar = attrs
.scalar
.as_ref()
.map(|s| quote!( #s ))
.unwrap_or_else(|| quote!(__S));
let impl_generics = attrs
.scalar
.as_ref()
.map(|_| quote!())
.unwrap_or_else(|| quote!(<__S>));
let _async = quote!(
impl#impl_generics ::juniper::GraphQLValueAsync<#scalar> for #ident
where
Self: Sync,
Self::TypeInfo: Sync,
Self::Context: Sync,
#scalar: ::juniper::ScalarValue + Send + Sync,
{
fn resolve_async<'a>(
&'a self,
info: &'a Self::TypeInfo,
selection_set: Option<&'a [::juniper::Selection<#scalar>]>,
executor: &'a ::juniper::Executor<Self::Context, #scalar>,
) -> ::juniper::BoxFuture<'a, ::juniper::ExecutionResult<#scalar>> {
use ::juniper::futures::future;
let v = ::juniper::GraphQLValue::<#scalar>::resolve(self, info, selection_set, executor);
Box::pin(future::ready(v))
}
}
);
let content = quote!(
#_async
impl#impl_generics ::juniper::GraphQLType<#scalar> for #ident
where
#scalar: ::juniper::ScalarValue,
{
fn name(_: &Self::TypeInfo) -> Option<&'static str> {
Some(#name)
}
fn meta<'r>(
info: &Self::TypeInfo,
registry: &mut ::juniper::Registry<'r, #scalar>,
) -> ::juniper::meta::MetaType<'r, #scalar>
where
#scalar: 'r,
{
registry.build_scalar_type::<Self>(info)
#description
#specified_by_url
.into_meta()
}
}
impl#impl_generics ::juniper::GraphQLValue<#scalar> for #ident
where
#scalar: ::juniper::ScalarValue,
{
type Context = ();
type TypeInfo = ();
fn type_name<'__i>(&self, info: &'__i Self::TypeInfo) -> Option<&'__i str> {
<Self as ::juniper::GraphQLType<#scalar>>::name(info)
}
fn resolve(
&self,
info: &(),
selection: Option<&[::juniper::Selection<#scalar>]>,
executor: &::juniper::Executor<Self::Context, #scalar>,
) -> ::juniper::ExecutionResult<#scalar> {
::juniper::GraphQLValue::<#scalar>::resolve(&self.0, info, selection, executor)
}
}
impl#impl_generics ::juniper::ToInputValue<#scalar> for #ident
where
#scalar: ::juniper::ScalarValue,
{
fn to_input_value(&self) -> ::juniper::InputValue<#scalar> {
::juniper::ToInputValue::<#scalar>::to_input_value(&self.0)
}
}
impl#impl_generics ::juniper::FromInputValue<#scalar> for #ident
where
#scalar: ::juniper::ScalarValue,
{
type Error = <#inner_ty as ::juniper::FromInputValue<#scalar>>::Error;
fn from_input_value(
v: &::juniper::InputValue<#scalar>
) -> Result<#ident, <#inner_ty as ::juniper::FromInputValue<#scalar>>::Error> {
let inner: #inner_ty = ::juniper::FromInputValue::<#scalar>::from_input_value(v)?;
Ok(#ident(inner))
}
}
impl#impl_generics ::juniper::ParseScalarValue<#scalar> for #ident
where
#scalar: ::juniper::ScalarValue,
{
fn from_str<'a>(
value: ::juniper::parser::ScalarToken<'a>,
) -> ::juniper::ParseScalarResult<'a, #scalar> {
<#inner_ty as ::juniper::ParseScalarValue<#scalar>>::from_str(value)
}
}
impl#impl_generics ::juniper::marker::IsOutputType<#scalar> for #ident
where #scalar: ::juniper::ScalarValue,
{ }
impl#impl_generics ::juniper::marker::IsInputType<#scalar> for #ident
where #scalar: ::juniper::ScalarValue,
{ }
impl#impl_generics ::juniper::macros::reflect::BaseType<#scalar> for #ident
where #scalar: ::juniper::ScalarValue,
{
const NAME: ::juniper::macros::reflect::Type = #name;
}
impl#impl_generics ::juniper::macros::reflect::BaseSubTypes<#scalar> for #ident
where #scalar: ::juniper::ScalarValue,
{
const NAMES: ::juniper::macros::reflect::Types =
&[<Self as ::juniper::macros::reflect::BaseType<#scalar>>::NAME];
}
impl#impl_generics ::juniper::macros::reflect::WrappedType<#scalar> for #ident
where #scalar: ::juniper::ScalarValue,
{
const VALUE: ::juniper::macros::reflect::WrappedValue = 1;
}
);
Ok(content)
}
fn impl_scalar_enum(
ident: &syn::Ident,
data: &syn::DataEnum,
error: GraphQLScope,
) -> syn::Result<TokenStream> {
let froms = data
.variants
.iter()
.map(|v| derive_from_variant(v, ident, &error))
.collect::<Result<Vec<_>, _>>()?;
let display = derive_display(data.variants.iter(), ident);
Ok(quote! {
#(#froms)*
#display
})
}
fn derive_display<'a, I>(variants: I, ident: &Ident) -> TokenStream
where
I: Iterator<Item = &'a Variant>,
{
let arms = variants.map(|v| {
let variant = &v.ident;
quote!(#ident::#variant(ref v) => write!(f, "{}", v),)
});
quote! {
impl std::fmt::Display for #ident {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match *self {
#(#arms)*
}
}
}
}
}
fn derive_from_variant(
variant: &Variant,
ident: &Ident,
error: &GraphQLScope,
) -> syn::Result<TokenStream> {
let ty = match variant.fields {
Fields::Unnamed(ref u) if u.unnamed.len() == 1 => &u.unnamed.first().unwrap().ty,
_ => {
return Err(error.custom_error(
variant.fields.span(),
"requires exact one field, e.g., Test(i32)",
))
}
};
let variant = &variant.ident;
Ok(quote! {
impl ::std::convert::From<#ty> for #ident {
fn from(t: #ty) -> Self {
#ident::#variant(t)
}
}
impl<'a> ::std::convert::From<&'a #ident> for std::option::Option<&'a #ty> {
fn from(t: &'a #ident) -> Self {
match *t {
#ident::#variant(ref t) => std::option::Option::Some(t),
_ => std::option::Option::None
}
}
}
impl ::std::convert::From<#ident> for std::option::Option<#ty> {
fn from(t: #ident) -> Self {
match t {
#ident::#variant(t) => std::option::Option::Some(t),
_ => std::option::Option::None
}
}
}
})
}

View file

@ -0,0 +1,154 @@
//! Code generation for `#[graphql_scalar]` macro.
use proc_macro2::{Span, TokenStream};
use quote::{quote, ToTokens};
use syn::{parse_quote, spanned::Spanned};
use crate::{
common::{parse, scalar},
graphql_scalar::TypeOrIdent,
GraphQLScope,
};
use super::{Attr, Definition, GraphQLScalarMethods, ParseToken};
const ERR: GraphQLScope = GraphQLScope::ScalarAttr;
/// Expands `#[graphql_scalar]` macro into generated code.
pub(crate) fn expand(attr_args: TokenStream, body: TokenStream) -> syn::Result<TokenStream> {
if let Ok(mut ast) = syn::parse2::<syn::ItemType>(body.clone()) {
let attrs = parse::attr::unite(("graphql_scalar", &attr_args), &ast.attrs);
ast.attrs = parse::attr::strip("graphql_scalar", ast.attrs);
return expand_on_type_alias(attrs, ast);
} else if let Ok(mut ast) = syn::parse2::<syn::DeriveInput>(body) {
let attrs = parse::attr::unite(("graphql_scalar", &attr_args), &ast.attrs);
ast.attrs = parse::attr::strip("graphql_scalar", ast.attrs);
return expand_on_derive_input(attrs, ast);
}
Err(syn::Error::new(
Span::call_site(),
"#[graphql_scalar] attribute is applicable to type aliases, structs, \
enums and unions only",
))
}
/// Expands `#[graphql_scalar]` macro placed on a type alias.
fn expand_on_type_alias(
attrs: Vec<syn::Attribute>,
ast: syn::ItemType,
) -> 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, 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 scalar = scalar::Type::parse(attr.scalar.as_deref(), &ast.generics);
let def = Definition {
ty: TypeOrIdent::Type(ast.ty.clone()),
where_clause: attr
.where_clause
.map_or_else(Vec::new, |cl| cl.into_inner()),
generics: ast.generics.clone(),
methods: field,
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();
Ok(quote! {
#ast
#def
})
}
// TODO: Support `#[graphql(transparent)]`.
/// Expands `#[graphql_scalar]` macro placed on a struct/enum/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 scalar = scalar::Type::parse(attr.scalar.as_deref(), &ast.generics);
let def = 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: field,
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();
Ok(quote! {
#ast
#def
})
}

View file

@ -0,0 +1,888 @@
//! Code generation for [GraphQL scalar][1].
//!
//! [1]: https://spec.graphql.org/October2021#sec-Scalars
use proc_macro2::{Literal, TokenStream};
use quote::{format_ident, quote, ToTokens, TokenStreamExt};
use syn::{
ext::IdentExt as _,
parse::{Parse, ParseStream},
parse_quote,
spanned::Spanned as _,
token,
visit_mut::VisitMut,
};
use url::Url;
use crate::{
common::{
parse::{
attr::{err, OptionExt as _},
ParseBufferExt as _,
},
scalar,
},
util::{filter_attrs, get_doc_comment, span_container::SpanContainer},
};
pub mod attr;
/// Available arguments behind `#[graphql]`/`#[graphql_scalar]` attributes when
/// generating code for [GraphQL scalar][1].
///
/// [1]: https://spec.graphql.org/October2021#sec-Scalars
#[derive(Debug, Default)]
struct Attr {
/// Name of this [GraphQL scalar][1] in GraphQL schema.
///
/// [1]: https://spec.graphql.org/October2021#sec-Scalars
name: Option<SpanContainer<String>>,
/// Description of this [GraphQL scalar][1] to put into GraphQL schema.
///
/// [1]: https://spec.graphql.org/October2021#sec-Scalars
description: Option<SpanContainer<String>>,
/// Spec [`Url`] of this [GraphQL scalar][1] to put into GraphQL schema.
///
/// [1]: https://spec.graphql.org/October2021#sec-Scalars
specified_by_url: Option<SpanContainer<Url>>,
/// Explicitly specified type (or type parameter with its bounds) of
/// [`ScalarValue`] to use for resolving this [GraphQL scalar][1] type with.
///
/// If [`None`], then generated code will be generic over any
/// [`ScalarValue`] type, which, in turn, requires all [scalar][1] fields to
/// be generic over any [`ScalarValue`] type too. That's why this type
/// should be specified only if one of the variants implements
/// [`GraphQLType`] in a non-generic way over [`ScalarValue`] type.
///
/// [`GraphQLType`]: juniper::GraphQLType
/// [`ScalarValue`]: juniper::ScalarValue
/// [1]: https://spec.graphql.org/October2021#sec-Scalars
scalar: Option<SpanContainer<scalar::AttrValue>>,
/// Explicitly specified function to be used as
/// [`ToInputValue::to_input_value`] implementation.
///
/// [`ToInputValue::to_input_value`]: juniper::ToInputValue::to_input_value
to_output: Option<SpanContainer<syn::ExprPath>>,
/// Explicitly specified function to be used as
/// [`FromInputValue::from_input_value`] implementation.
///
/// [`FromInputValue::from_input_value`]: juniper::FromInputValue::from_input_value
from_input: Option<SpanContainer<syn::ExprPath>>,
/// Explicitly specified resolver to be used as
/// [`ParseScalarValue::from_str`] implementation.
///
/// [`ParseScalarValue::from_str`]: juniper::ParseScalarValue::from_str
parse_token: Option<SpanContainer<ParseToken>>,
/// Explicitly specified module with all custom resolvers for
/// [`Self::to_output`], [`Self::from_input`] and [`Self::parse_token`].
with: Option<SpanContainer<syn::ExprPath>>,
/// Explicit where clause added to [`syn::WhereClause`].
where_clause: Option<SpanContainer<Vec<syn::WherePredicate>>>,
}
impl Parse for Attr {
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
let mut out = Self::default();
while !input.is_empty() {
let ident = input.parse_any_ident()?;
match ident.to_string().as_str() {
"name" => {
input.parse::<token::Eq>()?;
let name = input.parse::<syn::LitStr>()?;
out.name
.replace(SpanContainer::new(
ident.span(),
Some(name.span()),
name.value(),
))
.none_or_else(|_| err::dup_arg(&ident))?
}
"desc" | "description" => {
input.parse::<token::Eq>()?;
let desc = input.parse::<syn::LitStr>()?;
out.description
.replace(SpanContainer::new(
ident.span(),
Some(desc.span()),
desc.value(),
))
.none_or_else(|_| err::dup_arg(&ident))?
}
"specified_by_url" => {
input.parse::<token::Eq>()?;
let lit = input.parse::<syn::LitStr>()?;
let url = lit.value().parse::<Url>().map_err(|err| {
syn::Error::new(lit.span(), format!("Invalid URL: {}", err))
})?;
out.specified_by_url
.replace(SpanContainer::new(ident.span(), Some(lit.span()), url))
.none_or_else(|_| err::dup_arg(&ident))?
}
"scalar" | "Scalar" | "ScalarValue" => {
input.parse::<token::Eq>()?;
let scl = input.parse::<scalar::AttrValue>()?;
out.scalar
.replace(SpanContainer::new(ident.span(), Some(scl.span()), scl))
.none_or_else(|_| err::dup_arg(&ident))?
}
"to_output_with" => {
input.parse::<token::Eq>()?;
let scl = input.parse::<syn::ExprPath>()?;
out.to_output
.replace(SpanContainer::new(ident.span(), Some(scl.span()), scl))
.none_or_else(|_| err::dup_arg(&ident))?
}
"from_input_with" => {
input.parse::<token::Eq>()?;
let scl = input.parse::<syn::ExprPath>()?;
out.from_input
.replace(SpanContainer::new(ident.span(), Some(scl.span()), scl))
.none_or_else(|_| err::dup_arg(&ident))?
}
"parse_token_with" => {
input.parse::<token::Eq>()?;
let scl = input.parse::<syn::ExprPath>()?;
out.parse_token
.replace(SpanContainer::new(
ident.span(),
Some(scl.span()),
ParseToken::Custom(scl),
))
.none_or_else(|_| err::dup_arg(&ident))?
}
"parse_token" => {
let types;
let _ = syn::parenthesized!(types in input);
let parsed_types =
types.parse_terminated::<_, token::Comma>(syn::Type::parse)?;
if parsed_types.is_empty() {
return Err(syn::Error::new(ident.span(), "expected at least 1 type."));
}
out.parse_token
.replace(SpanContainer::new(
ident.span(),
Some(parsed_types.span()),
ParseToken::Delegated(parsed_types.into_iter().collect()),
))
.none_or_else(|_| err::dup_arg(&ident))?
}
"with" => {
input.parse::<token::Eq>()?;
let scl = input.parse::<syn::ExprPath>()?;
out.with
.replace(SpanContainer::new(ident.span(), Some(scl.span()), scl))
.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 predicates;
let _ = syn::parenthesized!(predicates in input);
let parsed_predicates = predicates
.parse_terminated::<_, token::Comma>(syn::WherePredicate::parse)?;
if parsed_predicates.is_empty() {
return Err(syn::Error::new(
ident.span(),
"expected at least 1 where predicate.",
));
}
(
parsed_predicates.span(),
parsed_predicates.into_iter().collect(),
)
};
out.where_clause
.replace(SpanContainer::new(
ident.span(),
Some(span),
parsed_predicates,
))
.none_or_else(|_| err::dup_arg(&ident))?
}
name => {
return Err(err::unknown_arg(&ident, name));
}
}
input.try_parse::<token::Comma>()?;
}
Ok(out)
}
}
impl Attr {
/// Tries to merge two [`Attr`]s into a single one, reporting about
/// duplicates, if any.
fn try_merge(self, mut another: Self) -> syn::Result<Self> {
Ok(Self {
name: try_merge_opt!(name: self, another),
description: try_merge_opt!(description: self, another),
specified_by_url: try_merge_opt!(specified_by_url: self, another),
scalar: try_merge_opt!(scalar: self, another),
to_output: try_merge_opt!(to_output: self, another),
from_input: try_merge_opt!(from_input: self, another),
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),
})
}
/// Parses [`Attr`] from the given multiple `name`d [`syn::Attribute`]s
/// placed on a trait definition.
fn from_attrs(name: &str, attrs: &[syn::Attribute]) -> syn::Result<Self> {
let mut attr = filter_attrs(name, attrs)
.map(|attr| attr.parse_args())
.try_fold(Self::default(), |prev, curr| prev.try_merge(curr?))?;
if attr.description.is_none() {
attr.description = get_doc_comment(attrs);
}
Ok(attr)
}
}
/// [`syn::Type`] in case of `#[graphql_scalar]` or [`syn::Ident`] in case of
/// `#[derive(GraphQLScalar)]`.
#[derive(Clone)]
enum TypeOrIdent {
/// [`syn::Type`].
Type(Box<syn::Type>),
/// [`syn::Ident`].
Ident(syn::Ident),
}
/// Definition of [GraphQL scalar][1] for code generation.
///
/// [1]: https://spec.graphql.org/October2021#sec-Scalars
struct Definition {
/// Name of this [GraphQL scalar][1] in GraphQL schema.
///
/// [1]: https://spec.graphql.org/October2021#sec-Scalars
name: String,
/// [`TypeOrIdent`] of this [GraphQL scalar][1] in GraphQL schema.
///
/// [1]: https://spec.graphql.org/October2021#sec-Scalars
ty: TypeOrIdent,
/// Additional [`Self::generics`] [`syn::WhereClause`] predicates.
where_clause: Vec<syn::WherePredicate>,
/// Generics of the Rust type that this [GraphQL scalar][1] is implemented
/// for.
///
/// [1]: https://spec.graphql.org/October2021#sec-Scalars
generics: syn::Generics,
/// [`GraphQLScalarMethods`] representing [GraphQL scalar][1].
///
/// [1]: https://spec.graphql.org/October2021#sec-Scalars
methods: GraphQLScalarMethods,
/// Description of this [GraphQL scalar][1] to put into GraphQL schema.
///
/// [1]: https://spec.graphql.org/October2021#sec-Scalars
description: Option<String>,
/// Spec [`Url`] of this [GraphQL scalar][1] to put into GraphQL schema.
///
/// [1]: https://spec.graphql.org/October2021#sec-Scalars
specified_by_url: Option<Url>,
/// [`ScalarValue`] parametrization to generate [`GraphQLType`]
/// implementation with for this [GraphQL scalar][1].
///
/// [`GraphQLType`]: juniper::GraphQLType
/// [`ScalarValue`]: juniper::ScalarValue
/// [1]: https://spec.graphql.org/October2021#sec-Scalars
scalar: scalar::Type,
}
impl ToTokens for Definition {
fn to_tokens(&self, into: &mut TokenStream) {
self.impl_output_and_input_type_tokens().to_tokens(into);
self.impl_type_tokens().to_tokens(into);
self.impl_value_tokens().to_tokens(into);
self.impl_value_async_tokens().to_tokens(into);
self.impl_to_input_value_tokens().to_tokens(into);
self.impl_from_input_value_tokens().to_tokens(into);
self.impl_parse_scalar_value_tokens().to_tokens(into);
self.impl_reflection_traits_tokens().to_tokens(into);
}
}
impl Definition {
/// Returns generated code implementing [`marker::IsInputType`] and
/// [`marker::IsOutputType`] trait for this [GraphQL scalar][1].
///
/// [`marker::IsInputType`]: juniper::marker::IsInputType
/// [`marker::IsOutputType`]: juniper::marker::IsOutputType
/// [1]: https://spec.graphql.org/October2021#sec-Scalars
#[must_use]
fn impl_output_and_input_type_tokens(&self) -> TokenStream {
let scalar = &self.scalar;
let (ty, generics) = self.impl_self_and_generics(false);
let (impl_gens, _, where_clause) = generics.split_for_impl();
quote! {
#[automatically_derived]
impl#impl_gens ::juniper::marker::IsInputType<#scalar> for #ty
#where_clause { }
#[automatically_derived]
impl#impl_gens ::juniper::marker::IsOutputType<#scalar> for #ty
#where_clause { }
}
}
/// Returns generated code implementing [`GraphQLType`] trait for this
/// [GraphQL scalar][1].
///
/// [`GraphQLType`]: juniper::GraphQLType
/// [1]: https://spec.graphql.org/October2021#sec-Scalars
fn impl_type_tokens(&self) -> TokenStream {
let scalar = &self.scalar;
let name = &self.name;
let description = self
.description
.as_ref()
.map(|val| quote! { .description(#val) });
let specified_by_url = self.specified_by_url.as_ref().map(|url| {
let url_lit = url.as_str();
quote! { .specified_by_url(#url_lit) }
});
let (ty, generics) = self.impl_self_and_generics(false);
let (impl_gens, _, where_clause) = generics.split_for_impl();
quote! {
#[automatically_derived]
impl#impl_gens ::juniper::GraphQLType<#scalar> for #ty
#where_clause
{
fn name(_: &Self::TypeInfo) -> Option<&'static str> {
Some(#name)
}
fn meta<'r>(
info: &Self::TypeInfo,
registry: &mut ::juniper::Registry<'r, #scalar>,
) -> ::juniper::meta::MetaType<'r, #scalar>
where
#scalar: 'r,
{
registry.build_scalar_type::<Self>(info)
#description
#specified_by_url
.into_meta()
}
}
}
}
/// Returns generated code implementing [`GraphQLValue`] trait for this
/// [GraphQL scalar][1].
///
/// [`GraphQLValue`]: juniper::GraphQLValue
/// [1]: https://spec.graphql.org/October2021#sec-Scalars
fn impl_value_tokens(&self) -> TokenStream {
let scalar = &self.scalar;
let resolve = self.methods.expand_resolve(scalar);
let (ty, generics) = self.impl_self_and_generics(false);
let (impl_gens, _, where_clause) = generics.split_for_impl();
quote! {
#[automatically_derived]
impl#impl_gens ::juniper::GraphQLValue<#scalar> for #ty
#where_clause
{
type Context = ();
type TypeInfo = ();
fn type_name<'i>(&self, info: &'i Self::TypeInfo) -> Option<&'i str> {
<Self as ::juniper::GraphQLType<#scalar>>::name(info)
}
fn resolve(
&self,
info: &(),
selection: Option<&[::juniper::Selection<#scalar>]>,
executor: &::juniper::Executor<Self::Context, #scalar>,
) -> ::juniper::ExecutionResult<#scalar> {
#resolve
}
}
}
}
/// Returns generated code implementing [`GraphQLValueAsync`] trait for this
/// [GraphQL scalar][1].
///
/// [`GraphQLValueAsync`]: juniper::GraphQLValueAsync
/// [1]: https://spec.graphql.org/October2021#sec-Scalars
fn impl_value_async_tokens(&self) -> TokenStream {
let scalar = &self.scalar;
let (ty, generics) = self.impl_self_and_generics(true);
let (impl_gens, _, where_clause) = generics.split_for_impl();
quote! {
#[automatically_derived]
impl#impl_gens ::juniper::GraphQLValueAsync<#scalar> for #ty
#where_clause
{
fn resolve_async<'b>(
&'b self,
info: &'b Self::TypeInfo,
selection_set: Option<&'b [::juniper::Selection<#scalar>]>,
executor: &'b ::juniper::Executor<Self::Context, #scalar>,
) -> ::juniper::BoxFuture<'b, ::juniper::ExecutionResult<#scalar>> {
use ::juniper::futures::future;
let v = ::juniper::GraphQLValue::resolve(self, info, selection_set, executor);
Box::pin(future::ready(v))
}
}
}
}
/// Returns generated code implementing [`InputValue`] trait for this
/// [GraphQL scalar][1].
///
/// [`InputValue`]: juniper::InputValue
/// [1]: https://spec.graphql.org/October2021#sec-Scalars
fn impl_to_input_value_tokens(&self) -> TokenStream {
let scalar = &self.scalar;
let to_input_value = self.methods.expand_to_input_value(scalar);
let (ty, generics) = self.impl_self_and_generics(false);
let (impl_gens, _, where_clause) = generics.split_for_impl();
quote! {
#[automatically_derived]
impl#impl_gens ::juniper::ToInputValue<#scalar> for #ty
#where_clause
{
fn to_input_value(&self) -> ::juniper::InputValue<#scalar> {
#to_input_value
}
}
}
}
/// Returns generated code implementing [`FromInputValue`] trait for this
/// [GraphQL scalar][1].
///
/// [`FromInputValue`]: juniper::FromInputValue
/// [1]: https://spec.graphql.org/October2021#sec-Scalars
fn impl_from_input_value_tokens(&self) -> TokenStream {
let scalar = &self.scalar;
let from_input_value = self.methods.expand_from_input_value(scalar);
let (ty, generics) = self.impl_self_and_generics(false);
let (impl_gens, _, where_clause) = generics.split_for_impl();
quote! {
#[automatically_derived]
impl#impl_gens ::juniper::FromInputValue<#scalar> for #ty
#where_clause
{
type Error = ::juniper::executor::FieldError<#scalar>;
fn from_input_value(input: &::juniper::InputValue<#scalar>) -> Result<Self, Self::Error> {
#from_input_value
.map_err(::juniper::executor::IntoFieldError::<#scalar>::into_field_error)
}
}
}
}
/// Returns generated code implementing [`ParseScalarValue`] trait for this
/// [GraphQL scalar][1].
///
/// [`ParseScalarValue`]: juniper::ParseScalarValue
/// [1]: https://spec.graphql.org/October2021#sec-Scalars
fn impl_parse_scalar_value_tokens(&self) -> TokenStream {
let scalar = &self.scalar;
let from_str = self.methods.expand_parse_scalar_value(scalar);
let (ty, generics) = self.impl_self_and_generics(false);
let (impl_gens, _, where_clause) = generics.split_for_impl();
quote! {
#[automatically_derived]
impl#impl_gens ::juniper::ParseScalarValue<#scalar> for #ty
#where_clause
{
fn from_str(
token: ::juniper::parser::ScalarToken<'_>,
) -> ::juniper::ParseScalarResult<'_, #scalar> {
#from_str
}
}
}
}
/// Returns generated code implementing [`BaseType`], [`BaseSubTypes`] and
/// [`WrappedType`] traits for this [GraphQL scalar][1].
///
/// [`BaseSubTypes`]: juniper::macros::reflection::BaseSubTypes
/// [`BaseType`]: juniper::macros::reflection::BaseType
/// [`WrappedType`]: juniper::macros::reflection::WrappedType
/// [1]: https://spec.graphql.org/October2021#sec-Scalars
fn impl_reflection_traits_tokens(&self) -> TokenStream {
let scalar = &self.scalar;
let name = &self.name;
let (ty, generics) = self.impl_self_and_generics(false);
let (impl_gens, _, where_clause) = generics.split_for_impl();
quote! {
#[automatically_derived]
impl#impl_gens ::juniper::macros::reflect::BaseType<#scalar> for #ty
#where_clause
{
const NAME: ::juniper::macros::reflect::Type = #name;
}
#[automatically_derived]
impl#impl_gens ::juniper::macros::reflect::BaseSubTypes<#scalar> for #ty
#where_clause
{
const NAMES: ::juniper::macros::reflect::Types =
&[<Self as ::juniper::macros::reflect::BaseType<#scalar>>::NAME];
}
#[automatically_derived]
impl#impl_gens ::juniper::macros::reflect::WrappedType<#scalar> for #ty
#where_clause
{
const VALUE: ::juniper::macros::reflect::WrappedValue = 1;
}
}
}
/// Returns prepared self type and [`syn::Generics`] for [`GraphQLType`]
/// trait (and similar) implementation.
///
/// If `for_async` is `true`, then additional predicates are added to suit
/// the [`GraphQLAsyncValue`] trait (and similar) requirements.
///
/// [`GraphQLAsyncValue`]: juniper::GraphQLAsyncValue
/// [`GraphQLType`]: juniper::GraphQLType
#[must_use]
fn impl_self_and_generics(&self, for_async: bool) -> (TokenStream, syn::Generics) {
let mut generics = self.generics.clone();
let ty = match &self.ty {
TypeOrIdent::Type(ty) => ty.into_token_stream(),
TypeOrIdent::Ident(ident) => {
let (_, ty_gen, _) = self.generics.split_for_impl();
quote! { #ident#ty_gen }
}
};
if !self.where_clause.is_empty() {
generics
.make_where_clause()
.predicates
.extend(self.where_clause.clone())
}
let scalar = &self.scalar;
if scalar.is_implicit_generic() {
generics.params.push(parse_quote! { #scalar });
}
if scalar.is_generic() {
generics
.make_where_clause()
.predicates
.push(parse_quote! { #scalar: ::juniper::ScalarValue });
}
if let Some(bound) = scalar.bounds() {
generics.make_where_clause().predicates.push(bound);
}
if for_async {
let self_ty = if self.generics.lifetimes().next().is_some() {
let mut generics = self.generics.clone();
ModifyLifetimes.visit_generics_mut(&mut generics);
let lifetimes = generics.lifetimes().map(|lt| &lt.lifetime);
let ty = match self.ty.clone() {
TypeOrIdent::Type(mut ty) => {
ModifyLifetimes.visit_type_mut(&mut ty);
ty.into_token_stream()
}
TypeOrIdent::Ident(ident) => {
let (_, ty_gens, _) = generics.split_for_impl();
quote! { #ident#ty_gens }
}
};
quote! { for<#( #lifetimes ),*> #ty }
} else {
quote! { Self }
};
generics
.make_where_clause()
.predicates
.push(parse_quote! { #self_ty: Sync });
if scalar.is_generic() {
generics
.make_where_clause()
.predicates
.push(parse_quote! { #scalar: Send + Sync });
}
}
(ty, generics)
}
}
/// Adds `__fa__` prefix to all lifetimes to avoid "lifetime name `'a` shadows a
/// lifetime name that is already in scope" error.
struct ModifyLifetimes;
impl VisitMut for ModifyLifetimes {
fn visit_lifetime_mut(&mut self, lf: &mut syn::Lifetime) {
lf.ident = format_ident!("__fa__{}", lf.ident.unraw());
}
}
/// Methods representing [GraphQL scalar][1].
///
/// [1]: https://spec.graphql.org/October2021#sec-Scalars
#[allow(dead_code)]
enum GraphQLScalarMethods {
/// [GraphQL scalar][1] represented with only custom resolvers.
///
/// [1]: https://spec.graphql.org/October2021#sec-Scalars
Custom {
/// Function provided with `#[graphql(to_output_with = ...)]`.
to_output: syn::ExprPath,
/// Function provided with `#[graphql(from_input_with = ...)]`.
from_input: syn::ExprPath,
/// [`ParseToken`] provided with `#[graphql(parse_token_with = ...)]`
/// or `#[graphql(parse_token(...))]`.
parse_token: ParseToken,
},
/// [GraphQL scalar][1] maybe partially represented with custom resolver.
/// Other methods are used from [`Field`].
///
/// [1]: https://spec.graphql.org/October2021#sec-Scalars
Delegated {
/// Function provided with `#[graphql(to_output_with = ...)]`.
to_output: Option<syn::ExprPath>,
/// Function provided with `#[graphql(from_input_with = ...)]`.
from_input: Option<syn::ExprPath>,
/// [`ParseToken`] provided with `#[graphql(parse_token_with = ...)]`
/// or `#[graphql(parse_token(...))]`.
parse_token: Option<ParseToken>,
/// [`Field`] to resolve not provided methods.
field: Box<Field>,
},
}
impl GraphQLScalarMethods {
/// Expands [`GraphQLValue::resolve`] method.
///
/// [`GraphQLValue::resolve`]: juniper::GraphQLValue::resolve
fn expand_resolve(&self, scalar: &scalar::Type) -> TokenStream {
match self {
Self::Custom { to_output, .. }
| Self::Delegated {
to_output: Some(to_output),
..
} => {
quote! { Ok(#to_output(self)) }
}
Self::Delegated { field, .. } => {
quote! {
::juniper::GraphQLValue::<#scalar>::resolve(
&self.#field,
info,
selection,
executor,
)
}
}
}
}
/// Expands [`ToInputValue::to_input_value`] method.
///
/// [`ToInputValue::to_input_value`]: juniper::ToInputValue::to_input_value
fn expand_to_input_value(&self, scalar: &scalar::Type) -> TokenStream {
match self {
Self::Custom { to_output, .. }
| Self::Delegated {
to_output: Some(to_output),
..
} => {
quote! {
let v = #to_output(self);
::juniper::ToInputValue::to_input_value(&v)
}
}
Self::Delegated { field, .. } => {
quote! { ::juniper::ToInputValue::<#scalar>::to_input_value(&self.#field) }
}
}
}
/// Expands [`FromInputValue::from_input_value`][1] method.
///
/// [1]: juniper::FromInputValue::from_input_value
fn expand_from_input_value(&self, scalar: &scalar::Type) -> TokenStream {
match self {
Self::Custom { from_input, .. }
| Self::Delegated {
from_input: Some(from_input),
..
} => {
quote! { #from_input(input) }
}
Self::Delegated { field, .. } => {
let field_ty = field.ty();
let self_constructor = field.closure_constructor();
quote! {
<#field_ty as ::juniper::FromInputValue<#scalar>>::from_input_value(input)
.map(#self_constructor)
}
}
}
}
/// Expands [`ParseScalarValue::from_str`] method.
///
/// [`ParseScalarValue::from_str`]: juniper::ParseScalarValue::from_str
fn expand_parse_scalar_value(&self, scalar: &scalar::Type) -> TokenStream {
match self {
Self::Custom { parse_token, .. }
| Self::Delegated {
parse_token: Some(parse_token),
..
} => {
let parse_token = parse_token.expand_from_str(scalar);
quote! { #parse_token }
}
Self::Delegated { field, .. } => {
let field_ty = field.ty();
quote! { <#field_ty as ::juniper::ParseScalarValue<#scalar>>::from_str(token) }
}
}
}
}
/// Representation of [`ParseScalarValue::from_str`] method.
///
/// [`ParseScalarValue::from_str`]: juniper::ParseScalarValue::from_str
#[derive(Clone, Debug)]
enum ParseToken {
/// Custom method.
Custom(syn::ExprPath),
/// Tries to parse using [`syn::Type`]s [`ParseScalarValue`] impls until
/// first success.
///
/// [`ParseScalarValue`]: juniper::ParseScalarValue
Delegated(Vec<syn::Type>),
}
impl ParseToken {
/// Expands [`ParseScalarValue::from_str`] method.
///
/// [`ParseScalarValue::from_str`]: juniper::ParseScalarValue::from_str
fn expand_from_str(&self, scalar: &scalar::Type) -> TokenStream {
match self {
Self::Custom(parse_token) => {
quote! { #parse_token(token) }
}
Self::Delegated(delegated) => delegated
.iter()
.fold(None, |acc, ty| {
acc.map_or_else(
|| Some(quote! { <#ty as ::juniper::ParseScalarValue<#scalar>>::from_str(token) }),
|prev| {
Some(quote! {
#prev.or_else(|_| {
<#ty as ::juniper::ParseScalarValue<#scalar>>::from_str(token)
})
})
}
)
})
.unwrap_or_default(),
}
}
}
/// Struct field to resolve not provided methods.
#[allow(dead_code)]
enum Field {
/// Named [`Field`].
Named(syn::Field),
/// Unnamed [`Field`].
Unnamed(syn::Field),
}
impl ToTokens for Field {
fn to_tokens(&self, tokens: &mut TokenStream) {
match self {
Self::Named(f) => f.ident.to_tokens(tokens),
Self::Unnamed(_) => tokens.append(Literal::u8_unsuffixed(0)),
}
}
}
impl Field {
/// [`syn::Type`] of this [`Field`].
fn ty(&self) -> &syn::Type {
match self {
Self::Named(f) | Self::Unnamed(f) => &f.ty,
}
}
/// Closure to construct [GraphQL scalar][1] struct from [`Field`].
///
/// [1]: https://spec.graphql.org/October2021#sec-Scalars
fn closure_constructor(&self) -> TokenStream {
match self {
Field::Named(syn::Field { ident, .. }) => {
quote! { |v| Self { #ident: v } }
}
Field::Unnamed(_) => quote! { Self },
}
}
}

View file

@ -1,352 +0,0 @@
#![allow(clippy::collapsible_if)]
use crate::{
result::GraphQLScope,
util::{self, span_container::SpanContainer},
};
use proc_macro2::TokenStream;
use quote::quote;
use syn::spanned::Spanned;
#[derive(Debug)]
struct ScalarCodegenInput {
impl_for_type: Option<syn::PathSegment>,
custom_data_type: Option<syn::PathSegment>,
custom_data_type_is_struct: bool,
resolve_body: Option<syn::Block>,
from_input_value_arg: Option<syn::Ident>,
from_input_value_body: Option<syn::Block>,
from_input_value_result: Option<syn::Type>,
from_str_arg: Option<syn::Ident>,
from_str_body: Option<syn::Block>,
from_str_result: Option<syn::Type>,
}
fn get_first_method_arg(
inputs: syn::punctuated::Punctuated<syn::FnArg, syn::Token![,]>,
) -> Option<syn::Ident> {
if let Some(syn::FnArg::Typed(pat_type)) = inputs.first() {
if let syn::Pat::Ident(pat_ident) = &*pat_type.pat {
return Some(pat_ident.ident.clone());
}
}
None
}
fn get_method_return_type(output: syn::ReturnType) -> Option<syn::Type> {
match output {
syn::ReturnType::Type(_, return_type) => Some(*return_type),
_ => None,
}
}
// Find the enum type by inspecting the type parameter on the return value
fn get_enum_type(return_type: &Option<syn::Type>) -> Option<syn::PathSegment> {
if let Some(syn::Type::Path(type_path)) = return_type {
let path_segment = type_path
.path
.segments
.iter()
.find(|ps| matches!(ps.arguments, syn::PathArguments::AngleBracketed(_)));
if let Some(path_segment) = path_segment {
if let syn::PathArguments::AngleBracketed(generic_args) = &path_segment.arguments {
let generic_type_arg = generic_args.args.iter().find(|generic_type_arg| {
matches!(generic_type_arg, syn::GenericArgument::Type(_))
});
if let Some(syn::GenericArgument::Type(syn::Type::Path(type_path))) =
generic_type_arg
{
if let Some(path_segment) = type_path.path.segments.first() {
return Some(path_segment.clone());
}
}
}
}
}
None
}
impl syn::parse::Parse for ScalarCodegenInput {
fn parse(input: syn::parse::ParseStream) -> syn::parse::Result<Self> {
let mut impl_for_type: Option<syn::PathSegment> = None;
let mut enum_data_type: Option<syn::PathSegment> = None;
let mut resolve_body: Option<syn::Block> = None;
let mut from_input_value_arg: Option<syn::Ident> = None;
let mut from_input_value_body: Option<syn::Block> = None;
let mut from_input_value_result: Option<syn::Type> = None;
let mut from_str_arg: Option<syn::Ident> = None;
let mut from_str_body: Option<syn::Block> = None;
let mut from_str_result: Option<syn::Type> = None;
let parse_custom_scalar_value_impl: syn::ItemImpl = input.parse()?;
// To implement a custom scalar for a struct, it's required to
// specify a generic type and a type bound
let custom_data_type_is_struct: bool =
!parse_custom_scalar_value_impl.generics.params.is_empty();
let mut self_ty = *parse_custom_scalar_value_impl.self_ty;
while let syn::Type::Group(type_group) = self_ty {
self_ty = *type_group.elem;
}
if let syn::Type::Path(type_path) = self_ty {
if let Some(path_segment) = type_path.path.segments.first() {
impl_for_type = Some(path_segment.clone());
}
}
for impl_item in parse_custom_scalar_value_impl.items {
if let syn::ImplItem::Method(method) = impl_item {
match method.sig.ident.to_string().as_str() {
"resolve" => {
resolve_body = Some(method.block);
}
"from_input_value" => {
from_input_value_arg = get_first_method_arg(method.sig.inputs);
from_input_value_result = get_method_return_type(method.sig.output);
from_input_value_body = Some(method.block);
}
"from_str" => {
from_str_arg = get_first_method_arg(method.sig.inputs);
from_str_result = get_method_return_type(method.sig.output);
if !custom_data_type_is_struct {
enum_data_type = get_enum_type(&from_str_result);
}
from_str_body = Some(method.block);
}
_ => (),
}
}
}
let custom_data_type = if custom_data_type_is_struct {
impl_for_type.clone()
} else {
enum_data_type
};
Ok(ScalarCodegenInput {
impl_for_type,
custom_data_type,
custom_data_type_is_struct,
resolve_body,
from_input_value_arg,
from_input_value_body,
from_input_value_result,
from_str_arg,
from_str_body,
from_str_result,
})
}
}
/// Generate code for the juniper::graphql_scalar proc macro.
pub fn build_scalar(
attributes: TokenStream,
body: TokenStream,
error: GraphQLScope,
) -> syn::Result<TokenStream> {
let body_span = body.span();
let attrs = syn::parse2::<util::FieldAttributes>(attributes)?;
let input = syn::parse2::<ScalarCodegenInput>(body)?;
let impl_for_type = input.impl_for_type.ok_or_else(|| {
error.custom_error(
body_span,
"unable to find target for implementation target for `GraphQLScalar`",
)
})?;
let custom_data_type = input
.custom_data_type
.ok_or_else(|| error.custom_error(body_span, "unable to find custom scalar data type"))?;
let resolve_body = input
.resolve_body
.ok_or_else(|| error.custom_error(body_span, "unable to find body of `resolve` method"))?;
let from_input_value_arg = input.from_input_value_arg.ok_or_else(|| {
error.custom_error(
body_span,
"unable to find argument for `from_input_value` method",
)
})?;
let from_input_value_body = input.from_input_value_body.ok_or_else(|| {
error.custom_error(
body_span,
"unable to find body of `from_input_value` method",
)
})?;
let from_input_value_result = input.from_input_value_result.ok_or_else(|| {
error.custom_error(
body_span,
"unable to find return type of `from_input_value` method",
)
})?;
let from_str_arg = input.from_str_arg.ok_or_else(|| {
error.custom_error(body_span, "unable to find argument for `from_str` method")
})?;
let from_str_body = input
.from_str_body
.ok_or_else(|| error.custom_error(body_span, "unable to find body of `from_str` method"))?;
let from_str_result = input.from_str_result.ok_or_else(|| {
error.custom_error(body_span, "unable to find return type of `from_str` method")
})?;
let name = attrs
.name
.map(SpanContainer::into_inner)
.unwrap_or_else(|| impl_for_type.ident.to_string());
let description = attrs.description.map(|val| quote!(.description(#val)));
let specified_by_url = attrs.specified_by_url.map(|url| {
let url_lit = url.as_str();
quote!(.specified_by_url(#url_lit))
});
let async_generic_type = match input.custom_data_type_is_struct {
true => quote!(__S),
_ => quote!(#custom_data_type),
};
let async_generic_type_decl = match input.custom_data_type_is_struct {
true => quote!(<#async_generic_type>),
_ => quote!(),
};
let generic_type = match input.custom_data_type_is_struct {
true => quote!(S),
_ => quote!(#custom_data_type),
};
let generic_type_decl = match input.custom_data_type_is_struct {
true => quote!(<#generic_type>),
_ => quote!(),
};
let generic_type_bound = match input.custom_data_type_is_struct {
true => quote!(where #generic_type: ::juniper::ScalarValue,),
_ => quote!(),
};
let _async = quote!(
impl#async_generic_type_decl ::juniper::GraphQLValueAsync<#async_generic_type> for #impl_for_type
where
Self: Sync,
Self::TypeInfo: Sync,
Self::Context: Sync,
#async_generic_type: ::juniper::ScalarValue + Send + Sync,
{
fn resolve_async<'a>(
&'a self,
info: &'a Self::TypeInfo,
selection_set: Option<&'a [::juniper::Selection<#async_generic_type>]>,
executor: &'a ::juniper::Executor<Self::Context, #async_generic_type>,
) -> ::juniper::BoxFuture<'a, ::juniper::ExecutionResult<#async_generic_type>> {
use ::juniper::futures::future;
let v = ::juniper::GraphQLValue::resolve(self, info, selection_set, executor);
Box::pin(future::ready(v))
}
}
);
let content = quote!(
#_async
impl#generic_type_decl ::juniper::marker::IsInputType<#generic_type> for #impl_for_type
#generic_type_bound { }
impl#generic_type_decl ::juniper::marker::IsOutputType<#generic_type> for #impl_for_type
#generic_type_bound { }
impl#generic_type_decl ::juniper::GraphQLType<#generic_type> for #impl_for_type
#generic_type_bound
{
fn name(_: &Self::TypeInfo) -> Option<&'static str> {
Some(#name)
}
fn meta<'r>(
info: &Self::TypeInfo,
registry: &mut ::juniper::Registry<'r, #generic_type>,
) -> ::juniper::meta::MetaType<'r, #generic_type>
where
#generic_type: 'r,
{
registry.build_scalar_type::<Self>(info)
#description
#specified_by_url
.into_meta()
}
}
impl#generic_type_decl ::juniper::GraphQLValue<#generic_type> for #impl_for_type
#generic_type_bound
{
type Context = ();
type TypeInfo = ();
fn type_name<'__i>(&self, info: &'__i Self::TypeInfo) -> Option<&'__i str> {
<Self as ::juniper::GraphQLType<#generic_type>>::name(info)
}
fn resolve(
&self,
info: &(),
selection: Option<&[::juniper::Selection<#generic_type>]>,
executor: &::juniper::Executor<Self::Context, #generic_type>,
) -> ::juniper::ExecutionResult<#generic_type> {
Ok(#resolve_body)
}
}
impl#generic_type_decl ::juniper::ToInputValue<#generic_type> for #impl_for_type
#generic_type_bound
{
fn to_input_value(&self) -> ::juniper::InputValue<#generic_type> {
let v = #resolve_body;
::juniper::ToInputValue::to_input_value(&v)
}
}
impl#generic_type_decl ::juniper::FromInputValue<#generic_type> for #impl_for_type
#generic_type_bound
{
type Error = <#from_input_value_result as ::juniper::macros::helper::ExtractError>::Error;
fn from_input_value(#from_input_value_arg: &::juniper::InputValue<#generic_type>) -> #from_input_value_result {
#from_input_value_body
}
}
impl#generic_type_decl ::juniper::ParseScalarValue<#generic_type> for #impl_for_type
#generic_type_bound
{
fn from_str<'a>(
#from_str_arg: ::juniper::parser::ScalarToken<'a>,
) -> #from_str_result {
#from_str_body
}
}
impl#generic_type_decl ::juniper::macros::reflect::BaseType<#generic_type> for #impl_for_type
#generic_type_bound
{
const NAME: ::juniper::macros::reflect::Type = #name;
}
impl#generic_type_decl ::juniper::macros::reflect::BaseSubTypes<#generic_type> for #impl_for_type
#generic_type_bound
{
const NAMES: ::juniper::macros::reflect::Types =
&[<Self as ::juniper::macros::reflect::BaseType<#generic_type>>::NAME];
}
impl#generic_type_decl ::juniper::macros::reflect::WrappedType<#generic_type> for #impl_for_type
#generic_type_bound
{
const VALUE: ::juniper::macros::reflect::WrappedValue = 1;
}
);
Ok(content)
}

View file

@ -108,12 +108,11 @@ macro_rules! try_merge_hashset {
mod derive_enum;
mod derive_input_object;
mod derive_scalar_value;
mod impl_scalar;
mod common;
mod graphql_interface;
mod graphql_object;
mod graphql_scalar;
mod graphql_subscription;
mod graphql_union;
@ -143,126 +142,116 @@ pub fn derive_input_object(input: TokenStream) -> TokenStream {
}
}
/// This custom derive macro implements the #[derive(GraphQLScalarValue)]
/// derive.
/// `#[graphql_scalar]` is interchangeable with `#[derive(`[`GraphQLScalar`]`)]`
/// macro:
///
/// This can be used for two purposes.
///
/// ## Transparent Newtype Wrapper
///
/// Sometimes, you want to create a custerm scalar type by wrapping
/// an existing type. In Rust, this is often called the "newtype" pattern.
/// Thanks to this custom derive, this becomes really easy:
///
/// ```rust
/// // Deriving GraphQLScalar is all that is required.
/// #[derive(juniper::GraphQLScalarValue)]
/// struct UserId(String);
///
/// #[derive(juniper::GraphQLObject)]
/// struct User {
/// id: UserId,
/// }
/// ```
///
/// The type can also be customized.
///
/// ```rust
/// ```rust,ignore
/// /// Doc comments are used for the GraphQL type description.
/// #[derive(juniper::GraphQLScalarValue)]
/// #[derive(juniper::GraphQLScalar)]
/// #[graphql(
/// transparent,
/// // Set a custom GraphQL name.
/// name= "MyUserId",
/// // A description can also specified in the attribute.
/// // This will the doc comment, if one exists.
/// description = "...",
/// // A specification URL.
/// specified_by_url = "https://tools.ietf.org/html/rfc4122",
/// // Set a custom GraphQL name.
/// name = "MyUserId",
/// // A description can also specified in the attribute.
/// // This will the doc comment, if one exists.
/// description = "...",
/// // A specification URL.
/// specified_by_url = "https://tools.ietf.org/html/rfc4122",
/// // Explicit generic scalar.
/// scalar = S: juniper::ScalarValue,
/// transparent,
/// )]
/// struct UserId(String);
/// ```
///
/// ### Base ScalarValue Enum
/// Is transformed into:
///
/// TODO: write documentation.
/// ```rust,ignore
/// /// Doc comments are used for the GraphQL type description.
/// #[juniper::graphql_scalar(
/// // Set a custom GraphQL name.
/// name = "MyUserId",
/// // A description can also specified in the attribute.
/// // This will the doc comment, if one exists.
/// description = "...",
/// // A specification URL.
/// specified_by_url = "https://tools.ietf.org/html/rfc4122",
/// // Explicit generic scalar.
/// scalar = S: juniper::ScalarValue,
/// transparent,
/// )]
/// struct UserId(String);
/// ```
///
#[proc_macro_error]
#[proc_macro_derive(GraphQLScalarValue, attributes(graphql))]
pub fn derive_scalar_value(input: TokenStream) -> TokenStream {
let ast = syn::parse::<syn::DeriveInput>(input).unwrap();
let gen = derive_scalar_value::impl_scalar_value(&ast, GraphQLScope::DeriveScalar);
match gen {
Ok(gen) => gen.into(),
Err(err) => proc_macro_error::abort!(err),
}
}
/// Expose GraphQL scalars
/// 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.
///
/// The GraphQL language defines a number of built-in scalars: strings, numbers, and
/// booleans. This macro can be used either to define new types of scalars (e.g.
/// timestamps), or expose other types as one of the built-in scalars (e.g. bigints
/// as numbers or strings).
/// All attributes are mirroring [`GraphQLScalar`] derive macro.
///
/// Since the preferred transport protocol for GraphQL responses is JSON, most
/// custom scalars will be transferred as strings. You therefore need to ensure that
/// the client library you are sending data to can parse the custom value into a
/// datatype appropriate for that platform.
///
/// By default the trait is implemented in terms of the default scalar value
/// representation provided by juniper. If that does not fit your needs it is
/// possible to specify a custom representation.
/// > __NOTE:__ To satisfy [orphan rules] you should provide local
/// > [`ScalarValue`] implementation.
///
/// ```rust
/// // The data type
/// struct UserID(String);
/// # mod date {
/// # pub struct Date;
/// #
/// # impl std::str::FromStr for Date {
/// # type Err = String;
/// #
/// # fn from_str(_value: &str) -> Result<Self, Self::Err> {
/// # unimplemented!()
/// # }
/// # }
/// #
/// # impl std::fmt::Display for Date {
/// # fn fmt(&self, _f: &mut std::fmt::Formatter) -> std::fmt::Result {
/// # unimplemented!()
/// # }
/// # }
/// # }
/// #
/// # use juniper::DefaultScalarValue as CustomScalarValue;
/// use juniper::{graphql_scalar, InputValue, ScalarValue, Value};
///
/// #[juniper::graphql_scalar(
/// // You can rename the type for GraphQL by specifying the name here.
/// name = "MyName",
/// // You can also specify a description here.
/// // If present, doc comments will be ignored.
/// description = "An opaque identifier, represented as a string",
/// // A specification URL.
/// specified_by_url = "https://tools.ietf.org/html/rfc4122",
/// #[graphql_scalar(
/// with = date_scalar,
/// parse_token(String),
/// scalar = CustomScalarValue,
/// // ^^^^^^^^^^^^^^^^^ Local `ScalarValue` implementation.
/// )]
/// impl<S> GraphQLScalar for UserID
/// where
/// S: juniper::ScalarValue
/// {
/// fn resolve(&self) -> juniper::Value {
/// juniper::Value::scalar(self.0.to_owned())
/// type Date = date::Date;
/// // ^^^^^^^^^^ 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>`.
/// fn from_input_value(value: &juniper::InputValue) -> Result<UserID, String> {
/// value.as_string_value()
/// .map(|s| UserID(s.to_owned()))
/// .ok_or_else(|| format!("Expected `String`, found: {}", value))
/// }
///
/// fn from_str<'a>(value: juniper::ScalarToken<'a>) -> juniper::ParseScalarResult<'a, S> {
/// <String as juniper::ParseScalarValue<S>>::from_str(value)
/// pub(super) fn from_input(v: &InputValue<CustomScalarValue>) -> Result<Date, String> {
/// v.as_string_value()
/// .ok_or_else(|| format!("Expected `String`, found: {}", v))
/// .and_then(|s| s.parse().map_err(|e| format!("Failed to parse `Date`: {}", e)))
/// }
/// }
///
/// #
/// # fn main() { }
/// ```
///
/// In addition to implementing `GraphQLType` for the type in question,
/// `FromInputValue` and `ToInputValue` is also implemented. This makes the type
/// usable as arguments and default values.
/// [orphan rules]: https://bit.ly/3glAGC2
/// [`GraphQLScalar`]: juniper::GraphQLScalar
/// [`ScalarValue`]: juniper::ScalarValue
#[proc_macro_error]
#[proc_macro_attribute]
pub fn graphql_scalar(args: TokenStream, input: TokenStream) -> TokenStream {
let args = proc_macro2::TokenStream::from(args);
let input = proc_macro2::TokenStream::from(input);
let gen = impl_scalar::build_scalar(args, input, GraphQLScope::ImplScalar);
match gen {
Ok(gen) => gen.into(),
Err(err) => proc_macro_error::abort!(err),
}
pub fn graphql_scalar(attr: TokenStream, body: TokenStream) -> TokenStream {
graphql_scalar::attr::expand(attr.into(), body.into())
.unwrap_or_abort()
.into()
}
/// `#[graphql_interface]` macro for generating a [GraphQL interface][1]

View file

@ -13,12 +13,13 @@ pub enum GraphQLScope {
InterfaceAttr,
ObjectAttr,
ObjectDerive,
ScalarAttr,
#[allow(dead_code)]
ScalarDerive,
UnionAttr,
UnionDerive,
DeriveInputObject,
DeriveEnum,
DeriveScalar,
ImplScalar,
}
impl GraphQLScope {
@ -26,10 +27,10 @@ impl GraphQLScope {
match self {
Self::InterfaceAttr => "#sec-Interfaces",
Self::ObjectAttr | Self::ObjectDerive => "#sec-Objects",
Self::ScalarAttr | Self::ScalarDerive => "#sec-Scalars",
Self::UnionAttr | Self::UnionDerive => "#sec-Unions",
Self::DeriveInputObject => "#sec-Input-Objects",
Self::DeriveEnum => "#sec-Enums",
Self::DeriveScalar | Self::ImplScalar => "#sec-Scalars",
}
}
}
@ -39,12 +40,11 @@ impl fmt::Display for GraphQLScope {
let name = match self {
Self::InterfaceAttr => "interface",
Self::ObjectAttr | Self::ObjectDerive => "object",
Self::ScalarAttr | Self::ScalarDerive => "scalar",
Self::UnionAttr | Self::UnionDerive => "union",
Self::DeriveInputObject => "input object",
Self::DeriveEnum => "enum",
Self::DeriveScalar | Self::ImplScalar => "scalar",
};
write!(f, "GraphQL {}", name)
}
}
@ -119,7 +119,7 @@ impl GraphQLScope {
duplicates
.into_iter()
.for_each(|dup| {
(&dup.spanned[1..])
dup.spanned[1..]
.iter()
.for_each(|spanned| {
Diagnostic::spanned(

View file

@ -17,7 +17,6 @@ use syn::{
spanned::Spanned,
token, Attribute, Ident, Lit, Meta, MetaList, MetaNameValue, NestedMeta,
};
use url::Url;
use crate::common::parse::ParseBufferExt as _;
@ -455,7 +454,6 @@ pub enum FieldAttributeParseMode {
enum FieldAttribute {
Name(SpanContainer<syn::LitStr>),
Description(SpanContainer<syn::LitStr>),
SpecifiedByUrl(SpanContainer<syn::LitStr>),
Deprecation(SpanContainer<DeprecationAttr>),
Skip(SpanContainer<syn::Ident>),
Arguments(HashMap<String, FieldAttributeArgument>),
@ -490,15 +488,6 @@ impl Parse for FieldAttribute {
lit,
)))
}
"specified_by_url" => {
input.parse::<token::Eq>()?;
let lit = input.parse::<syn::LitStr>()?;
Ok(FieldAttribute::SpecifiedByUrl(SpanContainer::new(
ident.span(),
Some(lit.span()),
lit,
)))
}
"deprecated" | "deprecation" => {
let reason = if input.peek(token::Eq) {
input.parse::<token::Eq>()?;
@ -553,8 +542,6 @@ pub struct FieldAttributes {
pub name: Option<SpanContainer<String>>,
pub description: Option<SpanContainer<String>>,
pub deprecation: Option<SpanContainer<DeprecationAttr>>,
/// Only relevant for scalar impl macro.
pub specified_by_url: Option<SpanContainer<Url>>,
/// Only relevant for GraphQLObject derive.
pub skip: Option<SpanContainer<syn::Ident>>,
/// Only relevant for object macro.
@ -577,18 +564,6 @@ impl Parse for FieldAttributes {
FieldAttribute::Description(name) => {
output.description = Some(name.map(|val| val.value()));
}
FieldAttribute::SpecifiedByUrl(url) => {
output.specified_by_url = Some(
url.map(|val| Url::parse(&val.value()))
.transpose()
.map_err(|e| {
syn::Error::new(
e.span_ident(),
format!("Invalid URL: {}", e.inner()),
)
})?,
);
}
FieldAttribute::Deprecation(attr) => {
output.deprecation = Some(attr);
}

View file

@ -58,15 +58,6 @@ impl<T> SpanContainer<T> {
}
}
impl<T, E> SpanContainer<Result<T, E>> {
pub fn transpose(self) -> Result<SpanContainer<T>, SpanContainer<E>> {
match self.val {
Ok(v) => Ok(SpanContainer::new(self.ident, self.expr, v)),
Err(e) => Err(SpanContainer::new(self.ident, self.expr, e)),
}
}
}
impl<T> AsRef<T> for SpanContainer<T> {
fn as_ref(&self) -> &T {
&self.val