1use crate::generators::Generator;
2use crate::generators::Generators;
3use crate::random::RandomProvider;
4use crate::random::ResponseError;
5use apollo_compiler::executable::Field;
6use apollo_compiler::executable::Selection;
7use apollo_compiler::executable::SelectionSet;
8use apollo_compiler::schema::ExtendedType;
9use apollo_compiler::validation::Valid;
10use apollo_compiler::ExecutableDocument;
11use apollo_compiler::Name;
12use apollo_compiler::Node;
13use apollo_compiler::Schema;
14use indexmap::IndexMap;
15use serde_json_bytes::json;
16use serde_json_bytes::Map;
17use serde_json_bytes::Value;
18
19const TYPENAME: &str = "__typename";
20
21pub struct ResponseBuilder<'a, 'doc, 'schema, R: RandomProvider> {
37 rng: &'a mut R,
38 doc: &'doc Valid<ExecutableDocument>,
39 schema: &'schema Valid<Schema>,
40 generators: Generators<R>,
41 min_list_size: usize,
42 max_list_size: usize,
43 null_ratio: Option<(u32, u32)>,
44 operation_name: Option<&'doc str>,
45}
46
47impl<'a, 'doc, 'schema, R: RandomProvider> ResponseBuilder<'a, 'doc, 'schema, R> {
48 pub fn new(
50 rng: &'a mut R,
51 doc: &'doc Valid<ExecutableDocument>,
52 schema: &'schema Valid<Schema>,
53 ) -> Self {
54 Self {
55 rng,
56 doc,
57 schema,
58 generators: Generators::default(),
59 min_list_size: 0,
60 max_list_size: 5,
61 null_ratio: None,
62 operation_name: None,
63 }
64 }
65
66 pub fn with_generator<G>(mut self, type_name: Name, generator: G) -> Self
77 where
78 G: Generator<R> + 'static,
79 {
80 self.generators.insert(type_name, Box::new(generator));
81 self
82 }
83
84 pub fn with_min_list_size(mut self, min_size: usize) -> Self {
86 self.min_list_size = min_size;
87 self
88 }
89
90 pub fn with_max_list_size(mut self, max_size: usize) -> Self {
92 self.max_list_size = max_size;
93 self
94 }
95
96 pub fn with_null_ratio(mut self, numerator: u32, denominator: u32) -> Self {
98 self.null_ratio = Some((numerator, denominator));
99 self
100 }
101
102 pub fn with_operation_name(mut self, operation_name: Option<&'doc str>) -> Self {
105 self.operation_name = operation_name;
106 self
107 }
108
109 pub fn build(mut self) -> Result<Value, ResponseError> {
111 if let Ok(operation) = self.doc.operations.get(self.operation_name) {
112 let data = self.selection_set(&operation.selection_set)?;
113 Ok(json!({ "data": data }))
114 } else {
115 Ok(json!({ "data": null }))
116 }
117 }
118
119 pub fn build_data(&mut self) -> Result<Value, ResponseError> {
123 if let Ok(operation) = self.doc.operations.get(self.operation_name) {
124 self.selection_set(&operation.selection_set)
125 } else {
126 Ok(Value::Null)
127 }
128 }
129
130 fn collect_fields(
136 &self,
137 selection_set: &SelectionSet,
138 concrete_type: &Name,
139 ) -> IndexMap<String, Vec<Node<Field>>> {
140 let mut collected: IndexMap<String, Vec<Node<Field>>> = IndexMap::new();
141
142 for selection in &selection_set.selections {
143 match selection {
144 Selection::Field(field) => {
145 let key = field.alias.as_ref().unwrap_or(&field.name).to_string();
146 collected.entry(key).or_default().push(field.clone());
147 }
148 Selection::FragmentSpread(fragment) => {
149 if let Some(fragment_def) = self.doc.fragments.get(&fragment.fragment_name) {
150 if self.type_condition_matches(fragment_def.type_condition(), concrete_type)
151 {
152 for (key, mut fields) in
153 self.collect_fields(&fragment_def.selection_set, concrete_type)
154 {
155 collected.entry(key).or_default().append(&mut fields);
156 }
157 }
158 }
159 }
160 Selection::InlineFragment(inline_fragment) => {
161 let matches = match &inline_fragment.type_condition {
162 None => true,
163 Some(cond) => self.type_condition_matches(cond, concrete_type),
164 };
165 if matches {
166 for (key, mut fields) in
167 self.collect_fields(&inline_fragment.selection_set, concrete_type)
168 {
169 collected.entry(key).or_default().append(&mut fields);
170 }
171 }
172 }
173 }
174 }
175
176 collected
177 }
178
179 fn concrete_type<'s>(&mut self, ty: &'s Name) -> Result<&'s Name, ResponseError>
184 where
185 'schema: 's,
186 {
187 match self.schema.types.get(ty) {
188 Some(ExtendedType::Union(union_ty)) => {
189 let idx = self.rng.choose_index(union_ty.members.len())?;
190 let member = union_ty
191 .members
192 .get_index(idx)
193 .expect("choose_index returned valid index");
194 Ok(&member.name)
195 }
196 Some(ExtendedType::Interface(_)) => {
197 let count = self
198 .schema
199 .types
200 .values()
201 .filter(|t| {
202 matches!(t, ExtendedType::Object(obj) if obj.implements_interfaces.contains(ty))
203 })
204 .count();
205 if count == 0 {
206 return Ok(ty);
207 }
208 let idx = self.rng.choose_index(count)?;
209 let chosen = self
210 .schema
211 .types
212 .iter()
213 .filter_map(|(name, t)| match t {
214 ExtendedType::Object(obj) if obj.implements_interfaces.contains(ty) => {
215 Some(name)
216 }
217 _ => None,
218 })
219 .nth(idx)
220 .expect("idx came from counting the same filter");
221 Ok(chosen)
222 }
223 _ => Ok(ty),
224 }
225 }
226
227 fn type_condition_matches(&self, cond: &Name, concrete: &Name) -> bool {
230 if cond == concrete {
231 return true;
232 }
233 match self.schema.types.get(cond) {
234 Some(ExtendedType::Interface(_)) => matches!(
235 self.schema.types.get(concrete),
236 Some(ExtendedType::Object(obj)) if obj.implements_interfaces.contains(cond)
237 ),
238 Some(ExtendedType::Union(union_ty)) => {
239 union_ty.members.iter().any(|m| m.name == *concrete)
240 }
241 _ => false,
242 }
243 }
244
245 fn selection_set(&mut self, selection_set: &SelectionSet) -> Result<Value, ResponseError> {
246 let concrete = self.concrete_type(&selection_set.ty)?;
247 let grouped_fields = self.collect_fields(selection_set, concrete);
248
249 if let Some(result) =
250 self.generators
251 .try_generate(&selection_set.ty, self.rng, &grouped_fields)
252 {
253 return result;
254 }
255
256 let mut result = Map::new();
257
258 for (key, fields) in grouped_fields {
259 let meta_field = &fields[0];
261
262 let val = if meta_field.name == TYPENAME {
263 Value::String(concrete.to_string().into())
264 } else if !meta_field.ty().is_non_null() && self.should_be_null()? {
265 Value::Null
266 } else {
267 self.generate_field_value(&fields, meta_field)?
268 };
269
270 result.insert(key, val);
271 }
272
273 Ok(Value::Object(result))
274 }
275
276 fn generate_field_value(
278 &mut self,
279 fields: &[Node<Field>],
280 meta_field: &Node<Field>,
281 ) -> Result<Value, ResponseError> {
282 let has_selection_set = !meta_field.selection_set.is_empty();
283 let is_list = meta_field.ty().is_list();
284
285 if has_selection_set {
286 let mut merged_selections = Vec::new();
288 for field in fields {
289 merged_selections.extend_from_slice(&field.selection_set.selections);
290 }
291 let full_selection_set = SelectionSet {
292 ty: meta_field.selection_set.ty.clone(),
293 selections: merged_selections,
294 };
295
296 if is_list {
297 self.repeated_selection_set(&full_selection_set)
298 } else {
299 self.selection_set(&full_selection_set)
300 }
301 } else if is_list {
302 self.repeated_leaf_field(meta_field.ty().inner_named_type())
303 } else {
304 self.leaf_field(meta_field.ty().inner_named_type())
305 }
306 }
307
308 fn repeated_selection_set(
309 &mut self,
310 selection_set: &SelectionSet,
311 ) -> Result<Value, ResponseError> {
312 let num_values = self.arbitrary_len()?;
313 let mut values = Vec::with_capacity(num_values);
314 for _ in 0..num_values {
315 values.push(self.selection_set(selection_set)?);
316 }
317 Ok(Value::Array(values))
318 }
319
320 fn leaf_field(&mut self, type_name: &Name) -> Result<Value, ResponseError> {
321 let extended_ty = self
322 .schema
323 .types
324 .get(type_name)
325 .expect("validated schema should contain the type");
326 match extended_ty {
327 ExtendedType::Enum(enum_ty) => {
328 let idx = self.rng.choose_index(enum_ty.values.len())?;
329 let enum_value = enum_ty
330 .values
331 .values()
332 .nth(idx)
333 .expect("choose_index returned valid index");
334 Ok(Value::String(enum_value.value.to_string().into()))
335 }
336 ExtendedType::Scalar(scalar) => self.generators.generate_scalar(&scalar.name, self.rng),
337 _ => unreachable!("A field with an empty selection set must be a scalar or enum type"),
338 }
339 }
340
341 fn repeated_leaf_field(&mut self, type_name: &Name) -> Result<Value, ResponseError> {
342 let num_values = self.arbitrary_len()?;
343 let mut values = Vec::with_capacity(num_values);
344 for _ in 0..num_values {
345 values.push(self.leaf_field(type_name)?);
346 }
347 Ok(Value::Array(values))
348 }
349
350 fn arbitrary_len(&mut self) -> Result<usize, ResponseError> {
351 self.rng
352 .gen_usize_range(self.min_list_size, self.max_list_size)
353 }
354
355 fn should_be_null(&mut self) -> Result<bool, ResponseError> {
356 if let Some((numerator, denominator)) = self.null_ratio {
357 self.rng.ratio(numerator, denominator)
358 } else {
359 Ok(false)
360 }
361 }
362}
363
364#[cfg(test)]
365mod tests {
366 use super::*;
367 use crate::RandProvider;
368
369 fn build_response(schema_sdl: &str, query: &str) -> Value {
371 let schema = Schema::parse_and_validate(schema_sdl, "schema.graphql").unwrap();
372 let doc = ExecutableDocument::parse_and_validate(&schema, query, "query.graphql").unwrap();
373 let mut rng = RandProvider(rand::rng());
374 ResponseBuilder::new(&mut rng, &doc, &schema)
375 .with_null_ratio(0, 1) .with_min_list_size(1)
377 .build()
378 .unwrap()
379 }
380
381 const SIMPLE_SCHEMA: &str = r#"
382 type Query {
383 user(id: ID!): User
384 posts: [Post!]!
385 }
386 type User {
387 id: ID!
388 name: String!
389 email: String!
390 address: Address!
391 is_active: Boolean!
392 distance: Float!
393 }
394 type Address {
395 city: String!
396 state: String!
397 }
398 type Post {
399 id: ID!
400 title: String!
401 author: User!
402 views: Int!
403 }
404 "#;
405
406 const UNION_SCHEMA: &str = r#"
407 type Query {
408 user(id: ID!): User
409 }
410 type User {
411 id: ID!
412 name: String!
413 content: [Content!]!
414 }
415 type Post {
416 title: String!
417 views: Int!
418 }
419 type Article {
420 title: String!
421 citations: [String!]!
422 }
423 union Content = Post | Article
424 "#;
425
426 const INTERFACE_SCHEMA: &str = r#"
427 type Query {
428 user(id: ID!): User
429 }
430 type User {
431 id: ID!
432 name: String!
433 content: [Content!]!
434 }
435 interface Content {
436 title: String!
437 }
438 type Post implements Content {
439 title: String!
440 views: Int!
441 }
442 type Article implements Content {
443 title: String!
444 citations: [String!]!
445 }
446 "#;
447
448 #[test]
449 fn basic_response_shape() {
450 let response = build_response(
451 SIMPLE_SCHEMA,
452 "query { user(id: \"1\") { id name email is_active distance } }",
453 );
454 let data = response.get("data").expect("missing data");
455 let user = data.get("user").expect("missing user");
456 assert!(user.get("id").is_some());
457 assert!(user.get("name").is_some());
458 assert!(user.get("email").is_some());
459 assert!(user.get("is_active").unwrap().is_boolean());
460 assert!(user.get("distance").unwrap().is_number());
461 }
462
463 #[test]
464 fn nested_objects() {
465 let response = build_response(
466 SIMPLE_SCHEMA,
467 "query { user(id: \"1\") { id address { city state } } }",
468 );
469 let user = response.get("data").unwrap().get("user").unwrap();
470 let address = user.get("address").expect("missing address");
471 assert!(address.get("city").is_some());
472 assert!(address.get("state").is_some());
473 }
474
475 #[test]
476 fn list_fields() {
477 let response = build_response(
478 SIMPLE_SCHEMA,
479 "query { posts { id title author { name } views } }",
480 );
481 let posts = response
482 .get("data")
483 .unwrap()
484 .get("posts")
485 .unwrap()
486 .as_array()
487 .expect("posts should be an array");
488 assert!(!posts.is_empty(), "min_list_size is 1");
489 for post in posts {
490 assert!(post.get("id").is_some());
491 assert!(post.get("title").is_some());
492 assert!(post.get("views").is_some());
493 assert!(post.get("author").unwrap().get("name").is_some());
494 }
495 }
496
497 #[test]
498 fn alias_support() {
499 let response = build_response(
500 SIMPLE_SCHEMA,
501 r#"query { user(id: "1") { userId: id fullName: name } }"#,
502 );
503 let user = response.get("data").unwrap().get("user").unwrap();
504 assert!(
505 user.get("userId").is_some(),
506 "alias 'userId' should be the key"
507 );
508 assert!(
509 user.get("fullName").is_some(),
510 "alias 'fullName' should be the key"
511 );
512 assert!(user.get("id").is_none());
514 assert!(user.get("name").is_none());
515 }
516
517 #[test]
518 fn inline_fragment() {
519 let response = build_response(
520 SIMPLE_SCHEMA,
521 r#"query { user(id: "1") { id ... on User { name email } } }"#,
522 );
523 let user = response.get("data").unwrap().get("user").unwrap();
524 assert!(user.get("id").is_some());
525 assert!(
526 user.get("name").is_some(),
527 "inline fragment field should be present"
528 );
529 assert!(
530 user.get("email").is_some(),
531 "inline fragment field should be present"
532 );
533 }
534
535 #[test]
536 fn fragment_spread() {
537 let response = build_response(
538 SIMPLE_SCHEMA,
539 r#"
540 fragment UserDetails on User {
541 name
542 email
543 address { city state }
544 }
545 query { user(id: "1") { id ...UserDetails } }
546 "#,
547 );
548 let user = response.get("data").unwrap().get("user").unwrap();
549 assert!(user.get("id").is_some());
550 assert!(
551 user.get("name").is_some(),
552 "fragment field should be present"
553 );
554 assert!(
555 user.get("email").is_some(),
556 "fragment field should be present"
557 );
558 let address = user
559 .get("address")
560 .expect("fragment nested object should be present");
561 assert!(address.get("city").is_some());
562 assert!(address.get("state").is_some());
563 }
564
565 #[test]
566 fn union_typename_picks_member() {
567 let schema = Schema::parse_and_validate(UNION_SCHEMA, "schema.graphql").unwrap();
568 let query = r#"
569 query {
570 user(id: "1") {
571 content {
572 __typename
573 ... on Post { title views }
574 ... on Article { title citations }
575 }
576 }
577 }
578 "#;
579 let doc = ExecutableDocument::parse_and_validate(&schema, query, "query.graphql").unwrap();
580
581 let mut seen_post = false;
582 let mut seen_article = false;
583
584 for _ in 0..100 {
586 let mut rng = RandProvider(rand::rng());
587 let response = ResponseBuilder::new(&mut rng, &doc, &schema)
588 .with_null_ratio(0, 1)
589 .with_min_list_size(1)
590 .build()
591 .unwrap();
592
593 let content = response
594 .get("data")
595 .unwrap()
596 .get("user")
597 .unwrap()
598 .get("content")
599 .unwrap()
600 .as_array()
601 .unwrap();
602
603 for item in content {
604 let typename = item.get("__typename").unwrap().as_str().unwrap();
605 assert_ne!(typename, "Content", "__typename must not be the union name");
606 assert!(
607 typename == "Post" || typename == "Article",
608 "unexpected __typename: {typename}"
609 );
610 match typename {
613 "Post" => {
614 assert!(item.get("title").is_some(), "Post should have title");
615 assert!(item.get("views").is_some(), "Post should have views");
616 assert!(
617 item.get("citations").is_none(),
618 "Post should not have citations"
619 );
620 seen_post = true;
621 }
622 "Article" => {
623 assert!(item.get("title").is_some(), "Article should have title");
624 assert!(
625 item.get("citations").is_some(),
626 "Article should have citations"
627 );
628 assert!(item.get("views").is_none(), "Article should not have views");
629 seen_article = true
630 }
631 _ => unreachable!(),
632 }
633 }
634 }
635
636 assert!(seen_post, "should have seen Post at least once in 100 runs");
637 assert!(
638 seen_article,
639 "should have seen Article at least once in 100 runs"
640 );
641 }
642
643 #[test]
644 fn interface_typename_picks_implementer() {
645 let schema = Schema::parse_and_validate(INTERFACE_SCHEMA, "schema.graphql").unwrap();
646 let query = r#"
649 query {
650 user(id: "1") {
651 content {
652 __typename
653 title
654 ... on Post { views }
655 ... on Article { citations }
656 }
657 }
658 }
659 "#;
660 let doc = ExecutableDocument::parse_and_validate(&schema, query, "query.graphql").unwrap();
661
662 let mut seen_post = false;
663 let mut seen_article = false;
664
665 for _ in 0..100 {
666 let mut rng = RandProvider(rand::rng());
667 let response = ResponseBuilder::new(&mut rng, &doc, &schema)
668 .with_null_ratio(0, 1)
669 .with_min_list_size(1)
670 .build()
671 .unwrap();
672
673 let content = response
674 .get("data")
675 .unwrap()
676 .get("user")
677 .unwrap()
678 .get("content")
679 .unwrap()
680 .as_array()
681 .unwrap();
682
683 for item in content {
684 let typename = item.get("__typename").unwrap().as_str().unwrap();
685 assert_ne!(
686 typename, "Content",
687 "__typename must not be the interface name"
688 );
689 assert!(
690 typename == "Post" || typename == "Article",
691 "unexpected __typename: {typename}"
692 );
693 assert!(
696 item.get("title").is_some(),
697 "title should always be present"
698 );
699 match typename {
700 "Post" => {
701 assert!(item.get("views").is_some(), "Post should have views");
702 assert!(
703 item.get("citations").is_none(),
704 "Post should not have citations"
705 );
706 seen_post = true;
707 }
708 "Article" => {
709 assert!(
710 item.get("citations").is_some(),
711 "Article should have citations"
712 );
713 assert!(item.get("views").is_none(), "Article should not have views");
714 seen_article = true;
715 }
716 _ => unreachable!(),
717 }
718 }
719 }
720
721 assert!(seen_post, "should have seen Post at least once in 100 runs");
722 assert!(
723 seen_article,
724 "should have seen Article at least once in 100 runs"
725 );
726 }
727
728 #[test]
729 fn custom_object_override() {
730 let schema_with_service = r#"
731 type Query {
732 _service: _Service!
733 }
734 type _Service {
735 sdl: String!
736 }
737 "#;
738 let schema = Schema::parse_and_validate(schema_with_service, "schema.graphql").unwrap();
739 let query = "query { _service { sdl } }";
740 let doc = ExecutableDocument::parse_and_validate(&schema, query, "query.graphql").unwrap();
741
742 struct SDLGenerator {
743 sdl: String,
744 }
745
746 impl<R: RandomProvider> Generator<R> for SDLGenerator {
747 fn generate(
748 &mut self,
749 _rng: &mut R,
750 _generators: &mut Generators<R>,
751 fields: &IndexMap<String, Vec<Node<Field>>>,
752 ) -> Result<Value, ResponseError> {
753 let mut service_obj = Map::new();
754 for (key, group) in fields {
755 if group[0].name == "sdl" {
756 service_obj.insert(key.clone(), Value::String(self.sdl.clone().into()));
757 }
758 }
759 Ok(Value::Object(service_obj))
760 }
761 }
762
763 let custom_sdl = "type Query { hello: String }";
764 let mut rng = RandProvider(rand::rng());
765 let response = ResponseBuilder::new(&mut rng, &doc, &schema)
766 .with_generator(
767 Name::new_unchecked("_Service"),
768 SDLGenerator {
769 sdl: custom_sdl.to_owned(),
770 },
771 )
772 .build()
773 .unwrap();
774
775 let sdl = response
776 .get("data")
777 .unwrap()
778 .get("_service")
779 .unwrap()
780 .get("sdl")
781 .unwrap()
782 .as_str()
783 .unwrap();
784
785 assert_eq!(sdl, custom_sdl);
786 }
787
788 #[test]
789 fn custom_scalar_override() {
790 let schema_sdl = r#"
791 scalar UUID
792 type Query {
793 id: UUID!
794 }
795 "#;
796 let schema = Schema::parse_and_validate(schema_sdl, "schema.graphql").unwrap();
797 let query = "query { id }";
798 let doc = ExecutableDocument::parse_and_validate(&schema, query, "query.graphql").unwrap();
799
800 struct ConstantGenerator(&'static str);
801
802 impl<R: RandomProvider> Generator<R> for ConstantGenerator {
803 fn generate(
804 &mut self,
805 _rng: &mut R,
806 _generators: &mut Generators<R>,
807 _fields: &IndexMap<String, Vec<Node<Field>>>,
808 ) -> Result<Value, ResponseError> {
809 Ok(Value::String(self.0.into()))
810 }
811 }
812
813 let mut rng = RandProvider(rand::rng());
814 let response = ResponseBuilder::new(&mut rng, &doc, &schema)
815 .with_generator(
816 Name::new_unchecked("UUID"),
817 ConstantGenerator("00000000-0000-0000-0000-000000000000"),
818 )
819 .build()
820 .unwrap();
821
822 let id = response
823 .get("data")
824 .unwrap()
825 .get("id")
826 .unwrap()
827 .as_str()
828 .unwrap();
829 assert_eq!(id, "00000000-0000-0000-0000-000000000000");
830 }
831
832 #[test]
833 fn object_generator_delegates_to_scalar_generators() {
834 let schema = Schema::parse_and_validate(SIMPLE_SCHEMA, "schema.graphql").unwrap();
838 let query = r#"query { user(id: "1") { id name } }"#;
839 let doc = ExecutableDocument::parse_and_validate(&schema, query, "query.graphql").unwrap();
840
841 struct UserGenerator;
842 impl<R: RandomProvider> Generator<R> for UserGenerator {
843 fn generate(
844 &mut self,
845 rng: &mut R,
846 generators: &mut Generators<R>,
847 fields: &IndexMap<String, Vec<Node<Field>>>,
848 ) -> Result<Value, ResponseError> {
849 let mut obj = Map::new();
850 for (key, group) in fields {
851 let scalar_name = group[0].ty().inner_named_type();
852 obj.insert(key.clone(), generators.generate_scalar(scalar_name, rng)?);
853 }
854 Ok(Value::Object(obj))
855 }
856 }
857
858 struct ConstantGenerator(&'static str);
859 impl<R: RandomProvider> Generator<R> for ConstantGenerator {
860 fn generate(
861 &mut self,
862 _rng: &mut R,
863 _generators: &mut Generators<R>,
864 _fields: &IndexMap<String, Vec<Node<Field>>>,
865 ) -> Result<Value, ResponseError> {
866 Ok(Value::String(self.0.into()))
867 }
868 }
869
870 let mut rng = RandProvider(rand::rng());
871 let response = ResponseBuilder::new(&mut rng, &doc, &schema)
872 .with_generator(Name::new_unchecked("ID"), ConstantGenerator("user-id"))
873 .with_generator(Name::new_unchecked("User"), UserGenerator)
874 .build()
875 .unwrap();
876
877 let user = response.get("data").unwrap().get("user").unwrap();
878 assert_eq!(user.get("id").unwrap().as_str().unwrap(), "user-id");
879 let name = user.get("name").unwrap().as_str().unwrap();
882 assert!((1..=10).contains(&name.len()));
883 assert!(name.chars().all(|c| c.is_ascii_alphanumeric()));
884 }
885}