1use std::collections::{HashMap, HashSet};
2use std::fmt::Write;
3
4use crate::container::descriptor::{ServiceDescriptor, ServiceId};
5use crate::container::ioc_container::IocContainer;
6use crate::container::module::ModuleRegistry;
7use crate::container::scope::ServiceScope;
8use crate::errors::CoreError;
9
10#[derive(Debug, Clone, PartialEq)]
12pub enum VisualizationFormat {
13 Dot,
15 Mermaid,
17 Ascii,
19 Json,
21 Html,
23}
24
25#[derive(Debug, Clone)]
27pub struct VisualizationStyle {
28 pub show_lifetimes: bool,
30 pub show_names: bool,
32 pub color_by_lifetime: bool,
34 pub group_by_module: bool,
36 pub filter_types: Option<Vec<String>>,
38 pub max_depth: Option<usize>,
40 pub include_stats: bool,
42}
43
44impl Default for VisualizationStyle {
45 fn default() -> Self {
46 Self {
47 show_lifetimes: true,
48 show_names: true,
49 color_by_lifetime: true,
50 group_by_module: false,
51 filter_types: None,
52 max_depth: None,
53 include_stats: false,
54 }
55 }
56}
57
58pub struct DependencyVisualizer {
61 descriptors: Vec<ServiceDescriptor>,
62 dependency_graph: HashMap<ServiceId, Vec<ServiceId>>,
63 reverse_graph: HashMap<ServiceId, Vec<ServiceId>>,
64 modules: Option<ModuleRegistry>,
65}
66
67impl DependencyVisualizer {
68 pub fn new(descriptors: Vec<ServiceDescriptor>) -> Self {
70 let mut dependency_graph = HashMap::new();
71 let mut reverse_graph: HashMap<ServiceId, Vec<ServiceId>> = HashMap::new();
72
73 for descriptor in &descriptors {
74 dependency_graph.insert(
75 descriptor.service_id.clone(),
76 descriptor.dependencies.clone(),
77 );
78
79 for dependency in &descriptor.dependencies {
81 reverse_graph
82 .entry(dependency.clone())
83 .or_default()
84 .push(descriptor.service_id.clone());
85 }
86 }
87
88 Self {
89 descriptors,
90 dependency_graph,
91 reverse_graph,
92 modules: None,
93 }
94 }
95
96 pub fn from_container(container: &IocContainer) -> Self {
98 let descriptors = container
99 .registered_services()
100 .into_iter()
101 .filter_map(|_service_id| {
102 None })
106 .collect();
107
108 Self::new(descriptors)
109 }
110
111 pub fn with_modules(mut self, modules: ModuleRegistry) -> Self {
113 self.modules = Some(modules);
114 self
115 }
116
117 pub fn visualize(
119 &self,
120 format: VisualizationFormat,
121 style: VisualizationStyle,
122 ) -> Result<String, CoreError> {
123 match format {
124 VisualizationFormat::Dot => self.generate_dot(style),
125 VisualizationFormat::Mermaid => self.generate_mermaid(style),
126 VisualizationFormat::Ascii => self.generate_ascii(style),
127 VisualizationFormat::Json => self.generate_json(style),
128 VisualizationFormat::Html => self.generate_html(style),
129 }
130 }
131
132 fn generate_dot(&self, style: VisualizationStyle) -> Result<String, CoreError> {
134 let mut dot = String::new();
135 writeln!(dot, "digraph ServiceDependencies {{").unwrap();
136 writeln!(dot, " rankdir=TB;").unwrap();
137 writeln!(dot, " node [shape=rectangle];").unwrap();
138 writeln!(dot).unwrap();
139
140 if style.color_by_lifetime {
142 writeln!(dot, " // Lifetime color scheme").unwrap();
143 writeln!(dot, " // Singleton: lightblue").unwrap();
144 writeln!(dot, " // Scoped: lightgreen").unwrap();
145 writeln!(dot, " // Transient: lightyellow").unwrap();
146 writeln!(dot).unwrap();
147 }
148
149 for descriptor in &self.descriptors {
151 if let Some(ref filter) = style.filter_types {
152 if !filter
153 .iter()
154 .any(|f| descriptor.service_id.type_name().contains(f))
155 {
156 continue;
157 }
158 }
159
160 let service_name = self.format_service_name(&descriptor.service_id, &style);
161 let mut node_attrs = Vec::new();
162
163 if style.color_by_lifetime {
164 let color = match descriptor.lifetime {
165 ServiceScope::Singleton => "lightblue",
166 ServiceScope::Scoped => "lightgreen",
167 ServiceScope::Transient => "lightyellow",
168 };
169 node_attrs.push(format!("fillcolor={}", color));
170 node_attrs.push("style=filled".to_string());
171 }
172
173 if style.show_lifetimes {
174 let lifetime_text = format!("\\n({:?})", descriptor.lifetime);
175 node_attrs.push(format!("label=\"{}{}\"", service_name, lifetime_text));
176 } else {
177 node_attrs.push(format!("label=\"{}\"", service_name));
178 }
179
180 writeln!(
181 dot,
182 " \"{}\" [{}];",
183 service_name,
184 node_attrs.join(", ")
185 )
186 .unwrap();
187 }
188
189 writeln!(dot).unwrap();
190
191 for (service_id, dependencies) in &self.dependency_graph {
193 if let Some(ref filter) = style.filter_types {
194 if !filter.iter().any(|f| service_id.type_name().contains(f)) {
195 continue;
196 }
197 }
198
199 for dependency in dependencies {
200 let source_name = self.format_service_name(service_id, &style);
201 let target_name = self.format_service_name(dependency, &style);
202 writeln!(
203 dot,
204 " \"{}\" -> \"{}\";",
205 source_name,
206 target_name
207 )
208 .unwrap();
209 }
210 }
211
212 writeln!(dot, "}}").unwrap();
213 Ok(dot)
214 }
215
216 fn generate_mermaid(&self, style: VisualizationStyle) -> Result<String, CoreError> {
218 let mut mermaid = String::new();
219 writeln!(mermaid, "graph TD").unwrap();
220
221 for descriptor in &self.descriptors {
223 if let Some(ref filter) = style.filter_types {
224 if !filter
225 .iter()
226 .any(|f| descriptor.service_id.type_name().contains(f))
227 {
228 continue;
229 }
230 }
231
232 let service_name = self.format_service_name(&descriptor.service_id, &style);
233 let node_id = self.sanitize_id(descriptor.service_id.type_name());
234
235 let lifetime_indicator = if style.show_lifetimes {
236 match descriptor.lifetime {
237 ServiceScope::Singleton => "●",
238 ServiceScope::Scoped => "◐",
239 ServiceScope::Transient => "○",
240 }
241 } else {
242 ""
243 };
244
245 if style.color_by_lifetime {
246 let class_name = match descriptor.lifetime {
247 ServiceScope::Singleton => "singleton",
248 ServiceScope::Scoped => "scoped",
249 ServiceScope::Transient => "transient",
250 };
251 writeln!(
252 mermaid,
253 " {}[\"{} {}\"]::{}",
254 node_id, lifetime_indicator, service_name, class_name
255 )
256 .unwrap();
257 } else {
258 writeln!(
259 mermaid,
260 " {}[\"{} {}\"]",
261 node_id, lifetime_indicator, service_name
262 )
263 .unwrap();
264 }
265 }
266
267 writeln!(mermaid).unwrap();
268
269 for (service_id, dependencies) in &self.dependency_graph {
271 if let Some(ref filter) = style.filter_types {
272 if !filter.iter().any(|f| service_id.type_name().contains(f)) {
273 continue;
274 }
275 }
276
277 let service_node_id = self.sanitize_id(service_id.type_name());
278 for dependency in dependencies {
279 let dep_node_id = self.sanitize_id(dependency.type_name());
280 writeln!(mermaid, " {} --> {}", service_node_id, dep_node_id).unwrap();
281 }
282 }
283
284 if style.color_by_lifetime {
286 writeln!(mermaid).unwrap();
287 writeln!(mermaid, " classDef singleton fill:#add8e6").unwrap();
288 writeln!(mermaid, " classDef scoped fill:#90ee90").unwrap();
289 writeln!(mermaid, " classDef transient fill:#ffffe0").unwrap();
290 }
291
292 Ok(mermaid)
293 }
294
295 fn generate_ascii(&self, style: VisualizationStyle) -> Result<String, CoreError> {
297 let mut ascii = String::new();
298 writeln!(ascii, "Service Dependency Tree").unwrap();
299 writeln!(ascii, "=======================").unwrap();
300 writeln!(ascii).unwrap();
301
302 let mut roots = Vec::new();
304 for descriptor in &self.descriptors {
305 if !self.reverse_graph.contains_key(&descriptor.service_id)
306 || self.reverse_graph[&descriptor.service_id].is_empty()
307 {
308 roots.push(&descriptor.service_id);
309 }
310 }
311
312 if roots.is_empty() {
314 for descriptor in &self.descriptors {
315 if descriptor.dependencies.is_empty() {
316 roots.push(&descriptor.service_id);
317 }
318 }
319 }
320
321 let mut visited = HashSet::new();
322 for root in roots {
323 self.generate_ascii_tree(
324 root,
325 &style,
326 &mut ascii,
327 0,
328 "",
329 &mut visited,
330 style.max_depth,
331 )?;
332 }
333
334 if style.include_stats {
335 writeln!(ascii).unwrap();
336 writeln!(ascii, "Statistics:").unwrap();
337 writeln!(ascii, "-----------").unwrap();
338 writeln!(ascii, "Total services: {}", self.descriptors.len()).unwrap();
339
340 let mut lifetime_counts = HashMap::new();
341 for desc in &self.descriptors {
342 *lifetime_counts.entry(desc.lifetime).or_insert(0) += 1;
343 }
344
345 for (lifetime, count) in lifetime_counts {
346 writeln!(ascii, "{:?}: {}", lifetime, count).unwrap();
347 }
348 }
349
350 Ok(ascii)
351 }
352
353 fn generate_ascii_tree(
355 &self,
356 service_id: &ServiceId,
357 style: &VisualizationStyle,
358 output: &mut String,
359 depth: usize,
360 prefix: &str,
361 visited: &mut HashSet<ServiceId>,
362 max_depth: Option<usize>,
363 ) -> Result<(), CoreError> {
364 if let Some(max_d) = max_depth {
365 if depth >= max_d {
366 return Ok(());
367 }
368 }
369
370 if visited.contains(service_id) {
371 writeln!(
372 output,
373 "{}├── {} (circular)",
374 prefix,
375 self.format_service_name(service_id, style)
376 )
377 .unwrap();
378 return Ok(());
379 }
380
381 visited.insert(service_id.clone());
382
383 let descriptor = self
384 .descriptors
385 .iter()
386 .find(|d| &d.service_id == service_id);
387
388 let service_display = if let Some(desc) = descriptor {
389 if style.show_lifetimes {
390 format!(
391 "{} ({:?})",
392 self.format_service_name(service_id, style),
393 desc.lifetime
394 )
395 } else {
396 self.format_service_name(service_id, style)
397 }
398 } else {
399 self.format_service_name(service_id, style)
400 };
401
402 writeln!(output, "{}├── {}", prefix, service_display).unwrap();
403
404 if let Some(dependencies) = self.dependency_graph.get(service_id) {
405 for (i, dependency) in dependencies.iter().enumerate() {
406 let is_last = i == dependencies.len() - 1;
407 let new_prefix = if is_last {
408 format!("{} ", prefix)
409 } else {
410 format!("{}│ ", prefix)
411 };
412
413 self.generate_ascii_tree(
414 dependency,
415 style,
416 output,
417 depth + 1,
418 &new_prefix,
419 visited,
420 max_depth,
421 )?;
422 }
423 }
424
425 visited.remove(service_id);
426 Ok(())
427 }
428
429 fn generate_json(&self, style: VisualizationStyle) -> Result<String, CoreError> {
431 use std::collections::BTreeMap; let mut json_data = BTreeMap::new();
434
435 let mut services = Vec::new();
437 for descriptor in &self.descriptors {
438 let service_name = self.format_service_name(&descriptor.service_id, &style);
439
440 if let Some(ref filter) = style.filter_types {
441 if !filter
442 .iter()
443 .any(|f| service_name.contains(f))
444 {
445 continue;
446 }
447 }
448
449 let mut service_data = BTreeMap::new();
450 service_data.insert(
451 "id".to_string(),
452 serde_json::Value::String(service_name.clone()),
453 );
454
455 if style.show_names {
456 service_data.insert(
457 "name".to_string(),
458 serde_json::Value::String(
459 self.format_service_name(&descriptor.service_id, &style),
460 ),
461 );
462 }
463
464 if style.show_lifetimes {
465 service_data.insert(
466 "lifetime".to_string(),
467 serde_json::Value::String(format!("{:?}", descriptor.lifetime)),
468 );
469 }
470
471 let deps: Vec<serde_json::Value> = descriptor
472 .dependencies
473 .iter()
474 .map(|dep| serde_json::Value::String(self.format_service_name(dep, &style)))
475 .collect();
476 service_data.insert("dependencies".to_string(), serde_json::Value::Array(deps));
477
478 services.push(serde_json::Value::Object(
479 service_data.into_iter().collect(),
480 ));
481 }
482
483 json_data.insert("services".to_string(), serde_json::Value::Array(services));
484
485 let mut edges = Vec::new();
487 for (service_id, dependencies) in &self.dependency_graph {
488 let source_name = self.format_service_name(service_id, &style);
489
490 if let Some(ref filter) = style.filter_types {
491 if !filter.iter().any(|f| source_name.contains(f)) {
492 continue;
493 }
494 }
495
496 for dependency in dependencies {
497 let target_name = self.format_service_name(dependency, &style);
498 let mut edge = BTreeMap::new();
499 edge.insert(
500 "from".to_string(),
501 serde_json::Value::String(source_name.clone()),
502 );
503 edge.insert(
504 "to".to_string(),
505 serde_json::Value::String(target_name),
506 );
507
508 edges.push(serde_json::Value::Object(edge.into_iter().collect()));
509 }
510 }
511
512 json_data.insert("edges".to_string(), serde_json::Value::Array(edges));
513
514 if style.include_stats {
516 let mut stats = BTreeMap::new();
517 stats.insert(
518 "total_services".to_string(),
519 serde_json::Value::Number(serde_json::Number::from(self.descriptors.len())),
520 );
521
522 let total_deps: usize = self.dependency_graph.values().map(|deps| deps.len()).sum();
523 stats.insert(
524 "total_dependencies".to_string(),
525 serde_json::Value::Number(serde_json::Number::from(total_deps)),
526 );
527
528 json_data.insert(
529 "statistics".to_string(),
530 serde_json::Value::Object(stats.into_iter().collect()),
531 );
532 }
533
534 let json_value = serde_json::Value::Object(json_data.into_iter().collect());
535 serde_json::to_string_pretty(&json_value).map_err(|e| CoreError::InvalidServiceDescriptor {
536 message: format!("Failed to serialize JSON: {}", e),
537 })
538 }
539
540 fn generate_html(&self, style: VisualizationStyle) -> Result<String, CoreError> {
542 let _json_data = self.generate_json(style)?;
543
544 Ok("<html><body><h1>Service Dependencies Visualization</h1><p>Interactive visualization would be generated here</p></body></html>".to_string())
546 }
547
548 fn format_service_name(&self, service_id: &ServiceId, style: &VisualizationStyle) -> String {
550 if !style.show_names {
551 return "Service".to_string();
552 }
553
554 if let Some(name) = &service_id.name {
555 name.clone()
556 } else {
557 let type_name = service_id.type_name();
559 type_name
560 .split("::")
561 .last()
562 .unwrap_or(type_name)
563 .to_string()
564 }
565 }
566
567 fn sanitize_id(&self, id: &str) -> String {
569 id.replace("::", "_")
570 .replace("<", "_")
571 .replace(">", "_")
572 .replace(" ", "_")
573 .replace("-", "_")
574 }
575}
576
577pub struct ServiceExplorer {
580 visualizer: DependencyVisualizer,
581}
582
583impl ServiceExplorer {
584 pub fn new(descriptors: Vec<ServiceDescriptor>) -> Self {
586 Self {
587 visualizer: DependencyVisualizer::new(descriptors),
588 }
589 }
590
591 pub fn find_paths(&self, from: &ServiceId, to: &ServiceId) -> Vec<Vec<ServiceId>> {
593 let mut paths = Vec::new();
594 let mut current_path = Vec::new();
595 let mut visited = HashSet::new();
596
597 self.find_paths_recursive(from, to, &mut current_path, &mut visited, &mut paths);
598 paths
599 }
600
601 fn find_paths_recursive(
603 &self,
604 current: &ServiceId,
605 target: &ServiceId,
606 path: &mut Vec<ServiceId>,
607 visited: &mut HashSet<ServiceId>,
608 paths: &mut Vec<Vec<ServiceId>>,
609 ) {
610 if visited.contains(current) {
611 return; }
613
614 path.push(current.clone());
615 visited.insert(current.clone());
616
617 if current == target {
618 paths.push(path.clone());
619 } else if let Some(dependencies) = self.visualizer.dependency_graph.get(current) {
620 for dependency in dependencies {
621 self.find_paths_recursive(dependency, target, path, visited, paths);
622 }
623 }
624
625 path.pop();
626 visited.remove(current);
627 }
628
629 pub fn get_dependents(&self, service_id: &ServiceId) -> Vec<&ServiceId> {
631 self.visualizer
632 .reverse_graph
633 .get(service_id)
634 .map(|deps| deps.iter().collect())
635 .unwrap_or_default()
636 }
637
638 pub fn get_dependency_depth(&self, service_id: &ServiceId) -> usize {
640 let mut max_depth = 0;
641 let mut visited = HashSet::new();
642
643 self.calculate_depth(service_id, 0, &mut max_depth, &mut visited);
644 max_depth
645 }
646
647 fn calculate_depth(
649 &self,
650 service_id: &ServiceId,
651 current_depth: usize,
652 max_depth: &mut usize,
653 visited: &mut HashSet<ServiceId>,
654 ) {
655 if visited.contains(service_id) {
656 return; }
658
659 *max_depth = (*max_depth).max(current_depth);
660 visited.insert(service_id.clone());
661
662 if let Some(dependencies) = self.visualizer.dependency_graph.get(service_id) {
663 for dependency in dependencies {
664 self.calculate_depth(dependency, current_depth + 1, max_depth, visited);
665 }
666 }
667
668 visited.remove(service_id);
669 }
670
671 pub fn generate_report(&self) -> String {
673 let mut report = String::new();
674
675 writeln!(report, "Service Dependency Analysis Report").unwrap();
676 writeln!(report, "===================================").unwrap();
677 writeln!(report).unwrap();
678
679 writeln!(report, "Summary:").unwrap();
680 writeln!(report, "--------").unwrap();
681 writeln!(
682 report,
683 "Total services: {}",
684 self.visualizer.descriptors.len()
685 )
686 .unwrap();
687 writeln!(
688 report,
689 "Total dependencies: {}",
690 self.visualizer
691 .dependency_graph
692 .values()
693 .map(|deps| deps.len())
694 .sum::<usize>()
695 )
696 .unwrap();
697 writeln!(report).unwrap();
698
699 let mut services_by_deps: Vec<_> = self
701 .visualizer
702 .descriptors
703 .iter()
704 .map(|desc| (desc, desc.dependencies.len()))
705 .collect();
706 services_by_deps.sort_by_key(|(_, count)| std::cmp::Reverse(*count));
707
708 writeln!(report, "Services with Most Dependencies:").unwrap();
709 writeln!(report, "-------------------------------").unwrap();
710 for (desc, count) in services_by_deps.iter().take(10) {
711 writeln!(
712 report,
713 "{}: {} dependencies",
714 desc.service_id.type_name(),
715 count
716 )
717 .unwrap();
718 }
719 writeln!(report).unwrap();
720
721 let mut dependents_count: HashMap<&ServiceId, usize> = HashMap::new();
723 for dependents in self.visualizer.reverse_graph.values() {
724 for dependent in dependents {
725 *dependents_count.entry(dependent).or_insert(0) += 1;
726 }
727 }
728
729 let mut services_by_dependents: Vec<_> = dependents_count.into_iter().collect();
730 services_by_dependents.sort_by_key(|(_, count)| std::cmp::Reverse(*count));
731
732 writeln!(report, "Most Depended-Upon Services:").unwrap();
733 writeln!(report, "---------------------------").unwrap();
734 for (service_id, count) in services_by_dependents.iter().take(10) {
735 writeln!(report, "{}: {} dependents", service_id.type_name(), count).unwrap();
736 }
737
738 report
739 }
740}
741
742#[cfg(test)]
743mod tests {
744 use super::*;
745 use crate::container::descriptor::{ServiceActivationStrategy, ServiceDescriptor};
746 use std::any::{Any, TypeId};
747
748 fn create_test_descriptor(
749 type_name: &str,
750 lifetime: ServiceScope,
751 deps: Vec<&str>,
752 ) -> ServiceDescriptor {
753 let service_id = ServiceId {
754 type_id: TypeId::of::<()>(),
755 type_name: "test_service",
756 name: Some(type_name.to_string()),
757 };
758
759 let dependencies: Vec<ServiceId> = deps
760 .iter()
761 .map(|dep| ServiceId {
762 type_id: TypeId::of::<()>(),
763 type_name: "test_service",
764 name: Some(dep.to_string()),
765 })
766 .collect();
767
768 ServiceDescriptor {
769 service_id,
770 implementation_id: TypeId::of::<()>(),
771 lifetime,
772 dependencies,
773 activation_strategy: ServiceActivationStrategy::Factory(Box::new(|| {
774 Ok(Box::new(()) as Box<dyn Any + Send + Sync>)
775 })),
776 }
777 }
778
779 #[test]
780 fn test_dot_generation() {
781 let descriptors = vec![
782 create_test_descriptor("ServiceA", ServiceScope::Singleton, vec![]),
783 create_test_descriptor("ServiceB", ServiceScope::Scoped, vec!["ServiceA"]),
784 ];
785
786 let visualizer = DependencyVisualizer::new(descriptors);
787 let style = VisualizationStyle::default();
788
789 let dot = visualizer.generate_dot(style).unwrap();
790
791 assert!(dot.contains("digraph ServiceDependencies"));
792 assert!(dot.contains("ServiceA"));
793 assert!(dot.contains("ServiceB"));
794 assert!(dot.contains("ServiceB\" -> \"ServiceA"));
795 }
796
797 #[test]
798 fn test_mermaid_generation() {
799 let descriptors = vec![
800 create_test_descriptor("ServiceA", ServiceScope::Singleton, vec![]),
801 create_test_descriptor("ServiceB", ServiceScope::Transient, vec!["ServiceA"]),
802 ];
803
804 let visualizer = DependencyVisualizer::new(descriptors);
805 let style = VisualizationStyle::default();
806
807 let mermaid = visualizer.generate_mermaid(style).unwrap();
808
809 assert!(mermaid.contains("graph TD"));
810 assert!(mermaid.contains("ServiceA"));
811 assert!(mermaid.contains("ServiceB"));
812 assert!(mermaid.contains("-->"));
813 }
814
815 #[test]
816 fn test_ascii_generation() {
817 let descriptors = vec![
818 create_test_descriptor("ServiceA", ServiceScope::Singleton, vec![]),
819 create_test_descriptor("ServiceB", ServiceScope::Scoped, vec!["ServiceA"]),
820 ];
821
822 let visualizer = DependencyVisualizer::new(descriptors);
823 let style = VisualizationStyle::default();
824
825 let ascii = visualizer.generate_ascii(style).unwrap();
826
827 assert!(ascii.contains("Service Dependency Tree"));
828 assert!(ascii.contains("ServiceA"));
829 assert!(ascii.contains("ServiceB"));
830 }
831
832 #[test]
833 fn test_json_generation() {
834 let descriptors = vec![
835 create_test_descriptor("ServiceA", ServiceScope::Singleton, vec![]),
836 create_test_descriptor("ServiceB", ServiceScope::Transient, vec!["ServiceA"]),
837 ];
838
839 let visualizer = DependencyVisualizer::new(descriptors);
840 let style = VisualizationStyle::default();
841
842 let json = visualizer.generate_json(style).unwrap();
843
844 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
846 assert!(parsed.get("services").is_some());
847 assert!(parsed.get("edges").is_some());
848 }
849
850 #[test]
851 fn test_service_explorer() {
852 let descriptors = vec![
853 create_test_descriptor("ServiceA", ServiceScope::Singleton, vec![]),
854 create_test_descriptor("ServiceB", ServiceScope::Scoped, vec!["ServiceA"]),
855 create_test_descriptor("ServiceC", ServiceScope::Transient, vec!["ServiceB"]),
856 ];
857
858 let explorer = ServiceExplorer::new(descriptors);
859
860 let service_a = ServiceId {
861 type_id: TypeId::of::<()>(),
862 type_name: "test_service",
863 name: Some("ServiceA".to_string()),
864 };
865 let service_c = ServiceId {
866 type_id: TypeId::of::<()>(),
867 type_name: "test_service",
868 name: Some("ServiceC".to_string()),
869 };
870
871 let paths = explorer.find_paths(&service_c, &service_a);
873 assert_eq!(paths.len(), 1);
874 assert_eq!(paths[0].len(), 3); let depth = explorer.get_dependency_depth(&service_c);
878 assert_eq!(depth, 2); }
880
881 #[test]
882 fn test_style_filtering() {
883 let descriptors = vec![
884 create_test_descriptor("UserService", ServiceScope::Singleton, vec![]),
885 create_test_descriptor("PaymentService", ServiceScope::Scoped, vec!["UserService"]),
886 create_test_descriptor("NotificationService", ServiceScope::Transient, vec![]),
887 ];
888
889 let visualizer = DependencyVisualizer::new(descriptors);
890 let mut style = VisualizationStyle::default();
891 style.filter_types = Some(vec!["User".to_string(), "Payment".to_string()]);
892
893 let json = visualizer.generate_json(style).unwrap();
894 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
895
896 let services = parsed["services"].as_array().unwrap();
897 assert_eq!(services.len(), 2); let service_names: Vec<&str> = services.iter().map(|s| s["id"].as_str().unwrap()).collect();
900 assert!(service_names.contains(&"UserService"));
901 assert!(service_names.contains(&"PaymentService"));
902 assert!(!service_names.contains(&"NotificationService"));
903 }
904}