Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Redesign #[graphql_scalar] macro #1014

Merged
merged 95 commits into from
Feb 24, 2022
Merged
Show file tree
Hide file tree
Changes from 84 commits
Commits
Show all changes
95 commits
Select commit Hold shift + click to select a range
01d7cb8
WIP
ilslv Dec 20, 2021
205cfcd
WIP
ilslv Dec 20, 2021
928cb9c
WIP (with IsSubtype)
ilslv Dec 20, 2021
3dad162
WIP (It looks like we're onto something)
ilslv Dec 20, 2021
af04486
WIP (return to const assertions)
ilslv Dec 21, 2021
15e527f
It's alive!
ilslv Dec 24, 2021
cb9cad5
Merge branch 'master' into 1000-new-interfaces
ilslv Dec 24, 2021
7a78f9e
Use Field inside object's field resolver
ilslv Dec 24, 2021
5c2087c
Add async object fields
ilslv Dec 24, 2021
e9c6046
Fix
ilslv Dec 24, 2021
0739c3e
Fix Wrapped type for tuple with context
ilslv Dec 27, 2021
263d544
WIP (Make graphql_interface_new attribute work)
ilslv Dec 28, 2021
f4c48a6
WIP (move to the new tests volume 1)
ilslv Dec 28, 2021
a1df387
WIP (move to the new tests volume 2)
ilslv Dec 29, 2021
5040d68
WIP (move to the new tests volume 3)
ilslv Dec 29, 2021
f5533e3
WIP (move to the new tests volume 4)
ilslv Dec 29, 2021
527325c
WIP (move to the new tests volume 5)
ilslv Dec 29, 2021
704f079
WIP (move to the new tests volume 5)
ilslv Dec 29, 2021
b84009e
WIP (refactor)
ilslv Dec 30, 2021
11345f7
WIP (refactor)
ilslv Dec 30, 2021
5e7230f
WIP (moving to readable const asserts)
ilslv Dec 30, 2021
d17ba4f
WIP (finally good const assert message)
ilslv Dec 30, 2021
8cb9b81
WIP (corrections)
ilslv Dec 30, 2021
116cdd9
WIP (pretty field arguments check)
ilslv Jan 3, 2022
7bd8c73
WIP (pretty subtype check)
ilslv Jan 3, 2022
3ebc8fd
WIP (corrections)
ilslv Jan 3, 2022
ce249b0
WIP (FieldMeta trait)
ilslv Jan 3, 2022
d9971a0
WIP (Refactor graphql_interface_new a bit)
ilslv Jan 3, 2022
8a966fb
WIP (Corrections)
ilslv Jan 3, 2022
21f4940
WIP (Prettify non-existent Field assertions)
ilslv Jan 4, 2022
e03901b
WIP (Move to macros::reflection module)
ilslv Jan 4, 2022
3ad8399
WIP (Docs vol. 1)
ilslv Jan 4, 2022
463bbb8
WIP (Docs vol. 2)
ilslv Jan 4, 2022
26ab27e
WIP (Refactoring)
ilslv Jan 4, 2022
cddb252
Merge branch 'master' into 1000-new-interfaces
ilslv Jan 4, 2022
9977746
WIP (Refactoring)
ilslv Jan 4, 2022
f5340a7
WIP (Tests corrections)
ilslv Jan 4, 2022
4c31c13
WIP (More tests vol. 1)
ilslv Jan 4, 2022
7bd09d0
WIP (Move everything to the new graphql_interface macro)
ilslv Jan 5, 2022
b5f552a
WIP (More codegen_fail tests)
ilslv Jan 5, 2022
66819e9
WIP (prettify missing references in `impl` and `for` attributes)
ilslv Jan 5, 2022
3d53dc1
WIP (more tests)
ilslv Jan 5, 2022
732bc13
WIP (Correct docs)
ilslv Jan 6, 2022
81d96ce
WIP (Forbid default trait method impls)
ilslv Jan 6, 2022
94164eb
WIP (Corrections)
ilslv Jan 6, 2022
1345bff
WIP (Corrections)
ilslv Jan 6, 2022
6cb5cef
WIP (Corrections)
ilslv Jan 6, 2022
41a2dcf
WIP (CHANGELOG)
ilslv Jan 6, 2022
eb8c56d
WIP (Correction)
ilslv Jan 6, 2022
7f45aef
WIP
ilslv Jan 10, 2022
81c4df9
Migrate to real GraphQLScalar trait
ilslv Jan 10, 2022
d359524
Corrections
ilslv Jan 10, 2022
888789a
Corrections
ilslv Jan 11, 2022
e28ca92
Add generic test and remove redundant lifetime
ilslv Jan 11, 2022
f531037
Some corrections [skip ci]
tyranron Jan 11, 2022
8f169cf
WIP
ilslv Jan 12, 2022
a2287dc
Minor tests corrections
ilslv Jan 12, 2022
ceb2cf6
Don't inject `#[async_trait]` in case some trait methods are async
ilslv Jan 12, 2022
d77a1ab
Corrections
ilslv Jan 13, 2022
8be555c
Merge branch '1000-new-interfaces' into generic-scalar
ilslv Jan 13, 2022
ebc0149
Merge branch 'generic-scalar' into new-derive-scalar
ilslv Jan 13, 2022
8fb4587
WIP
ilslv Jan 13, 2022
52c14b2
WIP
ilslv Jan 13, 2022
4b6df34
Corrections
ilslv Jan 14, 2022
0cb101d
Corrections
ilslv Jan 14, 2022
8d47ba7
Merge branch '1000-new-interfaces' into generic-scalar
ilslv Jan 14, 2022
22b3fe2
Merge branch 'generic-scalar' into new-derive-scalar
ilslv Jan 14, 2022
7635a1e
Corrections
ilslv Jan 14, 2022
3da33ba
Corrections
ilslv Jan 14, 2022
cc365d3
Corrections
ilslv Jan 14, 2022
30117a8
Docs and the book
ilslv Jan 14, 2022
2362b27
Fix The Book
ilslv Jan 14, 2022
193c77b
Merge branch 'master' into generic-scalar
ilslv Jan 28, 2022
86f1c10
CHANGELOG
ilslv Jan 28, 2022
7db2adc
Sprinkle `#[automatically_derived]` attributes
ilslv Jan 28, 2022
859b3be
Merge branch 'generic-scalar' into new-derive-scalar
ilslv Jan 28, 2022
6fb0dbf
Implement `#[graphql(with)]` attribute
ilslv Feb 2, 2022
f063728
CHANGELOG
ilslv Feb 2, 2022
60fcbbc
WIP
ilslv Feb 4, 2022
8b3e80a
Merge branch 'master' into new-derive-scalar
ilslv Feb 4, 2022
8706043
Corrections and tests
ilslv Feb 4, 2022
5b96861
Correction
ilslv Feb 4, 2022
0a551ef
Corrections
ilslv Feb 4, 2022
39c2dac
Ignore GraphQLScalar book tests
ilslv Feb 4, 2022
f184252
Correction
ilslv Feb 14, 2022
6715129
Correction
ilslv Feb 14, 2022
889016c
Correction
ilslv Feb 15, 2022
1fd2c8f
Add test for `with = Self` and document usage
ilslv Feb 17, 2022
a6f50a8
WIP
ilslv Feb 17, 2022
edd91f4
Corrections
ilslv Feb 18, 2022
545bcc6
Corrections
ilslv Feb 18, 2022
f4ebe2a
Corrections
ilslv Feb 18, 2022
d10aeb5
Corrections
ilslv Feb 18, 2022
0e6e7d2
Corrections
ilslv Feb 21, 2022
faee9cb
Corrections
tyranron Feb 24, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
273 changes: 232 additions & 41 deletions docs/book/content/types/scalars.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,107 +40,298 @@ crates. They are enabled via features that are on by default.
* url::Url
* bson::oid::ObjectId

## newtype pattern
## Custom scalars

### `#[derive(GraphQLScalar)]`

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)]
pub struct UserId(i32);

