Skip to content

Commit a05f4e5

Browse files
jmpunktLegNeato
andauthored
Derive macro for tagged enums (GraphQLUnion) (#618)
* Implemented device macro for GraphQLUnion's * Updated PR link in CHNAGELOG * Disabled documentation on enumeration fields * Disabled skip on fields * Changed implementation for std::convert::Into since skip is not possible * Added documentation for GraphQLUnion * Added tests for GraphQLUnion * Fixed typos in error messages (as suggested by review) * Fixed failing documentation example * Utilized `resolver_code` in `util::GraphQLTypeDefinitionField`. Simplifies code and provides the idea of using `util::GraphQLTypeDefinitionField` for different types than objects. * Removed wrong statement about skip annotation in docs. Co-authored-by: Christian Legnitto <LegNeato@users.noreply.github.com>
1 parent 47f7ffa commit a05f4e5

File tree

8 files changed

+645
-6
lines changed

8 files changed

+645
-6
lines changed

docs/book/content/types/unions.md

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33
From a server's point of view, GraphQL unions are similar to interfaces: the
44
only exception is that they don't contain fields on their own.
55

6-
In Juniper, the `graphql_union!` has identical syntax to the [interface
7-
macro](interfaces.md), but does not support defining fields. Therefore, the same
8-
considerations about using traits, placeholder types, or enums still apply to
9-
unions.
6+
In Juniper, the `graphql_union!` has identical syntax to the
7+
[interface macro](interfaces.md), but does not support defining
8+
fields. Therefore, the same considerations about using traits,
9+
placeholder types, or enums still apply to unions. For simple
10+
situations, Juniper provides `#[derive(GraphQLUnion)]` for enums.
1011

1112
If we look at the same examples as in the interfaces chapter, we see the
1213
similarities and the tradeoffs:
@@ -154,7 +155,7 @@ impl GraphQLUnion for Character {
154155
# fn main() {}
155156
```
156157

157-
## Enums
158+
## Enums (Impl)
158159

159160
```rust
160161
#[derive(juniper::GraphQLObject)]
@@ -187,3 +188,32 @@ impl Character {
187188

188189
# fn main() {}
189190
```
191+
192+
## Enums (Derive)
193+
194+
This example is similar to `Enums (Impl)`. To successfully use the
195+
derive macro, ensure that each variant of the enum has a different
196+
type. Since each variant is different, the device macro provides
197+
`std::convert::Into<T>` converter for each variant.
198+
199+
```rust
200+
#[derive(juniper::GraphQLObject)]
201+
struct Human {
202+
id: String,
203+
home_planet: String,
204+
}
205+
206+
#[derive(juniper::GraphQLObject)]
207+
struct Droid {
208+
id: String,
209+
primary_function: String,
210+
}
211+
212+
#[derive(juniper::GraphQLUnion)]
213+
enum Character {
214+
Human(Human),
215+
Droid(Droid),
216+
}
217+
218+
# fn main() {}
219+
```
Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
// Test for union's derive macro
2+
3+
#[cfg(test)]
4+
use fnv::FnvHashMap;
5+
6+
#[cfg(test)]
7+
use juniper::{
8+
self, execute, DefaultScalarValue, EmptyMutation, EmptySubscription, GraphQLType, RootNode,
9+
Value, Variables,
10+
};
11+
12+
#[derive(juniper::GraphQLObject)]
13+
pub struct Human {
14+
id: String,
15+
home_planet: String,
16+
}
17+
18+
#[derive(juniper::GraphQLObject)]
19+
pub struct Droid {
20+
id: String,
21+
primary_function: String,
22+
}
23+
24+
#[derive(juniper::GraphQLUnion)]
25+
#[graphql(description = "A Collection of things")]
26+
pub enum Character {
27+
One(Human),
28+
Two(Droid),
29+
}
30+
31+
// Context Test
32+
pub struct CustomContext {
33+
is_left: bool,
34+
}
35+
36+
impl juniper::Context for CustomContext {}
37+
38+
#[derive(juniper::GraphQLObject)]
39+
#[graphql(Context = CustomContext)]
40+
pub struct HumanContext {
41+
id: String,
42+
home_planet: String,
43+
}
44+
45+
#[derive(juniper::GraphQLObject)]
46+
#[graphql(Context = CustomContext)]
47+
pub struct DroidContext {
48+
id: String,
49+
primary_function: String,
50+
}
51+
52+
/// A Collection of things
53+
#[derive(juniper::GraphQLUnion)]
54+
#[graphql(Context = CustomContext)]
55+
pub enum CharacterContext {
56+
One(HumanContext),
57+
Two(DroidContext),
58+
}
59+
60+
// #[juniper::object] compatibility
61+
62+
pub struct HumanCompat {
63+
id: String,
64+
home_planet: String,
65+
}
66+
67+
#[juniper::graphql_object]
68+
impl HumanCompat {
69+
fn id(&self) -> &String {
70+
&self.id
71+
}
72+
73+
fn home_planet(&self) -> &String {
74+
&self.home_planet
75+
}
76+
}
77+
78+
pub struct DroidCompat {
79+
id: String,
80+
primary_function: String,
81+
}
82+
83+
#[juniper::graphql_object]
84+
impl DroidCompat {
85+
fn id(&self) -> &String {
86+
&self.id
87+
}
88+
89+
fn primary_function(&self) -> &String {
90+
&self.primary_function
91+
}
92+
}
93+
94+
// NOTICE: this can not compile due to generic implementation of GraphQLType<__S>
95+
// #[derive(juniper::GraphQLUnion)]
96+
// pub enum CharacterCompatFail {
97+
// One(HumanCompat),
98+
// Two(DroidCompat),
99+
// }
100+
101+
/// A Collection of things
102+
#[derive(juniper::GraphQLUnion)]
103+
#[graphql(Scalar = juniper::DefaultScalarValue)]
104+
pub enum CharacterCompat {
105+
One(HumanCompat),
106+
Two(DroidCompat),
107+
}
108+
109+
pub struct Query;
110+
111+
#[juniper::graphql_object(
112+
Context = CustomContext,
113+
)]
114+
impl Query {
115+
fn context(&self, ctx: &CustomContext) -> CharacterContext {
116+
if ctx.is_left {
117+
HumanContext {
118+
id: "human-32".to_string(),
119+
home_planet: "earth".to_string(),
120+
}
121+
.into()
122+
} else {
123+
DroidContext {
124+
id: "droid-99".to_string(),
125+
primary_function: "run".to_string(),
126+
}
127+
.into()
128+
}
129+
}
130+
}
131+
132+
#[tokio::test]
133+
async fn test_derived_union_doc_macro() {
134+
assert_eq!(
135+
<Character as GraphQLType<DefaultScalarValue>>::name(&()),
136+
Some("Character")
137+
);
138+
139+
let mut registry: juniper::Registry = juniper::Registry::new(FnvHashMap::default());
140+
let meta = Character::meta(&(), &mut registry);
141+
142+
assert_eq!(meta.name(), Some("Character"));
143+
assert_eq!(
144+
meta.description(),
145+
Some(&"A Collection of things".to_string())
146+
);
147+
}
148+
149+
#[tokio::test]
150+
async fn test_derived_union_doc_string() {
151+
assert_eq!(
152+
<CharacterContext as GraphQLType<DefaultScalarValue>>::name(&()),
153+
Some("CharacterContext")
154+
);
155+
156+
let mut registry: juniper::Registry = juniper::Registry::new(FnvHashMap::default());
157+
let meta = CharacterContext::meta(&(), &mut registry);
158+
159+
assert_eq!(meta.name(), Some("CharacterContext"));
160+
assert_eq!(
161+
meta.description(),
162+
Some(&"A Collection of things".to_string())
163+
);
164+
}
165+
166+
#[tokio::test]
167+
async fn test_derived_union_left() {
168+
let doc = r#"
169+
{
170+
context {
171+
... on HumanContext {
172+
humanId: id
173+
homePlanet
174+
}
175+
... on DroidContext {
176+
droidId: id
177+
primaryFunction
178+
}
179+
}
180+
}"#;
181+
182+
let schema = RootNode::new(
183+
Query,
184+
EmptyMutation::<CustomContext>::new(),
185+
EmptySubscription::<CustomContext>::new(),
186+
);
187+
188+
assert_eq!(
189+
execute(
190+
doc,
191+
None,
192+
&schema,
193+
&Variables::new(),
194+
&CustomContext { is_left: true }
195+
)
196+
.await,
197+
Ok((
198+
Value::object(
199+
vec![(
200+
"context",
201+
Value::object(
202+
vec![
203+
("humanId", Value::scalar("human-32".to_string())),
204+
("homePlanet", Value::scalar("earth".to_string())),
205+
]
206+
.into_iter()
207+
.collect(),
208+
),
209+
)]
210+
.into_iter()
211+
.collect()
212+
),
213+
vec![]
214+
))
215+
);
216+
}
217+
218+
#[tokio::test]
219+
async fn test_derived_union_right() {
220+
let doc = r#"
221+
{
222+
context {
223+
... on HumanContext {
224+
humanId: id
225+
homePlanet
226+
}
227+
... on DroidContext {
228+
droidId: id
229+
primaryFunction
230+
}
231+
}
232+
}"#;
233+
234+
let schema = RootNode::new(
235+
Query,
236+
EmptyMutation::<CustomContext>::new(),
237+
EmptySubscription::<CustomContext>::new(),
238+
);
239+
240+
assert_eq!(
241+
execute(
242+
doc,
243+
None,
244+
&schema,
245+
&Variables::new(),
246+
&CustomContext { is_left: false }
247+
)
248+
.await,
249+
Ok((
250+
Value::object(
251+
vec![(
252+
"context",
253+
Value::object(
254+
vec![
255+
("droidId", Value::scalar("droid-99".to_string())),
256+
("primaryFunction", Value::scalar("run".to_string())),
257+
]
258+
.into_iter()
259+
.collect(),
260+
),
261+
)]
262+
.into_iter()
263+
.collect()
264+
),
265+
vec![]
266+
))
267+
);
268+
}

integration_tests/juniper_tests/src/codegen/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@ mod derive_enum;
22
mod derive_input_object;
33
mod derive_object;
44
mod derive_object_with_raw_idents;
5+
mod derive_union;
56
mod impl_union;
67
mod scalar_value_transparent;

juniper/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ See [#419](https://github.com/graphql-rust/juniper/pull/419).
2222

2323
See [#569](https://github.com/graphql-rust/juniper/pull/569).
2424

25+
- GraphQLUnion derive support ("#[derive(GraphqQLUnion)]")
26+
- implements GraphQLAsyncType
27+
28+
See [#618](https://github.com/graphql-rust/juniper/pull/618).
29+
2530
## Breaking Changes
2631

2732
- `juniper::graphiql` has moved to `juniper::http::graphiql`

juniper/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ extern crate bson;
116116
// functionality automatically.
117117
pub use juniper_codegen::{
118118
graphql_object, graphql_subscription, graphql_union, GraphQLEnum, GraphQLInputObject,
119-
GraphQLObject, GraphQLScalarValue,
119+
GraphQLObject, GraphQLScalarValue, GraphQLUnion,
120120
};
121121
// Internal macros are not exported,
122122
// but declared at the root to make them easier to use.

0 commit comments

Comments
 (0)