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        if lines.is_empty() {
342            // A type this schema doesn't detail. `__typename` is always valid
343            // and keeps the query runnable.
344            lines.push("__typename".to_string());
345        }
346        lines
347    }
348
349    /// `... on User { … }` for each of an abstract type's concrete types.
350    fn inline_fragments(
351        &self,
352        rec: &SchemaRecord,
353        depth: usize,
354        deprecated: &mut Vec<String>,
355    ) -> Vec<String> {
356        /// Enough to show the shape without burying the query; a big union
357        /// lists the rest as a comment instead.
358        const MAX_MEMBERS: usize = 6;
359
360        if rec.possible_types.is_empty() {
361            return vec![
362                "__typename".to_string(),
363                "# add inline fragments: ... on ConcreteType { … }".to_string(),
364            ];
365        }
366        let mut lines = vec!["__typename".to_string()];
367        for member in rec.possible_types.iter().take(MAX_MEMBERS) {
368            let inner = self.selection(member, depth, deprecated);
369            if inner.is_empty() {
370                continue;
371            }
372            lines.push(format!("... on {member} {{"));
373            lines.extend(inner.into_iter().map(|l| format!("  {l}")));
374            lines.push("}".to_string());
375        }
376        if rec.possible_types.len() > MAX_MEMBERS {
377            lines.push(format!(
378                "# {} more: {}",
379                rec.possible_types.len() - MAX_MEMBERS,
380                rec.possible_types[MAX_MEMBERS..].join(", ")
381            ));
382        }
383        lines
384    }
385}
386
387/// The operation's variables: one per argument along the field chain.
388struct Variables {
389    /// `(depth, arg name, variable name, type)`
390    entries: Vec<(usize, String, String, String)>,
391}
392
393impl Variables {
394    /// Split the chain's arguments: the ones a caller must supply become
395    /// variables, the rest are returned as notes.
396    fn collect(chain: &[&SchemaRecord]) -> (Self, Vec<String>) {
397        let mut entries: Vec<(usize, String, String, String)> = Vec::new();
398        let mut optional = Vec::new();
399        for (depth, field) in chain.iter().enumerate() {
400            for arg in &field.args {
401                let Arg {
402                    name,
403                    type_ref,
404                    default,
405                } = split_arg(arg);
406                // A default means the server fills it in, so even a non-null
407                // argument needs nothing from the caller.
408                if !type_ref.ends_with('!') || default.is_some() {
409                    optional.push(format!("{}({})", field.name, arg.trim()));
410                    continue;
411                }
412                // Disambiguate a name already taken by an outer field's arg.
413                let taken = entries.iter().any(|(_, _, var, _)| var == name);
414                let var = if taken {
415                    format!("{}{}", field.name, pascal_case(name))
416                } else {
417                    name.to_string()
418                };
419                entries.push((depth, name.to_string(), var, type_ref.to_string()));
420            }
421        }
422        (Self { entries }, optional)
423    }
424
425    /// `($id: ID!, $first: Int)`, or empty when there are no arguments.
426    fn signature(&self) -> String {
427        if self.entries.is_empty() {
428            return String::new();
429        }
430        let inner: Vec<String> = self
431            .entries
432            .iter()
433            .map(|(_, _, var, ty)| format!("${var}: {ty}"))
434            .collect();
435        format!("({})", inner.join(", "))
436    }
437
438    /// `(id: $id, first: $first)` for the field at `depth`, or empty.
439    fn rendered_for(&self, depth: usize) -> String {
440        let inner: Vec<String> = self
441            .entries
442            .iter()
443            .filter(|(d, _, _, _)| *d == depth)
444            .map(|(_, name, var, _)| format!("{name}: ${var}"))
445            .collect();
446        if inner.is_empty() {
447            String::new()
448        } else {
449            format!("({})", inner.join(", "))
450        }
451    }
452
453    /// Every variable is required by construction, so each placeholder names
454    /// its type — unmistakably a blank to fill rather than a usable value.
455    fn placeholders(&self) -> Value {
456        let mut map = Map::new();
457        for (_, _, var, ty) in &self.entries {
458            map.insert(var.clone(), Value::String(format!("<{ty}>")));
459        }
460        Value::Object(map)
461    }
462}
463
464/// How many of a field's arguments are non-null, and so must be supplied.
465fn required_args(r: &SchemaRecord) -> usize {
466    r.args
467        .iter()
468        .map(|a| split_arg(a))
469        .filter(|a| a.type_ref.ends_with('!') && a.default.is_none())
470        .count()
471}
472
473/// A type reference with its list and non-null wrappers peeled: `[User!]!`
474/// → `User`.
475fn base_of(type_ref: &str) -> &str {
476    type_ref.trim_matches(|c| matches!(c, '[' | ']' | '!' | ' '))
477}
478
479/// One parsed argument signature.
480struct Arg<'a> {
481    name: &'a str,
482    type_ref: &'a str,
483    default: Option<&'a str>,
484}
485
486/// `"first: Int = 10"` → name `first`, type `Int`, default `10`. gqls renders
487/// arguments as `name: Type` with ` = default` appended when the schema has one.
488fn split_arg(arg: &str) -> Arg<'_> {
489    let (name, rest) = arg.split_once(':').unwrap_or((arg, ""));
490    let (type_ref, default) = match rest.split_once('=') {
491        Some((t, d)) => (t, Some(d.trim())),
492        None => (rest, None),
493    };
494    Arg {
495        name: name.trim(),
496        type_ref: type_ref.trim(),
497        default,
498    }
499}
500
501/// `updateEmployee` → `UpdateEmployee`, for the operation name.
502fn pascal_case(name: &str) -> String {
503    let mut chars = name.chars();
504    match chars.next() {
505        Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
506        None => String::new(),
507    }
508}
509
510#[cfg(test)]
511mod tests {
512    use super::*;
513
514    fn rec(
515        path: &str,
516        name: &str,
517        kind: Kind,
518        parent: Option<&str>,
519        type_ref: Option<&str>,
520        args: &[&str],
521    ) -> SchemaRecord {
522        SchemaRecord {
523            path: path.into(),
524            name: name.into(),
525            kind,
526            parent: parent.map(Into::into),
527            type_ref: type_ref.map(Into::into),
528            args: args.iter().map(|a| a.to_string()).collect(),
529            description: None,
530            deprecated: None,
531            directives: vec![],
532            possible_types: vec![],
533        }
534    }
535
536    /// Query.user(id) -> User { id name role posts(Post) }, plus a mutation
537    /// whose payload carries an errors block.
538    fn schema() -> Vec<SchemaRecord> {
539        vec![
540            rec("Query", "Query", Kind::Object, None, None, &[]),
541            rec("User", "User", Kind::Object, None, None, &[]),
542            rec("Post", "Post", Kind::Object, None, None, &[]),
543            rec("Role", "Role", Kind::Enum, None, None, &[]),
544            rec("Payload", "Payload", Kind::Object, None, None, &[]),
545            rec("UserError", "UserError", Kind::Object, None, None, &[]),
546            rec("Input", "Input", Kind::InputObject, None, None, &[]),
547            rec(
548                "Query.user",
549                "user",
550                Kind::Query,
551                Some("Query"),
552                Some("User"),
553                &["id: ID!"],
554            ),
555            rec(
556                "Query.count",
557                "count",
558                Kind::Query,
559                Some("Query"),
560                Some("Int!"),
561                &[],
562            ),
563            rec("User.id", "id", Kind::Field, Some("User"), Some("ID!"), &[]),
564            rec(
565                "User.name",
566                "name",
567                Kind::Field,
568                Some("User"),
569                Some("String"),
570                &[],
571            ),
572            rec(
573                "User.role",
574                "role",
575                Kind::Field,
576                Some("User"),
577                Some("Role!"),
578                &[],
579            ),
580            rec(
581                "User.posts",
582                "posts",
583                Kind::Field,
584                Some("User"),
585                Some("[Post!]!"),
586                &[],
587            ),
588            rec(
589                "User.avatar",
590                "avatar",
591                Kind::Field,
592                Some("User"),
593                Some("String"),
594                &["size: Int!"],
595            ),
596            rec(
597                "Mutation.save",
598                "save",
599                Kind::Mutation,
600                Some("Mutation"),
601                Some("Payload!"),
602                &["input: Input!", "dryRun: Boolean"],
603            ),
604            rec(
605                "Payload.ok",
606                "ok",
607                Kind::Field,
608                Some("Payload"),
609                Some("Boolean!"),
610                &[],
611            ),
612            rec(
613                "Payload.errors",
614                "errors",
615                Kind::Field,
616                Some("Payload"),
617                Some("[UserError!]!"),
618                &[],
619            ),
620            rec(
621                "UserError.message",
622                "message",
623                Kind::Field,
624                Some("UserError"),
625                Some("String!"),
626                &[],
627            ),
628            rec(
629                "Role.ADMIN",
630                "ADMIN",
631                Kind::EnumValue,
632                Some("Role"),
633                None,
634                &[],
635            ),
636        ]
637    }
638
639    fn build_for(path: &str) -> Example {
640        let records = schema();
641        let target = records.iter().find(|r| r.path == path).unwrap();
642        build(target, &records, 1).unwrap()
643    }
644
645    #[test]
646    fn root_field_becomes_a_parameterized_query() {
647        let ex = build_for("Query.user");
648        assert_eq!(
649            ex.operation,
650            "query User($id: ID!) {\n  \
651               user(id: $id) {\n    \
652                 id\n    \
653                 name\n    \
654                 role\n    \
655                 # posts: Post — add fields you need\n    \
656                 # avatar: String — needs arguments\n  \
657               }\n\
658             }\n"
659        );
660        assert_eq!(ex.variables, serde_json::json!({ "id": "<ID!>" }));
661    }
662
663    #[test]
664    fn a_scalar_return_gets_no_selection_set() {
665        let ex = build_for("Query.count");
666        assert_eq!(ex.operation, "query Count {\n  count\n}\n");
667        assert_eq!(ex.variables, serde_json::json!({}));
668    }
669
670    #[test]
671    fn mutation_expands_a_real_errors_block() {
672        let ex = build_for("Mutation.save");
673        assert!(
674            ex.operation
675                .starts_with("mutation Save($input: Input!) {\n  save(input: $input) {"),
676            "{}",
677            ex.operation
678        );
679        // the nullable arg is left out of the operation, but still surfaced
680        assert_eq!(ex.optional, ["save(dryRun: Boolean)"]);
681        assert!(
682            ex.operation.contains("errors {\n      message\n    }"),
683            "{}",
684            ex.operation
685        );
686        // only what the caller must supply, typed so it can't be mistaken
687        // for a usable value
688        assert_eq!(ex.variables, serde_json::json!({ "input": "<Input!>" }));
689    }
690
691    #[test]
692    fn nested_field_is_wrapped_in_a_root_that_returns_its_type() {
693        let ex = build_for("User.posts");
694        // User.posts isn't callable directly; Query.user returns a User
695        // Post has no fields in this fixture, so the selection falls back to
696        // __typename — always valid, and it keeps the query runnable.
697        assert_eq!(
698            ex.operation,
699            "query Posts($id: ID!) {\n  \
700               user(id: $id) {\n    \
701                 posts {\n      \
702                   __typename\n    \
703                 }\n  \
704               }\n\
705             }\n"
706        );
707    }
708
709    #[test]
710    fn an_unreachable_field_is_an_error_not_a_guess() {
711        let mut records = schema();
712        // nothing returns UserError, so UserError.message can't be reached
713        let target = records
714            .iter()
715            .position(|r| r.path == "UserError.message")
716            .unwrap();
717        let target = records.remove(target);
718        let err = build(&target, &records, 1).unwrap_err().to_string();
719        assert!(err.contains("no root field returns UserError"), "{err}");
720    }
721
722    #[test]
723    fn ambiguous_roots_are_reported_rather_than_hidden() {
724        let mut records = schema();
725        records.push(rec(
726            "Query.viewer",
727            "viewer",
728            Kind::Query,
729            Some("Query"),
730            Some("User"),
731            &[],
732        ));
733        let target = records.iter().find(|r| r.path == "User.name").unwrap();
734        let ex = build(target, &records, 1).unwrap();
735        // both Query.user and Query.viewer return User
736        assert_eq!(ex.alternatives.len(), 1);
737    }
738
739    #[test]
740    fn a_defaulted_argument_is_omitted_even_when_non_null() {
741        let records = vec![
742            rec("Query", "Query", Kind::Object, None, None, &[]),
743            rec(
744                "Query.feed",
745                "feed",
746                Kind::Query,
747                Some("Query"),
748                Some("Int!"),
749                // non-null, but the schema supplies a default — nothing is
750                // required of the caller
751                &["first: Int! = 10", "after: String"],
752            ),
753        ];
754        let target = records.iter().find(|r| r.path == "Query.feed").unwrap();
755        let ex = build(target, &records, 1).unwrap();
756        assert_eq!(ex.operation, "query Feed {\n  feed\n}\n");
757        assert_eq!(ex.variables, serde_json::json!({}));
758        assert_eq!(
759            ex.optional,
760            ["feed(first: Int! = 10)", "feed(after: String)"]
761        );
762    }
763
764    #[test]
765    fn a_self_referential_input_expands_once() {
766        let records = vec![
767            rec("Query", "Query", Kind::Object, None, None, &[]),
768            rec("Filter", "Filter", Kind::InputObject, None, None, &[]),
769            rec(
770                "Filter.and",
771                "and",
772                Kind::InputField,
773                Some("Filter"),
774                // Filter refers to itself — the expansion must terminate
775                Some("[Filter!]"),
776                &[],
777            ),
778            rec(
779                "Filter.eq",
780                "eq",
781                Kind::InputField,
782                Some("Filter"),
783                Some("String"),
784                &[],
785            ),
786            rec(
787                "Query.search",
788                "search",
789                Kind::Query,
790                Some("Query"),
791                Some("Int!"),
792                &["filter: Filter!"],
793            ),
794        ];
795        let target = records.iter().find(|r| r.path == "Query.search").unwrap();
796        let ex = build(target, &records, 1).unwrap();
797        assert_eq!(
798            ex.input_types,
799            vec![vec![
800                "Filter {".to_string(),
801                "  and: [Filter!]".to_string(),
802                "  eq: String".to_string(),
803                "}".to_string(),
804            ]]
805        );
806    }
807
808    /// The point of the whole module: what it prints must parse as GraphQL.
809    #[test]
810    fn every_drafted_operation_is_valid_graphql() {
811        for path in [
812            "Query.user",
813            "Query.count",
814            "Mutation.save",
815            "User.posts",
816            "User.name",
817        ] {
818            let ex = build_for(path);
819            graphql_parser::parse_query::<String>(&ex.operation).unwrap_or_else(|e| {
820                panic!("{path} drafted invalid GraphQL: {e}\n{}", ex.operation)
821            });
822        }
823    }
824
825    #[test]
826    fn colliding_argument_names_are_disambiguated() {
827        let records = vec![
828            rec("Query", "Query", Kind::Object, None, None, &[]),
829            rec("Role", "Role", Kind::Enum, None, None, &[]),
830            rec(
831                "Role.ADMIN",
832                "ADMIN",
833                Kind::EnumValue,
834                Some("Role"),
835                None,
836                &[],
837            ),
838            rec("Thing", "Thing", Kind::Object, None, None, &[]),
839            rec(
840                "Query.thing",
841                "thing",
842                Kind::Query,
843                Some("Query"),
844                Some("Thing"),
845                &["id: ID!"],
846            ),
847            rec(
848                "Thing.child",
849                "child",
850                Kind::Field,
851                Some("Thing"),
852                Some("String"),
853                &["id: ID!", "role: Role!"],
854            ),
855        ];
856        let target = records.iter().find(|r| r.path == "Thing.child").unwrap();
857        let ex = build(target, &records, 1).unwrap();
858        // the inner `id` collides with the root's, so it's prefixed
859        assert!(
860            ex.operation.contains("child(id: $childId, role: $role)"),
861            "{}",
862            ex.operation
863        );
864        assert_eq!(
865            ex.variables,
866            serde_json::json!({ "id": "<ID!>", "childId": "<ID!>", "role": "<Role!>" })
867        );
868    }
869}