#[derive(juniper::GraphQLObject)]
struct User {
id: UserId,
}

#
# fn main() {}
```

That's it, you can now user `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.
name = "MyUserId",
// Specify a custom description.
// A description in the attribute will overwrite a doc comment.
description = "My user id description",
)]
pub struct UserId(i32);
#
# fn main() {}
```

All the methods used from newtype's field can be replaced with attributes:

#### `#[graphql(to_output_with = <fn>)]` attribute

```rust,ignore
# use juniper::{GraphQLScalar, ScalarValue, Value};
#
#[derive(GraphQLScalar)]
#[graphql(to_output_with = to_output)]
struct Incremented(i32);

/// Increments [`Incremented`] before converting into a [`Value`].
fn to_output<S: ScalarValue>(v: &Incremented) -> Value<S> {
let inc = v.0 + 1;
Value::from(inc)
}
#
# fn main() {}
```

## Custom scalars
#### `#[graphql(from_input_with = <fn>, from_input_err = <type>)]` attributes

```rust,ignore
# use juniper::{DefaultScalarValue, GraphQLScalar, InputValue, ScalarValue};
#
#[derive(GraphQLScalar)]
#[graphql(scalar = DefaultScalarValue)]
#[graphql(from_input_with = Self::from_input, from_input_err = String)]
// Unfortunately for now there is no way to infer this ^^^^^^
struct UserId(String);

impl UserId {
/// Checks whether [`InputValue`] is `String` beginning with `id: ` and
/// strips it.
fn from_input(input: &InputValue) -> Result<UserId, String> {
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,
from_input_err = String,
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: 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))
}

fn parse_token<S: ScalarValue>(value: ScalarToken<'_>) -> ParseScalarResult<'_, S> {
<String as ParseScalarValue<S>>::from_str(value)
.or_else(|_| <i32 as ParseScalarValue<S>>::from_str(value))
}
#
# fn main() {}
```

