Skip to content

Commit 3d159f3

Browse files
committed
(codegen) Implement newtype derive for scalars.
This commit implements a newtype style custom derive for scalars via `#[derive(GraphQLScalarValue)]`, which now supports both deriving a base enum scalar type and newtypes. For newtypes, the `#[graphql(transparent)]` attribute is required. This commit: * implements the derive * adds integration tests * updates the book
1 parent c2f1196 commit 3d159f3

File tree

8 files changed

+436
-47
lines changed

8 files changed

+436
-47
lines changed

docs/book/content/types/scalars.md

Lines changed: 109 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -6,59 +6,131 @@ but this often requires coordination with the client library intended to consume
66
the API you're building.
77

88
Since any value going over the wire is eventually transformed into JSON, you're
9-
also limited in the data types you can use. Typically, you represent your custom
10-
scalars as strings.
9+
also limited in the data types you can use.
1110

12-
In Juniper, you use the `graphql_scalar!` macro to create a custom scalar. In
13-
this example, we're representing a user ID as a string wrapped in a custom type:
11+
There are two ways to define custom scalars.
12+
* For simple scalars that just wrap a primitive type, you can use the newtype pattern with
13+
a custom derive.
14+
* For more advanced use cases with custom validation, you can use
15+
the `graphql_scalar!` macro.
1416

15-
```rust
16-
use juniper::Value;
1717

18-
struct UserID(String);
18+
## Built-in scalars
1919

20-
juniper::graphql_scalar!(UserID where Scalar = <S> {
21-
description: "An opaque identifier, represented as a string"
20+
Juniper has built-in support for:
2221

23-
resolve(&self) -> Value {
24-
Value::scalar(self.0.clone())
25-
}
22+
* `i32` as `Int`
23+
* `f64` as `Float`
24+
* `String` and `&str` as `String`
25+
* `bool` as `Boolean`
26+
* `juniper::ID` as `ID`. This type is defined [in the
27+
spec](http://facebook.github.io/graphql/#sec-ID) as a type that is serialized
28+
as a string but can be parsed from both a string and an integer.
2629

27-
from_input_value(v: &InputValue) -> Option<UserID> {
28-
// If there's a parse error here, simply return None. Juniper will
29-
// present an error to the client.
30-
v.as_scalar_value::<String>().map(|s| UserID(s.to_owned()))
31-
}
30+
**Third party types**:
3231

33-
from_str<'a>(value: ScalarToken<'a>) -> juniper::ParseScalarResult<'a, S> {
34-
<String as juniper::ParseScalarValue<S>>::from_str(value)
35-
}
36-
});
32+
Juniper has built-in support for a few additional types from common third party
33+
crates. They are enabled via features that are on by default.
34+
35+
* uuid::Uuid
36+
* chrono::DateTime
37+
* url::Url
38+
39+
## newtype pattern
40+
41+
Often, you might need a custom scalar that just wraps an existing type.
42+
43+
This can be done with the newtype pattern and a custom derive, similar to how
44+
serde supports this pattern with `#[transparent]`.
45+
46+
```rust
47+
#[derive(juniper::GraphQLScalarValue)]
48+
#[graphql(transparent)]
49+
pub struct UserId(i32);
3750

3851
#[derive(juniper::GraphQLObject)]
3952
struct User {
40-
id: UserID,
41-
username: String,
53+
id: UserId,
4254
}
4355

4456
# fn main() {}
4557
```
4658

47-
## Built-in scalars
59+
That's it, you can now user `UserId` in your schema.
4860

49-
Juniper has built-in support for:
61+
The macro also allows for more customization:
5062

