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::Map;
17use serde_json_bytes::Value;
18
19const TYPENAME: &str = "__typename";
20
21/// Builds a GraphQL response which matches the shape of a given executable GraphQL document.
22///
23/// `ResponseBuilder` is generic over its randomness source via the [`RandomProvider`] trait.
24/// This allows it to be used with [`arbitrary::Unstructured`] for fuzz testing, with
25/// [`RandProvider`][crate::RandProvider] for standard random generation, or with any custom
26/// implementation.
27///
28/// # Example
29///
30/// ```ignore
31/// use apollo_smith::{ResponseBuilder, RandProvider};
32///
33/// let mut rng = RandProvider(rand::rng());
34/// let response = ResponseBuilder::new(&mut rng, &doc, &schema).build()?;
35/// ```
36pub 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    /// Create a new `ResponseBuilder`.
49    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    /// Register a [`Generator`] for the named GraphQL type.
67    ///
68    /// When the builder is about to produce a value of this type — at the root or any
69    /// nested position, including each item of a list — the generator is invoked
70    /// instead of the default behavior. For object, interface, and union types, the
71    /// generator receives the requested fields with fragment spreads and inline
72    /// fragments already flattened and grouped by response key, and its return value
73    /// is used as-is (the builder does not recurse into it). For scalar types, the
74    /// `fields` argument is empty and the generator's return value replaces the
75    /// configured default.
76    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    /// Set the minimum number of items per list field. If unset, defaults to 0.
85    pub fn with_min_list_size(mut self, min_size: usize) -> Self {
86        self.min_list_size = min_size;
87        self
88    }
89
90    /// Set the maximum number of items per list field. If unset, defaults to 5.
91    pub fn with_max_list_size(mut self, max_size: usize) -> Self {
92        self.max_list_size = max_size;
93        self
94    }
95
96    /// Set the frequency of null values for nullable fields. If unset, fields will never be null.
97    pub fn with_null_ratio(mut self, numerator: u32, denominator: u32) -> Self {
98        self.null_ratio = Some((numerator, denominator));
99        self
100    }
101
102    /// Set the operation name to generate a response for. If unset, uses the anonymous operation.
103    /// If the operation does not exist, returns a response with `data: null`.
104    pub fn with_operation_name(mut self, operation_name: Option<&'doc str>) -> Self {
105        self.operation_name = operation_name;
106        self
107    }
108
109    /// Builds a complete GraphQL response `Value` with a `data` key, matching the shape of `self.doc`.
110    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    /// Builds just the data portion of the response (without the `{ "data": ... }` wrapper).
120    ///
121    /// This is useful if you need to manipulate the response data before wrapping it into JSON.
122    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    /// Collect fields from a selection set, grouping by response key (alias or field name).
131    ///
132    /// Inline fragments and fragment spreads are flattened into the result, but only when
133    /// their type condition applies to `concrete_type`. Fields sharing a response key are
134    /// merged into a single entry.
135    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    /// Resolve the concrete object type that will be produced for `ty`.
180    ///
181    /// For unions, a random member is chosen. For interfaces, a random implementing
182    /// object type is chosen. For object (or any other) types, `ty` is returned as-is.
183    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    /// Whether a fragment with type condition `cond` should contribute fields when the
228    /// object being generated has concrete type `concrete`.
229    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            // The first field is representative for schema-defined metadata (type, nullability, etc.)
260            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    /// Generate the value for a (possibly merged) field group.
277    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            // Merge sub-selections from all occurrences of this field
287            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    /// Helper: parse schema + query, build a response with a seeded RNG.
370    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) // never null, so we can assert field presence
376            .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        // The original field names should NOT appear
513        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        // Run multiple times to increase confidence that we see both members
585        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                // Each item should only have the fields from the inline fragment whose
611                // type condition matches the chosen __typename.
612                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        // `title` is on the interface and is requested unconditionally; the inline
647        // fragments contribute fields specific to each implementer.
648        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                // `title` is selected at the interface level, so it should be present
694                // regardless of which concrete type was chosen.
695                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        // An ObjectGenerator that doesn't want to hand-roll every leaf field can
835        // delegate to the builder's registered scalar generators. Here we register a
836        // custom `ID` generator and assert the object generator reuses it.
837        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        // `name` is a String — no custom scalar registered, so it falls back to the
880        // default alphanumeric generator (length 1–10).
881        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}