async_graphql/dynamic/
union.rs

1use indexmap::IndexSet;
2
3use super::{directive::to_meta_directive_invocation, Directive};
4use crate::{
5    dynamic::SchemaError,
6    registry::{MetaType, Registry},
7};
8
9/// A GraphQL union type
10///
11/// # Examples
12///
13/// ```
14/// use async_graphql::{dynamic::*, value, Value};
15///
16/// let obj_a = Object::new("MyObjA")
17///     .field(Field::new("a", TypeRef::named_nn(TypeRef::INT), |_| {
18///         FieldFuture::new(async { Ok(Some(Value::from(100))) })
19///     }))
20///     .field(Field::new("b", TypeRef::named_nn(TypeRef::INT), |_| {
21///         FieldFuture::new(async { Ok(Some(Value::from(200))) })
22///     }));
23///
24/// let obj_b = Object::new("MyObjB")
25///     .field(Field::new("c", TypeRef::named_nn(TypeRef::INT), |_| {
26///         FieldFuture::new(async { Ok(Some(Value::from(300))) })
27///     }))
28///     .field(Field::new("d", TypeRef::named_nn(TypeRef::INT), |_| {
29///         FieldFuture::new(async { Ok(Some(Value::from(400))) })
30///     }));
31///
32/// let union = Union::new("MyUnion")
33///     .possible_type(obj_a.type_name())
34///     .possible_type(obj_b.type_name());
35///
36/// let query = Object::new("Query")
37///     .field(Field::new("valueA", TypeRef::named_nn(union.type_name()), |_| {
38///         FieldFuture::new(async {
39///             Ok(Some(FieldValue::with_type(FieldValue::NULL, "MyObjA")))
40///         })
41///     }))
42///     .field(Field::new("valueB", TypeRef::named_nn(union.type_name()), |_| {
43///         FieldFuture::new(async {
44///             Ok(Some(FieldValue::with_type(FieldValue::NULL, "MyObjB")))
45///         })
46///     }));
47///
48/// # tokio::runtime::Runtime::new().unwrap().block_on(async move {
49///
50/// let schema = Schema::build(query.type_name(), None, None)
51///     .register(obj_a)
52///     .register(obj_b)
53///     .register(union)
54///     .register(query)
55///     .finish()?;
56///
57/// let query = r#"
58///     {
59///         valueA { ... on MyObjA { a b } ... on MyObjB { c d } }
60///         valueB { ... on MyObjA { a b } ... on MyObjB { c d } }
61///     }
62/// "#;
63///
64/// assert_eq!(
65///     schema.execute(query).await.into_result().unwrap().data,
66///     value!({
67///         "valueA": {
68///             "a": 100,
69///             "b": 200,
70///         },
71///         "valueB": {
72///             "c": 300,
73///             "d": 400,
74///         }
75///     })
76/// );
77///
78/// # Ok::<_, SchemaError>(())
79/// # }).unwrap();
80/// ```
81#[derive(Debug)]
82pub struct Union {
83    pub(crate) name: String,
84    pub(crate) description: Option<String>,
85    pub(crate) possible_types: IndexSet<String>,
86    inaccessible: bool,
87    tags: Vec<String>,
88    pub(crate) directives: Vec<Directive>,
89}
90
91impl Union {
92    /// Create a GraphQL union type
93    #[inline]
94    pub fn new(name: impl Into<String>) -> Self {
95        Self {
96            name: name.into(),
97            description: None,
98            possible_types: Default::default(),
99            inaccessible: false,
100            tags: Vec::new(),
101            directives: Vec::new(),
102        }
103    }
104
105    impl_set_description!();
106    impl_set_inaccessible!();
107    impl_set_tags!();
108    impl_directive!();
109
110    /// Add a possible type to the union that must be an object
111    #[inline]
112    pub fn possible_type(mut self, ty: impl Into<String>) -> Self {
113        self.possible_types.insert(ty.into());
114        self
115    }
116
117    /// Returns the type name
118    #[inline]
119    pub fn type_name(&self) -> &str {
120        &self.name
121    }
122
123    pub(crate) fn register(&self, registry: &mut Registry) -> Result<(), SchemaError> {
124        registry.types.insert(
125            self.name.clone(),
126            MetaType::Union {
127                name: self.name.clone(),
128                description: self.description.clone(),
129                possible_types: self.possible_types.clone(),
130                visible: None,
131                inaccessible: self.inaccessible,
132                tags: self.tags.clone(),
133                rust_typename: None,
134                directive_invocations: to_meta_directive_invocation(self.directives.clone()),
135            },
136        );
137        Ok(())
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use async_graphql_parser::Pos;
144
145    use crate::{dynamic::*, value, PathSegment, Request, ServerError, Value};
146
147    #[tokio::test]
148    async fn basic_union() {
149        let obj_a = Object::new("MyObjA")
150            .field(Field::new("a", TypeRef::named_nn(TypeRef::INT), |_| {
151                FieldFuture::new(async { Ok(Some(Value::from(100))) })
152            }))
153            .field(Field::new("b", TypeRef::named_nn(TypeRef::INT), |_| {
154                FieldFuture::new(async { Ok(Some(Value::from(200))) })
155            }));
156
157        let obj_b = Object::new("MyObjB")
158            .field(Field::new("c", TypeRef::named_nn(TypeRef::INT), |_| {
159                FieldFuture::new(async { Ok(Some(Value::from(300))) })
160            }))
161            .field(Field::new("d", TypeRef::named_nn(TypeRef::INT), |_| {
162                FieldFuture::new(async { Ok(Some(Value::from(400))) })
163            }));
164
165        let union = Union::new("MyUnion")
166            .possible_type(obj_a.type_name())
167            .possible_type(obj_b.type_name());
168
169        let query = Object::new("Query")
170            .field(Field::new(
171                "valueA",
172                TypeRef::named_nn(union.type_name()),
173                |_| FieldFuture::new(async { Ok(Some(FieldValue::NULL.with_type("MyObjA"))) }),
174            ))
175            .field(Field::new(
176                "valueB",
177                TypeRef::named_nn(union.type_name()),
178                |_| FieldFuture::new(async { Ok(Some(FieldValue::NULL.with_type("MyObjB"))) }),
179            ));
180
181        let schema = Schema::build(query.type_name(), None, None)
182            .register(obj_a)
183            .register(obj_b)
184            .register(union)
185            .register(query)
186            .finish()
187            .unwrap();
188
189        let query = r#"
190            {
191                valueA { __typename ... on MyObjA { a b } ... on MyObjB { c d } }
192                valueB { __typename ... on MyObjA { a b } ... on MyObjB { c d } }
193            }
194        "#;
195        assert_eq!(
196            schema.execute(query).await.into_result().unwrap().data,
197            value!({
198                "valueA": {
199                    "__typename": "MyObjA",
200                    "a": 100,
201                    "b": 200,
202                },
203                "valueB": {
204                    "__typename": "MyObjB",
205                    "c": 300,
206                    "d": 400,
207                }
208            })
209        );
210    }
211
212    #[tokio::test]
213    async fn does_not_contain() {
214        let obj_a = Object::new("MyObjA")
215            .field(Field::new("a", TypeRef::named_nn(TypeRef::INT), |_| {
216                FieldFuture::new(async { Ok(Some(Value::from(100))) })
217            }))
218            .field(Field::new("b", TypeRef::named_nn(TypeRef::INT), |_| {
219                FieldFuture::new(async { Ok(Some(Value::from(200))) })
220            }));
221
222        let obj_b = Object::new("MyObjB")
223            .field(Field::new("c", TypeRef::named_nn(TypeRef::INT), |_| {
224                FieldFuture::new(async { Ok(Some(Value::from(300))) })
225            }))
226            .field(Field::new("d", TypeRef::named_nn(TypeRef::INT), |_| {
227                FieldFuture::new(async { Ok(Some(Value::from(400))) })
228            }));
229
230        let union = Union::new("MyUnion").possible_type(obj_a.type_name());
231
232        let query = Object::new("Query").field(Field::new(
233            "valueA",
234            TypeRef::named_nn(union.type_name()),
235            |_| FieldFuture::new(async { Ok(Some(FieldValue::NULL.with_type("MyObjB"))) }),
236        ));
237
238        let schema = Schema::build(query.type_name(), None, None)
239            .register(obj_a)
240            .register(obj_b)
241            .register(union)
242            .register(query)
243            .finish()
244            .unwrap();
245
246        let query = r#"
247            {
248                valueA { ... on MyObjA { a b } }
249            }
250        "#;
251        assert_eq!(
252            schema.execute(query).await.into_result().unwrap_err(),
253            vec![ServerError {
254                message: "internal: union \"MyUnion\" does not contain object \"MyObjB\""
255                    .to_owned(),
256                source: None,
257                locations: vec![Pos {
258                    column: 17,
259                    line: 3
260                }],
261                path: vec![PathSegment::Field("valueA".to_owned())],
262                extensions: None,
263            }]
264        );
265    }
266
267    #[tokio::test]
268    async fn test_query() {
269        struct Dog;
270        struct Cat;
271        struct Snake;
272        // enum
273        #[allow(dead_code)]
274        enum Animal {
275            Dog(Dog),
276            Cat(Cat),
277            Snake(Snake),
278        }
279        struct Query {
280            pet: Animal,
281        }
282
283        impl Animal {
284            fn to_field_value(&self) -> FieldValue {
285                match self {
286                    Animal::Dog(dog) => FieldValue::borrowed_any(dog).with_type("Dog"),
287                    Animal::Cat(cat) => FieldValue::borrowed_any(cat).with_type("Cat"),
288                    Animal::Snake(snake) => FieldValue::borrowed_any(snake).with_type("Snake"),
289                }
290            }
291        }
292        fn create_schema() -> Schema {
293            // interface
294            let named = Interface::new("Named");
295            let named = named.field(InterfaceField::new(
296                "name",
297                TypeRef::named_nn(TypeRef::STRING),
298            ));
299            // dog
300            let dog = Object::new("Dog");
301            let dog = dog.field(Field::new(
302                "name",
303                TypeRef::named_nn(TypeRef::STRING),
304                |_ctx| FieldFuture::new(async move { Ok(Some(Value::from("dog"))) }),
305            ));
306            let dog = dog.field(Field::new(
307                "power",
308                TypeRef::named_nn(TypeRef::INT),
309                |_ctx| FieldFuture::new(async move { Ok(Some(Value::from(100))) }),
310            ));
311            let dog = dog.implement("Named");
312            // cat
313            let cat = Object::new("Cat");
314            let cat = cat.field(Field::new(
315                "name",
316                TypeRef::named_nn(TypeRef::STRING),
317                |_ctx| FieldFuture::new(async move { Ok(Some(Value::from("cat"))) }),
318            ));
319            let cat = cat.field(Field::new(
320                "life",
321                TypeRef::named_nn(TypeRef::INT),
322                |_ctx| FieldFuture::new(async move { Ok(Some(Value::from(9))) }),
323            ));
324            let cat = cat.implement("Named");
325            // snake
326            let snake = Object::new("Snake");
327            let snake = snake.field(Field::new(
328                "length",
329                TypeRef::named_nn(TypeRef::INT),
330                |_ctx| FieldFuture::new(async move { Ok(Some(Value::from(200))) }),
331            ));
332            // animal
333            let animal = Union::new("Animal");
334            let animal = animal.possible_type("Dog");
335            let animal = animal.possible_type("Cat");
336            let animal = animal.possible_type("Snake");
337            // query
338
339            let query = Object::new("Query");
340            let query = query.field(Field::new("pet", TypeRef::named_nn("Animal"), |ctx| {
341                FieldFuture::new(async move {
342                    let query = ctx.parent_value.try_downcast_ref::<Query>()?;
343                    Ok(Some(query.pet.to_field_value()))
344                })
345            }));
346
347            let schema = Schema::build(query.type_name(), None, None);
348            let schema = schema
349                .register(query)
350                .register(named)
351                .register(dog)
352                .register(cat)
353                .register(snake)
354                .register(animal);
355
356            schema.finish().unwrap()
357        }
358
359        let schema = create_schema();
360        let query = r#"
361            query {
362                dog: pet {
363                    ... on Dog {
364                        __dog_typename: __typename
365                        name
366                        power
367                    }
368                }
369                named: pet {
370                    ... on Named {
371                        __named_typename: __typename
372                        name
373                    }
374                }
375            }
376        "#;
377        let root = Query {
378            pet: Animal::Dog(Dog),
379        };
380        let req = Request::new(query).root_value(FieldValue::owned_any(root));
381        let res = schema.execute(req).await;
382
383        assert_eq!(
384            res.data.into_json().unwrap(),
385            serde_json::json!({
386                "dog": {
387                    "__dog_typename": "Dog",
388                    "name": "dog",
389                    "power": 100
390                },
391                "named": {
392                    "__named_typename": "Dog",
393                    "name": "dog"
394                }
395            })
396        );
397    }
398}