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::ByteString;
17use serde_json_bytes::Map;
18use serde_json_bytes::Value;
19
20const TYPENAME: &str = "__typename";
21
22pub struct ResponseBuilder<'a, 'doc, 'schema, R: RandomProvider> {
38 rng: &'a mut R,
39 doc: &'doc Valid<ExecutableDocument>,
40 schema: &'schema Valid<Schema>,
41 generators: Generators<R>,
42 min_list_size: usize,
43 max_list_size: usize,
44 null_ratio: Option<(u32, u32)>,
45 operation_name: Option<&'doc str>,
46 partial_data: Option<Value>,
47}
48
49impl<'a, 'doc, 'schema, R: RandomProvider> ResponseBuilder<'a, 'doc, 'schema, R> {
50 pub fn new(
52 rng: &'a mut R,
53 doc: &'doc Valid<ExecutableDocument>,
54 schema: &'schema Valid<Schema>,
55 ) -> Self {
56 Self {
57 rng,
58 doc,
59 schema,
60 generators: Generators::default(),
61 min_list_size: 0,
62 max_list_size: 5,
63 null_ratio: None,
64 operation_name: None,
65 partial_data: None,
66 }
67 }
68
69 pub fn with_generator<G>(mut self, type_name: Name, generator: G) -> Self
80 where
81 G: Generator<R> + 'static,
82 {
83 self.generators.insert(type_name, Box::new(generator));
84 self
85 }
86
87 pub fn with_min_list_size(mut self, min_size: usize) -> Self {
89 self.min_list_size = min_size;
90 self
91 }
92
93 pub fn with_max_list_size(mut self, max_size: usize) -> Self {
95 self.max_list_size = max_size;
96 self
97 }
98
99 pub fn with_null_ratio(mut self, numerator: u32, denominator: u32) -> Self {
101 self.null_ratio = Some((numerator, denominator));
102 self
103 }
104
105 pub fn with_operation_name(mut self, operation_name: Option<&'doc str>) -> Self {
108 self.operation_name = operation_name;
109 self
110 }
111
112 pub fn with_partial_data(mut self, data: Value) -> Self {
131 self.partial_data = Some(data);
132 self
133 }
134
135 pub fn build(mut self) -> Result<Value, ResponseError> {
137 let data = self.build_data()?;
138 Ok(json!({ "data": data }))
139 }
140
141 pub fn build_data(&mut self) -> Result<Value, ResponseError> {
145 let Ok(operation) = self.doc.operations.get(self.operation_name) else {
146 return Ok(Value::Null);
147 };
148
149 let partial_data = self.partial_data.take();
152 let result = match &partial_data {
153 Some(Value::Object(overlay)) => {
154 let selection_set = &operation.selection_set;
155 let concrete = self.concrete_type(&selection_set.ty)?.clone();
156 self.overlaid_object(selection_set, &concrete, overlay)
157 }
158 _ => self.selection_set(&operation.selection_set),
159 };
160 self.partial_data = partial_data;
161 result
162 }
163
164 fn collect_fields(
170 &self,
171 selection_set: &SelectionSet,
172 concrete_type: &Name,
173 ) -> IndexMap<String, Vec<Node<Field>>> {
174 let mut collected: IndexMap<String, Vec<Node<Field>>> = IndexMap::new();
175
176 for selection in &selection_set.selections {
177 match selection {
178 Selection::Field(field) => {
179 let key = field.alias.as_ref().unwrap_or(&field.name).to_string();
180 collected.entry(key).or_default().push(field.clone());
181 }
182 Selection::FragmentSpread(fragment) => {
183 if let Some(fragment_def) = self.doc.fragments.get(&fragment.fragment_name) {
184 if self.type_condition_matches(fragment_def.type_condition(), concrete_type)
185 {
186 for (key, mut fields) in
187 self.collect_fields(&fragment_def.selection_set, concrete_type)
188 {
189 collected.entry(key).or_default().append(&mut fields);
190 }
191 }
192 }
193 }
194 Selection::InlineFragment(inline_fragment) => {
195 let matches = match &inline_fragment.type_condition {
196 None => true,
197 Some(cond) => self.type_condition_matches(cond, concrete_type),
198 };
199 if matches {
200 for (key, mut fields) in
201 self.collect_fields(&inline_fragment.selection_set, concrete_type)
202 {
203 collected.entry(key).or_default().append(&mut fields);
204 }
205 }
206 }
207 }
208 }
209
210 collected
211 }
212
213 fn concrete_type<'s>(&mut self, ty: &'s Name) -> Result<&'s Name, ResponseError>
218 where
219 'schema: 's,
220 {
221 match self.schema.types.get(ty) {
222 Some(ExtendedType::Union(union_ty)) => {
223 let idx = self.rng.choose_index(union_ty.members.len())?;
224 let member = union_ty
225 .members
226 .get_index(idx)
227 .expect("choose_index returned valid index");
228 Ok(member.as_ref())
229 }
230 Some(ExtendedType::Interface(_)) => {
231 let count = self
232 .schema
233 .types
234 .values()
235 .filter(|t| {
236 matches!(t, ExtendedType::Object(obj) if obj.implements_interfaces.contains(ty))
237 })
238 .count();
239 if count == 0 {
240 return Ok(ty);
241 }
242 let idx = self.rng.choose_index(count)?;
243 let chosen = self
244 .schema
245 .types
246 .iter()
247 .filter_map(|(name, t)| match t {
248 ExtendedType::Object(obj) if obj.implements_interfaces.contains(ty) => {
249 Some(name)
250 }
251 _ => None,
252 })
253 .nth(idx)
254 .expect("idx came from counting the same filter");
255 Ok(chosen)
256 }
257 _ => Ok(ty),
258 }
259 }
260
261 fn type_condition_matches(&self, cond: &Name, concrete: &Name) -> bool {
264 if cond == concrete {
265 return true;
266 }
267 match self.schema.types.get(cond) {
268 Some(ExtendedType::Interface(_)) => matches!(
269 self.schema.types.get(concrete),
270 Some(ExtendedType::Object(obj)) if obj.implements_interfaces.contains(cond)
271 ),
272 Some(ExtendedType::Union(union_ty)) => {
273 union_ty.members.iter().any(|m| **m == *concrete)
274 }
275 _ => false,
276 }
277 }
278
279 fn selection_set(&mut self, selection_set: &SelectionSet) -> Result<Value, ResponseError> {
280 let concrete = self.concrete_type(&selection_set.ty)?;
281 let grouped_fields = self.collect_fields(selection_set, concrete);
282
283 if let Some(result) =
284 self.generators
285 .try_generate(&selection_set.ty, self.rng, &grouped_fields)
286 {
287 return result;
288 }
289
290 let mut result = Map::new();
291
292 for (key, fields) in grouped_fields {
293 let meta_field = &fields[0];
295
296 let val = if meta_field.name == TYPENAME {
297 Value::String(concrete.to_string().into())
298 } else if !meta_field.ty().is_non_null() && self.should_be_null()? {
299 Value::Null
300 } else {
301 self.generate_field_value(&fields, meta_field)?
302 };
303
304 result.insert(key, val);
305 }
306
307 Ok(Value::Object(result))
308 }
309
310 fn generate_field_value(
312 &mut self,
313 fields: &[Node<Field>],
314 meta_field: &Node<Field>,
315 ) -> Result<Value, ResponseError> {
316 let has_selection_set = !meta_field.selection_set.is_empty();
317 let is_list = meta_field.ty().is_list();
318
319 if has_selection_set {
320 let mut merged_selections = Vec::new();
322 for field in fields {
323 merged_selections.extend_from_slice(&field.selection_set.selections);
324 }
325 let full_selection_set = SelectionSet {
326 ty: meta_field.selection_set.ty.clone(),
327 selections: merged_selections,
328 };
329
330 if is_list {
331 self.repeated_selection_set(&full_selection_set)
332 } else {
333 self.selection_set(&full_selection_set)
334 }
335 } else if is_list {
336 self.repeated_leaf_field(meta_field.ty().inner_named_type())
337 } else {
338 self.leaf_field(meta_field.ty().inner_named_type())
339 }
340 }
341
342 fn repeated_selection_set(
343 &mut self,
344 selection_set: &SelectionSet,
345 ) -> Result<Value, ResponseError> {
346 let num_values = self.arbitrary_len()?;
347 let mut values = Vec::with_capacity(num_values);
348 for _ in 0..num_values {
349 values.push(self.selection_set(selection_set)?);
350 }
351 Ok(Value::Array(values))
352 }
353
354 fn overlaid_object(
359 &mut self,
360 selection_set: &SelectionSet,
361 concrete: &Name,
362 overlay: &Map<ByteString, Value>,
363 ) -> Result<Value, ResponseError> {
364 let grouped_fields = self.collect_fields(selection_set, concrete);
365
366 let mut result = Map::new();
367
368 for (key, fields) in grouped_fields {
369 let meta_field = &fields[0];
370
371 let val = if meta_field.name == TYPENAME {
372 Value::String(concrete.to_string().into())
373 } else if let Some(overlay_value) = overlay.get(key.as_str()) {
374 self.overlaid_value(&fields, meta_field, overlay_value)?
375 } else if !meta_field.ty().is_non_null() && self.should_be_null()? {
376 Value::Null
377 } else {
378 self.generate_field_value(&fields, meta_field)?
379 };
380
381 result.insert(key, val);
382 }
383
384 Ok(Value::Object(result))
385 }
386
387 fn overlaid_value(
391 &mut self,
392 fields: &[Node<Field>],
393 meta_field: &Node<Field>,
394 overlay_value: &Value,
395 ) -> Result<Value, ResponseError> {
396 if meta_field.selection_set.is_empty() {
397 return Ok(overlay_value.clone());
398 }
399
400 let mut merged_selections = Vec::new();
401 for field in fields {
402 merged_selections.extend_from_slice(&field.selection_set.selections);
403 }
404 let full_selection_set = SelectionSet {
405 ty: meta_field.selection_set.ty.clone(),
406 selections: merged_selections,
407 };
408
409 if meta_field.ty().is_list() {
410 if let Some(items) = overlay_value.as_array() {
411 let mut values = Vec::with_capacity(items.len());
412 for item in items {
413 values.push(self.overlaid_composite(&full_selection_set, item)?);
414 }
415 Ok(Value::Array(values))
416 } else {
417 Ok(overlay_value.clone())
418 }
419 } else {
420 self.overlaid_composite(&full_selection_set, overlay_value)
421 }
422 }
423
424 fn overlaid_composite(
428 &mut self,
429 selection_set: &SelectionSet,
430 overlay_value: &Value,
431 ) -> Result<Value, ResponseError> {
432 let Some(overlay) = overlay_value.as_object() else {
433 return Ok(overlay_value.clone());
434 };
435 let concrete = match overlay.get(TYPENAME).and_then(|value| value.as_str()) {
436 Some(type_name) => match Name::new(type_name) {
437 Ok(name) if self.schema.types.contains_key(&name) => name,
438 _ => return Ok(Value::Null),
439 },
440 None => self.concrete_type(&selection_set.ty)?.clone(),
441 };
442 self.overlaid_object(selection_set, &concrete, overlay)
443 }
444
445 fn leaf_field(&mut self, type_name: &Name) -> Result<Value, ResponseError> {
446 let extended_ty = self
447 .schema
448 .types
449 .get(type_name)
450 .expect("validated schema should contain the type");
451 match extended_ty {
452 ExtendedType::Enum(enum_ty) => {
453 let idx = self.rng.choose_index(enum_ty.values.len())?;
454 let enum_value = enum_ty
455 .values
456 .values()
457 .nth(idx)
458 .expect("choose_index returned valid index");
459 Ok(Value::String(enum_value.value.to_string().into()))
460 }
461 ExtendedType::Scalar(scalar) => self.generators.generate_scalar(&scalar.name, self.rng),
462 _ => unreachable!("A field with an empty selection set must be a scalar or enum type"),
463 }
464 }
465
466 fn repeated_leaf_field(&mut self, type_name: &Name) -> Result<Value, ResponseError> {
467 let num_values = self.arbitrary_len()?;
468 let mut values = Vec::with_capacity(num_values);
469 for _ in 0..num_values {
470 values.push(self.leaf_field(type_name)?);
471 }
472 Ok(Value::Array(values))
473 }
474
475 fn arbitrary_len(&mut self) -> Result<usize, ResponseError> {
476 self.rng
477 .gen_usize_range(self.min_list_size, self.max_list_size)
478 }
479
480 fn should_be_null(&mut self) -> Result<bool, ResponseError> {
481 if let Some((numerator, denominator)) = self.null_ratio {
482 self.rng.ratio(numerator, denominator)
483 } else {
484 Ok(false)
485 }
486 }
487}
488
489#[cfg(test)]
490mod tests {
491 use super::*;
492 use crate::RandProvider;
493
494 fn build_response(schema_sdl: &str, query: &str) -> Value {
496 let schema = Schema::parse_and_validate(schema_sdl, "schema.graphql").unwrap();
497 let doc = ExecutableDocument::parse_and_validate(&schema, query, "query.graphql").unwrap();
498 let mut rng = RandProvider(rand::rng());
499 ResponseBuilder::new(&mut rng, &doc, &schema)
500 .with_null_ratio(0, 1) .with_min_list_size(1)
502 .build()
503 .unwrap()
504 }
505
506 const SIMPLE_SCHEMA: &str = r#"
507 type Query {
508 user(id: ID!): User
509 posts: [Post!]!
510 }
511 type User {
512 id: ID!
513 name: String!
514 email: String!
515 address: Address!
516 is_active: Boolean!
517 distance: Float!
518 }
519 type Address {
520 city: String!
521 state: String!
522 }
523 type Post {
524 id: ID!
525 title: String!
526 author: User!
527 views: Int!
528 }
529 "#;
530
531 const UNION_SCHEMA: &str = r#"
532 type Query {
533 user(id: ID!): User
534 }
535 type User {
536 id: ID!
537 name: String!
538 content: [Content!]!
539 }
540 type Post {
541 title: String!
542 views: Int!
543 }
544 type Article {
545 title: String!
546 citations: [String!]!
547 }
548 union Content = Post | Article
549 "#;
550
551 const INTERFACE_SCHEMA: &str = r#"
552 type Query {
553 user(id: ID!): User
554 }
555 type User {
556 id: ID!
557 name: String!
558 content: [Content!]!
559 }
560 interface Content {
561 title: String!
562 }
563 type Post implements Content {
564 title: String!
565 views: Int!
566 }
567 type Article implements Content {
568 title: String!
569 citations: [String!]!
570 }
571 "#;
572
573 #[test]
574 fn basic_response_shape() {
575 let response = build_response(
576 SIMPLE_SCHEMA,
577 "query { user(id: \"1\") { id name email is_active distance } }",
578 );
579 let data = response.get("data").expect("missing data");
580 let user = data.get("user").expect("missing user");
581 assert!(user.get("id").is_some());
582 assert!(user.get("name").is_some());
583 assert!(user.get("email").is_some());
584 assert!(user.get("is_active").unwrap().is_boolean());
585 assert!(user.get("distance").unwrap().is_number());
586 }
587
588 #[test]
589 fn nested_objects() {
590 let response = build_response(
591 SIMPLE_SCHEMA,
592 "query { user(id: \"1\") { id address { city state } } }",
593 );
594 let user = response.get("data").unwrap().get("user").unwrap();
595 let address = user.get("address").expect("missing address");
596 assert!(address.get("city").is_some());
597 assert!(address.get("state").is_some());
598 }
599
600 #[test]
601 fn list_fields() {
602 let response = build_response(
603 SIMPLE_SCHEMA,
604 "query { posts { id title author { name } views } }",
605 );
606 let posts = response
607 .get("data")
608 .unwrap()
609 .get("posts")
610 .unwrap()
611 .as_array()
612 .expect("posts should be an array");
613 assert!(!posts.is_empty(), "min_list_size is 1");
614 for post in posts {
615 assert!(post.get("id").is_some());
616 assert!(post.get("title").is_some());
617 assert!(post.get("views").is_some());
618 assert!(post.get("author").unwrap().get("name").is_some());
619 }
620 }
621
622 #[test]
623 fn alias_support() {
624 let response = build_response(
625 SIMPLE_SCHEMA,
626 r#"query { user(id: "1") { userId: id fullName: name } }"#,
627 );
628 let user = response.get("data").unwrap().get("user").unwrap();
629 assert!(
630 user.get("userId").is_some(),
631 "alias 'userId' should be the key"
632 );
633 assert!(
634 user.get("fullName").is_some(),
635 "alias 'fullName' should be the key"
636 );
637 assert!(user.get("id").is_none());
639 assert!(user.get("name").is_none());
640 }
641
642 #[test]
643 fn inline_fragment() {
644 let response = build_response(
645 SIMPLE_SCHEMA,
646 r#"query { user(id: "1") { id ... on User { name email } } }"#,
647 );
648 let user = response.get("data").unwrap().get("user").unwrap();
649 assert!(user.get("id").is_some());
650 assert!(
651 user.get("name").is_some(),
652 "inline fragment field should be present"
653 );
654 assert!(
655 user.get("email").is_some(),
656 "inline fragment field should be present"
657 );
658 }
659
660 #[test]
661 fn fragment_spread() {
662 let response = build_response(
663 SIMPLE_SCHEMA,
664 r#"
665 fragment UserDetails on User {
666 name
667 email
668 address { city state }
669 }
670 query { user(id: "1") { id ...UserDetails } }
671 "#,
672 );
673 let user = response.get("data").unwrap().get("user").unwrap();
674 assert!(user.get("id").is_some());
675 assert!(
676 user.get("name").is_some(),
677 "fragment field should be present"
678 );
679 assert!(
680 user.get("email").is_some(),
681 "fragment field should be present"
682 );
683 let address = user
684 .get("address")
685 .expect("fragment nested object should be present");
686 assert!(address.get("city").is_some());
687 assert!(address.get("state").is_some());
688 }
689
690 #[test]
691 fn union_typename_picks_member() {
692 let schema = Schema::parse_and_validate(UNION_SCHEMA, "schema.graphql").unwrap();
693 let query = r#"
694 query {
695 user(id: "1") {
696 content {
697 __typename
698 ... on Post { title views }
699 ... on Article { title citations }
700 }
701 }
702 }
703 "#;
704 let doc = ExecutableDocument::parse_and_validate(&schema, query, "query.graphql").unwrap();
705
706 let mut seen_post = false;
707 let mut seen_article = false;
708
709 for _ in 0..100 {
711 let mut rng = RandProvider(rand::rng());
712 let response = ResponseBuilder::new(&mut rng, &doc, &schema)
713 .with_null_ratio(0, 1)
714 .with_min_list_size(1)
715 .build()
716 .unwrap();
717
718 let content = response
719 .get("data")
720 .unwrap()
721 .get("user")
722 .unwrap()
723 .get("content")
724 .unwrap()
725 .as_array()
726 .unwrap();
727
728 for item in content {
729 let typename = item.get("__typename").unwrap().as_str().unwrap();
730 assert_ne!(typename, "Content", "__typename must not be the union name");
731 assert!(
732 typename == "Post" || typename == "Article",
733 "unexpected __typename: {typename}"
734 );
735 match typename {
738 "Post" => {
739 assert!(item.get("title").is_some(), "Post should have title");
740 assert!(item.get("views").is_some(), "Post should have views");
741 assert!(
742 item.get("citations").is_none(),
743 "Post should not have citations"
744 );
745 seen_post = true;
746 }
747 "Article" => {
748 assert!(item.get("title").is_some(), "Article should have title");
749 assert!(
750 item.get("citations").is_some(),
751 "Article should have citations"
752 );
753 assert!(item.get("views").is_none(), "Article should not have views");
754 seen_article = true
755 }
756 _ => unreachable!(),
757 }
758 }
759 }
760
761 assert!(seen_post, "should have seen Post at least once in 100 runs");
762 assert!(
763 seen_article,
764 "should have seen Article at least once in 100 runs"
765 );
766 }
767
768 #[test]
769 fn interface_typename_picks_implementer() {
770 let schema = Schema::parse_and_validate(INTERFACE_SCHEMA, "schema.graphql").unwrap();
771 let query = r#"
774 query {
775 user(id: "1") {
776 content {
777 __typename
778 title
779 ... on Post { views }
780 ... on Article { citations }
781 }
782 }
783 }
784 "#;
785 let doc = ExecutableDocument::parse_and_validate(&schema, query, "query.graphql").unwrap();
786
787 let mut seen_post = false;
788 let mut seen_article = false;
789
790 for _ in 0..100 {
791 let mut rng = RandProvider(rand::rng());
792 let response = ResponseBuilder::new(&mut rng, &doc, &schema)
793 .with_null_ratio(0, 1)
794 .with_min_list_size(1)
795 .build()
796 .unwrap();
797
798 let content = response
799 .get("data")
800 .unwrap()
801 .get("user")
802 .unwrap()
803 .get("content")
804 .unwrap()
805 .as_array()
806 .unwrap();
807
808 for item in content {
809 let typename = item.get("__typename").unwrap().as_str().unwrap();
810 assert_ne!(
811 typename, "Content",
812 "__typename must not be the interface name"
813 );
814 assert!(
815 typename == "Post" || typename == "Article",
816 "unexpected __typename: {typename}"
817 );
818 assert!(
821 item.get("title").is_some(),
822 "title should always be present"
823 );
824 match typename {
825 "Post" => {
826 assert!(item.get("views").is_some(), "Post should have views");
827 assert!(
828 item.get("citations").is_none(),
829 "Post should not have citations"
830 );
831 seen_post = true;
832 }
833 "Article" => {
834 assert!(
835 item.get("citations").is_some(),
836 "Article should have citations"
837 );
838 assert!(item.get("views").is_none(), "Article should not have views");
839 seen_article = true;
840 }
841 _ => unreachable!(),
842 }
843 }
844 }
845
846 assert!(seen_post, "should have seen Post at least once in 100 runs");
847 assert!(
848 seen_article,
849 "should have seen Article at least once in 100 runs"
850 );
851 }
852
853 #[test]
854 fn custom_object_override() {
855 let schema_with_service = r#"
856 type Query {
857 _service: _Service!
858 }
859 type _Service {
860 sdl: String!
861 }
862 "#;
863 let schema = Schema::parse_and_validate(schema_with_service, "schema.graphql").unwrap();
864 let query = "query { _service { sdl } }";
865 let doc = ExecutableDocument::parse_and_validate(&schema, query, "query.graphql").unwrap();
866
867 struct SDLGenerator {
868 sdl: String,
869 }
870
871 impl<R: RandomProvider> Generator<R> for SDLGenerator {
872 fn generate(
873 &mut self,
874 _rng: &mut R,
875 _generators: &mut Generators<R>,
876 fields: &IndexMap<String, Vec<Node<Field>>>,
877 ) -> Result<Value, ResponseError> {
878 let mut service_obj = Map::new();
879 for (key, group) in fields {
880 if group[0].name == "sdl" {
881 service_obj.insert(key.clone(), Value::String(self.sdl.clone().into()));
882 }
883 }
884 Ok(Value::Object(service_obj))
885 }
886 }
887
888 let custom_sdl = "type Query { hello: String }";
889 let mut rng = RandProvider(rand::rng());
890 let response = ResponseBuilder::new(&mut rng, &doc, &schema)
891 .with_generator(
892 Name::new_unchecked("_Service"),
893 SDLGenerator {
894 sdl: custom_sdl.to_owned(),
895 },
896 )
897 .build()
898 .unwrap();
899
900 let sdl = response
901 .get("data")
902 .unwrap()
903 .get("_service")
904 .unwrap()
905 .get("sdl")
906 .unwrap()
907 .as_str()
908 .unwrap();
909
910 assert_eq!(sdl, custom_sdl);
911 }
912
913 #[test]
914 fn custom_scalar_override() {
915 let schema_sdl = r#"
916 scalar UUID
917 type Query {
918 id: UUID!
919 }
920 "#;
921 let schema = Schema::parse_and_validate(schema_sdl, "schema.graphql").unwrap();
922 let query = "query { id }";
923 let doc = ExecutableDocument::parse_and_validate(&schema, query, "query.graphql").unwrap();
924
925 struct ConstantGenerator(&'static str);
926
927 impl<R: RandomProvider> Generator<R> for ConstantGenerator {
928 fn generate(
929 &mut self,
930 _rng: &mut R,
931 _generators: &mut Generators<R>,
932 _fields: &IndexMap<String, Vec<Node<Field>>>,
933 ) -> Result<Value, ResponseError> {
934 Ok(Value::String(self.0.into()))
935 }
936 }
937
938 let mut rng = RandProvider(rand::rng());
939 let response = ResponseBuilder::new(&mut rng, &doc, &schema)
940 .with_generator(
941 Name::new_unchecked("UUID"),
942 ConstantGenerator("00000000-0000-0000-0000-000000000000"),
943 )
944 .build()
945 .unwrap();
946
947 let id = response
948 .get("data")
949 .unwrap()
950 .get("id")
951 .unwrap()
952 .as_str()
953 .unwrap();
954 assert_eq!(id, "00000000-0000-0000-0000-000000000000");
955 }
956
957 #[test]
958 fn object_generator_delegates_to_scalar_generators() {
959 let schema = Schema::parse_and_validate(SIMPLE_SCHEMA, "schema.graphql").unwrap();
963 let query = r#"query { user(id: "1") { id name } }"#;
964 let doc = ExecutableDocument::parse_and_validate(&schema, query, "query.graphql").unwrap();
965
966 struct UserGenerator;
967 impl<R: RandomProvider> Generator<R> for UserGenerator {
968 fn generate(
969 &mut self,
970 rng: &mut R,
971 generators: &mut Generators<R>,
972 fields: &IndexMap<String, Vec<Node<Field>>>,
973 ) -> Result<Value, ResponseError> {
974 let mut obj = Map::new();
975 for (key, group) in fields {
976 let scalar_name = group[0].ty().inner_named_type();
977 obj.insert(key.clone(), generators.generate_scalar(scalar_name, rng)?);
978 }
979 Ok(Value::Object(obj))
980 }
981 }
982
983 struct ConstantGenerator(&'static str);
984 impl<R: RandomProvider> Generator<R> for ConstantGenerator {
985 fn generate(
986 &mut self,
987 _rng: &mut R,
988 _generators: &mut Generators<R>,
989 _fields: &IndexMap<String, Vec<Node<Field>>>,
990 ) -> Result<Value, ResponseError> {
991 Ok(Value::String(self.0.into()))
992 }
993 }
994
995 let mut rng = RandProvider(rand::rng());
996 let response = ResponseBuilder::new(&mut rng, &doc, &schema)
997 .with_generator(Name::new_unchecked("ID"), ConstantGenerator("user-id"))
998 .with_generator(Name::new_unchecked("User"), UserGenerator)
999 .build()
1000 .unwrap();
1001
1002 let user = response.get("data").unwrap().get("user").unwrap();
1003 assert_eq!(user.get("id").unwrap().as_str().unwrap(), "user-id");
1004 let name = user.get("name").unwrap().as_str().unwrap();
1007 assert!((1..=10).contains(&name.len()));
1008 assert!(name.chars().all(|c| c.is_ascii_alphanumeric()));
1009 }
1010
1011 fn build_with_partial_data(schema_sdl: &str, query: &str, partial: Value) -> Value {
1013 let schema = Schema::parse_and_validate(schema_sdl, "schema.graphql").unwrap();
1014 let doc = ExecutableDocument::parse_and_validate(&schema, query, "query.graphql").unwrap();
1015 let mut rng = RandProvider(rand::rng());
1016 ResponseBuilder::new(&mut rng, &doc, &schema)
1017 .with_null_ratio(0, 1)
1018 .with_min_list_size(1)
1019 .with_partial_data(partial)
1020 .build()
1021 .unwrap()
1022 }
1023
1024 #[test]
1025 fn partial_data_echoes_covered_fields_and_generates_the_rest() {
1026 let response = build_with_partial_data(
1027 SIMPLE_SCHEMA,
1028 r#"query { user(id: "1") { id name email } }"#,
1029 json!({ "user": { "id": "user-1", "name": "Alice" } }),
1030 );
1031 let user = response.get("data").unwrap().get("user").unwrap();
1032 assert_eq!(user.get("id").unwrap().as_str().unwrap(), "user-1");
1033 assert_eq!(user.get("name").unwrap().as_str().unwrap(), "Alice");
1034 assert!(
1035 user.get("email").unwrap().is_string(),
1036 "uncovered field should be generated"
1037 );
1038 }
1039
1040 #[test]
1041 fn partial_data_pins_list_length_and_order() {
1042 let response = build_with_partial_data(
1043 SIMPLE_SCHEMA,
1044 "query { posts { id title views } }",
1045 json!({ "posts": [{ "id": "p-1" }, { "id": "p-2" }, { "id": "p-3" }] }),
1046 );
1047 let posts = response
1048 .get("data")
1049 .unwrap()
1050 .get("posts")
1051 .unwrap()
1052 .as_array()
1053 .unwrap();
1054 assert_eq!(posts.len(), 3, "list length must match the partial array");
1055 for (i, post) in posts.iter().enumerate() {
1056 assert_eq!(
1057 post.get("id").unwrap().as_str().unwrap(),
1058 format!("p-{}", i + 1),
1059 "list order must match the partial array"
1060 );
1061 assert!(post.get("title").unwrap().is_string());
1062 assert!(post.get("views").unwrap().is_number());
1063 }
1064 }
1065
1066 #[test]
1067 fn partial_data_recurses_into_nested_objects() {
1068 let response = build_with_partial_data(
1069 SIMPLE_SCHEMA,
1070 r#"query { user(id: "1") { id address { city state } } }"#,
1071 json!({ "user": { "id": "user-1", "address": { "city": "Cleveland" } } }),
1072 );
1073 let user = response.get("data").unwrap().get("user").unwrap();
1074 let address = user.get("address").unwrap();
1075 assert_eq!(address.get("city").unwrap().as_str().unwrap(), "Cleveland");
1076 assert!(
1077 address.get("state").unwrap().is_string(),
1078 "uncovered nested field should be generated"
1079 );
1080 }
1081
1082 #[test]
1083 fn partial_data_typename_pins_abstract_types() {
1084 let query = r#"
1085 query {
1086 user(id: "1") {
1087 content {
1088 __typename
1089 ... on Post { title views }
1090 ... on Article { title citations }
1091 }
1092 }
1093 }
1094 "#;
1095 let response = build_with_partial_data(
1096 UNION_SCHEMA,
1097 query,
1098 json!({
1099 "user": {
1100 "content": [
1101 { "__typename": "Post", "title": "pinned post" },
1102 { "__typename": "Article" },
1103 ],
1104 },
1105 }),
1106 );
1107 let content = response
1108 .get("data")
1109 .unwrap()
1110 .get("user")
1111 .unwrap()
1112 .get("content")
1113 .unwrap()
1114 .as_array()
1115 .unwrap();
1116 assert_eq!(content.len(), 2);
1117
1118 let post = &content[0];
1119 assert_eq!(post.get("__typename").unwrap().as_str().unwrap(), "Post");
1120 assert_eq!(post.get("title").unwrap().as_str().unwrap(), "pinned post");
1121 assert!(post.get("views").unwrap().is_number());
1122 assert!(post.get("citations").is_none());
1123
1124 let article = &content[1];
1125 assert_eq!(
1126 article.get("__typename").unwrap().as_str().unwrap(),
1127 "Article"
1128 );
1129 assert!(article.get("title").unwrap().is_string());
1130 assert!(article.get("citations").unwrap().is_array());
1131 assert!(article.get("views").is_none());
1132 }
1133
1134 #[test]
1135 fn partial_data_unknown_typename_produces_null() {
1136 let query = r#"
1137 query {
1138 user(id: "1") {
1139 content {
1140 __typename
1141 ... on Post { title }
1142 }
1143 }
1144 }
1145 "#;
1146 let response = build_with_partial_data(
1147 UNION_SCHEMA,
1148 query,
1149 json!({
1150 "user": {
1151 "content": [{ "__typename": "NoSuchType" }],
1152 },
1153 }),
1154 );
1155 let content = response
1156 .get("data")
1157 .unwrap()
1158 .get("user")
1159 .unwrap()
1160 .get("content")
1161 .unwrap()
1162 .as_array()
1163 .unwrap();
1164 assert_eq!(content.len(), 1);
1165 assert!(content[0].is_null());
1166 }
1167
1168 #[test]
1169 fn partial_data_matches_by_response_key() {
1170 let response = build_with_partial_data(
1171 SIMPLE_SCHEMA,
1172 r#"query { user(id: "1") { userId: id name } }"#,
1173 json!({ "user": { "userId": "user-1" } }),
1174 );
1175 let user = response.get("data").unwrap().get("user").unwrap();
1176 assert_eq!(user.get("userId").unwrap().as_str().unwrap(), "user-1");
1177 }
1178
1179 #[test]
1180 fn partial_data_is_never_nulled() {
1181 let schema_sdl = r#"
1182 type Query {
1183 user: User
1184 }
1185 type User {
1186 id: ID
1187 name: String
1188 }
1189 "#;
1190 let schema = Schema::parse_and_validate(schema_sdl, "schema.graphql").unwrap();
1191 let query = "query { user { id name } }";
1192 let doc = ExecutableDocument::parse_and_validate(&schema, query, "query.graphql").unwrap();
1193 let mut rng = RandProvider(rand::rng());
1194 let response = ResponseBuilder::new(&mut rng, &doc, &schema)
1195 .with_null_ratio(1, 1)
1197 .with_partial_data(json!({ "user": { "id": "user-1" } }))
1198 .build()
1199 .unwrap();
1200 let user = response.get("data").unwrap().get("user").unwrap();
1201 assert_eq!(user.get("id").unwrap().as_str().unwrap(), "user-1");
1203 assert!(user.get("name").unwrap().is_null());
1204 }
1205}