1use std::collections::{BTreeMap, BTreeSet};
9
10use serde_json::{Value, json};
11
12use super::docs::{Doc, Docs};
13use super::foreign::{TypeMapping, TypeTable};
14use super::naming;
15use super::wire_names::WireNames;
16use super::{Gap, graphql_attrs, serde_attrs};
17
18pub type Subst = BTreeMap<String, (Value, String)>;
21
22pub struct Resolver<'a> {
28 docs: Vec<Doc<'a>>,
30 cur: usize,
34 table: &'a TypeTable,
35 pub components: BTreeMap<String, Value>,
37 stems: BTreeMap<String, String>,
42 owners: BTreeMap<String, String>,
47 pub gaps: Vec<Gap>,
48 pub aux_resolved: BTreeSet<String>,
51 stack: BTreeSet<String>,
53}
54
55impl<'a> Resolver<'a> {
56 pub fn new(docs: impl Into<Docs<'a>>, table: &'a TypeTable) -> Result<Self, String> {
59 let docs = docs.into();
60 let mut open = vec![Doc::open(docs.primary())?];
61 open.extend(docs.aux().iter().filter_map(|d| Doc::open(d).ok()));
62 let walkable: BTreeSet<String> = open
63 .iter()
64 .flat_map(Doc::walkable)
65 .map(String::clone)
66 .collect();
67 Ok(Self {
68 stems: naming::stems(&walkable),
69 docs: open,
70 cur: 0,
71 table,
72 components: BTreeMap::new(),
73 owners: BTreeMap::new(),
74 gaps: Vec::new(),
75 aux_resolved: BTreeSet::new(),
76 stack: BTreeSet::new(),
77 })
78 }
79
80 pub(super) fn item(&self, id: &Value) -> Option<&'a Value> {
81 self.docs[self.cur].item(id)
82 }
83
84 pub fn find_type_id(&self, name: &str) -> Option<Value> {
87 self.docs[0].scan_type_id(name)
88 }
89
90 pub fn find_by_path(&self, canonical: &str) -> Option<&'a Value> {
95 let doc = &self.docs[0];
96 let (id, _) = doc.paths.iter().find(|(_, entry)| {
97 entry["path"]
98 .as_array()
99 .map(|segs| {
100 let joined: Vec<&str> = segs.iter().filter_map(|s| s.as_str()).collect();
101 joined.join("::") == canonical
102 })
103 .unwrap_or(false)
104 })?;
105 doc.index.get(id)
106 }
107
108 pub fn resolve_by_name(&mut self, name: &str, at: &str) -> Option<Value> {
111 if let Some(id) = self.find_type_id(name) {
112 let node = json!({ "resolved_path": { "path": name, "id": id, "args": null } });
113 return Some(self.resolve(&node, &Subst::new(), at));
114 }
115 let found =
116 (1..self.docs.len()).find_map(|i| self.docs[i].locate_name(name).map(|id| (i, id)));
117 let (home, id) = found?;
118 self.note_aux(home, &id);
119 Some(self.resolve_named(home, &id, &[], &Subst::new(), at))
120 }
121
122 pub(super) fn canonical(&self, id: &Value) -> Option<String> {
124 self.docs[self.cur].canonical(id)
125 }
126
127 fn note_aux(&mut self, home: usize, id: &Value) {
129 if let Some(path) = self.docs[home].canonical(id) {
130 self.aux_resolved.insert(path);
131 }
132 }
133
134 pub(super) fn record(&mut self, gap: Gap) {
135 if !self.gaps.contains(&gap) {
136 self.gaps.push(gap);
137 }
138 }
139
140 pub fn is_option(ty: &Value) -> bool {
143 ty["resolved_path"]["path"]
144 .as_str()
145 .is_some_and(|p| p == "Option" || p.ends_with("::Option"))
146 }
147
148 pub fn resolve(&mut self, ty: &Value, subst: &Subst, at: &str) -> Value {
152 if let Some(p) = ty.get("primitive").and_then(|v| v.as_str()) {
153 return primitive(p);
154 }
155 if let Some(name) = ty.get("generic").and_then(|v| v.as_str()) {
156 return match subst.get(name) {
158 Some((schema, _)) => schema.clone(),
159 None => json!({}),
160 };
161 }
162 if let Some(bounds) = ty.get("impl_trait") {
163 return self.erased(bounds, at);
164 }
165 if let Some(dy) = ty.get("dyn_trait") {
166 return self.erased(&dy["traits"], at);
167 }
168 if let Some(inner) = ty.get("borrowed_ref") {
169 return self.resolve(&inner["type"], subst, at);
170 }
171 if let Some(inner) = ty.get("slice") {
172 let items = self.resolve(inner, subst, at);
173 return json!({ "type": "array", "items": items });
174 }
175 if let Some(arr) = ty.get("array") {
176 let items = self.resolve(&arr["type"], subst, at);
177 let mut schema = json!({ "type": "array", "items": items });
178 if let Some(len) = arr["len"].as_str().and_then(|l| l.parse::<u64>().ok()) {
179 schema["minItems"] = json!(len);
180 schema["maxItems"] = json!(len);
181 }
182 return schema;
183 }
184 if let Some(elems) = ty.get("tuple").and_then(|v| v.as_array()) {
185 if elems.is_empty() {
187 return json!({ "type": "null" });
188 }
189 let items: Vec<Value> = elems.iter().map(|e| self.resolve(e, subst, at)).collect();
190 let n = items.len();
191 return json!({ "type": "array", "prefixItems": items,
192 "minItems": n, "maxItems": n });
193 }
194 if ty.get("resolved_path").is_some() {
195 return self.resolve_path(&ty["resolved_path"], subst, at);
196 }
197 json!({})
200 }
201
202 fn erased(&mut self, bounds: &Value, at: &str) -> Value {
203 let bound = bounds
204 .as_array()
205 .and_then(|b| b.first())
206 .and_then(|b| b["trait_bound"]["trait"]["path"].as_str())
207 .unwrap_or("?")
208 .to_string();
209 self.record(Gap::Erased {
210 at: at.to_string(),
211 bound,
212 });
213 json!({})
214 }
215
216 fn resolve_path(&mut self, rp: &Value, subst: &Subst, at: &str) -> Value {
217 let display = rp["path"].as_str().unwrap_or_default();
218 let args = generic_args(rp);
219 let last = display.rsplit("::").next().unwrap_or(display);
220
221 match last {
224 "Option" => {
225 return match args.first() {
228 Some(inner) => self.resolve(inner, subst, at),
229 None => json!({}),
230 };
231 }
232 "Box" | "Arc" | "Rc" | "Cow" | "RefCell" | "Cell" | "Mutex" | "RwLock" => {
233 return match args.first() {
234 Some(inner) => self.resolve(inner, subst, at),
235 None => json!({}),
236 };
237 }
238 "Vec" | "VecDeque" | "HashSet" | "BTreeSet" | "BinaryHeap" | "LinkedList" => {
239 let items = match args.first() {
240 Some(inner) => self.resolve(inner, subst, at),
241 None => json!({}),
242 };
243 let mut schema = json!({ "type": "array", "items": items });
244 if matches!(last, "HashSet" | "BTreeSet") {
245 schema["uniqueItems"] = json!(true);
246 }
247 return schema;
248 }
249 "HashMap" | "BTreeMap" => {
250 let values = match args.get(1) {
251 Some(v) => self.resolve(v, subst, at),
252 None => json!({}),
253 };
254 return json!({ "type": "object", "additionalProperties": values });
255 }
256 "Result" => {
257 let ok = args
259 .first()
260 .map(|t| self.resolve(t, subst, at))
261 .unwrap_or(json!({}));
262 let err = args
263 .get(1)
264 .map(|t| self.resolve(t, subst, at))
265 .unwrap_or(json!({}));
266 return json!({ "oneOf": [
267 { "type": "object", "properties": { "Ok": ok },
268 "required": ["Ok"], "additionalProperties": false },
269 { "type": "object", "properties": { "Err": err },
270 "required": ["Err"], "additionalProperties": false },
271 ]});
272 }
273 "Connection" => {
282 let node = args
283 .get(1)
284 .map(|t| self.resolve(t, subst, at))
285 .unwrap_or(json!({}));
286 return json!({
287 "type": "object",
288 "properties": {
289 "pageInfo": {
290 "type": "object",
291 "properties": {
292 "hasPreviousPage": { "type": "boolean" },
293 "hasNextPage": { "type": "boolean" },
294 "startCursor": { "type": ["string", "null"] },
295 "endCursor": { "type": ["string", "null"] },
296 },
297 "required": ["hasPreviousPage", "hasNextPage"],
298 },
299 "edges": {
300 "type": "array",
301 "items": {
302 "type": "object",
303 "properties": { "cursor": { "type": "string" }, "node": node.clone() },
304 "required": ["cursor", "node"],
305 },
306 },
307 "nodes": { "type": "array", "items": node },
308 },
309 "required": ["pageInfo", "edges", "nodes"],
310 });
311 }
312 _ => {}
313 }
314
315 let id = &rp["id"];
316 if self.item(id).is_some() {
319 return self.resolve_named(self.cur, id, &args, subst, at);
320 }
321 let canonical = self.canonical(id).unwrap_or_else(|| display.to_string());
322 match self.table.lookup(&canonical).cloned() {
325 Some(TypeMapping::Literal(schema)) => return schema,
326 Some(TypeMapping::Transparent { arg }) => {
327 return self.transparent(&canonical, arg, &args, subst, at);
328 }
329 None => {}
330 }
331 if let Some((home, aux_id)) = self.locate_aux(&canonical) {
334 self.aux_resolved.insert(canonical);
335 return self.resolve_named(home, &aux_id, &args, subst, at);
336 }
337 if last == "String" || last == "str" {
338 return json!({ "type": "string" });
339 }
340 self.record(Gap::UnmappedForeign {
341 at: at.to_string(),
342 path: canonical,
343 });
344 json!({})
345 }
346
347 fn transparent(
351 &mut self,
352 path: &str,
353 arg: usize,
354 args: &[Value],
355 subst: &Subst,
356 at: &str,
357 ) -> Value {
358 match args.get(arg) {
359 Some(inner) => self.resolve(inner, subst, at),
360 None => {
361 self.record(Gap::TransparentWithoutArgument {
363 at: at.to_string(),
364 path: path.to_string(),
365 arg,
366 });
367 json!({})
368 }
369 }
370 }
371
372 fn locate_aux(&self, canonical: &str) -> Option<(usize, Value)> {
374 (1..self.docs.len()).find_map(|i| self.docs[i].locate(canonical).map(|id| (i, id)))
375 }
376
377 fn resolve_named(
383 &mut self,
384 home: usize,
385 id: &Value,
386 args: &[Value],
387 subst: &Subst,
388 at: &str,
389 ) -> Value {
390 let Some(item) = self.docs[home].item(id) else {
391 return json!({});
392 };
393 let base = item["name"].as_str().unwrap_or("Anonymous").to_string();
394
395 let params = generic_param_names(item);
397 let mut inner_subst = Subst::new();
398 let mut arg_names = Vec::new();
399 for (i, param) in params.iter().enumerate() {
400 let Some(arg) = args.get(i) else { continue };
401 let schema = self.resolve(arg, subst, at);
402 let name = type_display_name(arg, subst);
403 arg_names.push(name.clone());
404 inner_subst.insert(param.clone(), (schema, name));
405 }
406 let suffix = |stem: &str| match arg_names.is_empty() {
407 true => stem.to_string(),
408 false => format!("{stem}_{}", arg_names.join("_")),
409 };
410 let canonical = self.docs[home].canonical(id);
413 let stem = canonical
414 .as_ref()
415 .and_then(|c| self.stems.get(c))
416 .unwrap_or(&base);
417 let identity = suffix(canonical.as_ref().unwrap_or(&base));
418 let component = self.claim(&suffix(stem), &identity);
419 let reference = json!({ "$ref": format!("#/components/schemas/{component}") });
420
421 if self.components.contains_key(&component) || self.stack.contains(&component) {
422 return reference;
423 }
424 self.stack.insert(component.clone());
425 let outer = std::mem::replace(&mut self.cur, home);
427 let defined = self.define(item, &inner_subst, &component);
428 self.cur = outer;
429 self.stack.remove(&component);
430 self.components.insert(component, defined);
431 reference
432 }
433
434 fn claim(&mut self, display: &str, identity: &str) -> String {
442 let mut candidate = display.to_string();
443 let mut n = 1;
444 loop {
445 match self.owners.get(&candidate) {
446 Some(owner) if owner == identity => return candidate,
447 Some(_) => {
449 n += 1;
450 candidate = format!("{display}_{n}");
451 }
452 None => {
453 self.owners.insert(candidate.clone(), identity.to_string());
454 return candidate;
455 }
456 }
457 }
458 }
459
460 fn define(&mut self, item: &'a Value, subst: &Subst, at: &str) -> Value {
462 let inner = &item["inner"];
463 if let Some(s) = inner.get("struct") {
464 return self.walk_struct(item, s, subst, at);
465 }
466 if let Some(e) = inner.get("enum") {
467 return self.walk_enum(item, e, subst, at);
468 }
469 if let Some(t) = inner.get("type_alias") {
470 return self.resolve(&t["type"], subst, at);
471 }
472 json!({})
473 }
474
475 pub(super) fn describe(item: &Value, schema: &mut Value) {
477 if let Some(doc) = item["docs"].as_str().filter(|d| !d.is_empty())
478 && let Some(obj) = schema.as_object_mut()
479 {
480 let first = doc.split("\n\n").next().unwrap_or(doc);
481 obj.insert("description".into(), json!(first.trim()));
482 }
483 }
484
485 pub(super) fn container_attrs(&mut self, item: &Value, at: &str) -> WireNames {
487 let empty = Vec::new();
488 let attrs = item["attrs"].as_array().unwrap_or(&empty);
489 let serde = serde_attrs::container(attrs);
490
491 let gql = graphql_attrs::container(attrs);
495 let served = gql.present || self.docs[self.cur].serves_graphql(item);
496 let unknown = match served {
497 true => gql.unknown_rename_rule.clone(),
498 false => serde.unknown_rename_all.clone(),
499 };
500 if let Some(rule) = unknown {
501 self.record(Gap::UnknownRenameRule {
502 at: at.to_string(),
503 rule,
504 });
505 }
506 WireNames {
507 serde,
508 graphql: served.then_some(gql),
509 }
510 }
511}
512
513pub(super) fn generic_args(rp: &Value) -> Vec<Value> {
515 rp["args"]["angle_bracketed"]["args"]
516 .as_array()
517 .map(|args| args.iter().filter_map(|a| a.get("type").cloned()).collect())
518 .unwrap_or_default()
519}
520
521fn generic_param_names(item: &Value) -> Vec<String> {
523 let inner = &item["inner"];
524 let generics = inner
525 .get("struct")
526 .or_else(|| inner.get("enum"))
527 .or_else(|| inner.get("type_alias"))
528 .map(|k| &k["generics"]);
529 generics
530 .and_then(|g| g["params"].as_array())
531 .map(|ps| {
532 ps.iter()
533 .filter(|p| p["kind"].get("type").is_some())
534 .filter_map(|p| p["name"].as_str().map(String::from))
535 .collect()
536 })
537 .unwrap_or_default()
538}
539
540fn type_display_name(ty: &Value, subst: &Subst) -> String {
543 if let Some(p) = ty.get("primitive").and_then(|v| v.as_str()) {
544 return p.to_string();
545 }
546 if let Some(g) = ty.get("generic").and_then(|v| v.as_str()) {
547 return subst
548 .get(g)
549 .map(|(_, n)| n.clone())
550 .unwrap_or_else(|| g.to_string());
551 }
552 if let Some(path) = ty["resolved_path"]["path"].as_str() {
553 let base = path.rsplit("::").next().unwrap_or(path).to_string();
554 let args = generic_args(&ty["resolved_path"]);
555 if args.is_empty() {
556 return base;
557 }
558 let inner: Vec<String> = args.iter().map(|a| type_display_name(a, subst)).collect();
559 return format!("{base}_{}", inner.join("_"));
560 }
561 "T".to_string()
562}
563
564fn primitive(p: &str) -> Value {
566 match p {
567 "bool" => json!({ "type": "boolean" }),
568 "char" => json!({ "type": "string", "minLength": 1, "maxLength": 1 }),
569 "str" => json!({ "type": "string" }),
570 "f32" => json!({ "type": "number", "format": "float" }),
571 "f64" => json!({ "type": "number", "format": "double" }),
572 "i8" | "i16" | "i32" => json!({ "type": "integer", "format": "int32" }),
573 "u8" | "u16" | "u32" => json!({ "type": "integer", "format": "int32", "minimum": 0 }),
574 "i64" | "i128" | "isize" => json!({ "type": "integer", "format": "int64" }),
575 "u64" | "u128" | "usize" => {
576 json!({ "type": "integer", "format": "int64", "minimum": 0 })
577 }
578 "()" | "never" => json!({ "type": "null" }),
579 _ => json!({}),
580 }
581}