ferrous_di/
graph_export.rs

1//! Graph export functionality for dependency visualization and UI integration.
2//!
3//! This module provides tools for exporting the dependency injection container's
4//! structure as graphs for visualization, debugging, and UI presentation.
5//! Essential for n8n-style workflow engines where understanding service
6//! relationships is critical.
7
8use std::collections::{HashMap, HashSet};
9
10#[cfg(feature = "graph-export")]
11use serde::{Serialize, Deserialize};
12
13/// A node in the dependency graph representing a service or trait registration.
14///
15/// Contains metadata about the service including its type, lifetime, 
16/// dependencies, and registration details.
17#[derive(Debug, Clone)]
18#[cfg_attr(feature = "graph-export", derive(Serialize, Deserialize))]
19pub struct GraphNode {
20    /// Unique identifier for this node
21    pub id: String,
22    /// Display name of the service type
23    pub type_name: String,
24    /// Service lifetime (Singleton, Scoped, Transient)
25    pub lifetime: String,
26    /// Whether this is a trait registration
27    pub is_trait: bool,
28    /// List of dependency type names this service requires
29    pub dependencies: Vec<String>,
30    /// Additional metadata about the service
31    pub metadata: HashMap<String, String>,
32    /// Visual positioning hints for UI (optional)
33    pub position: Option<NodePosition>,
34}
35
36/// Visual positioning information for graph layout.
37#[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/// An edge in the dependency graph representing a dependency relationship.
46///
47/// Connects services that depend on each other, with optional metadata
48/// about the relationship type and strength.
49#[derive(Debug, Clone)]
50#[cfg_attr(feature = "graph-export", derive(Serialize, Deserialize))]
51pub struct GraphEdge {
52    /// Source node ID (the service that depends on another)
53    pub from: String,
54    /// Target node ID (the service being depended upon)
55    pub to: String,
56    /// Type of dependency (required, optional, multiple, etc.)
57    pub dependency_type: DependencyType,
58    /// Additional metadata about this relationship
59    pub metadata: HashMap<String, String>,
60}
61
62/// Types of dependency relationships between services.
63#[derive(Debug, Clone, PartialEq, Eq)]
64#[cfg_attr(feature = "graph-export", derive(Serialize, Deserialize))]
65pub enum DependencyType {
66    /// Required single dependency
67    Required,
68    /// Optional dependency (might not be present)
69    Optional,
70    /// Multiple instances of the same service
71    Multiple,
72    /// Trait dependency
73    Trait,
74    /// Factory dependency (service creates other services)
75    Factory,
76    /// Scoped dependency (specific to scope context)
77    Scoped,
78    /// Decorated dependency (wrapped by decorators)
79    Decorated,
80}
81
82/// Complete dependency graph export containing all nodes and relationships.
83///
84/// This structure can be serialized to JSON, YAML, or other formats for
85/// consumption by visualization tools, debuggers, or workflow UIs.
86#[derive(Debug, Clone)]
87#[cfg_attr(feature = "graph-export", derive(Serialize, Deserialize))]
88pub struct DependencyGraph {
89    /// All service nodes in the graph
90    pub nodes: Vec<GraphNode>,
91    /// All dependency relationships between nodes
92    pub edges: Vec<GraphEdge>,
93    /// Graph-level metadata
94    pub metadata: GraphMetadata,
95    /// Layout information for visualization
96    pub layout: Option<GraphLayout>,
97}
98
99/// Metadata about the entire dependency graph.
100#[derive(Debug, Clone)]
101#[cfg_attr(feature = "graph-export", derive(Serialize, Deserialize))]
102pub struct GraphMetadata {
103    /// Total number of registered services
104    pub service_count: usize,
105    /// Total number of trait registrations
106    pub trait_count: usize,
107    /// Number of singleton services
108    pub singleton_count: usize,
109    /// Number of scoped services
110    pub scoped_count: usize,
111    /// Number of transient services
112    pub transient_count: usize,
113    /// Whether circular dependencies were detected
114    pub has_circular_dependencies: bool,
115    /// Export timestamp
116    pub exported_at: String,
117    /// Export format version
118    pub version: String,
119}
120
121/// Layout information for graph visualization.
122#[derive(Debug, Clone)]
123#[cfg_attr(feature = "graph-export", derive(Serialize, Deserialize))]
124pub struct GraphLayout {
125    /// Suggested layout algorithm
126    pub algorithm: String,
127    /// Layout-specific parameters
128    #[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    /// Viewport bounds for the graph
133    pub bounds: Option<LayoutBounds>,
134}
135
136/// Viewport bounds for graph layout.
137#[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/// Graph export configuration options.
147#[derive(Debug, Clone)]
148pub struct ExportOptions {
149    /// Include dependency details in nodes
150    pub include_dependencies: bool,
151    /// Include lifetime information
152    pub include_lifetimes: bool,
153    /// Include metadata in export
154    pub include_metadata: bool,
155    /// Generate layout hints for visualization
156    pub include_layout: bool,
157    /// Filter to specific service types (empty = all)
158    pub type_filter: HashSet<String>,
159    /// Maximum depth for dependency traversal
160    pub max_depth: Option<usize>,
161    /// Include internal/system services
162    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/// Export formats supported for dependency graphs.
180#[derive(Debug, Clone, Copy, PartialEq, Eq)]
181pub enum ExportFormat {
182    /// JSON format for web UIs and APIs
183    Json,
184    /// YAML format for human-readable configuration
185    Yaml,
186    /// DOT format for Graphviz visualization
187    Dot,
188    /// Mermaid format for documentation
189    Mermaid,
190    /// Custom format for specific workflow engines
191    Custom(&'static str),
192}
193
194/// Graph exporter for generating dependency visualizations.
195///
196/// This trait allows different export strategies and formats while
197/// maintaining a consistent interface for graph generation.
198pub trait GraphExporter {
199    /// Exports the dependency graph in the specified format.
200    ///
201    /// # Arguments
202    ///
203    /// * `graph` - The dependency graph to export
204    /// * `format` - The target export format
205    /// * `options` - Export configuration options
206    ///
207    /// # Returns
208    ///
209    /// The exported graph as a string in the specified format.
210    fn export(&self, graph: &DependencyGraph, format: ExportFormat, options: &ExportOptions) -> crate::DiResult<String>;
211}
212
213/// Default graph exporter implementation.
214///
215/// Supports common formats like JSON, YAML, DOT, and Mermaid for
216/// integration with popular visualization tools.
217#[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    /// Exports graph as JSON.
236    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            // Fallback manual JSON generation
245            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    /// Exports graph as YAML.
278    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            // Fallback manual YAML generation
287            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    /// Exports graph as DOT format for Graphviz.
310    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        // Export nodes
317        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        // Export edges
339        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    /// Exports graph as Mermaid format.
359    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        // Export nodes with styling
364        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        // Export edges
379        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        // Add styling
390        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
408/// Builder for creating dependency graphs from service collections.
409///
410/// Analyzes the registered services and their dependencies to build
411/// a complete graph representation suitable for export and visualization.
412pub struct GraphBuilder {
413    options: ExportOptions,
414    exporter: Box<dyn GraphExporter>,
415}
416
417impl GraphBuilder {
418    /// Creates a new graph builder with default options.
419    pub fn new() -> Self {
420        Self {
421            options: ExportOptions::default(),
422            exporter: Box::new(DefaultGraphExporter),
423        }
424    }
425
426    /// Sets export options for the graph builder.
427    pub fn with_options(mut self, options: ExportOptions) -> Self {
428        self.options = options;
429        self
430    }
431
432    /// Sets a custom graph exporter.
433    pub fn with_exporter(mut self, exporter: Box<dyn GraphExporter>) -> Self {
434        self.exporter = exporter;
435        self
436    }
437
438    /// Builds a dependency graph from the service collection.
439    ///
440    /// This method analyzes all registered services to extract their
441    /// dependencies and relationships, creating a complete graph structure.
442    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        // Introspect actual service registrations from the provider
448        let registry = &provider.inner().registry;
449        
450        // Process single-binding services from small Vec
451        for (key, registration) in &registry.one_small {
452            let node_id = format!("service_{}", nodes.len());
453            let service_name = key.display_name();
454            
455            // Create node for this service
456            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(), // TODO: Extract from factory functions
462                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        // Process single-binding services from large HashMap
476        for (key, registration) in &registry.one_large {
477            let node_id = format!("service_{}", nodes.len());
478            let service_name = key.display_name();
479            
480            // Create node for this service
481            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(), // TODO: Extract from factory functions
487                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        // Process multi-binding trait services
501        for (trait_name, registrations) in &registry.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(), // TODO: Extract from factory functions
512                    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        // Add dependency analysis by runtime introspection
528        self.analyze_dependencies(provider, &mut nodes, &mut edges, &node_ids)?;
529        
530        // Calculate metadata counts
531        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        // Count lifetimes from single services (small Vec)
537        for (_key, registration) in &registry.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        // Count lifetimes from single services (large HashMap)
546        for (_key, registration) in &registry.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        // Count lifetimes from multi-binding services
555        for (_trait_name, registrations) in &registry.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    /// Analyzes dependencies by runtime introspection of factory functions.
601    ///
602    /// This method executes factory functions in a controlled environment
603    /// to capture what dependencies they actually request.
604    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        // Create a dependency tracking resolver wrapper
616        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                // Record this dependency
624                if let Ok(mut deps) = self.dependencies.lock() {
625                    deps.push(key.display_name().to_string());
626                }
627                // Delegate to the real resolver
628                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                // Record this dependency
633                if let Ok(mut deps) = self.dependencies.lock() {
634                    deps.push(format!("{}[*]", key.display_name()));
635                }
636                // Delegate to the real resolver
637                self.inner.resolve_many(key)
638            }
639
640            fn push_sync_disposer(&self, f: Box<dyn FnOnce() + Send>) {
641                // Delegate to the real resolver
642                self.inner.push_sync_disposer(f);
643            }
644
645            fn push_async_disposer(&self, f: Box<dyn FnOnce() -> crate::internal::BoxFutureUnit + Send>) {
646                // Delegate to the real resolver
647                self.inner.push_async_disposer(f);
648            }
649        }
650        
651        let registry = &provider.inner().registry;
652        
653        // Analyze dependencies for single-binding services from small Vec
654        for (key, registration) in &registry.one_small {
655            let service_name = key.display_name();
656            if let Some(from_node_id) = node_ids.get(service_name) {
657                // Create dependency tracking wrapper
658                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                // Execute factory with dependency tracking (ignore result, we just want dependencies)
665                let ctx = ResolverContext::new(&tracker);
666                let _ = (registration.ctor)(&ctx); // Ignore errors during analysis
667                
668                // Extract captured dependencies
669                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                        // Create edge from this service to its dependency
680                        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                        // Update the node's dependencies list
693                        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        // Analyze dependencies for single-binding services from large HashMap
704        for (key, registration) in &registry.one_large {
705            let service_name = key.display_name();
706            if let Some(from_node_id) = node_ids.get(service_name) {
707                // Create dependency tracking wrapper
708                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                // Execute factory with dependency tracking (ignore result, we just want dependencies)
715                let ctx = ResolverContext::new(&tracker);
716                let _ = (registration.ctor)(&ctx); // Ignore errors during analysis
717                
718                // Extract captured dependencies
719                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                        // Create edge from this service to its dependency
730                        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                        // Update the node's dependencies list
743                        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        // Analyze dependencies for multi-binding trait services
754        for (trait_name, registrations) in &registry.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                    // Create dependency tracking wrapper
759                    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                    // Execute factory with dependency tracking
766                    let ctx = ResolverContext::new(&tracker);
767                    let _ = (registration.ctor)(&ctx); // Ignore errors during analysis
768                    
769                    // Extract captured dependencies
770                    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                            // Create edge from this service to its dependency
781                            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                            // Update the node's dependencies list
795                            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    /// Exports the dependency graph in the specified format.
810    pub fn export(&self, graph: &DependencyGraph, format: ExportFormat) -> crate::DiResult<String> {
811        self.exporter.export(graph, format, &self.options)
812    }
813
814    /// Builds and exports a dependency graph in one operation.
815    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
827/// Convenience functions for quick graph exports.
828pub mod exports {
829    use super::*;
830
831    /// Exports a service provider's dependency graph as JSON.
832    pub fn to_json(provider: &crate::ServiceProvider) -> crate::DiResult<String> {
833        GraphBuilder::new().build_and_export(provider, ExportFormat::Json)
834    }
835
836    /// Exports a service provider's dependency graph as YAML.
837    pub fn to_yaml(provider: &crate::ServiceProvider) -> crate::DiResult<String> {
838        GraphBuilder::new().build_and_export(provider, ExportFormat::Yaml)
839    }
840
841    /// Exports a service provider's dependency graph as DOT format.
842    pub fn to_dot(provider: &crate::ServiceProvider) -> crate::DiResult<String> {
843        GraphBuilder::new().build_and_export(provider, ExportFormat::Dot)
844    }
845
846    /// Exports a service provider's dependency graph as Mermaid format.
847    pub fn to_mermaid(provider: &crate::ServiceProvider) -> crate::DiResult<String> {
848        GraphBuilder::new().build_and_export(provider, ExportFormat::Mermaid)
849    }
850
851    /// Exports with custom options.
852    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
859/// Integration with n8n-style workflow engines.
860pub mod workflow_integration {
861    use super::*;
862
863    /// Workflow-specific graph export that includes run context and node relationships.
864    ///
865    /// This extends the basic dependency graph with workflow-specific information
866    /// like node execution order, workflow metadata, and run context.
867    #[derive(Debug, Clone)]
868    #[cfg_attr(feature = "graph-export", derive(Serialize, Deserialize))]
869    pub struct WorkflowGraph {
870        /// Base dependency graph
871        pub dependency_graph: DependencyGraph,
872        /// Workflow execution metadata
873        pub workflow_metadata: WorkflowMetadata,
874        /// Execution nodes in the workflow
875        pub execution_nodes: Vec<ExecutionNode>,
876        /// Execution flow between nodes
877        pub execution_flow: Vec<ExecutionEdge>,
878    }
879
880    /// Metadata about the workflow execution context.
881    #[derive(Debug, Clone)]
882    #[cfg_attr(feature = "graph-export", derive(Serialize, Deserialize))]
883    pub struct WorkflowMetadata {
884        /// Workflow identifier
885        pub workflow_id: String,
886        /// Workflow name
887        pub workflow_name: String,
888        /// Current run ID
889        pub run_id: Option<String>,
890        /// Execution status
891        pub status: ExecutionStatus,
892        /// Start time
893        pub started_at: Option<String>,
894        /// End time
895        pub completed_at: Option<String>,
896        /// Total execution time
897        pub duration: Option<String>,
898    }
899
900    /// Execution status of the workflow.
901    #[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    /// A single execution node in the workflow.
912    #[derive(Debug, Clone)]
913    #[cfg_attr(feature = "graph-export", derive(Serialize, Deserialize))]
914    pub struct ExecutionNode {
915        /// Node identifier
916        pub node_id: String,
917        /// Node name/title
918        pub name: String,
919        /// Node type (e.g., "HttpRequest", "DataTransform", etc.)
920        pub node_type: String,
921        /// Services this node depends on
922        pub service_dependencies: Vec<String>,
923        /// Execution status of this node
924        pub status: ExecutionStatus,
925        /// Input/output data types
926        pub data_types: Vec<String>,
927        /// Node position in the workflow UI
928        pub position: Option<NodePosition>,
929    }
930
931    /// Execution flow edge between workflow nodes.
932    #[derive(Debug, Clone)]
933    #[cfg_attr(feature = "graph-export", derive(Serialize, Deserialize))]
934    pub struct ExecutionEdge {
935        /// Source node
936        pub from_node: String,
937        /// Target node
938        pub to_node: String,
939        /// Condition for this flow (if any)
940        pub condition: Option<String>,
941        /// Data passed between nodes
942        pub data_mapping: HashMap<String, String>,
943    }
944
945    /// Exports a workflow graph with both dependency and execution information.
946    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(), // Would be populated with actual workflow nodes
967            execution_flow: Vec::new(),  // Would be populated with actual execution flow
968        };
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        // Should not panic and should create successfully
1050        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            // Without graph-export feature, we can still test the structure
1080            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}