> __NOTE:__ As you can see, once you provide all 3 custom resolvers, there
> is no need to follow `newtype` pattern.

For more complex situations where you also need custom parsing or validation,
you can use the `graphql_scalar` proc macro.
#### `#[graphql(with = <module>)]` attribute

Typically, you represent your custom scalars as strings.
Instead of providing all custom resolvers, you can provide module with `to_output`, `from_input`, `parse_token` functions and `Error` struct or type alias.

The example below implements a custom scalar for a custom `Date` type.
```rust,ignore
# use juniper::{GraphQLScalar, InputValue, ParseScalarResult, ParseScalarValue, ScalarValue, ScalarToken, Value};
#
#[derive(GraphQLScalar)]
#[graphql(with = string_or_int)]
enum StringOrInt {
String(String),
Int(i32),
}

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.
mod string_or_int {
use super::*;

pub(super) type Error = String;

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

The example below is used just for illustration.
Also, you can partially override `#[graphql(with)]` attribute with other custom scalars.

**Note**: the example assumes that the `Date` type implements
`std::fmt::Display` and `std::str::FromStr`.
```rust,ignore
# use juniper::{GraphQLScalar, InputValue, ParseScalarResult, ScalarValue, ScalarToken, Value};
#
#[derive(GraphQLScalar)]
#[graphql(
with = string_or_int,
from_input_err = String,
parse_token(String, i32)
)]
enum StringOrInt {
String(String),
Int(i32),
}

mod string_or_int {
use super::*;

pub(super) fn to_output<S>(v: &StringOrInt) -> Value<S>
where
S: ScalarValue,
{
match v {
StringOrInt::String(str) => Value::scalar(str.to_owned()),
StringOrInt::Int(i) => Value::scalar(*i),
}
}

pub(super) fn from_input<S>(v: &InputValue<S>) -> Result<StringOrInt, String>
where
S: ScalarValue,
{
v.as_string_value()
.map(|s| StringOrInt::String(s.to_owned()))
.or_else(|| v.as_int_value().map(|i| StringOrInt::Int(i)))
.ok_or_else(|| format!("Expected `String` or `Int`, found: {}", v))
}
}
#
# fn main() {}
```

---

### `#[graphql_scalar]` attribute

For implementing custom scalars on foreign types there is `#[graphql_scalar]` attribute macro.

> __NOTE:__ To satisfy [orphan rule] 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!()
# }
# }
# }
#
use juniper::{Value, ParseScalarResult, ParseScalarValue};
use date::Date;

#[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())
}
# use juniper::DefaultScalarValue as CustomScalarValue;
use juniper::{graphql_scalar, InputValue, ScalarValue, Value};

#[graphql_scalar(
with = date_scalar,
parse_token = String,
scalar = CustomScalarValue,
// ^^^^^^^^^^^^^^^^^ Local `ScalarValue` implementation.
)]
type Date = date::Date;
// ^^^^^^^^^^ Type from another crate.

// 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)))
mod date_scalar {
use super::*;

pub(super) type Error = String;

pub(super) fn to_output(v: &Date) -> Value<CustomScalarValue> {
Value::scalar(v.to_string())
}

// 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, Error> {
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 rule]: https://doc.rust-lang.org/reference/items/implementations.html#orphan-rules
[`ScalarValue`]: https://docs.rs/juniper/latest/juniper/trait.ScalarValue.html

This file was deleted.

This file was deleted.

32 changes: 19 additions & 13 deletions integration_tests/codegen_fail/fail/scalar/impl_invalid_url.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,27 @@
use juniper::graphql_scalar;
use juniper::{graphql_scalar, InputValue, ScalarValue, Value};

struct ScalarSpecifiedByUrl(i32);
struct ScalarSpecifiedByUrl;

#[graphql_scalar(specified_by_url = "not an url")]
impl GraphQLScalar for ScalarSpecifiedByUrl {
fn resolve(&self) -> Value {
Value::scalar(self.0)
}
#[graphql_scalar(
specified_by_url = "not an url",
with = scalar,
parse_token = i32,
)]
type Scalar = ScalarSpecifiedByUrl;

mod scalar {
use super::*;

pub(super) type Error = String;

fn from_input_value(v: &InputValue) -> Result<ScalarSpecifiedByUrl, String> {
v.as_int_value()
.map(ScalarSpecifiedByUrl)
.ok_or_else(|| format!("Expected `Int`, found: {}", v))
pub(super) fn to_output<S: ScalarValue>(_: &ScalarSpecifiedByUrl) -> Value<S> {
Value::scalar(0)
}

fn from_str<'a>(value: ScalarToken<'a>) -> ParseScalarResult<'a, DefaultScalarValue> {
<i32 as ParseScalarValue>::from_str(value)
pub(super) fn from_input<S: ScalarValue>(
_: &InputValue<S>,
) -> Result<ScalarSpecifiedByUrl, Error> {
Ok(ScalarSpecifiedByUrl)
}
}

Expand Down
Loading