1use std::collections::HashMap;
28
29use anyhow::{bail, Result};
30use serde_json::{Map, Value};
31
32use crate::model::{Kind, SchemaRecord};
33
34#[derive(Debug)]
36pub struct Example {
37 pub operation: String,
39 pub variables: Value,
41 pub optional: Vec<String>,
44 pub via: Option<String>,
46 pub alternatives: Vec<String>,
49}
50
51pub fn build(target: &SchemaRecord, records: &[SchemaRecord]) -> Result<Example> {
53 let schema = Schema::index(records);
54
55 let (chain, via, alternatives) = match target.kind {
58 Kind::Query | Kind::Mutation | Kind::Subscription => (vec![target], None, Vec::new()),
59 Kind::Field => {
60 let parent = target
61 .parent
62 .as_deref()
63 .ok_or_else(|| anyhow::anyhow!("{} has no enclosing type", target.path))?;
64 let mut roots = schema.roots_returning(parent);
65 if roots.is_empty() {
66 bail!(
67 "{} isn't reachable in one hop — no root field returns {parent}. \
68 Try `gqls --returns {parent}` to see what's close.",
69 target.path
70 );
71 }
72 let chosen = roots.remove(0);
73 let via = Some(chosen.path.clone());
74 let alternatives = roots.iter().map(|r| r.path.clone()).collect();
75 (vec![chosen, target], via, alternatives)
76 }
77 other => bail!(
78 "can't draft an operation for a {} — pick a field, query, or mutation",
79 other.as_str()
80 ),
81 };
82
83 let operation_kind = match chain[0].kind {
84 Kind::Mutation => "mutation",
85 Kind::Subscription => "subscription",
86 _ => "query",
87 };
88
89 let (vars, optional) = Variables::collect(&chain);
92
93 let leaf_type = chain
95 .last()
96 .and_then(|r| r.base_type())
97 .unwrap_or_default()
98 .to_string();
99 let mut body = schema.selection(&leaf_type);
100
101 for (depth, field) in chain.iter().enumerate().rev() {
102 let args = vars.rendered_for(depth);
103 body = if body.is_empty() {
104 vec![format!("{}{}", field.name, args)]
106 } else {
107 let mut wrapped = vec![format!("{}{} {{", field.name, args)];
108 wrapped.extend(body.into_iter().map(|l| format!(" {l}")));
109 wrapped.push("}".to_string());
110 wrapped
111 };
112 }
113
114 let mut operation = String::new();
115 operation.push_str(operation_kind);
116 operation.push(' ');
117 operation.push_str(&pascal_case(&chain.last().unwrap().name));
118 operation.push_str(&vars.signature());
119 operation.push_str(" {\n");
120 for line in &body {
121 operation.push_str(" ");
122 operation.push_str(line);
123 operation.push('\n');
124 }
125 operation.push_str("}\n");
126
127 Ok(Example {
128 operation,
129 variables: vars.placeholders(),
130 optional,
131 via,
132 alternatives,
133 })
134}
135
136struct Schema<'a> {
139 kinds: HashMap<&'a str, Kind>,
140 fields: HashMap<&'a str, Vec<&'a SchemaRecord>>,
141 roots: Vec<&'a SchemaRecord>,
142}
143
144impl<'a> Schema<'a> {
145 fn index(records: &'a [SchemaRecord]) -> Self {
146 let mut kinds = HashMap::new();
147 let mut fields: HashMap<&str, Vec<&SchemaRecord>> = HashMap::new();
148 let mut roots = Vec::new();
149 for r in records {
150 match r.kind {
151 Kind::Query | Kind::Mutation | Kind::Subscription => {
152 roots.push(r);
153 if let Some(p) = r.parent.as_deref() {
154 fields.entry(p).or_default().push(r);
155 }
156 }
157 Kind::Field | Kind::InputField | Kind::EnumValue => {
158 if let Some(p) = r.parent.as_deref() {
159 fields.entry(p).or_default().push(r);
160 }
161 }
162 _ => {
163 kinds.insert(r.name.as_str(), r.kind);
164 }
165 }
166 }
167 Self {
168 kinds,
169 fields,
170 roots,
171 }
172 }
173
174 fn roots_returning(&self, type_name: &str) -> Vec<&'a SchemaRecord> {
178 let mut hits: Vec<&SchemaRecord> = self
179 .roots
180 .iter()
181 .copied()
182 .filter(|r| {
183 r.base_type()
184 .is_some_and(|t| t.eq_ignore_ascii_case(type_name))
185 })
186 .collect();
187 hits.sort_by_key(|r| (required_args(r), r.path.len(), r.path.clone()));
188 hits
189 }
190
191 fn is_leaf(&self, type_name: &str) -> bool {
194 !matches!(
195 self.kinds.get(type_name),
196 Some(Kind::Object | Kind::Interface | Kind::Union | Kind::InputObject)
197 )
198 }
199
200 fn selection(&self, type_name: &str) -> Vec<String> {
203 if type_name.is_empty() || self.is_leaf(type_name) {
204 return Vec::new();
205 }
206 let mut lines = Vec::new();
207 let mut deferred = Vec::new();
208 for f in self.fields.get(type_name).into_iter().flatten() {
209 if f.kind != Kind::Field {
210 continue;
211 }
212 let Some(base) = f.base_type() else { continue };
213 if self.is_leaf(base) {
214 if f.args.iter().any(|a| a.trim_end().ends_with('!')) {
216 deferred.push(format!("# {}: {} — needs arguments", f.name, base));
217 } else {
218 lines.push(f.name.clone());
219 }
220 } else if f.name.eq_ignore_ascii_case("errors") {
221 let inner = self.selection(base);
224 lines.push(format!("{} {{", f.name));
225 lines.extend(inner.into_iter().map(|l| format!(" {l}")));
226 lines.push("}".to_string());
227 } else {
228 deferred.push(format!("# {}: {} — add fields you need", f.name, base));
229 }
230 }
231 lines.extend(deferred);
232 if lines.is_empty() {
233 lines.push("__typename".to_string());
236 lines.push("# add inline fragments: ... on ConcreteType { … }".to_string());
237 }
238 lines
239 }
240}
241
242struct Variables {
244 entries: Vec<(usize, String, String, String)>,
246}
247
248impl Variables {
249 fn collect(chain: &[&SchemaRecord]) -> (Self, Vec<String>) {
252 let mut entries: Vec<(usize, String, String, String)> = Vec::new();
253 let mut optional = Vec::new();
254 for (depth, field) in chain.iter().enumerate() {
255 for arg in &field.args {
256 let Arg {
257 name,
258 type_ref,
259 default,
260 } = split_arg(arg);
261 if !type_ref.ends_with('!') || default.is_some() {
264 optional.push(format!("{}({})", field.name, arg.trim()));
265 continue;
266 }
267 let taken = entries.iter().any(|(_, _, var, _)| var == name);
269 let var = if taken {
270 format!("{}{}", field.name, pascal_case(name))
271 } else {
272 name.to_string()
273 };
274 entries.push((depth, name.to_string(), var, type_ref.to_string()));
275 }
276 }
277 (Self { entries }, optional)
278 }
279
280 fn signature(&self) -> String {
282 if self.entries.is_empty() {
283 return String::new();
284 }
285 let inner: Vec<String> = self
286 .entries
287 .iter()
288 .map(|(_, _, var, ty)| format!("${var}: {ty}"))
289 .collect();
290 format!("({})", inner.join(", "))
291 }
292
293 fn rendered_for(&self, depth: usize) -> String {
295 let inner: Vec<String> = self
296 .entries
297 .iter()
298 .filter(|(d, _, _, _)| *d == depth)
299 .map(|(_, name, var, _)| format!("{name}: ${var}"))
300 .collect();
301 if inner.is_empty() {
302 String::new()
303 } else {
304 format!("({})", inner.join(", "))
305 }
306 }
307
308 fn placeholders(&self) -> Value {
311 let mut map = Map::new();
312 for (_, _, var, ty) in &self.entries {
313 map.insert(var.clone(), Value::String(format!("<{ty}>")));
314 }
315 Value::Object(map)
316 }
317}
318
319fn required_args(r: &SchemaRecord) -> usize {
321 r.args
322 .iter()
323 .map(|a| split_arg(a))
324 .filter(|a| a.type_ref.ends_with('!') && a.default.is_none())
325 .count()
326}
327
328struct Arg<'a> {
330 name: &'a str,
331 type_ref: &'a str,
332 default: Option<&'a str>,
333}
334
335fn split_arg(arg: &str) -> Arg<'_> {
338 let (name, rest) = arg.split_once(':').unwrap_or((arg, ""));
339 let (type_ref, default) = match rest.split_once('=') {
340 Some((t, d)) => (t, Some(d.trim())),
341 None => (rest, None),
342 };
343 Arg {
344 name: name.trim(),
345 type_ref: type_ref.trim(),
346 default,
347 }
348}
349
350fn pascal_case(name: &str) -> String {
352 let mut chars = name.chars();
353 match chars.next() {
354 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
355 None => String::new(),
356 }
357}
358
359#[cfg(test)]
360mod tests {
361 use super::*;
362
363 fn rec(
364 path: &str,
365 name: &str,
366 kind: Kind,
367 parent: Option<&str>,
368 type_ref: Option<&str>,
369 args: &[&str],
370 ) -> SchemaRecord {
371 SchemaRecord {
372 path: path.into(),
373 name: name.into(),
374 kind,
375 parent: parent.map(Into::into),
376 type_ref: type_ref.map(Into::into),
377 args: args.iter().map(|a| a.to_string()).collect(),
378 description: None,
379 deprecated: None,
380 directives: vec![],
381 }
382 }
383
384 fn schema() -> Vec<SchemaRecord> {
387 vec![
388 rec("Query", "Query", Kind::Object, None, None, &[]),
389 rec("User", "User", Kind::Object, None, None, &[]),
390 rec("Post", "Post", Kind::Object, None, None, &[]),
391 rec("Role", "Role", Kind::Enum, None, None, &[]),
392 rec("Payload", "Payload", Kind::Object, None, None, &[]),
393 rec("UserError", "UserError", Kind::Object, None, None, &[]),
394 rec("Input", "Input", Kind::InputObject, None, None, &[]),
395 rec(
396 "Query.user",
397 "user",
398 Kind::Query,
399 Some("Query"),
400 Some("User"),
401 &["id: ID!"],
402 ),
403 rec(
404 "Query.count",
405 "count",
406 Kind::Query,
407 Some("Query"),
408 Some("Int!"),
409 &[],
410 ),
411 rec("User.id", "id", Kind::Field, Some("User"), Some("ID!"), &[]),
412 rec(
413 "User.name",
414 "name",
415 Kind::Field,
416 Some("User"),
417 Some("String"),
418 &[],
419 ),
420 rec(
421 "User.role",
422 "role",
423 Kind::Field,
424 Some("User"),
425 Some("Role!"),
426 &[],
427 ),
428 rec(
429 "User.posts",
430 "posts",
431 Kind::Field,
432 Some("User"),
433 Some("[Post!]!"),
434 &[],
435 ),
436 rec(
437 "User.avatar",
438 "avatar",
439 Kind::Field,
440 Some("User"),
441 Some("String"),
442 &["size: Int!"],
443 ),
444 rec(
445 "Mutation.save",
446 "save",
447 Kind::Mutation,
448 Some("Mutation"),
449 Some("Payload!"),
450 &["input: Input!", "dryRun: Boolean"],
451 ),
452 rec(
453 "Payload.ok",
454 "ok",
455 Kind::Field,
456 Some("Payload"),
457 Some("Boolean!"),
458 &[],
459 ),
460 rec(
461 "Payload.errors",
462 "errors",
463 Kind::Field,
464 Some("Payload"),
465 Some("[UserError!]!"),
466 &[],
467 ),
468 rec(
469 "UserError.message",
470 "message",
471 Kind::Field,
472 Some("UserError"),
473 Some("String!"),
474 &[],
475 ),
476 rec(
477 "Role.ADMIN",
478 "ADMIN",
479 Kind::EnumValue,
480 Some("Role"),
481 None,
482 &[],
483 ),
484 ]
485 }
486
487 fn build_for(path: &str) -> Example {
488 let records = schema();
489 let target = records.iter().find(|r| r.path == path).unwrap();
490 build(target, &records).unwrap()
491 }
492
493 #[test]
494 fn root_field_becomes_a_parameterized_query() {
495 let ex = build_for("Query.user");
496 assert_eq!(
497 ex.operation,
498 "query User($id: ID!) {\n \
499 user(id: $id) {\n \
500 id\n \
501 name\n \
502 role\n \
503 # posts: Post — add fields you need\n \
504 # avatar: String — needs arguments\n \
505 }\n\
506 }\n"
507 );
508 assert_eq!(ex.variables, serde_json::json!({ "id": "<ID!>" }));
509 }
510
511 #[test]
512 fn a_scalar_return_gets_no_selection_set() {
513 let ex = build_for("Query.count");
514 assert_eq!(ex.operation, "query Count {\n count\n}\n");
515 assert_eq!(ex.variables, serde_json::json!({}));
516 }
517
518 #[test]
519 fn mutation_expands_a_real_errors_block() {
520 let ex = build_for("Mutation.save");
521 assert!(
522 ex.operation
523 .starts_with("mutation Save($input: Input!) {\n save(input: $input) {"),
524 "{}",
525 ex.operation
526 );
527 assert_eq!(ex.optional, ["save(dryRun: Boolean)"]);
529 assert!(
530 ex.operation.contains("errors {\n message\n }"),
531 "{}",
532 ex.operation
533 );
534 assert_eq!(ex.variables, serde_json::json!({ "input": "<Input!>" }));
537 }
538
539 #[test]
540 fn nested_field_is_wrapped_in_a_root_that_returns_its_type() {
541 let ex = build_for("User.posts");
542 assert_eq!(
544 ex.operation,
545 "query Posts($id: ID!) {\n \
546 user(id: $id) {\n \
547 posts {\n \
548 __typename\n \
549 # add inline fragments: ... on ConcreteType { … }\n \
550 }\n \
551 }\n\
552 }\n"
553 );
554 }
555
556 #[test]
557 fn an_unreachable_field_is_an_error_not_a_guess() {
558 let mut records = schema();
559 let target = records
561 .iter()
562 .position(|r| r.path == "UserError.message")
563 .unwrap();
564 let target = records.remove(target);
565 let err = build(&target, &records).unwrap_err().to_string();
566 assert!(err.contains("no root field returns UserError"), "{err}");
567 }
568
569 #[test]
570 fn ambiguous_roots_are_reported_rather_than_hidden() {
571 let mut records = schema();
572 records.push(rec(
573 "Query.viewer",
574 "viewer",
575 Kind::Query,
576 Some("Query"),
577 Some("User"),
578 &[],
579 ));
580 let target = records.iter().find(|r| r.path == "User.name").unwrap();
581 let ex = build(target, &records).unwrap();
582 assert_eq!(ex.alternatives.len(), 1);
584 }
585
586 #[test]
587 fn a_defaulted_argument_is_omitted_even_when_non_null() {
588 let records = vec![
589 rec("Query", "Query", Kind::Object, None, None, &[]),
590 rec(
591 "Query.feed",
592 "feed",
593 Kind::Query,
594 Some("Query"),
595 Some("Int!"),
596 &["first: Int! = 10", "after: String"],
599 ),
600 ];
601 let target = records.iter().find(|r| r.path == "Query.feed").unwrap();
602 let ex = build(target, &records).unwrap();
603 assert_eq!(ex.operation, "query Feed {\n feed\n}\n");
604 assert_eq!(ex.variables, serde_json::json!({}));
605 assert_eq!(
606 ex.optional,
607 ["feed(first: Int! = 10)", "feed(after: String)"]
608 );
609 }
610
611 #[test]
613 fn every_drafted_operation_is_valid_graphql() {
614 for path in [
615 "Query.user",
616 "Query.count",
617 "Mutation.save",
618 "User.posts",
619 "User.name",
620 ] {
621 let ex = build_for(path);
622 graphql_parser::parse_query::<String>(&ex.operation).unwrap_or_else(|e| {
623 panic!("{path} drafted invalid GraphQL: {e}\n{}", ex.operation)
624 });
625 }
626 }
627
628 #[test]
629 fn colliding_argument_names_are_disambiguated() {
630 let records = vec![
631 rec("Query", "Query", Kind::Object, None, None, &[]),
632 rec("Role", "Role", Kind::Enum, None, None, &[]),
633 rec(
634 "Role.ADMIN",
635 "ADMIN",
636 Kind::EnumValue,
637 Some("Role"),
638 None,
639 &[],
640 ),
641 rec("Thing", "Thing", Kind::Object, None, None, &[]),
642 rec(
643 "Query.thing",
644 "thing",
645 Kind::Query,
646 Some("Query"),
647 Some("Thing"),
648 &["id: ID!"],
649 ),
650 rec(
651 "Thing.child",
652 "child",
653 Kind::Field,
654 Some("Thing"),
655 Some("String"),
656 &["id: ID!", "role: Role!"],
657 ),
658 ];
659 let target = records.iter().find(|r| r.path == "Thing.child").unwrap();
660 let ex = build(target, &records).unwrap();
661 assert!(
663 ex.operation.contains("child(id: $childId, role: $role)"),
664 "{}",
665 ex.operation
666 );
667 assert_eq!(
668 ex.variables,
669 serde_json::json!({ "id": "<ID!>", "childId": "<ID!>", "role": "<Role!>" })
670 );
671 }
672}