1use indexmap::IndexSet;
2
3use super::{directive::to_meta_directive_invocation, Directive};
4use crate::{
5 dynamic::SchemaError,
6 registry::{MetaType, Registry},
7};
8
9#[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 #[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 #[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 #[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 #[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 let named = Interface::new("Named");
295 let named = named.field(InterfaceField::new(
296 "name",
297 TypeRef::named_nn(TypeRef::STRING),
298 ));
299 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 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 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 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 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}