Skip to main content

gqls/
example.rs

1//! Draft a ready-to-paste example operation for a matched field.
2//!
3//! Everything here is mechanical — the schema already says what the arguments
4//! are, what the field returns, and which of that type's fields are leaves. The
5//! rules, and why each one:
6//!
7//! * **Arguments you must supply become variables.** Never inline a literal
8//!   into the query body; a pasted operation should be parameterized from the
9//!   start. An argument the server can fill in for itself — nullable, or with
10//!   a schema default — is left out of the operation entirely and listed
11//!   underneath, so the query runs as-is and the knobs are still discoverable.
12//! * **A placeholder names its type.** `"<ID!>"` says both what to put there
13//!   and that it's required; `""` or `0` look like real values and get pasted
14//!   by accident.
15//! * **One level of selection by default, leaves only.** A scalar or enum
16//!   return needs no selection set at all. An object return gets its scalar/enum
17//!   fields, and a `# add fields you need` marker for the object-valued ones —
18//!   guessing how deep someone wants to go is worse than leaving a hole, and
19//!   `--depth` asks for more when you do want it.
20//! * **Abstract types get inline fragments.** A union has no fields of its own,
21//!   so it's written as `... on Member { … }` over its concrete types — the only
22//!   form a server will accept.
23//! * **Deprecated fields are flagged, not dropped.** They're still selected and
24//!   marked `# deprecated: reason`, because silently omitting a field the schema
25//!   still serves is its own surprise.
26//! * **An `errors` block only when the schema has one.** The payload/errors
27//!   convention is widespread but not universal, so it's expanded only when
28//!   that field really exists.
29//! * **A nested field is reached through a root.** `Company.employee` isn't
30//!   callable on its own, so it's wrapped in a root field that returns
31//!   `Company`. When several roots qualify, the caller is told, rather than the
32//!   pick being passed off as obvious.
33
34use std::collections::HashMap;
35
36use anyhow::{bail, Result};
37use serde_json::{Map, Value};
38
39use crate::model::{Kind, SchemaRecord};
40
41/// A drafted operation and the variables it expects.
42#[derive(Debug)]
43pub struct Example {
44    /// The GraphQL document, ready to paste.
45    pub operation: String,
46    /// A JSON object of placeholder variable values, one per required argument.
47    pub variables: Value,
48    /// Arguments left out because the server can supply them — rendered as
49    /// `field(name: Type = default)`, ready to paste back in.
50    pub optional: Vec<String>,
51    /// The input objects and enums the arguments refer to, expanded so the
52    /// variables can be filled in without opening the schema. Each entry is a
53    /// block of lines; nested input objects appear as their own entries.
54    pub input_types: Vec<Vec<String>>,
55    /// Deprecated fields the draft touched — the target itself, or anything
56    /// selected. Flagged inline too; this is for the caller to warn about.
57    pub deprecated: Vec<String>,
58    /// The root field a nested target was reached through, if it needed one.
59    pub via: Option<String>,
60    /// Other root fields that could have reached a nested target. Non-empty
61    /// only when the choice was ambiguous.
62    pub alternatives: Vec<String>,
63}
64
65/// Draft an operation that reaches `target`.
66pub fn build(target: &SchemaRecord, records: &[SchemaRecord], depth: usize) -> Result<Example> {
67    let schema = Schema::index(records);
68
69    // The chain of fields to nest, outermost first. A root operation field is
70    // already reachable; anything else needs a root that returns its parent.
71    let (chain, via, alternatives) = match target.kind {
72        Kind::Query | Kind::Mutation | Kind::Subscription => (vec![target], None, Vec::new()),
73        Kind::Field => {
74            let parent = target
75                .parent
76                .as_deref()
77                .ok_or_else(|| anyhow::anyhow!("{} has no enclosing type", target.path))?;
78            let mut roots = schema.roots_returning(parent);
79            if roots.is_empty() {
80                bail!(
81                    "{} isn't reachable in one hop — no root field returns {parent}. \
82                     Try `gqls --returns {parent}` to see what's close.",
83                    target.path
84                );
85            }
86            let chosen = roots.remove(0);
87            let via = Some(chosen.path.clone());
88            let alternatives = roots.iter().map(|r| r.path.clone()).collect();
89            (vec![chosen, target], via, alternatives)
90        }
91        other => bail!(
92            "can't draft an operation for a {} — pick a field, query, or mutation",
93            other.as_str()
94        ),
95    };
96
97    let operation_kind = match chain[0].kind {
98        Kind::Mutation => "mutation",
99        Kind::Subscription => "subscription",
100        _ => "query",
101    };
102
103    // Variables first: every argument along the chain, deduped so a repeated
104    // name (two `id`s) doesn't collide in the signature.
105    let (vars, optional) = Variables::collect(&chain);
106
107    // Then the selection, innermost outward.
108    let leaf_type = chain
109        .last()
110        .and_then(|r| r.base_type())
111        .unwrap_or_default()
112        .to_string();
113    let mut deprecated = Vec::new();
114    if let Some(reason) = &target.deprecated {
115        deprecated.push(match reason.is_empty() {
116            true => target.path.clone(),
117            false => format!("{} ({reason})", target.path),
118        });
119    }
120    let mut body = schema.selection(&leaf_type, depth.max(1), &mut deprecated);
121
122    for (depth, field) in chain.iter().enumerate().rev() {
123        let args = vars.rendered_for(depth);
124        body = if body.is_empty() {
125            // A leaf-returning field takes no selection set at all.
126            vec![format!("{}{}", field.name, args)]
127        } else {
128            let mut wrapped = vec![format!("{}{} {{", field.name, args)];
129            wrapped.extend(body.into_iter().map(|l| format!("  {l}")));
130            wrapped.push("}".to_string());
131            wrapped
132        };
133    }
134
135    let mut operation = String::new();
136    operation.push_str(operation_kind);
137    operation.push(' ');
138    operation.push_str(&pascal_case(&chain.last().unwrap().name));
139    operation.push_str(&vars.signature());
140    operation.push_str(" {\n");
141    for line in &body {
142        operation.push_str("  ");
143        operation.push_str(line);
144        operation.push('\n');
145    }
146    operation.push_str("}\n");
147
148    Ok(Example {
149        operation,
150        variables: vars.placeholders(),
151        input_types: schema.input_types(&chain),
152        optional,
153        deprecated,
154        via,
155        alternatives,
156    })
157}
158
159/// Records indexed the two ways drafting needs: what kind a type name is, and
160/// what fields a type has.
161struct Schema<'a> {
162    kinds: HashMap<&'a str, Kind>,
163    /// The type definitions themselves, for what `kinds` can't answer —
164    /// chiefly a union's members.
165    types: HashMap<&'a str, &'a SchemaRecord>,
166    fields: HashMap<&'a str, Vec<&'a SchemaRecord>>,
167    roots: Vec<&'a SchemaRecord>,
168}
169
170impl<'a> Schema<'a> {
171    fn index(records: &'a [SchemaRecord]) -> Self {
172        let mut kinds = HashMap::new();
173        let mut types = HashMap::new();
174        let mut fields: HashMap<&str, Vec<&SchemaRecord>> = HashMap::new();
175        let mut roots = Vec::new();
176        for r in records {
177            match r.kind {
178                Kind::Query | Kind::Mutation | Kind::Subscription => {
179                    roots.push(r);
180                    if let Some(p) = r.parent.as_deref() {
181                        fields.entry(p).or_default().push(r);
182                    }
183                }
184                Kind::Field | Kind::InputField | Kind::EnumValue => {
185                    if let Some(p) = r.parent.as_deref() {
186                        fields.entry(p).or_default().push(r);
187                    }
188                }
189                _ => {
190                    kinds.insert(r.name.as_str(), r.kind);
191                    types.insert(r.name.as_str(), r);
192                }
193            }
194        }
195        Self {
196            kinds,
197            types,
198            fields,
199            roots,
200        }
201    }
202
203    /// Expand the input objects and enums an operation's arguments refer to,
204    /// so the variables can be filled in without going back to the schema. A
205    /// nested input object becomes its own entry rather than a deeper
206    /// indent — flat reads better and makes cycles (`Filter { and: [Filter] }`)
207    /// a non-issue, since each type is expanded once.
208    fn input_types(&self, chain: &[&SchemaRecord]) -> Vec<Vec<String>> {
209        /// Enough for any real argument list; a guard, not a policy.
210        const MAX_TYPES: usize = 12;
211
212        let mut queue: Vec<String> = chain
213            .iter()
214            .flat_map(|f| f.args.iter())
215            .map(|a| base_of(split_arg(a).type_ref).to_string())
216            .collect();
217        let mut seen: Vec<String> = Vec::new();
218        let mut out = Vec::new();
219
220        while let Some(name) = queue.pop() {
221            if seen.contains(&name) || out.len() >= MAX_TYPES {
222                continue;
223            }
224            seen.push(name.clone());
225            match self.kinds.get(name.as_str()) {
226                Some(Kind::InputObject) => {
227                    let mut block = vec![format!("{name} {{")];
228                    for f in self.fields.get(name.as_str()).into_iter().flatten() {
229                        let ty = f.type_ref.as_deref().unwrap_or("");
230                        block.push(format!("  {}: {}", f.name, ty));
231                        queue.push(base_of(ty).to_string());
232                    }
233                    block.push("}".to_string());
234                    out.push(block);
235                }
236                Some(Kind::Enum) => {
237                    let values: Vec<&str> = self
238                        .fields
239                        .get(name.as_str())
240                        .into_iter()
241                        .flatten()
242                        .map(|v| v.name.as_str())
243                        .collect();
244                    if !values.is_empty() {
245                        out.push(vec![format!("{name} = {}", values.join(" | "))]);
246                    }
247                }
248                _ => {}
249            }
250        }
251        out.sort();
252        out
253    }
254
255    /// Root operation fields returning `type_name`, best first. Fewest required
256    /// arguments wins: `viewer` is a friendlier entry point than `node(id:)`,
257    /// which needs one you may not have yet.
258    fn roots_returning(&self, type_name: &str) -> Vec<&'a SchemaRecord> {
259        let mut hits: Vec<&SchemaRecord> = self
260            .roots
261            .iter()
262            .copied()
263            .filter(|r| {
264                r.base_type()
265                    .is_some_and(|t| t.eq_ignore_ascii_case(type_name))
266            })
267            .collect();
268        hits.sort_by_key(|r| (required_args(r), r.path.len(), r.path.clone()));
269        hits
270    }
271
272    /// Whether a type needs no selection set — a scalar, an enum, or a name
273    /// the schema never defines (the built-in scalars, which SDL omits).
274    fn is_leaf(&self, type_name: &str) -> bool {
275        !matches!(
276            self.kinds.get(type_name),
277            Some(Kind::Object | Kind::Interface | Kind::Union | Kind::InputObject)
278        )
279    }
280
281    /// The selection set for `type_name`: its leaf fields, plus a marker for
282    /// each object-valued field so the hole is visible. Empty for a leaf type.
283    fn selection(
284        &self,
285        type_name: &str,
286        depth: usize,
287        deprecated: &mut Vec<String>,
288    ) -> Vec<String> {
289        if type_name.is_empty() || self.is_leaf(type_name) || depth == 0 {
290            return Vec::new();
291        }
292        // An abstract type has no fields of its own to select — a union never,
293        // an interface only the common ones — so what the caller actually wants
294        // is spelled with inline fragments over the concrete types.
295        if let Some(rec) = self.types.get(type_name) {
296            if rec.kind == Kind::Union {
297                return self.inline_fragments(rec, depth, deprecated, &[]);
298            }
299        }
300
301        let mut lines = Vec::new();
302        let mut deferred = Vec::new();
303        for f in self.fields.get(type_name).into_iter().flatten() {
304            if f.kind != Kind::Field {
305                continue;
306            }
307            let Some(base) = f.base_type() else { continue };
308            // A field with required arguments can't be selected bare.
309            if f.args.iter().any(|a| a.trim_end().ends_with('!')) {
310                deferred.push(format!("# {}: {} — needs arguments", f.name, base));
311                continue;
312            }
313            let note = match &f.deprecated {
314                // Flagged where it's selected rather than dropped: silently
315                // omitting a field the schema still serves would be its own
316                // surprise. The caller also gets a warning.
317                Some(reason) if reason.is_empty() => {
318                    deprecated.push(f.path.clone());
319                    "  # deprecated".to_string()
320                }
321                Some(reason) => {
322                    deprecated.push(f.path.clone());
323                    format!("  # deprecated: {reason}")
324                }
325                None => String::new(),
326            };
327            if self.is_leaf(base) {
328                lines.push(format!("{}{note}", f.name));
329            } else if depth > 1 || f.name.eq_ignore_ascii_case("errors") {
330                // Deeper levels on request; the payload/errors convention is
331                // always expanded, because a mutation without it reads wrong.
332                let inner = self.selection(base, depth.saturating_sub(1).max(1), deprecated);
333                lines.push(format!("{}{note} {{", f.name));
334                lines.extend(inner.into_iter().map(|l| format!("  {l}")));
335                lines.push("}".to_string());
336            } else {
337                deferred.push(format!("# {}: {} — add fields you need", f.name, base));
338            }
339        }
340        lines.extend(deferred);
341
342        // An interface's own fields are only the common ones. Its implementors
343        // usually carry the fields you actually came for, and they're
344        // unreachable without fragments — so append what each one adds.
345        if let Some(rec) = self.types.get(type_name) {
346            if rec.kind == Kind::Interface {
347                let common: Vec<&str> = self
348                    .fields
349                    .get(type_name)
350                    .into_iter()
351                    .flatten()
352                    .map(|f| f.name.as_str())
353                    .collect();
354                lines.extend(self.inline_fragments(rec, depth, deprecated, &common));
355            }
356        }
357
358        if lines.is_empty() {
359            // A type this schema doesn't detail. `__typename` is always valid
360            // and keeps the query runnable.
361            lines.push("__typename".to_string());
362        }
363        lines
364    }
365
366    /// `... on User { … }` for each of an abstract type's concrete types.
367    /// `skip` names fields already selected on the abstract type itself, so an
368    /// interface's fragments show only what each implementor adds.
369    fn inline_fragments(
370        &self,
371        rec: &SchemaRecord,
372        depth: usize,
373        deprecated: &mut Vec<String>,
374        skip: &[&str],
375    ) -> Vec<String> {
376        /// Enough to show the shape without burying the query; a big union
377        /// lists the rest as a comment instead.
378        const MAX_MEMBERS: usize = 6;
379
380        if rec.possible_types.is_empty() {
381            // A union we can't detail is the only case with nothing to select.
382            return match rec.kind {
383                Kind::Union => vec![
384                    "__typename".to_string(),
385                    "# add inline fragments: ... on ConcreteType { … }".to_string(),
386                ],
387                _ => Vec::new(),
388            };
389        }
390        let mut fragments = Vec::new();
391        for member in rec.possible_types.iter().take(MAX_MEMBERS) {
392            let inner: Vec<String> = self
393                .selection(member, depth, deprecated)
394                .into_iter()
395                // Drop what the abstract type already selected, and the
396                // markers that come with it — repeating them adds nothing.
397                .filter(|l| {
398                    let name = l.split([' ', '{']).next().unwrap_or(l);
399                    !skip.contains(&name) && !(l.starts_with('#') && !skip.is_empty())
400                })
401                .collect();
402            if inner.is_empty() {
403                continue; // this implementor adds nothing of its own
404            }
405            fragments.push(format!("... on {member} {{"));
406            fragments.extend(inner.into_iter().map(|l| format!("  {l}")));
407            fragments.push("}".to_string());
408        }
409        if fragments.is_empty() {
410            return Vec::new();
411        }
412        // `__typename` is what makes the fragments interpretable in a response.
413        let mut lines = vec!["__typename".to_string()];
414        lines.append(&mut fragments);
415        if rec.possible_types.len() > MAX_MEMBERS {
416            lines.push(format!(
417                "# {} more: {}",
418                rec.possible_types.len() - MAX_MEMBERS,
419                rec.possible_types[MAX_MEMBERS..].join(", ")
420            ));
421        }
422        lines
423    }
424}
425
426/// The operation's variables: one per argument along the field chain.
427struct Variables {
428    /// `(depth, arg name, variable name, type)`
429    entries: Vec<(usize, String, String, String)>,
430}
431
432impl Variables {
433    /// Split the chain's arguments: the ones a caller must supply become
434    /// variables, the rest are returned as notes.
435    fn collect(chain: &[&SchemaRecord]) -> (Self, Vec<String>) {
436        let mut entries: Vec<(usize, String, String, String)> = Vec::new();
437        let mut optional = Vec::new();
438        for (depth, field) in chain.iter().enumerate() {
439            for arg in &field.args {
440                let Arg {
441                    name,
442                    type_ref,
443                    default,
444                } = split_arg(arg);
445                // A default means the server fills it in, so even a non-null
446                // argument needs nothing from the caller.
447                if !type_ref.ends_with('!') || default.is_some() {
448                    optional.push(format!("{}({})", field.name, arg.trim()));
449                    continue;
450                }
451                // Disambiguate a name already taken by an outer field's arg.
452                let taken = entries.iter().any(|(_, _, var, _)| var == name);
453                let var = if taken {
454                    format!("{}{}", field.name, pascal_case(name))
455                } else {
456                    name.to_string()
457                };
458                entries.push((depth, name.to_string(), var, type_ref.to_string()));
459            }
460        }
461        (Self { entries }, optional)
462    }
463
464    /// `($id: ID!, $first: Int)`, or empty when there are no arguments.
465    fn signature(&self) -> String {
466        if self.entries.is_empty() {
467            return String::new();
468        }
469        let inner: Vec<String> = self
470            .entries
471            .iter()
472            .map(|(_, _, var, ty)| format!("${var}: {ty}"))
473            .collect();
474        format!("({})", inner.join(", "))
475    }
476
477    /// `(id: $id, first: $first)` for the field at `depth`, or empty.
478    fn rendered_for(&self, depth: usize) -> String {
479        let inner: Vec<String> = self
480            .entries
481            .iter()
482            .filter(|(d, _, _, _)| *d == depth)
483            .map(|(_, name, var, _)| format!("{name}: ${var}"))
484            .collect();
485        if inner.is_empty() {
486            String::new()
487        } else {
488            format!("({})", inner.join(", "))
489        }
490    }
491
492    /// Every variable is required by construction, so each placeholder names
493    /// its type — unmistakably a blank to fill rather than a usable value.
494    fn placeholders(&self) -> Value {
495        let mut map = Map::new();
496        for (_, _, var, ty) in &self.entries {
497            map.insert(var.clone(), Value::String(format!("<{ty}>")));
498        }
499        Value::Object(map)
500    }
501}
502
503/// How many of a field's arguments are non-null, and so must be supplied.
504fn required_args(r: &SchemaRecord) -> usize {
505    r.args
506        .iter()
507        .map(|a| split_arg(a))
508        .filter(|a| a.type_ref.ends_with('!') && a.default.is_none())
509        .count()
510}
511
512/// A type reference with its list and non-null wrappers peeled: `[User!]!`
513/// → `User`.
514fn base_of(type_ref: &str) -> &str {
515    type_ref.trim_matches(|c| matches!(c, '[' | ']' | '!' | ' '))
516}
517
518/// One parsed argument signature.
519struct Arg<'a> {
520    name: &'a str,
521    type_ref: &'a str,
522    default: Option<&'a str>,
523}
524
525/// `"first: Int = 10"` → name `first`, type `Int`, default `10`. gqls renders
526/// arguments as `name: Type` with ` = default` appended when the schema has one.
527fn split_arg(arg: &str) -> Arg<'_> {
528    let (name, rest) = arg.split_once(':').unwrap_or((arg, ""));
529    let (type_ref, default) = match rest.split_once('=') {
530        Some((t, d)) => (t, Some(d.trim())),
531        None => (rest, None),
532    };
533    Arg {
534        name: name.trim(),
535        type_ref: type_ref.trim(),
536        default,
537    }
538}
539
540/// `updateEmployee` → `UpdateEmployee`, for the operation name.
541fn pascal_case(name: &str) -> String {
542    let mut chars = name.chars();
543    match chars.next() {
544        Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
545        None => String::new(),
546    }
547}
548
549#[cfg(test)]
550mod tests {
551    use super::*;
552
553    fn rec(
554        path: &str,
555        name: &str,
556        kind: Kind,
557        parent: Option<&str>,
558        type_ref: Option<&str>,
559        args: &[&str],
560    ) -> SchemaRecord {
561        SchemaRecord {
562            path: path.into(),
563            name: name.into(),
564            kind,
565            parent: parent.map(Into::into),
566            type_ref: type_ref.map(Into::into),
567            args: args.iter().map(|a| a.to_string()).collect(),
568            description: None,
569            deprecated: None,
570            directives: vec![],
571            possible_types: vec![],
572        }
573    }
574
575    /// Query.user(id) -> User { id name role posts(Post) }, plus a mutation
576    /// whose payload carries an errors block.
577    fn schema() -> Vec<SchemaRecord> {
578        vec![
579            rec("Query", "Query", Kind::Object, None, None, &[]),
580            rec("User", "User", Kind::Object, None, None, &[]),
581            rec("Post", "Post", Kind::Object, None, None, &[]),
582            rec("Role", "Role", Kind::Enum, None, None, &[]),
583            rec("Payload", "Payload", Kind::Object, None, None, &[]),
584            rec("UserError", "UserError", Kind::Object, None, None, &[]),
585            rec("Input", "Input", Kind::InputObject, None, None, &[]),
586            rec(
587                "Query.user",
588                "user",
589                Kind::Query,
590                Some("Query"),
591                Some("User"),
592                &["id: ID!"],
593            ),
594            rec(
595                "Query.count",
596                "count",
597                Kind::Query,
598                Some("Query"),
599                Some("Int!"),
600                &[],
601            ),
602            rec("User.id", "id", Kind::Field, Some("User"), Some("ID!"), &[]),
603            rec(
604                "User.name",
605                "name",
606                Kind::Field,
607                Some("User"),
608                Some("String"),
609                &[],
610            ),
611            rec(
612                "User.role",
613                "role",
614                Kind::Field,
615                Some("User"),
616                Some("Role!"),
617                &[],
618            ),
619            rec(
620                "User.posts",
621                "posts",
622                Kind::Field,
623                Some("User"),
624                Some("[Post!]!"),
625                &[],
626            ),
627            rec(
628                "User.avatar",
629                "avatar",
630                Kind::Field,
631                Some("User"),
632                Some("String"),
633                &["size: Int!"],
634            ),
635            rec(
636                "Mutation.save",
637                "save",
638                Kind::Mutation,
639                Some("Mutation"),
640                Some("Payload!"),
641                &["input: Input!", "dryRun: Boolean"],
642            ),
643            rec(
644                "Payload.ok",
645                "ok",
646                Kind::Field,
647                Some("Payload"),
648                Some("Boolean!"),
649                &[],
650            ),
651            rec(
652                "Payload.errors",
653                "errors",
654                Kind::Field,
655                Some("Payload"),
656                Some("[UserError!]!"),
657                &[],
658            ),
659            rec(
660                "UserError.message",
661                "message",
662                Kind::Field,
663                Some("UserError"),
664                Some("String!"),
665                &[],
666            ),
667            rec(
668                "Role.ADMIN",
669                "ADMIN",
670                Kind::EnumValue,
671                Some("Role"),
672                None,
673                &[],
674            ),
675        ]
676    }
677
678    fn build_for(path: &str) -> Example {
679        let records = schema();
680        let target = records.iter().find(|r| r.path == path).unwrap();
681        build(target, &records, 1).unwrap()
682    }
683
684    #[test]
685    fn root_field_becomes_a_parameterized_query() {
686        let ex = build_for("Query.user");
687        assert_eq!(
688            ex.operation,
689            "query User($id: ID!) {\n  \
690               user(id: $id) {\n    \
691                 id\n    \
692                 name\n    \
693                 role\n    \
694                 # posts: Post — add fields you need\n    \
695                 # avatar: String — needs arguments\n  \
696               }\n\
697             }\n"
698        );
699        assert_eq!(ex.variables, serde_json::json!({ "id": "<ID!>" }));
700    }
701
702    #[test]
703    fn a_scalar_return_gets_no_selection_set() {
704        let ex = build_for("Query.count");
705        assert_eq!(ex.operation, "query Count {\n  count\n}\n");
706        assert_eq!(ex.variables, serde_json::json!({}));
707    }
708
709    #[test]
710    fn mutation_expands_a_real_errors_block() {
711        let ex = build_for("Mutation.save");
712        assert!(
713            ex.operation
714                .starts_with("mutation Save($input: Input!) {\n  save(input: $input) {"),
715            "{}",
716            ex.operation
717        );
718        // the nullable arg is left out of the operation, but still surfaced
719        assert_eq!(ex.optional, ["save(dryRun: Boolean)"]);
720        assert!(
721            ex.operation.contains("errors {\n      message\n    }"),
722            "{}",
723            ex.operation
724        );
725        // only what the caller must supply, typed so it can't be mistaken
726        // for a usable value
727        assert_eq!(ex.variables, serde_json::json!({ "input": "<Input!>" }));
728    }
729
730    #[test]
731    fn nested_field_is_wrapped_in_a_root_that_returns_its_type() {
732        let ex = build_for("User.posts");
733        // User.posts isn't callable directly; Query.user returns a User
734        // Post has no fields in this fixture, so the selection falls back to
735        // __typename — always valid, and it keeps the query runnable.
736        assert_eq!(
737            ex.operation,
738            "query Posts($id: ID!) {\n  \
739               user(id: $id) {\n    \
740                 posts {\n      \
741                   __typename\n    \
742                 }\n  \
743               }\n\
744             }\n"
745        );
746    }
747
748    #[test]
749    fn an_unreachable_field_is_an_error_not_a_guess() {
750        let mut records = schema();
751        // nothing returns UserError, so UserError.message can't be reached
752        let target = records
753            .iter()
754            .position(|r| r.path == "UserError.message")
755            .unwrap();
756        let target = records.remove(target);
757        let err = build(&target, &records, 1).unwrap_err().to_string();
758        assert!(err.contains("no root field returns UserError"), "{err}");
759    }
760
761    #[test]
762    fn ambiguous_roots_are_reported_rather_than_hidden() {
763        let mut records = schema();
764        records.push(rec(
765            "Query.viewer",
766            "viewer",
767            Kind::Query,
768            Some("Query"),
769            Some("User"),
770            &[],
771        ));
772        let target = records.iter().find(|r| r.path == "User.name").unwrap();
773        let ex = build(target, &records, 1).unwrap();
774        // both Query.user and Query.viewer return User
775        assert_eq!(ex.alternatives.len(), 1);
776    }
777
778    #[test]
779    fn a_defaulted_argument_is_omitted_even_when_non_null() {
780        let records = vec![
781            rec("Query", "Query", Kind::Object, None, None, &[]),
782            rec(
783                "Query.feed",
784                "feed",
785                Kind::Query,
786                Some("Query"),
787                Some("Int!"),
788                // non-null, but the schema supplies a default — nothing is
789                // required of the caller
790                &["first: Int! = 10", "after: String"],
791            ),
792        ];
793        let target = records.iter().find(|r| r.path == "Query.feed").unwrap();
794        let ex = build(target, &records, 1).unwrap();
795        assert_eq!(ex.operation, "query Feed {\n  feed\n}\n");
796        assert_eq!(ex.variables, serde_json::json!({}));
797        assert_eq!(
798            ex.optional,
799            ["feed(first: Int! = 10)", "feed(after: String)"]
800        );
801    }
802
803    #[test]
804    fn a_self_referential_input_expands_once() {
805        let records = vec![
806            rec("Query", "Query", Kind::Object, None, None, &[]),
807            rec("Filter", "Filter", Kind::InputObject, None, None, &[]),
808            rec(
809                "Filter.and",
810                "and",
811                Kind::InputField,
812                Some("Filter"),
813                // Filter refers to itself — the expansion must terminate
814                Some("[Filter!]"),
815                &[],
816            ),
817            rec(
818                "Filter.eq",
819                "eq",
820                Kind::InputField,
821                Some("Filter"),
822                Some("String"),
823                &[],
824            ),
825            rec(
826                "Query.search",
827                "search",
828                Kind::Query,
829                Some("Query"),
830                Some("Int!"),
831                &["filter: Filter!"],
832            ),
833        ];
834        let target = records.iter().find(|r| r.path == "Query.search").unwrap();
835        let ex = build(target, &records, 1).unwrap();
836        assert_eq!(
837            ex.input_types,
838            vec![vec![
839                "Filter {".to_string(),
840                "  and: [Filter!]".to_string(),
841                "  eq: String".to_string(),
842                "}".to_string(),
843            ]]
844        );
845    }
846
847    /// The point of the whole module: what it prints must parse as GraphQL.
848    #[test]
849    fn every_drafted_operation_is_valid_graphql() {
850        for path in [
851            "Query.user",
852            "Query.count",
853            "Mutation.save",
854            "User.posts",
855            "User.name",
856        ] {
857            let ex = build_for(path);
858            graphql_parser::parse_query::<String>(&ex.operation).unwrap_or_else(|e| {
859                panic!("{path} drafted invalid GraphQL: {e}\n{}", ex.operation)
860            });
861        }
862    }
863
864    #[test]
865    fn colliding_argument_names_are_disambiguated() {
866        let records = vec![
867            rec("Query", "Query", Kind::Object, None, None, &[]),
868            rec("Role", "Role", Kind::Enum, None, None, &[]),
869            rec(
870                "Role.ADMIN",
871                "ADMIN",
872                Kind::EnumValue,
873                Some("Role"),
874                None,
875                &[],
876            ),
877            rec("Thing", "Thing", Kind::Object, None, None, &[]),
878            rec(
879                "Query.thing",
880                "thing",
881                Kind::Query,
882                Some("Query"),
883                Some("Thing"),
884                &["id: ID!"],
885            ),
886            rec(
887                "Thing.child",
888                "child",
889                Kind::Field,
890                Some("Thing"),
891                Some("String"),
892                &["id: ID!", "role: Role!"],
893            ),
894        ];
895        let target = records.iter().find(|r| r.path == "Thing.child").unwrap();
896        let ex = build(target, &records, 1).unwrap();
897        // the inner `id` collides with the root's, so it's prefixed
898        assert!(
899            ex.operation.contains("child(id: $childId, role: $role)"),
900            "{}",
901            ex.operation
902        );
903        assert_eq!(
904            ex.variables,
905            serde_json::json!({ "id": "<ID!>", "childId": "<ID!>", "role": "<Role!>" })
906        );
907    }
908}