fastapi_output/components/
dependency_tree.rs1use crate::mode::OutputMode;
7use crate::themes::FastApiTheme;
8
9const ANSI_RESET: &str = "\x1b[0m";
10
11#[derive(Debug, Clone)]
13pub struct DependencyNode {
14 pub name: String,
16 pub children: Vec<DependencyNode>,
18 pub cached: bool,
20 pub scope: Option<String>,
22 pub note: Option<String>,
24 pub cycle: bool,
26}
27
28impl DependencyNode {
29 #[must_use]
31 pub fn new(name: impl Into<String>) -> Self {
32 Self {
33 name: name.into(),
34 children: Vec::new(),
35 cached: false,
36 scope: None,
37 note: None,
38 cycle: false,
39 }
40 }
41
42 #[must_use]
44 pub fn child(mut self, child: DependencyNode) -> Self {
45 self.children.push(child);
46 self
47 }
48
49 #[must_use]
51 pub fn children(mut self, children: Vec<DependencyNode>) -> Self {
52 self.children = children;
53 self
54 }
55
56 #[must_use]
58 pub fn cached(mut self) -> Self {
59 self.cached = true;
60 self
61 }
62
63 #[must_use]
65 pub fn scope(mut self, scope: impl Into<String>) -> Self {
66 self.scope = Some(scope.into());
67 self
68 }
69
70 #[must_use]
72 pub fn note(mut self, note: impl Into<String>) -> Self {
73 self.note = Some(note.into());
74 self
75 }
76
77 #[must_use]
79 pub fn cycle(mut self) -> Self {
80 self.cycle = true;
81 self
82 }
83}
84
85#[derive(Debug, Clone)]
87pub struct DependencyTreeDisplay {
88 mode: OutputMode,
89 theme: FastApiTheme,
90 roots: Vec<DependencyNode>,
91 show_cached: bool,
92 show_scopes: bool,
93 show_notes: bool,
94 title: Option<String>,
95 cycle_paths: Vec<Vec<String>>,
96}
97
98impl DependencyTreeDisplay {
99 #[must_use]
101 pub fn new(mode: OutputMode, roots: Vec<DependencyNode>) -> Self {
102 Self {
103 mode,
104 theme: FastApiTheme::default(),
105 roots,
106 show_cached: true,
107 show_scopes: true,
108 show_notes: true,
109 title: Some("Dependency Tree".to_string()),
110 cycle_paths: Vec::new(),
111 }
112 }
113
114 #[must_use]
116 pub fn theme(mut self, theme: FastApiTheme) -> Self {
117 self.theme = theme;
118 self
119 }
120
121 #[must_use]
123 pub fn hide_cached(mut self) -> Self {
124 self.show_cached = false;
125 self
126 }
127
128 #[must_use]
130 pub fn hide_scopes(mut self) -> Self {
131 self.show_scopes = false;
132 self
133 }
134
135 #[must_use]
137 pub fn hide_notes(mut self) -> Self {
138 self.show_notes = false;
139 self
140 }
141
142 #[must_use]
144 pub fn title(mut self, title: Option<String>) -> Self {
145 self.title = title;
146 self
147 }
148
149 #[must_use]
151 pub fn with_cycle_path(mut self, path: Vec<String>) -> Self {
152 if !path.is_empty() {
153 self.cycle_paths.push(path);
154 }
155 self
156 }
157
158 #[must_use]
160 pub fn render(&self) -> String {
161 if self.roots.is_empty() {
162 return "No dependencies registered.".to_string();
163 }
164
165 let glyphs = TreeGlyphs::for_mode(self.mode);
166 let mut lines = Vec::new();
167
168 if let Some(title) = &self.title {
169 lines.push(title.clone());
170 lines.push("-".repeat(title.len()));
171 }
172
173 for (idx, root) in self.roots.iter().enumerate() {
174 let is_last = idx + 1 == self.roots.len();
175 self.render_node(&mut lines, "", root, is_last, &glyphs);
176 }
177
178 if !self.cycle_paths.is_empty() {
179 lines.push(String::new());
180 lines.push(self.render_cycles_header());
181 for cycle in &self.cycle_paths {
182 lines.push(format!(" {}", cycle.join(" -> ")));
183 }
184 }
185
186 lines.join("\n")
187 }
188
189 fn render_cycles_header(&self) -> String {
190 if self.mode.uses_ansi() {
191 let error = self.theme.error.to_ansi_fg();
192 format!("{error}Cycles detected:{ANSI_RESET}")
193 } else {
194 "Cycles detected:".to_string()
195 }
196 }
197
198 fn render_node(
199 &self,
200 lines: &mut Vec<String>,
201 prefix: &str,
202 node: &DependencyNode,
203 is_last: bool,
204 glyphs: &TreeGlyphs,
205 ) {
206 let connector = if is_last { glyphs.last } else { glyphs.branch };
207 let label = self.render_label(node);
208 lines.push(format!("{prefix}{connector} {label}"));
209
210 let next_prefix = if is_last {
211 format!("{prefix}{}", glyphs.spacer)
212 } else {
213 format!("{prefix}{}", glyphs.vertical)
214 };
215
216 for (idx, child) in node.children.iter().enumerate() {
217 let child_is_last = idx + 1 == node.children.len();
218 self.render_node(lines, &next_prefix, child, child_is_last, glyphs);
219 }
220 }
221
222 fn render_label(&self, node: &DependencyNode) -> String {
223 let mut parts = Vec::new();
224 let name = if self.mode.uses_ansi() {
225 format!(
226 "{}{}{}",
227 self.theme.primary.to_ansi_fg(),
228 node.name,
229 ANSI_RESET
230 )
231 } else {
232 node.name.clone()
233 };
234 parts.push(name);
235
236 if self.show_cached && node.cached {
237 let cached = if self.mode.uses_ansi() {
238 format!("{}[cached]{}", self.theme.muted.to_ansi_fg(), ANSI_RESET)
239 } else {
240 "[cached]".to_string()
241 };
242 parts.push(cached);
243 }
244
245 if self.show_scopes {
246 if let Some(scope) = &node.scope {
247 let scope_text = if self.mode.uses_ansi() {
248 format!(
249 "{}(scope: {}){}",
250 self.theme.secondary.to_ansi_fg(),
251 scope,
252 ANSI_RESET
253 )
254 } else {
255 format!("(scope: {scope})")
256 };
257 parts.push(scope_text);
258 }
259 }
260
261 if node.cycle {
262 let cycle = if self.mode.uses_ansi() {
263 format!("{}[cycle]{}", self.theme.error.to_ansi_fg(), ANSI_RESET)
264 } else {
265 "[cycle]".to_string()
266 };
267 parts.push(cycle);
268 }
269
270 if self.show_notes {
271 if let Some(note) = &node.note {
272 parts.push(format!("- {note}"));
273 }
274 }
275
276 parts.join(" ")
277 }
278}
279
280struct TreeGlyphs {
281 branch: &'static str,
282 last: &'static str,
283 vertical: &'static str,
284 spacer: &'static str,
285}
286
287impl TreeGlyphs {
288 fn for_mode(mode: OutputMode) -> Self {
289 match mode {
290 OutputMode::Plain => Self {
291 branch: "+-",
292 last: "\\-",
293 vertical: "| ",
294 spacer: " ",
295 },
296 OutputMode::Minimal | OutputMode::Rich => Self {
297 branch: "├─",
298 last: "└─",
299 vertical: "│ ",
300 spacer: " ",
301 },
302 }
303 }
304}
305
306#[cfg(test)]
307mod tests {
308 use super::*;
309 use crate::testing::{assert_contains, assert_has_ansi, assert_no_ansi};
310
311 #[test]
316 fn test_dependency_node_new() {
317 let node = DependencyNode::new("TestService");
318 assert_eq!(node.name, "TestService");
319 assert!(node.children.is_empty());
320 assert!(!node.cached);
321 assert!(node.scope.is_none());
322 assert!(node.note.is_none());
323 assert!(!node.cycle);
324 }
325
326 #[test]
327 fn test_dependency_node_child() {
328 let node = DependencyNode::new("Parent")
329 .child(DependencyNode::new("Child1"))
330 .child(DependencyNode::new("Child2"));
331 assert_eq!(node.children.len(), 2);
332 assert_eq!(node.children[0].name, "Child1");
333 assert_eq!(node.children[1].name, "Child2");
334 }
335
336 #[test]
337 fn test_dependency_node_children() {
338 let children = vec![
339 DependencyNode::new("A"),
340 DependencyNode::new("B"),
341 DependencyNode::new("C"),
342 ];
343 let node = DependencyNode::new("Root").children(children);
344 assert_eq!(node.children.len(), 3);
345 assert_eq!(node.children[2].name, "C");
346 }
347
348 #[test]
349 fn test_dependency_node_cached() {
350 let node = DependencyNode::new("Cached").cached();
351 assert!(node.cached);
352 }
353
354 #[test]
355 fn test_dependency_node_scope() {
356 let node = DependencyNode::new("Scoped").scope("singleton");
357 assert_eq!(node.scope, Some("singleton".to_string()));
358 }
359
360 #[test]
361 fn test_dependency_node_note() {
362 let node = DependencyNode::new("Noted").note("Important service");
363 assert_eq!(node.note, Some("Important service".to_string()));
364 }
365
366 #[test]
367 fn test_dependency_node_cycle() {
368 let node = DependencyNode::new("Circular").cycle();
369 assert!(node.cycle);
370 }
371
372 #[test]
373 fn test_dependency_node_full_builder() {
374 let node = DependencyNode::new("FullService")
375 .cached()
376 .scope("request")
377 .note("Main service entry")
378 .cycle()
379 .child(DependencyNode::new("Dep1"));
380
381 assert_eq!(node.name, "FullService");
382 assert!(node.cached);
383 assert_eq!(node.scope, Some("request".to_string()));
384 assert_eq!(node.note, Some("Main service entry".to_string()));
385 assert!(node.cycle);
386 assert_eq!(node.children.len(), 1);
387 }
388
389 #[test]
394 fn test_empty_roots() {
395 let display = DependencyTreeDisplay::new(OutputMode::Plain, vec![]);
396 let output = display.render();
397 assert_eq!(output, "No dependencies registered.");
398 }
399
400 #[test]
401 fn test_custom_title() {
402 let roots = vec![DependencyNode::new("Service")];
403 let display = DependencyTreeDisplay::new(OutputMode::Plain, roots)
404 .title(Some("Custom DI Tree".to_string()));
405 let output = display.render();
406 assert_contains(&output, "Custom DI Tree");
407 assert!(!output.contains("Dependency Tree"));
408 }
409
410 #[test]
411 fn test_no_title() {
412 let roots = vec![DependencyNode::new("Service")];
413 let display = DependencyTreeDisplay::new(OutputMode::Plain, roots).title(None);
414 let output = display.render();
415 assert!(!output.contains("Dependency Tree"));
416 assert_contains(&output, "Service");
417 }
418
419 #[test]
420 fn test_hide_cached() {
421 let roots = vec![DependencyNode::new("Cached").cached()];
422 let display = DependencyTreeDisplay::new(OutputMode::Plain, roots).hide_cached();
423 let output = display.render();
424 assert!(!output.contains("[cached]"));
425 }
426
427 #[test]
428 fn test_hide_scopes() {
429 let roots = vec![DependencyNode::new("Scoped").scope("singleton")];
430 let display = DependencyTreeDisplay::new(OutputMode::Plain, roots).hide_scopes();
431 let output = display.render();
432 assert!(!output.contains("scope:"));
433 }
434
435 #[test]
436 fn test_hide_notes() {
437 let roots = vec![DependencyNode::new("Noted").note("Important note")];
438 let display = DependencyTreeDisplay::new(OutputMode::Plain, roots).hide_notes();
439 let output = display.render();
440 assert!(!output.contains("Important note"));
441 }
442
443 #[test]
448 fn renders_plain_tree() {
449 let roots = vec![
450 DependencyNode::new("Database")
451 .cached()
452 .scope("request")
453 .child(DependencyNode::new("Config")),
454 ];
455 let display = DependencyTreeDisplay::new(OutputMode::Plain, roots);
456 let output = display.render();
457
458 assert_contains(&output, "Dependency Tree");
459 assert_contains(&output, "Database");
460 assert_contains(&output, "[cached]");
461 assert_contains(&output, "scope: request");
462 assert_contains(&output, "Config");
463 assert_no_ansi(&output);
464 assert!(output.contains("+-") || output.contains("\\-"));
466 }
467
468 #[test]
469 fn renders_rich_tree_with_ansi() {
470 let roots = vec![DependencyNode::new("Service").cached().scope("request")];
471 let display = DependencyTreeDisplay::new(OutputMode::Rich, roots);
472 let output = display.render();
473
474 assert_has_ansi(&output);
475 assert_contains(&output, "Service");
476 assert!(output.contains("├─") || output.contains("└─"));
478 }
479
480 #[test]
481 fn renders_minimal_tree_with_unicode() {
482 let roots = vec![DependencyNode::new("Service")];
483 let display = DependencyTreeDisplay::new(OutputMode::Minimal, roots);
484 let output = display.render();
485
486 assert!(output.contains("└─"));
488 }
489
490 #[test]
495 fn test_multiple_roots() {
496 let roots = vec![
497 DependencyNode::new("Root1"),
498 DependencyNode::new("Root2"),
499 DependencyNode::new("Root3"),
500 ];
501 let display = DependencyTreeDisplay::new(OutputMode::Plain, roots);
502 let output = display.render();
503
504 assert_contains(&output, "Root1");
505 assert_contains(&output, "Root2");
506 assert_contains(&output, "Root3");
507 }
508
509 #[test]
510 fn test_deep_nesting() {
511 let roots = vec![
512 DependencyNode::new("Level0").child(
513 DependencyNode::new("Level1")
514 .child(DependencyNode::new("Level2").child(DependencyNode::new("Level3"))),
515 ),
516 ];
517 let display = DependencyTreeDisplay::new(OutputMode::Plain, roots);
518 let output = display.render();
519
520 assert_contains(&output, "Level0");
521 assert_contains(&output, "Level1");
522 assert_contains(&output, "Level2");
523 assert_contains(&output, "Level3");
524 }
525
526 #[test]
527 fn test_wide_tree() {
528 let children = (0..5)
529 .map(|i| DependencyNode::new(format!("Child{i}")))
530 .collect();
531 let roots = vec![DependencyNode::new("Root").children(children)];
532 let display = DependencyTreeDisplay::new(OutputMode::Plain, roots);
533 let output = display.render();
534
535 for i in 0..5 {
536 assert_contains(&output, &format!("Child{i}"));
537 }
538 }
539
540 #[test]
545 fn renders_cycle_marker() {
546 let roots = vec![
547 DependencyNode::new("Auth")
548 .child(DependencyNode::new("Db").cycle())
549 .child(DependencyNode::new("Cache")),
550 ];
551 let display = DependencyTreeDisplay::new(OutputMode::Plain, roots).with_cycle_path(vec![
552 "Auth".into(),
553 "Db".into(),
554 "Auth".into(),
555 ]);
556 let output = display.render();
557
558 assert_contains(&output, "[cycle]");
559 assert_contains(&output, "Cycles detected:");
560 assert_contains(&output, "Auth -> Db -> Auth");
561 }
562
563 #[test]
564 fn test_multiple_cycle_paths() {
565 let roots = vec![
566 DependencyNode::new("A")
567 .child(DependencyNode::new("B").cycle())
568 .child(DependencyNode::new("C").cycle()),
569 ];
570 let display = DependencyTreeDisplay::new(OutputMode::Plain, roots)
571 .with_cycle_path(vec!["A".into(), "B".into(), "A".into()])
572 .with_cycle_path(vec!["A".into(), "C".into(), "A".into()]);
573 let output = display.render();
574
575 assert_contains(&output, "A -> B -> A");
576 assert_contains(&output, "A -> C -> A");
577 }
578
579 #[test]
580 fn test_empty_cycle_path_ignored() {
581 let roots = vec![DependencyNode::new("Root")];
582 let display = DependencyTreeDisplay::new(OutputMode::Plain, roots).with_cycle_path(vec![]);
583 let output = display.render();
584
585 assert!(!output.contains("Cycles detected:"));
587 }
588
589 #[test]
594 fn test_node_with_all_annotations() {
595 let roots = vec![
596 DependencyNode::new("FullAnnotated")
597 .cached()
598 .scope("singleton")
599 .note("Main dependency")
600 .cycle(),
601 ];
602 let display = DependencyTreeDisplay::new(OutputMode::Plain, roots);
603 let output = display.render();
604
605 assert_contains(&output, "FullAnnotated");
606 assert_contains(&output, "[cached]");
607 assert_contains(&output, "(scope: singleton)");
608 assert_contains(&output, "[cycle]");
609 assert_contains(&output, "- Main dependency");
610 }
611
612 #[test]
613 fn test_rich_mode_cycles_header_styled() {
614 let roots = vec![DependencyNode::new("A").cycle()];
615 let display = DependencyTreeDisplay::new(OutputMode::Rich, roots)
616 .with_cycle_path(vec!["A".into(), "B".into()]);
617 let output = display.render();
618
619 assert_has_ansi(&output);
621 assert_contains(&output, "Cycles detected:");
622 }
623}