Skip to main content

apollo_smith/
response.rs

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
22/// Builds a GraphQL response which matches the shape of a given executable GraphQL document.
23///
24/// `ResponseBuilder` is generic over its randomness source via the [`RandomProvider`] trait.
25/// This allows it to be used with [`arbitrary::Unstructured`] for fuzz testing, with
26/// [`RandProvider`][crate::RandProvider] for standard random generation, or with any custom
27/// implementation.
28///
29/// # Example
30///
31/// ```ignore
32/// use apollo_smith::{ResponseBuilder, RandProvider};
33///
34/// let mut rng = RandProvider(rand::rng());
35/// let response = ResponseBuilder::new(&mut rng, &doc, &schema).build()?;
36/// ```
37pub 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    /// Create a new `ResponseBuilder`.
51    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    /// Register a [`Generator`] for the named GraphQL type.
70    ///
71    /// When the builder is about to produce a value of this type — at the root or any
72    /// nested position, including each item of a list — the generator is invoked
73    /// instead of the default behavior. For object, interface, and union types, the
74    /// generator receives the requested fields with fragment spreads and inline
75    /// fragments already flattened and grouped by response key, and its return value
76    /// is used as-is (the builder does not recurse into it). For scalar types, the
77    /// `fields` argument is empty and the generator's return value replaces the
78    /// configured default.
79    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    /// Set the minimum number of items per list field. If unset, defaults to 0.
88    pub fn with_min_list_size(mut self, min_size: usize) -> Self {
89        self.min_list_size = min_size;
90        self
91    }
92
93    /// Set the maximum number of items per list field. If unset, defaults to 5.
94    pub fn with_max_list_size(mut self, max_size: usize) -> Self {
95        self.max_list_size = max_size;
96        self
97    }
98
99    /// Set the frequency of null values for nullable fields. If unset, fields will never be null.
100    pub fn with_null_ratio(mut self, numerator: u32, denominator: u32) -> Self {
101        self.null_ratio = Some((numerator, denominator));
102        self
103    }
104
105    /// Set the operation name to generate a response for. If unset, uses the anonymous operation.
106    /// If the operation does not exist, returns a response with `data: null`.
107    pub fn with_operation_name(mut self, operation_name: Option<&'doc str>) -> Self {
108        self.operation_name = operation_name;
109        self
110    }
111
112    /// Provide partial response data that the generated response must include verbatim.
113    ///
114    /// The value is a response-shaped fragment, matched against the operation's selection
115    /// set from the root. Wherever the partial data covers a requested field (matched by
116    /// response key — the alias if present, otherwise the field name), its value takes
117    /// precedence over random generation:
118    ///
119    /// - **Leaf positions** echo the partial value verbatim (never nulled by
120    ///   [`with_null_ratio`][Self::with_null_ratio]).
121    /// - **Object positions** recurse: covered sub-fields echo, uncovered sub-fields are
122    ///   generated as usual. A `__typename` entry in the partial object pins the concrete
123    ///   type used for abstract (union/interface) positions; an unknown `__typename`
124    ///   produces `null` for that position.
125    /// - **List positions** take their length and order from the partial array, recursing
126    ///   into each element.
127    ///
128    /// Fields not covered by the partial data are generated as usual. Custom
129    /// [`Generator`]s are not consulted for object positions covered by partial data.
130    pub fn with_partial_data(mut self, data: Value) -> Self {
131        self.partial_data = Some(data);
132        self
133    }
134
135    /// Builds a complete GraphQL response `Value` with a `data` key, matching the shape of `self.doc`.
136    pub fn build(mut self) -> Result<Value, ResponseError> {
137        let data = self.build_data()?;
138        Ok(json!({ "data": data }))
139    }
140
141    /// Builds just the data portion of the response (without the `{ "data": ... }` wrapper).
142    ///
143    /// This is useful if you need to manipulate the response data before wrapping it into JSON.
144    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        // Take the partial data out of `self` so the overlay walk can borrow it while
150        // generating through `&mut self`, then restore it for any subsequent build.
151        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    /// Collect fields from a selection set, grouping by response key (alias or field name).
165    ///
166    /// Inline fragments and fragment spreads are flattened into the result, but only when
167    /// their type condition applies to `concrete_type`. Fields sharing a response key are
168    /// merged into a single entry.
169    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    /// Resolve the concrete object type that will be produced for `ty`.
214    ///
215    /// For unions, a random member is chosen. For interfaces, a random implementing
216    /// object type is chosen. For object (or any other) types, `ty` is returned as-is.
217    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    /// Whether a fragment with type condition `cond` should contribute fields when the
262    /// object being generated has concrete type `concrete`.
263    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            // The first field is representative for schema-defined metadata (type, nullability, etc.)
294            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    /// Generate the value for a (possibly merged) field group.
311    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            // Merge sub-selections from all occurrences of this field
321            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    /// Like [`selection_set`][Self::selection_set], but with a fixed concrete type and an
355    /// overlay object whose values take precedence over generated ones. Requested fields
356    /// covered by the overlay (matched by response key) echo the overlay's value; the
357    /// rest are generated as usual.
358    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    /// Produce the value for a field covered by an overlay. Leaf values are echoed
388    /// verbatim; composite values recurse with the overlay's corresponding object so
389    /// that sub-selections not present in the overlay are still generated.
390    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    /// Recurse into a composite overlay value: objects are walked with
425    /// [`overlaid_object`][Self::overlaid_object] (concrete type from the object's
426    /// `__typename` when present), anything else is echoed verbatim.
427    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    /// Helper: parse schema + query, build a response with a seeded RNG.
495    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) // never null, so we can assert field presence
501            .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        // The original field names should NOT appear
638        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        // Run multiple times to increase confidence that we see both members
710        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                // Each item should only have the fields from the inline fragment whose
736                // type condition matches the chosen __typename.
737                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        // `title` is on the interface and is requested unconditionally; the inline
772        // fragments contribute fields specific to each implementer.
773        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                // `title` is selected at the interface level, so it should be present
819                // regardless of which concrete type was chosen.
820                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        // An ObjectGenerator that doesn't want to hand-roll every leaf field can
960        // delegate to the builder's registered scalar generators. Here we register a
961        // custom `ID` generator and assert the object generator reuses it.
962        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        // `name` is a String — no custom scalar registered, so it falls back to the
1005        // default alphanumeric generator (length 1–10).
1006        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    /// Helper: build a response with partial data provided.
1012    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            // Every nullable generated field becomes null...
1196            .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        // ...but the covered field is echoed, not nulled.
1202        assert_eq!(user.get("id").unwrap().as_str().unwrap(), "user-1");
1203        assert!(user.get("name").unwrap().is_null());
1204    }
1205}