51-
* `i32` as `Int`
52-
* `f64` as `Float`
53-
* `String` and `&str` as `String`
54-
* `bool` as `Boolean`
55-
* `juniper::ID` as `ID`. This type is defined [in the
56-
spec](http://facebook.github.io/graphql/#sec-ID) as a type that is serialized
57-
as a string but can be parsed from both a string and an integer.
63+
```rust
64+
/// You can use a doc comment to specify a description.
65+
#[derive(juniper::GraphQLScalarValue)]
66+
#[graphql(
67+
transparent,
68+
// Overwrite the GraphQL type name.
69+
name = "MyUserId",
70+
// Specify a custom description.
71+
// A description in the attribute will overwrite a doc comment.
72+
description = "My user id description",
73+
)]
74+
pub struct UserId(i32);
75+
76+
# fn main() {}
77+
```
78+
79+
## Custom scalars
80+
81+
For more complex situations where you also need custom parsing or validation,
82+
you can use the `graphql_scalar!` macro.
83+
84+
Typically, you represent your custom scalars as strings.
85+
86+
The example below implements a custom scalar for a custom `Date` type.
87+
88+
Note: juniper already has built-in support for the `chrono::DateTime` type
89+
via `chrono` feature, which is enabled by default and should be used for this
90+
purpose.
5891

59-
### Non-standard scalars
92+
The example below is used just for illustration.
6093

61-
Juniper has built-in support for UUIDs from the [uuid
62-
crate](https://doc.rust-lang.org/uuid/uuid/index.html). This support is enabled
63-
by default, but can be disabled if you want to reduce the number of dependencies
64-
in your application.
94+
**Note**: the example assumes that the `Date` type implements
95+
`std::fmt::Display` and `std::str::FromStr`.
96+
97+
98+
```rust
99+
# mod date {
100+
# pub struct Date;
101+
# impl std::str::FromStr for Date{
102+
# type Err = String; fn from_str(_value: &str) -> Result<Self, Self::Err> { unimplemented!() }
103+
# }
104+
# // And we define how to represent date as a string.
105+
# impl std::fmt::Display for Date {
106+
# fn fmt(&self, _f: &mut std::fmt::Formatter) -> std::fmt::Result {
107+
# unimplemented!()
108+
# }
109+
# }
110+
# }
111+
112+
use juniper::{Value, ParseScalarResult, ParseScalarValue};
113+
use date::Date;
114+
115+
juniper::graphql_scalar!(Date where Scalar = <S> {
116+
description: "Date"
117+
118+
// Define how to convert your custom scalar into a primitive type.
119+
resolve(&self) -> Value {
120+
Value::scalar(self.to_string())
121+
}
122+
123+
// Define how to parse a primitive type into your custom scalar.
124+
from_input_value(v: &InputValue) -> Option<Date> {
125+
v.as_scalar_value::<String>()
126+
.and_then(|s| s.parse().ok())
127+
}
128+
129+
// Define how to parse a string value.
130+
from_str<'a>(value: ScalarToken<'a>) -> ParseScalarResult<'a, S> {
131+
<String as ParseScalarValue<S>>::from_str(value)
132+
}
133+
});
134+
135+
# fn main() {}
136+
```

docs/book/tests/Cargo.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,12 @@ build = "build.rs"
99
juniper = { path = "../../../juniper" }
1010
juniper_iron = { path = "../../../juniper_iron" }
1111

12-
iron = "^0.5.0"
13-
mount = "^0.3.0"
12+
iron = "0.5.0"
13+
mount = "0.4.0"
1414

1515
skeptic = "0.13"
1616
serde_json = "1.0.39"
17+
uuid = "0.7.4"
1718

1819
[build-dependencies]
1920
skeptic = "0.13"

integration_tests/juniper_tests/src/codegen/derive_input_object.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
#[cfg(test)]
21
use fnv::FnvHashMap;
32

43
use juniper::DefaultScalarValue;

integration_tests/juniper_tests/src/codegen/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ mod util;
33
mod derive_enum;
44
mod derive_input_object;
55
mod derive_object;
6+
mod scalar_value_transparent;
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
use fnv::FnvHashMap;
2+
use juniper::{DefaultScalarValue, FromInputValue, GraphQLType, InputValue, ToInputValue};
3+
4+
#[derive(juniper::GraphQLScalarValue, PartialEq, Eq, Debug)]
5+
#[graphql(transparent)]
6+
struct UserId(String);
7+
8+
#[derive(juniper::GraphQLScalarValue, PartialEq, Eq, Debug)]
9+
#[graphql(transparent, name = "MyUserId", description = "custom description...")]
10+
struct CustomUserId(String);
11+
12+
/// The doc comment...
13+
#[derive(juniper::GraphQLScalarValue, PartialEq, Eq, Debug)]
14+
#[graphql(transparent)]
15+
struct IdWithDocComment(i32);
16+
17+
#[derive(juniper::GraphQLObject)]
18+
struct User {
19+
id: UserId,
20+
id_custom: CustomUserId,
21+
}
22+
23+
struct User2;
24+
25+
#[juniper::object]
26+
impl User2 {
27+
fn id(&self) -> UserId {
28+
UserId("id".to_string())
29+
}
30+
}
31+
32+
#[test]
33+
fn test_scalar_value_simple() {
34+
assert_eq!(<UserId as GraphQLType>::name(&()), Some("UserId"));
35+
36+
let mut registry: juniper::Registry = juniper::Registry::new(FnvHashMap::default());
37+
let meta = UserId::meta(&(), &mut registry);
38+
assert_eq!(meta.name(), Some("UserId"));
39+
assert_eq!(meta.description(), None);
40+
41+
let input: InputValue = serde_json::from_value(serde_json::json!("userId1")).unwrap();
42+
let output: UserId = FromInputValue::from_input_value(&input).unwrap();
43+
assert_eq!(output, UserId("userId1".into()),);
44+
45+
let id = UserId("111".into());
46+
let output = ToInputValue::<DefaultScalarValue>::to_input_value(&id);
47+
assert_eq!(output, InputValue::scalar("111"),);
48+
}
49+
50+
#[test]
51+
fn test_scalar_value_custom() {
52+
assert_eq!(<CustomUserId as GraphQLType>::name(&()), Some("MyUserId"));
53+
54+
let mut registry: juniper::Registry = juniper::Registry::new(FnvHashMap::default());
55+
let meta = CustomUserId::meta(&(), &mut registry);
56+
assert_eq!(meta.name(), Some("MyUserId"));
57+
assert_eq!(
58+
meta.description(),
59+
Some(&"custom description...".to_string())
60+
);
61+
62+
let input: InputValue = serde_json::from_value(serde_json::json!("userId1")).unwrap();
63+
let output: CustomUserId = FromInputValue::from_input_value(&input).unwrap();
64+
assert_eq!(output, CustomUserId("userId1".into()),);
65+
66+
let id = CustomUserId("111".into());
67+
let output = ToInputValue::<DefaultScalarValue>::to_input_value(&id);
68+
assert_eq!(output, InputValue::scalar("111"),);
69+
}
70+
71+
#[test]
72+
fn test_scalar_value_doc_comment() {
73+
let mut registry: juniper::Registry = juniper::Registry::new(FnvHashMap::default());
74+
let meta = IdWithDocComment::meta(&(), &mut registry);
75+
assert_eq!(meta.description(), Some(&"The doc comment...".to_string()));
76+
}

juniper/CHANGELOG.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,23 @@
11
# master
22

3+
### newtype ScalarValue derive
4+
5+
See [#345](https://github.com/graphql-rust/juniper/pull/345).
6+
7+
The newtype pattern can now be used with the `GraphQLScalarValue` custom derive
8+
to easily implement custom scalar values that just wrap another scalar,
9+
similar to serdes `#[transparent]` functionality.
10+
11+
Example:
12+
13+
```rust
14+
#[derive(juniper::GraphQLScalarValue)]
15+
#[graphql(transparent)]
16+
struct UserId(i32);
17+
```
18+
19+
### Other Changes
20+
321
- The `ID` scalar now implements Serde's `Serialize` and `Deserialize`
422

523
# [[0.12.0] 2019-05-16](https://github.com/graphql-rust/juniper/releases/tag/juniper-0.12.0)

0 commit comments

Comments
 (0)