1use std::collections::HashSet;
11use std::process::Command;
12
13use anyhow::{anyhow, bail, Result};
14use serde::{Deserialize, Serialize};
15
16use crate::model::{Kind, SchemaRecord};
17
18#[derive(Debug, Deserialize, Serialize)]
20pub struct RqHit {
21 pub name: String,
22 pub file: String,
23 pub line: u64,
24 #[serde(default)]
25 pub kind: String,
26 #[serde(default)]
27 pub confidence: f64,
28 #[serde(default, skip_deserializing)]
31 pub via: String,
32}
33
34pub fn resolve(rec: &SchemaRecord, code_dir: Option<&str>, limit: usize) -> Result<Vec<RqHit>> {
36 let mut seen = HashSet::new();
37 let mut out = Vec::new();
38 for cand in candidates(rec) {
39 for mut hit in run_rq(&cand, code_dir)? {
40 if seen.insert(format!("{}:{}", hit.file, hit.line)) {
41 hit.via = cand.clone();
42 out.push(hit);
43 if out.len() >= limit {
44 return Ok(out);
45 }
46 }
47 }
48 }
49 Ok(out)
50}
51
52pub fn candidates(rec: &SchemaRecord) -> Vec<String> {
55 let field = &rec.name;
56 let snake = to_snake(field);
57 let pascal = to_pascal(field);
58 let mut c = Vec::new();
59
60 match rec.kind {
61 Kind::Mutation => {
62 c.push(format!("Mutations::{pascal}"));
63 c.push(pascal.clone());
64 c.push(format!("{pascal}Mutation"));
65 if let Some(t) = &rec.parent {
66 c.push(format!("{t}Type#{snake}"));
67 }
68 c.push(snake.clone());
69 }
70 Kind::Query | Kind::Subscription => {
71 c.push(format!("Resolvers::{pascal}"));
72 c.push(format!("{pascal}Resolver"));
73 c.push(format!("Resolvers::{pascal}Resolver"));
74 if let Some(t) = &rec.parent {
75 c.push(format!("{t}Type#{snake}"));
76 }
77 c.push(snake.clone());
78 }
79 Kind::Field | Kind::InputField => {
80 if let Some(t) = &rec.parent {
81 c.push(format!("{t}Type#{snake}"));
82 c.push(format!("{t}#{snake}"));
83 c.push(format!("Types::{t}Type#{snake}"));
84 }
85 c.push(snake.clone());
86 }
87 Kind::Object | Kind::Interface | Kind::InputObject | Kind::Union | Kind::Enum
89 | Kind::Scalar => {
90 let t = &rec.name;
91 c.push(format!("{t}Type"));
92 c.push(format!("Types::{t}"));
93 c.push(format!("Types::{t}Type"));
94 c.push(t.clone());
95 }
96 Kind::EnumValue | Kind::Directive => {
97 c.push(pascal.clone());
98 c.push(snake.clone());
99 }
100 }
101 c
102}
103
104fn run_rq(query: &str, dir: Option<&str>) -> Result<Vec<RqHit>> {
105 let mut cmd = Command::new("rq");
106 cmd.arg("--json").arg("--limit").arg("5").arg(query);
107 if let Some(d) = dir {
110 cmd.current_dir(d);
111 }
112 let output = cmd
113 .output()
114 .map_err(|e| anyhow!("running rq: {e} — is `rq` installed and on PATH?"))?;
115 match output.status.code() {
119 Some(0) | Some(1) => Ok(serde_json::from_slice(&output.stdout).unwrap_or_default()),
120 other => {
121 let msg = String::from_utf8_lossy(&output.stderr);
122 bail!(
123 "rq failed (exit {other:?}) for {query:?}: {}",
124 msg.lines().next().unwrap_or("").trim()
125 )
126 }
127 }
128}
129
130fn to_snake(s: &str) -> String {
132 let mut out = String::new();
133 for (i, c) in s.chars().enumerate() {
134 if c.is_uppercase() {
135 if i > 0 {
136 out.push('_');
137 }
138 out.extend(c.to_lowercase());
139 } else {
140 out.push(c);
141 }
142 }
143 out
144}
145
146fn to_pascal(s: &str) -> String {
148 let mut chars = s.chars();
149 match chars.next() {
150 Some(f) => f.to_uppercase().collect::<String>() + chars.as_str(),
151 None => String::new(),
152 }
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158
159 fn rec(name: &str, parent: Option<&str>, kind: Kind) -> SchemaRecord {
160 SchemaRecord {
161 path: parent.map_or_else(|| name.into(), |p| format!("{p}.{name}")),
162 name: name.into(),
163 kind,
164 parent: parent.map(String::from),
165 type_ref: None,
166 args: vec![],
167 description: None,
168 deprecated: None,
169 directives: vec![],
170 }
171 }
172
173 #[test]
174 fn casing() {
175 assert_eq!(to_snake("nameWithOwner"), "name_with_owner");
176 assert_eq!(to_pascal("createUser"), "CreateUser");
177 }
178
179 #[test]
180 fn root_query_prefers_resolver_then_type_method() {
181 let c = candidates(&rec("user", Some("Query"), Kind::Query));
182 assert_eq!(c[0], "Resolvers::User");
183 assert!(c.contains(&"QueryType#user".to_string()));
184 }
185
186 #[test]
187 fn mutation_prefers_mutation_class() {
188 let c = candidates(&rec("createUser", Some("Mutation"), Kind::Mutation));
189 assert_eq!(c[0], "Mutations::CreateUser");
190 }
191
192 #[test]
193 fn object_field_targets_the_type_method() {
194 let c = candidates(&rec("nameWithOwner", Some("Repository"), Kind::Field));
195 assert_eq!(c[0], "RepositoryType#name_with_owner");
196 }
197}