1use std::collections::HashMap;
35
36use anyhow::{bail, Result};
37use serde_json::{Map, Value};
38
39use crate::model::{Kind, SchemaRecord};
40
41#[derive(Debug)]
43pub struct Example {
44 pub operation: String,
46 pub variables: Value,
48 pub optional: Vec<String>,
51 pub input_types: Vec<Vec<String>>,
55 pub deprecated: Vec<String>,
58 pub via: Option<String>,
60 pub alternatives: Vec<String>,
63}
64
65pub fn build(target: &SchemaRecord, records: &[SchemaRecord], depth: usize) -> Result<Example> {
67 let schema = Schema::index(records);
68
69 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 let (vars, optional) = Variables::collect(&chain);
106
107 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 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
159struct Schema<'a> {
162 kinds: HashMap<&'a str, Kind>,
163 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 fn input_types(&self, chain: &[&SchemaRecord]) -> Vec<Vec<String>> {
209 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 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 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 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 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 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 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 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 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 lines.push("__typename".to_string());
362 }
363 lines
364 }
365
366 fn inline_fragments(
370 &self,
371 rec: &SchemaRecord,
372 depth: usize,
373 deprecated: &mut Vec<String>,
374 skip: &[&str],
375 ) -> Vec<String> {
376 const MAX_MEMBERS: usize = 6;
379
380 if rec.possible_types.is_empty() {
381 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 .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; }
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 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
426struct Variables {
428 entries: Vec<(usize, String, String, String)>,
430}
431
432impl Variables {
433 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 if !type_ref.ends_with('!') || default.is_some() {
448 optional.push(format!("{}({})", field.name, arg.trim()));
449 continue;
450 }
451 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 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 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 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
503fn 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
512fn base_of(type_ref: &str) -> &str {
515 type_ref.trim_matches(|c| matches!(c, '[' | ']' | '!' | ' '))
516}
517
518struct Arg<'a> {
520 name: &'a str,
521 type_ref: &'a str,
522 default: Option<&'a str>,
523}
524
525fn 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
540fn 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 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 assert_eq!(ex.optional, ["save(dryRun: Boolean)"]);
720 assert!(
721 ex.operation.contains("errors {\n message\n }"),
722 "{}",
723 ex.operation
724 );
725 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 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 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 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 &["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 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 #[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 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}