1use std::collections::{HashMap, HashSet};
9
10#[cfg(feature = "graph-export")]
11use serde::{Serialize, Deserialize};
12
13#[derive(Debug, Clone)]
18#[cfg_attr(feature = "graph-export", derive(Serialize, Deserialize))]
19pub struct GraphNode {
20 pub id: String,
22 pub type_name: String,
24 pub lifetime: String,
26 pub is_trait: bool,
28 pub dependencies: Vec<String>,
30 pub metadata: HashMap<String, String>,
32 pub position: Option<NodePosition>,
34}
35
36#[derive(Debug, Clone)]
38#[cfg_attr(feature = "graph-export", derive(Serialize, Deserialize))]
39pub struct NodePosition {
40 pub x: f64,
41 pub y: f64,
42 pub z: Option<f64>,
43}
44
45#[derive(Debug, Clone)]
50#[cfg_attr(feature = "graph-export", derive(Serialize, Deserialize))]
51pub struct GraphEdge {
52 pub from: String,
54 pub to: String,
56 pub dependency_type: DependencyType,
58 pub metadata: HashMap<String, String>,
60}
61
62#[derive(Debug, Clone, PartialEq, Eq)]
64#[cfg_attr(feature = "graph-export", derive(Serialize, Deserialize))]
65pub enum DependencyType {
66 Required,
68 Optional,
70 Multiple,
72 Trait,
74 Factory,
76 Scoped,
78 Decorated,
80}
81
82#[derive(Debug, Clone)]
87#[cfg_attr(feature = "graph-export", derive(Serialize, Deserialize))]
88pub struct DependencyGraph {
89 pub nodes: Vec<GraphNode>,
91 pub edges: Vec<GraphEdge>,
93 pub metadata: GraphMetadata,
95 pub layout: Option<GraphLayout>,
97}
98
99#[derive(Debug, Clone)]
101#[cfg_attr(feature = "graph-export", derive(Serialize, Deserialize))]
102pub struct GraphMetadata {
103 pub service_count: usize,
105 pub trait_count: usize,
107 pub singleton_count: usize,
109 pub scoped_count: usize,
111 pub transient_count: usize,
113 pub has_circular_dependencies: bool,
115 pub exported_at: String,
117 pub version: String,
119}
120
121#[derive(Debug, Clone)]
123#[cfg_attr(feature = "graph-export", derive(Serialize, Deserialize))]
124pub struct GraphLayout {
125 pub algorithm: String,
127 #[cfg(feature = "graph-export")]
129 pub parameters: HashMap<String, serde_json::Value>,
130 #[cfg(not(feature = "graph-export"))]
131 pub parameters: HashMap<String, String>,
132 pub bounds: Option<LayoutBounds>,
134}
135
136#[derive(Debug, Clone)]
138#[cfg_attr(feature = "graph-export", derive(Serialize, Deserialize))]
139pub struct LayoutBounds {
140 pub min_x: f64,
141 pub min_y: f64,
142 pub max_x: f64,
143 pub max_y: f64,
144}
145
146#[derive(Debug, Clone)]
148pub struct ExportOptions {
149 pub include_dependencies: bool,
151 pub include_lifetimes: bool,
153 pub include_metadata: bool,
155 pub include_layout: bool,
157 pub type_filter: HashSet<String>,
159 pub max_depth: Option<usize>,
161 pub include_internal: bool,
163}
164
165impl Default for ExportOptions {
166 fn default() -> Self {
167 Self {
168 include_dependencies: true,
169 include_lifetimes: true,
170 include_metadata: true,
171 include_layout: false,
172 type_filter: HashSet::new(),
173 max_depth: None,
174 include_internal: false,
175 }
176 }
177}
178
179#[derive(Debug, Clone, Copy, PartialEq, Eq)]
181pub enum ExportFormat {
182 Json,
184 Yaml,
186 Dot,
188 Mermaid,
190 Custom(&'static str),
192}
193
194pub trait GraphExporter {
199 fn export(&self, graph: &DependencyGraph, format: ExportFormat, options: &ExportOptions) -> crate::DiResult<String>;
211}
212
213#[derive(Default)]
218pub struct DefaultGraphExporter;
219
220impl GraphExporter for DefaultGraphExporter {
221 fn export(&self, graph: &DependencyGraph, format: ExportFormat, options: &ExportOptions) -> crate::DiResult<String> {
222 match format {
223 ExportFormat::Json => self.export_json(graph, options),
224 ExportFormat::Yaml => self.export_yaml(graph, options),
225 ExportFormat::Dot => self.export_dot(graph, options),
226 ExportFormat::Mermaid => self.export_mermaid(graph, options),
227 ExportFormat::Custom(name) => Err(crate::DiError::NotFound(
228 Box::leak(format!("Unsupported custom format: {}", name).into_boxed_str())
229 )),
230 }
231 }
232}
233
234impl DefaultGraphExporter {
235 fn export_json(&self, graph: &DependencyGraph, _options: &ExportOptions) -> crate::DiResult<String> {
237 #[cfg(feature = "graph-export")]
238 {
239 serde_json::to_string_pretty(graph)
240 .map_err(|_| crate::DiError::TypeMismatch("JSON serialization failed"))
241 }
242 #[cfg(not(feature = "graph-export"))]
243 {
244 let mut json = String::from("{\n");
246 json.push_str(&format!(" \"metadata\": {{\n"));
247 json.push_str(&format!(" \"service_count\": {},\n", graph.metadata.service_count));
248 json.push_str(&format!(" \"trait_count\": {},\n", graph.metadata.trait_count));
249 json.push_str(&format!(" \"exported_at\": \"{}\"\n", graph.metadata.exported_at));
250 json.push_str(" },\n");
251 json.push_str(" \"nodes\": [\n");
252 for (i, node) in graph.nodes.iter().enumerate() {
253 if i > 0 { json.push_str(",\n"); }
254 json.push_str(&format!(" {{\n"));
255 json.push_str(&format!(" \"id\": \"{}\",\n", node.id));
256 json.push_str(&format!(" \"type_name\": \"{}\",\n", node.type_name));
257 json.push_str(&format!(" \"is_trait\": {},\n", node.is_trait));
258 json.push_str(&format!(" \"lifetime\": \"{:?}\"\n", node.lifetime));
259 json.push_str(" }");
260 }
261 json.push_str("\n ],\n");
262 json.push_str(" \"edges\": [\n");
263 for (i, edge) in graph.edges.iter().enumerate() {
264 if i > 0 { json.push_str(",\n"); }
265 json.push_str(&format!(" {{\n"));
266 json.push_str(&format!(" \"from\": \"{}\",\n", edge.from));
267 json.push_str(&format!(" \"to\": \"{}\",\n", edge.to));
268 json.push_str(&format!(" \"dependency_type\": \"{:?}\"\n", edge.dependency_type));
269 json.push_str(" }");
270 }
271 json.push_str("\n ]\n");
272 json.push_str("}");
273 Ok(json)
274 }
275 }
276
277 fn export_yaml(&self, graph: &DependencyGraph, _options: &ExportOptions) -> crate::DiResult<String> {
279 #[cfg(feature = "graph-export")]
280 {
281 serde_yaml::to_string(graph)
282 .map_err(|_| crate::DiError::TypeMismatch("YAML serialization failed"))
283 }
284 #[cfg(not(feature = "graph-export"))]
285 {
286 let mut yaml = String::new();
288 yaml.push_str("metadata:\n");
289 yaml.push_str(&format!(" service_count: {}\n", graph.metadata.service_count));
290 yaml.push_str(&format!(" trait_count: {}\n", graph.metadata.trait_count));
291 yaml.push_str(&format!(" exported_at: \"{}\"\n", graph.metadata.exported_at));
292 yaml.push_str("nodes:\n");
293 for node in &graph.nodes {
294 yaml.push_str(&format!(" - id: \"{}\"\n", node.id));
295 yaml.push_str(&format!(" type_name: \"{}\"\n", node.type_name));
296 yaml.push_str(&format!(" is_trait: {}\n", node.is_trait));
297 yaml.push_str(&format!(" lifetime: {:?}\n", node.lifetime));
298 }
299 yaml.push_str("edges:\n");
300 for edge in &graph.edges {
301 yaml.push_str(&format!(" - from: \"{}\"\n", edge.from));
302 yaml.push_str(&format!(" to: \"{}\"\n", edge.to));
303 yaml.push_str(&format!(" dependency_type: {:?}\n", edge.dependency_type));
304 }
305 Ok(yaml)
306 }
307 }
308
309 fn export_dot(&self, graph: &DependencyGraph, options: &ExportOptions) -> crate::DiResult<String> {
311 let mut output = String::new();
312 output.push_str("digraph DependencyGraph {\n");
313 output.push_str(" rankdir=TB;\n");
314 output.push_str(" node [shape=box];\n\n");
315
316 for node in &graph.nodes {
318 if !options.type_filter.is_empty() && !options.type_filter.contains(&node.type_name) {
319 continue;
320 }
321
322 let shape = if node.is_trait { "ellipse" } else { "box" };
323 let color = match node.lifetime.as_str() {
324 "Singleton" => "lightblue",
325 "Scoped" => "lightgreen",
326 "Transient" => "lightyellow",
327 _ => "white",
328 };
329
330 output.push_str(&format!(
331 " \"{}\" [label=\"{}\\n({})\", shape={}, fillcolor={}, style=filled];\n",
332 node.id, node.type_name, node.lifetime, shape, color
333 ));
334 }
335
336 output.push_str("\n");
337
338 for edge in &graph.edges {
340 let style = match edge.dependency_type {
341 DependencyType::Required => "solid",
342 DependencyType::Optional => "dashed",
343 DependencyType::Multiple => "bold",
344 DependencyType::Trait => "dotted",
345 _ => "solid",
346 };
347
348 output.push_str(&format!(
349 " \"{}\" -> \"{}\" [style={}];\n",
350 edge.from, edge.to, style
351 ));
352 }
353
354 output.push_str("}\n");
355 Ok(output)
356 }
357
358 fn export_mermaid(&self, graph: &DependencyGraph, options: &ExportOptions) -> crate::DiResult<String> {
360 let mut output = String::new();
361 output.push_str("graph TD\n");
362
363 for node in &graph.nodes {
365 if !options.type_filter.is_empty() && !options.type_filter.contains(&node.type_name) {
366 continue;
367 }
368
369 let shape = if node.is_trait {
370 format!("{}({})", node.id, node.type_name)
371 } else {
372 format!("{}[{}]", node.id, node.type_name)
373 };
374
375 output.push_str(&format!(" {}\n", shape));
376 }
377
378 for edge in &graph.edges {
380 let arrow = match edge.dependency_type {
381 DependencyType::Optional => "-.->",
382 DependencyType::Multiple => "==>",
383 _ => "-->",
384 };
385
386 output.push_str(&format!(" {} {} {}\n", edge.from, arrow, edge.to));
387 }
388
389 output.push_str("\n classDef singleton fill:#e1f5fe\n");
391 output.push_str(" classDef scoped fill:#e8f5e8\n");
392 output.push_str(" classDef transient fill:#fff3e0\n");
393
394 for node in &graph.nodes {
395 let class = match node.lifetime.as_str() {
396 "Singleton" => "singleton",
397 "Scoped" => "scoped",
398 "Transient" => "transient",
399 _ => continue,
400 };
401 output.push_str(&format!(" class {} {}\n", node.id, class));
402 }
403
404 Ok(output)
405 }
406}
407
408pub struct GraphBuilder {
413 options: ExportOptions,
414 exporter: Box<dyn GraphExporter>,
415}
416
417impl GraphBuilder {
418 pub fn new() -> Self {
420 Self {
421 options: ExportOptions::default(),
422 exporter: Box::new(DefaultGraphExporter),
423 }
424 }
425
426 pub fn with_options(mut self, options: ExportOptions) -> Self {
428 self.options = options;
429 self
430 }
431
432 pub fn with_exporter(mut self, exporter: Box<dyn GraphExporter>) -> Self {
434 self.exporter = exporter;
435 self
436 }
437
438 pub fn build_graph(&self, provider: &crate::ServiceProvider) -> crate::DiResult<DependencyGraph> {
443 let mut nodes = Vec::new();
444 let mut edges = Vec::new();
445 let mut node_ids: HashMap<String, String> = HashMap::new();
446
447 let registry = &provider.inner().registry;
449
450 for (key, registration) in ®istry.one_small {
452 let node_id = format!("service_{}", nodes.len());
453 let service_name = key.display_name();
454
455 let node = GraphNode {
457 id: node_id.clone(),
458 type_name: service_name.to_string(),
459 lifetime: format!("{:?}", registration.lifetime),
460 is_trait: matches!(key, crate::Key::Trait(_)),
461 dependencies: Vec::new(), metadata: {
463 let mut meta = HashMap::new();
464 meta.insert("key".to_string(), service_name.to_string());
465 meta.insert("lifetime".to_string(), format!("{:?}", registration.lifetime));
466 meta
467 },
468 position: None,
469 };
470
471 node_ids.insert(service_name.to_string(), node_id.clone());
472 nodes.push(node);
473 }
474
475 for (key, registration) in ®istry.one_large {
477 let node_id = format!("service_{}", nodes.len());
478 let service_name = key.display_name();
479
480 let node = GraphNode {
482 id: node_id.clone(),
483 type_name: service_name.to_string(),
484 lifetime: format!("{:?}", registration.lifetime),
485 is_trait: matches!(key, crate::Key::Trait(_)),
486 dependencies: Vec::new(), metadata: {
488 let mut meta = HashMap::new();
489 meta.insert("key".to_string(), service_name.to_string());
490 meta.insert("lifetime".to_string(), format!("{:?}", registration.lifetime));
491 meta
492 },
493 position: None,
494 };
495
496 node_ids.insert(service_name.to_string(), node_id.clone());
497 nodes.push(node);
498 }
499
500 for (trait_name, registrations) in ®istry.many {
502 for (idx, registration) in registrations.iter().enumerate() {
503 let node_id = format!("trait_impl_{}_{}", trait_name.replace("::", "_"), idx);
504 let service_name = format!("{}[{}]", trait_name, idx);
505
506 let node = GraphNode {
507 id: node_id.clone(),
508 type_name: service_name.to_string(),
509 lifetime: format!("{:?}", registration.lifetime),
510 is_trait: true,
511 dependencies: Vec::new(), metadata: {
513 let mut meta = HashMap::new();
514 meta.insert("trait_name".to_string(), trait_name.to_string());
515 meta.insert("implementation_index".to_string(), idx.to_string());
516 meta.insert("lifetime".to_string(), format!("{:?}", registration.lifetime));
517 meta
518 },
519 position: None,
520 };
521
522 node_ids.insert(service_name.to_string(), node_id.clone());
523 nodes.push(node);
524 }
525 }
526
527 self.analyze_dependencies(provider, &mut nodes, &mut edges, &node_ids)?;
529
530 let trait_count = registry.many.len();
532 let mut singleton_count = 0;
533 let mut scoped_count = 0;
534 let mut transient_count = 0;
535
536 for (_key, registration) in ®istry.one_small {
538 match registration.lifetime {
539 crate::Lifetime::Singleton => singleton_count += 1,
540 crate::Lifetime::Scoped => scoped_count += 1,
541 crate::Lifetime::Transient => transient_count += 1,
542 }
543 }
544
545 for (_key, registration) in ®istry.one_large {
547 match registration.lifetime {
548 crate::Lifetime::Singleton => singleton_count += 1,
549 crate::Lifetime::Scoped => scoped_count += 1,
550 crate::Lifetime::Transient => transient_count += 1,
551 }
552 }
553
554 for (_trait_name, registrations) in ®istry.many {
556 for registration in registrations {
557 match registration.lifetime {
558 crate::Lifetime::Singleton => singleton_count += 1,
559 crate::Lifetime::Scoped => scoped_count += 1,
560 crate::Lifetime::Transient => transient_count += 1,
561 }
562 }
563 }
564
565 let metadata = GraphMetadata {
566 service_count: nodes.len(),
567 trait_count,
568 singleton_count,
569 scoped_count,
570 transient_count,
571 has_circular_dependencies: false,
572 exported_at: {
573 #[cfg(feature = "graph-export")]
574 { chrono::Utc::now().to_rfc3339() }
575 #[cfg(not(feature = "graph-export"))]
576 { std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH)
577 .unwrap_or_default().as_secs().to_string() }
578 },
579 version: "1.0.0".to_string(),
580 };
581
582 let layout = if self.options.include_layout {
583 Some(GraphLayout {
584 algorithm: "hierarchical".to_string(),
585 parameters: HashMap::new(),
586 bounds: None,
587 })
588 } else {
589 None
590 };
591
592 Ok(DependencyGraph {
593 nodes,
594 edges,
595 metadata,
596 layout,
597 })
598 }
599
600 fn analyze_dependencies(
605 &self,
606 provider: &crate::ServiceProvider,
607 nodes: &mut Vec<GraphNode>,
608 edges: &mut Vec<GraphEdge>,
609 node_ids: &HashMap<String, String>,
610 ) -> crate::DiResult<()> {
611 use std::sync::{Arc, Mutex};
612 use crate::provider::context::ResolverContext;
613 use crate::traits::ResolverCore;
614
615 struct DependencyTracker {
617 inner: Arc<dyn ResolverCore>,
618 dependencies: Arc<Mutex<Vec<String>>>,
619 }
620
621 impl ResolverCore for DependencyTracker {
622 fn resolve_any(&self, key: &crate::Key) -> crate::DiResult<crate::registration::AnyArc> {
623 if let Ok(mut deps) = self.dependencies.lock() {
625 deps.push(key.display_name().to_string());
626 }
627 self.inner.resolve_any(key)
629 }
630
631 fn resolve_many(&self, key: &crate::Key) -> crate::DiResult<Vec<std::sync::Arc<dyn std::any::Any + Send + Sync>>> {
632 if let Ok(mut deps) = self.dependencies.lock() {
634 deps.push(format!("{}[*]", key.display_name()));
635 }
636 self.inner.resolve_many(key)
638 }
639
640 fn push_sync_disposer(&self, f: Box<dyn FnOnce() + Send>) {
641 self.inner.push_sync_disposer(f);
643 }
644
645 fn push_async_disposer(&self, f: Box<dyn FnOnce() -> crate::internal::BoxFutureUnit + Send>) {
646 self.inner.push_async_disposer(f);
648 }
649 }
650
651 let registry = &provider.inner().registry;
652
653 for (key, registration) in ®istry.one_small {
655 let service_name = key.display_name();
656 if let Some(from_node_id) = node_ids.get(service_name) {
657 let dependencies = Arc::new(Mutex::new(Vec::new()));
659 let tracker = DependencyTracker {
660 inner: Arc::new(provider.clone()),
661 dependencies: dependencies.clone(),
662 };
663
664 let ctx = ResolverContext::new(&tracker);
666 let _ = (registration.ctor)(&ctx); let captured_deps = {
670 if let Ok(deps) = dependencies.lock() {
671 deps.clone()
672 } else {
673 Vec::new()
674 }
675 };
676
677 for dep_name in &captured_deps {
678 if let Some(to_node_id) = node_ids.get(dep_name) {
679 edges.push(GraphEdge {
681 from: from_node_id.clone(),
682 to: to_node_id.clone(),
683 dependency_type: DependencyType::Required,
684 metadata: {
685 let mut meta = HashMap::new();
686 meta.insert("source".to_string(), "factory_analysis".to_string());
687 meta.insert("dependency_name".to_string(), dep_name.clone());
688 meta
689 },
690 });
691
692 if let Some(node) = nodes.iter_mut().find(|n| n.id == *from_node_id) {
694 if !node.dependencies.contains(dep_name) {
695 node.dependencies.push(dep_name.clone());
696 }
697 }
698 }
699 }
700 }
701 }
702
703 for (key, registration) in ®istry.one_large {
705 let service_name = key.display_name();
706 if let Some(from_node_id) = node_ids.get(service_name) {
707 let dependencies = Arc::new(Mutex::new(Vec::new()));
709 let tracker = DependencyTracker {
710 inner: Arc::new(provider.clone()),
711 dependencies: dependencies.clone(),
712 };
713
714 let ctx = ResolverContext::new(&tracker);
716 let _ = (registration.ctor)(&ctx); let captured_deps = {
720 if let Ok(deps) = dependencies.lock() {
721 deps.clone()
722 } else {
723 Vec::new()
724 }
725 };
726
727 for dep_name in &captured_deps {
728 if let Some(to_node_id) = node_ids.get(dep_name) {
729 edges.push(GraphEdge {
731 from: from_node_id.clone(),
732 to: to_node_id.clone(),
733 dependency_type: DependencyType::Required,
734 metadata: {
735 let mut meta = HashMap::new();
736 meta.insert("source".to_string(), "factory_analysis".to_string());
737 meta.insert("dependency_name".to_string(), dep_name.clone());
738 meta
739 },
740 });
741
742 if let Some(node) = nodes.iter_mut().find(|n| n.id == *from_node_id) {
744 if !node.dependencies.contains(dep_name) {
745 node.dependencies.push(dep_name.clone());
746 }
747 }
748 }
749 }
750 }
751 }
752
753 for (trait_name, registrations) in ®istry.many {
755 for (idx, registration) in registrations.iter().enumerate() {
756 let service_name = format!("{}[{}]", trait_name, idx);
757 if let Some(from_node_id) = node_ids.get(&service_name) {
758 let dependencies = Arc::new(Mutex::new(Vec::new()));
760 let tracker = DependencyTracker {
761 inner: Arc::new(provider.clone()),
762 dependencies: dependencies.clone(),
763 };
764
765 let ctx = ResolverContext::new(&tracker);
767 let _ = (registration.ctor)(&ctx); let captured_deps = {
771 if let Ok(deps) = dependencies.lock() {
772 deps.clone()
773 } else {
774 Vec::new()
775 }
776 };
777
778 for dep_name in &captured_deps {
779 if let Some(to_node_id) = node_ids.get(dep_name) {
780 edges.push(GraphEdge {
782 from: from_node_id.clone(),
783 to: to_node_id.clone(),
784 dependency_type: DependencyType::Required,
785 metadata: {
786 let mut meta = HashMap::new();
787 meta.insert("source".to_string(), "factory_analysis".to_string());
788 meta.insert("dependency_name".to_string(), dep_name.clone());
789 meta.insert("trait_implementation".to_string(), idx.to_string());
790 meta
791 },
792 });
793
794 if let Some(node) = nodes.iter_mut().find(|n| n.id == *from_node_id) {
796 if !node.dependencies.contains(dep_name) {
797 node.dependencies.push(dep_name.clone());
798 }
799 }
800 }
801 }
802 }
803 }
804 }
805
806 Ok(())
807 }
808
809 pub fn export(&self, graph: &DependencyGraph, format: ExportFormat) -> crate::DiResult<String> {
811 self.exporter.export(graph, format, &self.options)
812 }
813
814 pub fn build_and_export(&self, provider: &crate::ServiceProvider, format: ExportFormat) -> crate::DiResult<String> {
816 let graph = self.build_graph(provider)?;
817 self.export(&graph, format)
818 }
819}
820
821impl Default for GraphBuilder {
822 fn default() -> Self {
823 Self::new()
824 }
825}
826
827pub mod exports {
829 use super::*;
830
831 pub fn to_json(provider: &crate::ServiceProvider) -> crate::DiResult<String> {
833 GraphBuilder::new().build_and_export(provider, ExportFormat::Json)
834 }
835
836 pub fn to_yaml(provider: &crate::ServiceProvider) -> crate::DiResult<String> {
838 GraphBuilder::new().build_and_export(provider, ExportFormat::Yaml)
839 }
840
841 pub fn to_dot(provider: &crate::ServiceProvider) -> crate::DiResult<String> {
843 GraphBuilder::new().build_and_export(provider, ExportFormat::Dot)
844 }
845
846 pub fn to_mermaid(provider: &crate::ServiceProvider) -> crate::DiResult<String> {
848 GraphBuilder::new().build_and_export(provider, ExportFormat::Mermaid)
849 }
850
851 pub fn with_options(provider: &crate::ServiceProvider, format: ExportFormat, options: ExportOptions) -> crate::DiResult<String> {
853 GraphBuilder::new()
854 .with_options(options)
855 .build_and_export(provider, format)
856 }
857}
858
859pub mod workflow_integration {
861 use super::*;
862
863 #[derive(Debug, Clone)]
868 #[cfg_attr(feature = "graph-export", derive(Serialize, Deserialize))]
869 pub struct WorkflowGraph {
870 pub dependency_graph: DependencyGraph,
872 pub workflow_metadata: WorkflowMetadata,
874 pub execution_nodes: Vec<ExecutionNode>,
876 pub execution_flow: Vec<ExecutionEdge>,
878 }
879
880 #[derive(Debug, Clone)]
882 #[cfg_attr(feature = "graph-export", derive(Serialize, Deserialize))]
883 pub struct WorkflowMetadata {
884 pub workflow_id: String,
886 pub workflow_name: String,
888 pub run_id: Option<String>,
890 pub status: ExecutionStatus,
892 pub started_at: Option<String>,
894 pub completed_at: Option<String>,
896 pub duration: Option<String>,
898 }
899
900 #[derive(Debug, Clone, PartialEq, Eq)]
902 #[cfg_attr(feature = "graph-export", derive(Serialize, Deserialize))]
903 pub enum ExecutionStatus {
904 NotStarted,
905 Running,
906 Completed,
907 Failed,
908 Cancelled,
909 }
910
911 #[derive(Debug, Clone)]
913 #[cfg_attr(feature = "graph-export", derive(Serialize, Deserialize))]
914 pub struct ExecutionNode {
915 pub node_id: String,
917 pub name: String,
919 pub node_type: String,
921 pub service_dependencies: Vec<String>,
923 pub status: ExecutionStatus,
925 pub data_types: Vec<String>,
927 pub position: Option<NodePosition>,
929 }
930
931 #[derive(Debug, Clone)]
933 #[cfg_attr(feature = "graph-export", derive(Serialize, Deserialize))]
934 pub struct ExecutionEdge {
935 pub from_node: String,
937 pub to_node: String,
939 pub condition: Option<String>,
941 pub data_mapping: HashMap<String, String>,
943 }
944
945 pub fn export_workflow_graph(
947 provider: &crate::ServiceProvider,
948 workflow_context: &crate::WorkflowContext,
949 format: ExportFormat,
950 ) -> crate::DiResult<String> {
951 let dependency_graph = GraphBuilder::new().build_graph(provider)?;
952
953 let workflow_metadata = WorkflowMetadata {
954 workflow_id: workflow_context.workflow_name().to_string(),
955 workflow_name: workflow_context.workflow_name().to_string(),
956 run_id: Some(workflow_context.run_id().to_string()),
957 status: ExecutionStatus::Running,
958 started_at: Some(format!("{:?}", workflow_context.started_at())),
959 completed_at: None,
960 duration: Some(format!("{:?}", workflow_context.elapsed())),
961 };
962
963 let _workflow_graph = WorkflowGraph {
964 dependency_graph,
965 workflow_metadata,
966 execution_nodes: Vec::new(), execution_flow: Vec::new(), };
969
970 match format {
971 ExportFormat::Json => {
972 #[cfg(feature = "graph-export")]
973 {
974 serde_json::to_string_pretty(&_workflow_graph)
975 .map_err(|_| crate::DiError::TypeMismatch("JSON serialization failed"))
976 }
977 #[cfg(not(feature = "graph-export"))]
978 {
979 Err(crate::DiError::NotFound("JSON export requires 'graph-export' feature"))
980 }
981 },
982 ExportFormat::Yaml => {
983 #[cfg(feature = "graph-export")]
984 {
985 serde_yaml::to_string(&_workflow_graph)
986 .map_err(|_| crate::DiError::TypeMismatch("YAML serialization failed"))
987 }
988 #[cfg(not(feature = "graph-export"))]
989 {
990 Err(crate::DiError::NotFound("YAML export requires 'graph-export' feature"))
991 }
992 },
993 _ => Err(crate::DiError::NotFound("Workflow format not supported")),
994 }
995 }
996}
997
998#[cfg(test)]
999mod tests {
1000 use super::*;
1001
1002 #[test]
1003 fn test_graph_node_creation() {
1004 let node = GraphNode {
1005 id: "service_1".to_string(),
1006 type_name: "UserService".to_string(),
1007 lifetime: "Singleton".to_string(),
1008 is_trait: false,
1009 dependencies: vec!["DatabaseService".to_string()],
1010 metadata: HashMap::new(),
1011 position: Some(NodePosition { x: 0.0, y: 0.0, z: None }),
1012 };
1013
1014 assert_eq!(node.id, "service_1");
1015 assert_eq!(node.type_name, "UserService");
1016 assert!(!node.is_trait);
1017 assert_eq!(node.dependencies.len(), 1);
1018 }
1019
1020 #[test]
1021 fn test_graph_edge_creation() {
1022 let edge = GraphEdge {
1023 from: "service_1".to_string(),
1024 to: "service_2".to_string(),
1025 dependency_type: DependencyType::Required,
1026 metadata: HashMap::new(),
1027 };
1028
1029 assert_eq!(edge.from, "service_1");
1030 assert_eq!(edge.to, "service_2");
1031 assert_eq!(edge.dependency_type, DependencyType::Required);
1032 }
1033
1034 #[test]
1035 fn test_export_options_default() {
1036 let options = ExportOptions::default();
1037 assert!(options.include_dependencies);
1038 assert!(options.include_lifetimes);
1039 assert!(options.include_metadata);
1040 assert!(!options.include_layout);
1041 assert!(options.type_filter.is_empty());
1042 assert!(options.max_depth.is_none());
1043 assert!(!options.include_internal);
1044 }
1045
1046 #[test]
1047 fn test_graph_builder_creation() {
1048 let builder = GraphBuilder::new();
1049 drop(builder);
1051 }
1052
1053 #[test]
1054 fn test_dependency_graph_serialization() {
1055 let graph = DependencyGraph {
1056 nodes: vec![],
1057 edges: vec![],
1058 metadata: GraphMetadata {
1059 service_count: 0,
1060 trait_count: 0,
1061 singleton_count: 0,
1062 scoped_count: 0,
1063 transient_count: 0,
1064 has_circular_dependencies: false,
1065 exported_at: "2024-01-01T00:00:00Z".to_string(),
1066 version: "1.0.0".to_string(),
1067 },
1068 layout: None,
1069 };
1070
1071 #[cfg(feature = "graph-export")]
1072 {
1073 let json = serde_json::to_string(&graph).unwrap();
1074 assert!(json.contains("service_count"));
1075 assert!(json.contains("1.0.0"));
1076 }
1077 #[cfg(not(feature = "graph-export"))]
1078 {
1079 assert_eq!(graph.metadata.version, "1.0.0");
1081 }
1082 }
1083
1084 #[test]
1085 fn test_workflow_status() {
1086 assert_eq!(workflow_integration::ExecutionStatus::Running, workflow_integration::ExecutionStatus::Running);
1087 assert_ne!(workflow_integration::ExecutionStatus::Running, workflow_integration::ExecutionStatus::Completed);
1088 }
1089}