1use std::collections::BTreeMap;
28use std::collections::BTreeSet;
29
30use crate::error::Error;
31use crate::error::Result;
32use crate::ir::EnumKind;
33use crate::ir::Item;
34use crate::ir::Module;
35use crate::ir::RustType;
36use crate::naming::Case;
37use crate::naming::to_ident;
38
39pub fn box_recursive_types(module: &mut Module) -> Result<()> {
43 let graph = Graph::of(module);
44 graph.check_alias_cycles()?;
45 graph.check_union_cycles(module)?;
46
47 for item in &mut module.items {
48 let owner = canonical(item.name());
49 match item {
50 Item::Struct(strukt) => {
51 for field in &mut strukt.fields {
52 box_held(&mut field.ty, &owner, &graph);
53 }
54 }
55 Item::Enum(enumeration) => {
56 if let EnumKind::Union(variants) | EnumKind::AnyOf(variants) = &mut enumeration.kind {
57 for variant in variants {
58 box_held(&mut variant.ty, &owner, &graph);
59 }
60 }
61 }
62 Item::Alias(_) => {}
65 }
66 }
67 return Ok(());
68}
69
70struct Graph {
80 names: Vec<String>,
82 nodes: BTreeMap<String, usize>,
84 edges: Vec<Vec<usize>>,
86 is_alias: Vec<bool>,
88 component: Vec<usize>,
90}
91
92impl Graph {
93 fn of(module: &Module) -> Self {
95 let names: Vec<String> = module.items.iter().map(|item| return canonical(item.name())).collect();
96 let nodes: BTreeMap<String, usize> = names
97 .iter()
98 .enumerate()
99 .map(|(node, name)| return (name.clone(), node))
100 .collect();
101
102 let mut edges = Vec::with_capacity(module.items.len());
103 let mut is_alias = Vec::with_capacity(module.items.len());
104 for item in &module.items {
105 let mut targets = BTreeSet::new();
106 match item {
107 Item::Struct(strukt) => {
108 for field in &strukt.fields {
109 collect_held(&field.ty, &mut targets);
110 }
111 }
112 Item::Enum(enumeration) => {
113 if let EnumKind::Union(variants) | EnumKind::AnyOf(variants) = &enumeration.kind {
114 for variant in variants {
115 collect_held(&variant.ty, &mut targets);
116 }
117 }
118 }
119 Item::Alias(alias) => collect_held(&alias.ty, &mut targets),
120 }
121 is_alias.push(matches!(item, Item::Alias(_)));
122 edges.push(
125 targets
126 .iter()
127 .filter_map(|target| return nodes.get(target).copied())
128 .collect(),
129 );
130 }
131
132 let component = components(&edges);
133 return Self {
134 names,
135 nodes,
136 edges,
137 is_alias,
138 component,
139 };
140 }
141
142 fn on_a_cycle(&self, from: &str, to: &str) -> bool {
147 let (Some(from), Some(to)) = (self.nodes.get(from), self.nodes.get(to)) else {
148 return false;
149 };
150 let (Some(from), Some(to)) = (self.component.get(*from), self.component.get(*to)) else {
151 return false;
152 };
153 return from == to;
154 }
155
156 fn check_alias_cycles(&self) -> Result<()> {
161 let mut members: BTreeMap<usize, Vec<usize>> = BTreeMap::new();
162 for (node, component) in self.component.iter().enumerate() {
163 members.entry(*component).or_default().push(node);
164 }
165 for group in members.values() {
166 if !self.is_cyclic(group) || !group.iter().all(|node| return self.is_alias(*node)) {
167 continue;
168 }
169 return Err(Error::RecursiveAlias {
170 cycle: self.cycle_through(group),
171 hint: "Give one of these schemas `type: object` with properties, so the generator emits a struct it \
172 can box, or break the chain of `$ref`s."
173 .to_owned(),
174 });
175 }
176 return Ok(());
177 }
178
179 fn check_union_cycles(&self, module: &Module) -> Result<()> {
180 let edges: Vec<Vec<usize>> = self
182 .edges
183 .iter()
184 .zip(&module.items)
185 .map(|(targets, item)| {
186 return if matches!(item, Item::Struct(_)) {
187 Vec::new()
188 } else {
189 targets.clone()
190 };
191 })
192 .collect();
193 let component = components(&edges);
194 for (node, targets) in edges.iter().enumerate() {
195 if !self.is_alias(node)
196 && targets
197 .iter()
198 .any(|target| return component.get(node) == component.get(*target))
199 {
200 return Err(Error::UnsupportedSchema {
201 path: self.name_of(node),
202 reason: "union recursion must pass through an object property or array element to consume input"
203 .to_owned(),
204 });
205 }
206 }
207 return Ok(());
208 }
209
210 fn is_cyclic(&self, group: &[usize]) -> bool {
215 if group.len() > 1 {
216 return true;
217 }
218 return group
219 .first()
220 .is_some_and(|node| return self.edges_of(*node).contains(node));
221 }
222
223 fn edges_of(&self, node: usize) -> &[usize] {
225 return self.edges.get(node).map_or(&[], Vec::as_slice);
226 }
227
228 fn is_alias(&self, node: usize) -> bool {
230 return self.is_alias.get(node).copied().unwrap_or(false);
231 }
232
233 fn name_of(&self, node: usize) -> String {
235 return self.names.get(node).cloned().unwrap_or_default();
236 }
237
238 fn cycle_through(&self, group: &[usize]) -> Vec<String> {
243 let Some(start) = group.first().copied() else {
244 return Vec::new();
245 };
246 let mut path = vec![self.name_of(start)];
247 let mut current = start;
248 for _ in 0..group.len() {
249 let Some(next) = self.edges_of(current).first().copied() else {
250 break;
251 };
252 path.push(self.name_of(next));
253 if next == start {
254 break;
255 }
256 current = next;
257 }
258 return path;
259 }
260}
261
262fn components(edges: &[Vec<usize>]) -> Vec<usize> {
267 let mut walk = Walk::over(edges.len());
268 let mut work: Vec<(usize, usize)> = Vec::new();
270
271 for root in 0..edges.len() {
272 if walk.is_open_or_done(root) {
273 continue;
274 }
275 walk.open(root);
276 work.push((root, 0));
277
278 while let Some(&(node, taken)) = work.last() {
279 let next = edges.get(node).and_then(|held| return held.get(taken)).copied();
280 if let Some(next) = next {
281 if let Some(entry) = work.last_mut() {
282 entry.1 += 1;
283 }
284 walk.step(node, next, &mut work);
285 continue;
286 }
287
288 work.pop();
289 if let Some(&(parent, _)) = work.last() {
290 walk.carry_up(parent, node);
291 }
292 walk.close(node);
293 }
294 }
295 return walk.component;
296}
297
298struct Walk {
305 order: Vec<usize>,
307 lowest: Vec<usize>,
309 open: Vec<bool>,
311 component: Vec<usize>,
313 path: Vec<usize>,
315 next_order: usize,
317 next_component: usize,
319}
320
321impl Walk {
322 const UNREACHED: usize = usize::MAX;
324
325 fn over(count: usize) -> Self {
327 return Self {
328 order: vec![Self::UNREACHED; count],
329 lowest: vec![0; count],
330 open: vec![false; count],
331 component: vec![0; count],
332 path: Vec::new(),
333 next_order: 0,
334 next_component: 0,
335 };
336 }
337
338 fn is_open_or_done(&self, node: usize) -> bool {
340 return self.order_of(node) != Self::UNREACHED;
341 }
342
343 fn order_of(&self, node: usize) -> usize {
345 return self.order.get(node).copied().unwrap_or(Self::UNREACHED);
346 }
347
348 fn lowest_of(&self, node: usize) -> usize {
350 return self.lowest.get(node).copied().unwrap_or(Self::UNREACHED);
351 }
352
353 fn open(&mut self, node: usize) {
355 if let Some(order) = self.order.get_mut(node) {
356 *order = self.next_order;
357 }
358 if let Some(lowest) = self.lowest.get_mut(node) {
359 *lowest = self.next_order;
360 }
361 if let Some(open) = self.open.get_mut(node) {
362 *open = true;
363 }
364 self.next_order += 1;
365 self.path.push(node);
366 }
367
368 fn step(&mut self, node: usize, next: usize, work: &mut Vec<(usize, usize)>) {
373 if !self.is_open_or_done(next) {
374 self.open(next);
375 work.push((next, 0));
376 return;
377 }
378 if self.open.get(next).copied().unwrap_or(false) {
379 self.lower(node, self.order_of(next));
380 }
381 }
382
383 fn carry_up(&mut self, parent: usize, node: usize) {
385 self.lower(parent, self.lowest_of(node));
386 }
387
388 fn lower(&mut self, node: usize, order: usize) {
390 if let Some(lowest) = self.lowest.get_mut(node) {
391 *lowest = (*lowest).min(order);
392 }
393 }
394
395 fn close(&mut self, node: usize) {
400 if self.lowest_of(node) != self.order_of(node) {
401 return;
402 }
403 while let Some(member) = self.path.pop() {
404 if let Some(open) = self.open.get_mut(member) {
405 *open = false;
406 }
407 if let Some(component) = self.component.get_mut(member) {
408 *component = self.next_component;
409 }
410 if member == node {
411 break;
412 }
413 }
414 self.next_component += 1;
415 }
416}
417
418fn collect_held(ty: &RustType, out: &mut BTreeSet<String>) {
425 match ty {
426 RustType::Named(name) => {
427 out.insert(canonical(name));
428 }
429 RustType::Option(inner) | RustType::Nullable(inner) => collect_held(inner, out),
430 _ => {}
431 }
432}
433
434fn box_held(ty: &mut RustType, owner: &str, graph: &Graph) {
439 match ty {
440 RustType::Named(name) => {
441 let target = canonical(name);
442 if graph.on_a_cycle(&target, owner) {
443 let inner = std::mem::replace(ty, RustType::Bool);
444 *ty = RustType::Boxed(Box::new(inner));
445 }
446 }
447 RustType::Option(inner) | RustType::Nullable(inner) => box_held(inner, owner, graph),
448 _ => {}
449 }
450}
451
452fn canonical(name: &str) -> String {
454 return to_ident(name, Case::Pascal).logical().to_owned();
455}
456
457#[cfg(test)]
458mod tests {
459 use super::*;
460 use crate::ir::Access;
461 use crate::ir::Alias;
462 use crate::ir::Enum;
463 use crate::ir::Field;
464 use crate::ir::Struct;
465 use crate::ir::UnionVariant;
466
467 fn one_field(name: &str, field: &str, ty: RustType) -> Item {
469 return Item::Struct(Struct {
470 name: to_ident(name, Case::Pascal),
471 doc: None,
472 deprecated: None,
473 fields: vec![Field {
474 name: to_ident(field, Case::Snake),
475 rename: None,
476 doc: None,
477 deprecated: None,
478 ty,
479 required: true,
480 omit_empty: None,
481 serde_skip: false,
482 default: None,
483 constraints: None,
484 access: Access::ReadWrite,
485 }],
486 additional_properties: None,
487 deny_unknown_fields: false,
488 });
489 }
490
491 fn field_type(module: &Module, name: &str) -> RustType {
493 for item in &module.items {
494 if let Item::Struct(strukt) = item
495 && strukt.name.logical() == name
496 {
497 return strukt.fields[0].ty.clone();
498 }
499 }
500 panic!("no struct named `{name}`");
501 }
502
503 fn named(name: &str) -> RustType {
504 return RustType::Named(name.to_owned());
505 }
506
507 fn boxed(inner: RustType) -> RustType {
508 return RustType::Boxed(Box::new(inner));
509 }
510
511 #[test]
514 fn only_a_field_that_holds_its_owner_is_boxed() {
515 let cases: &[(&str, RustType, RustType)] = &[
516 ("direct", named("Node"), boxed(named("Node"))),
517 (
518 "through an option",
519 RustType::Option(Box::new(named("Node"))),
520 RustType::Option(Box::new(boxed(named("Node")))),
521 ),
522 (
523 "through a vec",
524 RustType::Vec(Box::new(named("Node"))),
525 RustType::Vec(Box::new(named("Node"))),
526 ),
527 (
528 "through a map",
529 RustType::Map(Box::new(named("Node"))),
530 RustType::Map(Box::new(named("Node"))),
531 ),
532 ("a scalar", RustType::String, RustType::String),
533 ];
534
535 for (label, input, want) in cases {
536 let mut module = Module {
537 items: vec![one_field("Node", "child", input.clone())],
538 };
539 box_recursive_types(&mut module).expect("no alias cycle in this module");
540 assert_eq!(field_type(&module, "Node"), *want, "self-reference {label}");
541 }
542 }
543
544 #[test]
547 fn both_sides_of_a_mutual_cycle_are_boxed() {
548 let mut module = Module {
549 items: vec![
550 one_field("Parent", "child", named("Kid")),
551 one_field("Kid", "parent", named("Parent")),
552 ],
553 };
554 box_recursive_types(&mut module).expect("no alias cycle in this module");
555 assert_eq!(field_type(&module, "Parent"), boxed(named("Kid")));
556 assert_eq!(field_type(&module, "Kid"), boxed(named("Parent")));
557 }
558
559 #[test]
562 fn a_type_the_cycle_only_points_at_is_left_alone() {
563 let mut module = Module {
564 items: vec![
565 one_field("Node", "child", named("Node")),
566 one_field("Holder", "node", named("Node")),
567 ],
568 };
569 box_recursive_types(&mut module).expect("no alias cycle in this module");
570 assert_eq!(field_type(&module, "Holder"), named("Node"));
571 }
572
573 #[test]
574 fn a_union_cycle_through_an_object_is_boxed() {
575 let mut module = Module {
576 items: vec![
577 Item::Enum(Enum {
578 name: to_ident("Expression", Case::Pascal),
579 doc: None,
580 deprecated: None,
581 kind: EnumKind::Union(vec![
582 UnionVariant {
583 name: to_ident("Text", Case::Pascal),
584 ty: RustType::String,
585 },
586 UnionVariant {
587 name: to_ident("Nested", Case::Pascal),
588 ty: named("Nested"),
589 },
590 ]),
591 }),
592 one_field("Nested", "expression", named("Expression")),
593 ],
594 };
595 box_recursive_types(&mut module).expect("no alias cycle in this module");
596 let Item::Enum(enumeration) = &module.items[0] else {
597 panic!("the item is an enum");
598 };
599 let EnumKind::Union(variants) = &enumeration.kind else {
600 panic!("the enum is a union");
601 };
602 assert_eq!(variants[0].ty, RustType::String);
603 assert_eq!(variants[1].ty, boxed(named("Nested")));
604 assert_eq!(field_type(&module, "Nested"), boxed(named("Expression")));
605 }
606
607 #[test]
608 fn non_consuming_union_cycles_are_rejected() {
609 for inclusive in [false, true] {
610 for through_alias in [false, true] {
611 let variants = vec![
612 UnionVariant {
613 name: to_ident("Text", Case::Pascal),
614 ty: RustType::String,
615 },
616 UnionVariant {
617 name: to_ident("Loop", Case::Pascal),
618 ty: named(if through_alias { "Alias" } else { "Loop" }),
619 },
620 ];
621 let mut module = Module {
622 items: vec![Item::Enum(Enum {
623 name: to_ident("Loop", Case::Pascal),
624 doc: None,
625 deprecated: None,
626 kind: if inclusive {
627 EnumKind::AnyOf(variants)
628 } else {
629 EnumKind::Union(variants)
630 },
631 })],
632 };
633 if through_alias {
634 module.items.push(Item::Alias(Alias {
635 name: to_ident("Alias", Case::Pascal),
636 doc: None,
637 deprecated: None,
638 ty: named("Loop"),
639 }));
640 }
641 assert!(matches!(
642 box_recursive_types(&mut module),
643 Err(Error::UnsupportedSchema { .. })
644 ));
645 }
646 }
647 }
648
649 #[test]
652 fn a_cycle_through_an_alias_is_boxed_at_the_struct() {
653 let mut module = Module {
654 items: vec![
655 Item::Alias(Alias {
656 name: to_ident("Wrapper", Case::Pascal),
657 doc: None,
658 deprecated: None,
659 ty: named("Holder"),
660 }),
661 one_field("Holder", "wrapped", named("Wrapper")),
662 ],
663 };
664 box_recursive_types(&mut module).expect("this cycle holds a struct, so it is not alias-only");
665 assert_eq!(field_type(&module, "Holder"), boxed(named("Wrapper")));
666 }
667
668 #[test]
670 fn an_alias_only_cycle_is_rejected() {
671 let alias = |name: &str, target: &str| {
672 return Item::Alias(Alias {
673 name: to_ident(name, Case::Pascal),
674 doc: None,
675 deprecated: None,
676 ty: named(target),
677 });
678 };
679 let mut module = Module {
680 items: vec![alias("Loop", "Ring"), alias("Ring", "Loop")],
681 };
682 let outcome = box_recursive_types(&mut module);
683 assert!(
684 matches!(outcome, Err(Error::RecursiveAlias { .. })),
685 "a cycle of aliases must be rejected, and gave: {outcome:?}",
686 );
687 }
688
689 #[test]
691 fn a_self_referencing_alias_is_rejected() {
692 let mut module = Module {
693 items: vec![Item::Alias(Alias {
694 name: to_ident("Loop", Case::Pascal),
695 doc: None,
696 deprecated: None,
697 ty: named("Loop"),
698 })],
699 };
700 assert!(matches!(
701 box_recursive_types(&mut module),
702 Err(Error::RecursiveAlias { .. })
703 ));
704 }
705
706 #[test]
708 fn a_module_without_a_cycle_is_unchanged() {
709 let mut module = Module {
710 items: vec![
711 one_field("Holder", "node", named("Node")),
712 one_field("Node", "id", RustType::String),
713 ],
714 };
715 let before = module.clone();
716 box_recursive_types(&mut module).expect("no alias cycle in this module");
717 assert_eq!(module, before);
718 }
719
720 #[test]
727 fn an_existing_box_breaks_the_cycle_for_every_other_edge() {
728 let mut module = Module {
729 items: vec![
730 one_field("Parent", "child", boxed(named("Kid"))),
731 one_field("Kid", "parent", named("Parent")),
732 ],
733 };
734 box_recursive_types(&mut module).expect("no alias cycle in this module");
735 assert_eq!(field_type(&module, "Parent"), boxed(named("Kid")));
736 assert_eq!(field_type(&module, "Kid"), named("Parent"));
737 }
738}