ferrous_di/labeled_scopes.rs
1//! Hierarchical labeled scopes for workflow engines.
2//!
3//! This module provides labeled scope capabilities essential for n8n-style workflow
4//! engines where you need nested contexts (workflow → run → node) with proper
5//! hierarchical organization and cleanup.
6
7use std::sync::Arc;
8use std::collections::HashMap;
9use crate::ServiceProvider;
10
11/// A hierarchical scope with a label for context and tracing.
12///
13/// Essential for workflow engines where you need nested execution contexts
14/// like workflow → run → node, each with their own label for tracing.
15///
16/// # Examples
17///
18/// ```
19/// use ferrous_di::{ServiceCollection, ServiceProvider, LabeledScope, LabeledScopeExt};
20///
21/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
22/// let mut services = ServiceCollection::new();
23///
24/// let provider = services.build();
25///
26/// // Create workflow scope
27/// let workflow_scope = provider.create_labeled_scope("workflow_123");
28///
29/// // Create run scope within workflow
30/// let run_scope = workflow_scope.fork("run_456");
31///
32/// // Create node scope within run
33/// let node_scope = run_scope.fork("node_789");
34///
35/// assert_eq!(workflow_scope.label(), "workflow_123");
36/// assert_eq!(run_scope.label(), "run_456");
37/// assert_eq!(node_scope.label(), "node_789");
38///
39/// // Labels can be used for tracing, logging, cleanup, etc.
40/// println!("Executing in context: {}", node_scope.full_path());
41/// # Ok(())
42/// # }
43/// ```
44#[derive(Clone)]
45pub struct LabeledScope {
46 inner: Arc<LabeledScopeInner>,
47}
48
49struct LabeledScopeInner {
50 label: &'static str,
51 parent: Option<LabeledScope>,
52 scope: crate::provider::Scope,
53 depth: usize,
54}
55
56impl LabeledScope {
57 /// Creates a new labeled scope from a regular scope.
58 pub(crate) fn new(scope: crate::provider::Scope, label: &'static str) -> Self {
59 Self {
60 inner: Arc::new(LabeledScopeInner {
61 label,
62 parent: None,
63 scope,
64 depth: 0,
65 })
66 }
67 }
68
69 /// Creates a child scope with the given label.
70 ///
71 /// Perfect for hierarchical workflow execution (flow → run → node).
72 ///
73 /// # Examples
74 ///
75 /// ```
76 /// use ferrous_di::{ServiceCollection, ServiceProvider, LabeledScopeExt};
77 ///
78 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
79 /// let mut services = ServiceCollection::new();
80 /// let provider = services.build();
81 ///
82 /// let workflow_scope = provider.create_labeled_scope("workflow");
83 /// let run_scope = workflow_scope.fork("run_001");
84 /// let node_scope = run_scope.fork("transform_node");
85 ///
86 /// assert_eq!(node_scope.depth(), 2);
87 /// assert_eq!(node_scope.full_path(), "workflow/run_001/transform_node");
88 /// # Ok(())
89 /// # }
90 /// ```
91 pub fn fork(&self, label: &'static str) -> Self {
92 let child_scope = self.inner.scope.create_child();
93
94 Self {
95 inner: Arc::new(LabeledScopeInner {
96 label,
97 parent: Some(self.clone()),
98 scope: child_scope,
99 depth: self.inner.depth + 1,
100 })
101 }
102 }
103
104 /// Returns the label of this scope.
105 pub fn label(&self) -> &'static str {
106 self.inner.label
107 }
108
109 /// Returns the depth of this scope (0 for root, 1 for first child, etc.).
110 pub fn depth(&self) -> usize {
111 self.inner.depth
112 }
113
114 /// Returns the parent scope, if any.
115 pub fn parent(&self) -> Option<&LabeledScope> {
116 self.inner.parent.as_ref()
117 }
118
119 /// Returns the full hierarchical path (e.g., "workflow/run_001/node_123").
120 ///
121 /// Perfect for logging, tracing, and debugging workflow execution.
122 ///
123 /// # Examples
124 ///
125 /// ```
126 /// use ferrous_di::{ServiceCollection, ServiceProvider, LabeledScopeExt};
127 ///
128 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
129 /// let mut services = ServiceCollection::new();
130 /// let provider = services.build();
131 ///
132 /// let scope = provider.create_labeled_scope("workflow")
133 /// .fork("run_001")
134 /// .fork("transform_node");
135 ///
136 /// assert_eq!(scope.full_path(), "workflow/run_001/transform_node");
137 /// # Ok(())
138 /// # }
139 /// ```
140 pub fn full_path(&self) -> String {
141 let mut path_parts = Vec::new();
142 let mut current = Some(self);
143
144 while let Some(scope) = current {
145 path_parts.push(scope.label());
146 current = scope.parent();
147 }
148
149 path_parts.reverse();
150 path_parts.join("/")
151 }
152
153 /// Returns all ancestor labels from root to this scope.
154 ///
155 /// Useful for hierarchical context tracking in workflow engines.
156 ///
157 /// # Examples
158 ///
159 /// ```
160 /// use ferrous_di::{ServiceCollection, ServiceProvider, LabeledScopeExt};
161 ///
162 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
163 /// let mut services = ServiceCollection::new();
164 /// let provider = services.build();
165 ///
166 /// let scope = provider.create_labeled_scope("workflow")
167 /// .fork("run_001")
168 /// .fork("transform");
169 ///
170 /// let ancestors = scope.ancestors();
171 /// assert_eq!(ancestors, vec!["workflow", "run_001", "transform"]);
172 /// # Ok(())
173 /// # }
174 /// ```
175 pub fn ancestors(&self) -> Vec<&'static str> {
176 let mut ancestors = Vec::new();
177 let mut current = Some(self);
178
179 while let Some(scope) = current {
180 ancestors.push(scope.label());
181 current = scope.parent();
182 }
183
184 ancestors.reverse();
185 ancestors
186 }
187
188 /// Finds an ancestor scope with the given label.
189 ///
190 /// Perfect for workflow engines where you need to access parent contexts.
191 ///
192 /// # Examples
193 ///
194 /// ```
195 /// use ferrous_di::{ServiceCollection, ServiceProvider, LabeledScopeExt};
196 ///
197 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
198 /// let mut services = ServiceCollection::new();
199 /// let provider = services.build();
200 ///
201 /// let workflow_scope = provider.create_labeled_scope("workflow_123");
202 /// let run_scope = workflow_scope.fork("run_456");
203 /// let node_scope = run_scope.fork("node_789");
204 ///
205 /// let workflow = node_scope.find_ancestor("workflow_123").unwrap();
206 /// assert_eq!(workflow.label(), "workflow_123");
207 /// # Ok(())
208 /// # }
209 /// ```
210 pub fn find_ancestor(&self, label: &str) -> Option<&LabeledScope> {
211 let mut current = Some(self);
212
213 while let Some(scope) = current {
214 if scope.label() == label {
215 return Some(scope);
216 }
217 current = scope.parent();
218 }
219
220 None
221 }
222
223 /// Returns true if this scope is an ancestor of (or equal to) the other scope.
224 ///
225 /// Useful for validating scope hierarchies in workflow engines.
226 ///
227 /// # Examples
228 ///
229 /// ```
230 /// use ferrous_di::{ServiceCollection, ServiceProvider, LabeledScopeExt};
231 ///
232 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
233 /// let mut services = ServiceCollection::new();
234 /// let provider = services.build();
235 ///
236 /// let workflow_scope = provider.create_labeled_scope("workflow");
237 /// let run_scope = workflow_scope.fork("run");
238 /// let node_scope = run_scope.fork("node");
239 ///
240 /// assert!(workflow_scope.is_ancestor_of(&node_scope));
241 /// assert!(run_scope.is_ancestor_of(&node_scope));
242 /// assert!(!node_scope.is_ancestor_of(&workflow_scope));
243 /// # Ok(())
244 /// # }
245 /// ```
246 pub fn is_ancestor_of(&self, other: &LabeledScope) -> bool {
247 let mut current = Some(other);
248
249 while let Some(scope) = current {
250 if std::ptr::eq(self.inner.as_ref(), scope.inner.as_ref()) {
251 return true;
252 }
253 current = scope.parent();
254 }
255
256 false
257 }
258
259 /// Returns metadata about this scope for tracing and debugging.
260 ///
261 /// Essential for workflow engines that need rich context information.
262 ///
263 /// # Examples
264 ///
265 /// ```
266 /// use ferrous_di::{ServiceCollection, ServiceProvider, LabeledScopeExt};
267 ///
268 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
269 /// let mut services = ServiceCollection::new();
270 /// let provider = services.build();
271 ///
272 /// let scope = provider.create_labeled_scope("workflow")
273 /// .fork("run_001");
274 ///
275 /// let metadata = scope.metadata();
276 /// assert_eq!(metadata.label, "run_001");
277 /// assert_eq!(metadata.depth, 1);
278 /// assert_eq!(metadata.full_path, "workflow/run_001");
279 /// assert_eq!(metadata.ancestors, vec!["workflow", "run_001"]);
280 /// # Ok(())
281 /// # }
282 /// ```
283 pub fn metadata(&self) -> ScopeMetadata {
284 ScopeMetadata {
285 label: self.label(),
286 depth: self.depth(),
287 full_path: self.full_path(),
288 ancestors: self.ancestors(),
289 parent_label: self.parent().map(|p| p.label()),
290 }
291 }
292
293 /// Access the underlying scope for service resolution.
294 ///
295 /// This provides access to all the DI functionality while maintaining
296 /// the labeled scope context.
297 pub fn as_scope(&self) -> &crate::provider::Scope {
298 &self.inner.scope
299 }
300}
301
302/// Metadata about a labeled scope for tracing and debugging.
303#[derive(Debug, Clone, PartialEq)]
304pub struct ScopeMetadata {
305 /// The label of this scope
306 pub label: &'static str,
307 /// The depth in the hierarchy (0 for root)
308 pub depth: usize,
309 /// The full hierarchical path
310 pub full_path: String,
311 /// All ancestor labels from root to this scope
312 pub ancestors: Vec<&'static str>,
313 /// The parent scope's label, if any
314 pub parent_label: Option<&'static str>,
315}
316
317/// Extension trait for ServiceProvider to create labeled scopes.
318pub trait LabeledScopeExt {
319 /// Creates a new labeled scope.
320 ///
321 /// This is the entry point for hierarchical workflow contexts.
322 ///
323 /// # Examples
324 ///
325 /// ```
326 /// use ferrous_di::{ServiceCollection, LabeledScopeExt};
327 ///
328 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
329 /// let mut services = ServiceCollection::new();
330 /// let provider = services.build();
331 ///
332 /// let workflow_scope = provider.create_labeled_scope("workflow_123");
333 /// assert_eq!(workflow_scope.label(), "workflow_123");
334 /// assert_eq!(workflow_scope.depth(), 0);
335 /// # Ok(())
336 /// # }
337 /// ```
338 fn create_labeled_scope(&self, label: &'static str) -> LabeledScope;
339}
340
341impl LabeledScopeExt for ServiceProvider {
342 fn create_labeled_scope(&self, label: &'static str) -> LabeledScope {
343 let scope = self.create_scope();
344 LabeledScope::new(scope, label)
345 }
346}
347
348/// Helper for creating scoped context in workflow engines.
349///
350/// This provides a convenient way to pass labeled scope context
351/// through workflow execution without manual parameter threading.
352///
353/// # Examples
354///
355/// ```
356/// use ferrous_di::{ServiceCollection, LabeledScopeContext, LabeledScopeExt, Resolver};
357///
358/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
359/// let mut services = ServiceCollection::new();
360///
361/// // Register the scope context with a factory
362/// services.add_scoped_factory::<LabeledScopeContext, _>(|_| {
363/// // This would be populated by the labeled scope in practice
364/// panic!("Should be replaced by labeled scope")
365/// });
366///
367/// let provider = services.build();
368/// let scope = provider.create_labeled_scope("workflow");
369///
370/// // In practice, labeled scopes would register themselves in the context
371/// // assert_eq!(context.current_label(), "workflow");
372/// # Ok(())
373/// # }
374/// ```
375#[derive(Clone)]
376pub struct LabeledScopeContext {
377 scope: LabeledScope,
378}
379
380impl LabeledScopeContext {
381 /// Creates a new scope context.
382 pub fn new(scope: LabeledScope) -> Self {
383 Self { scope }
384 }
385
386 /// Returns the current scope's label.
387 pub fn current_label(&self) -> &'static str {
388 self.scope.label()
389 }
390
391 /// Returns the current scope's depth.
392 pub fn current_depth(&self) -> usize {
393 self.scope.depth()
394 }
395
396 /// Returns the full hierarchical path.
397 pub fn full_path(&self) -> String {
398 self.scope.full_path()
399 }
400
401 /// Returns the labeled scope for advanced operations.
402 pub fn labeled_scope(&self) -> &LabeledScope {
403 &self.scope
404 }
405
406 /// Returns metadata about the current scope.
407 pub fn metadata(&self) -> ScopeMetadata {
408 self.scope.metadata()
409 }
410}
411
412/// Registry for tracking active labeled scopes in workflow engines.
413///
414/// Essential for debugging, monitoring, and cleanup in complex workflows.
415///
416/// # Examples
417///
418/// ```
419/// use ferrous_di::{ServiceCollection, LabeledScopeRegistry, LabeledScopeExt, Resolver};
420/// use std::sync::Arc;
421///
422/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
423/// let mut services = ServiceCollection::new();
424/// services.add_singleton_factory::<LabeledScopeRegistry, _>(|_| LabeledScopeRegistry::new());
425///
426/// let provider = services.build();
427/// let registry = provider.get_required::<LabeledScopeRegistry>();
428///
429/// let scope = provider.create_labeled_scope("workflow_123");
430/// registry.register(&scope);
431///
432/// let active_scopes = registry.active_scopes();
433/// assert_eq!(active_scopes.len(), 1);
434/// assert_eq!(active_scopes[0].label(), "workflow_123");
435/// # Ok(())
436/// # }
437/// ```
438#[derive(Default)]
439pub struct LabeledScopeRegistry {
440 scopes: std::sync::Mutex<HashMap<String, LabeledScope>>,
441}
442
443impl LabeledScopeRegistry {
444 /// Creates a new scope registry.
445 pub fn new() -> Self {
446 Self::default()
447 }
448
449 /// Registers a labeled scope for tracking.
450 ///
451 /// Useful for monitoring active workflows and cleanup on shutdown.
452 pub fn register(&self, scope: &LabeledScope) {
453 let mut scopes = self.scopes.lock().unwrap();
454 scopes.insert(scope.full_path(), scope.clone());
455 }
456
457 /// Unregisters a labeled scope.
458 ///
459 /// Should be called when workflow execution completes.
460 pub fn unregister(&self, scope: &LabeledScope) {
461 let mut scopes = self.scopes.lock().unwrap();
462 scopes.remove(&scope.full_path());
463 }
464
465 /// Returns all currently active scopes.
466 ///
467 /// Perfect for debugging and monitoring workflow state.
468 pub fn active_scopes(&self) -> Vec<LabeledScope> {
469 let scopes = self.scopes.lock().unwrap();
470 scopes.values().cloned().collect()
471 }
472
473 /// Finds active scopes with the given label.
474 ///
475 /// Useful for finding all active workflows or runs with a specific identifier.
476 pub fn find_by_label(&self, label: &str) -> Vec<LabeledScope> {
477 let scopes = self.scopes.lock().unwrap();
478 scopes.values()
479 .filter(|scope| scope.label() == label)
480 .cloned()
481 .collect()
482 }
483
484 /// Finds active scopes at the given depth.
485 ///
486 /// Useful for finding all workflows (depth 0), runs (depth 1), or nodes (depth 2).
487 pub fn find_by_depth(&self, depth: usize) -> Vec<LabeledScope> {
488 let scopes = self.scopes.lock().unwrap();
489 scopes.values()
490 .filter(|scope| scope.depth() == depth)
491 .cloned()
492 .collect()
493 }
494
495 /// Returns the count of active scopes.
496 pub fn active_count(&self) -> usize {
497 let scopes = self.scopes.lock().unwrap();
498 scopes.len()
499 }
500
501 /// Clears all registered scopes.
502 ///
503 /// Useful for cleanup during shutdown.
504 pub fn clear(&self) {
505 let mut scopes = self.scopes.lock().unwrap();
506 scopes.clear();
507 }
508}
509
510#[cfg(test)]
511mod tests {
512 use super::*;
513 use crate::ServiceCollection;
514
515 #[test]
516 fn test_labeled_scope_creation() {
517 let services = ServiceCollection::new();
518 let provider = services.build();
519
520 let scope = provider.create_labeled_scope("test_workflow");
521 assert_eq!(scope.label(), "test_workflow");
522 assert_eq!(scope.depth(), 0);
523 assert!(scope.parent().is_none());
524 }
525
526 #[test]
527 fn test_scope_forking() {
528 let services = ServiceCollection::new();
529 let provider = services.build();
530
531 let workflow = provider.create_labeled_scope("workflow");
532 let run = workflow.fork("run_001");
533 let node = run.fork("transform");
534
535 assert_eq!(workflow.label(), "workflow");
536 assert_eq!(run.label(), "run_001");
537 assert_eq!(node.label(), "transform");
538
539 assert_eq!(workflow.depth(), 0);
540 assert_eq!(run.depth(), 1);
541 assert_eq!(node.depth(), 2);
542 }
543
544 #[test]
545 fn test_full_path() {
546 let services = ServiceCollection::new();
547 let provider = services.build();
548
549 let scope = provider.create_labeled_scope("workflow")
550 .fork("run_001")
551 .fork("transform_node");
552
553 assert_eq!(scope.full_path(), "workflow/run_001/transform_node");
554 }
555
556 #[test]
557 fn test_ancestors() {
558 let services = ServiceCollection::new();
559 let provider = services.build();
560
561 let scope = provider.create_labeled_scope("workflow")
562 .fork("run_001")
563 .fork("transform");
564
565 let ancestors = scope.ancestors();
566 assert_eq!(ancestors, vec!["workflow", "run_001", "transform"]);
567 }
568
569 #[test]
570 fn test_find_ancestor() {
571 let services = ServiceCollection::new();
572 let provider = services.build();
573
574 let workflow = provider.create_labeled_scope("workflow");
575 let run = workflow.fork("run_001");
576 let node = run.fork("transform");
577
578 let found_workflow = node.find_ancestor("workflow").unwrap();
579 assert_eq!(found_workflow.label(), "workflow");
580
581 let found_run = node.find_ancestor("run_001").unwrap();
582 assert_eq!(found_run.label(), "run_001");
583
584 assert!(node.find_ancestor("nonexistent").is_none());
585 }
586
587 #[test]
588 fn test_is_ancestor_of() {
589 let services = ServiceCollection::new();
590 let provider = services.build();
591
592 let workflow = provider.create_labeled_scope("workflow");
593 let run = workflow.fork("run");
594 let node = run.fork("node");
595
596 assert!(workflow.is_ancestor_of(&node));
597 assert!(run.is_ancestor_of(&node));
598 assert!(node.is_ancestor_of(&node)); // Self is ancestor
599 assert!(!node.is_ancestor_of(&workflow));
600 }
601
602 #[test]
603 fn test_scope_metadata() {
604 let services = ServiceCollection::new();
605 let provider = services.build();
606
607 let scope = provider.create_labeled_scope("workflow")
608 .fork("run_001");
609
610 let metadata = scope.metadata();
611 assert_eq!(metadata.label, "run_001");
612 assert_eq!(metadata.depth, 1);
613 assert_eq!(metadata.full_path, "workflow/run_001");
614 assert_eq!(metadata.ancestors, vec!["workflow", "run_001"]);
615 assert_eq!(metadata.parent_label, Some("workflow"));
616 }
617
618 #[test]
619 fn test_scope_registry() {
620 let registry = LabeledScopeRegistry::new();
621 let services = ServiceCollection::new();
622 let provider = services.build();
623
624 let scope1 = provider.create_labeled_scope("workflow_1");
625 let scope2 = provider.create_labeled_scope("workflow_2");
626
627 registry.register(&scope1);
628 registry.register(&scope2);
629
630 assert_eq!(registry.active_count(), 2);
631
632 let active = registry.active_scopes();
633 assert_eq!(active.len(), 2);
634
635 registry.unregister(&scope1);
636 assert_eq!(registry.active_count(), 1);
637
638 registry.clear();
639 assert_eq!(registry.active_count(), 0);
640 }
641
642 #[test]
643 fn test_registry_find_operations() {
644 let registry = LabeledScopeRegistry::new();
645 let services = ServiceCollection::new();
646 let provider = services.build();
647
648 let workflow1 = provider.create_labeled_scope("workflow");
649 let workflow2 = provider.create_labeled_scope("another_workflow");
650 let run1 = workflow1.fork("run");
651
652 registry.register(&workflow1);
653 registry.register(&workflow2);
654 registry.register(&run1);
655
656 // Find by label
657 let workflows = registry.find_by_label("workflow");
658 assert_eq!(workflows.len(), 1);
659
660 let runs = registry.find_by_label("run");
661 assert_eq!(runs.len(), 1);
662
663 // Find by depth
664 let depth_0 = registry.find_by_depth(0);
665 assert_eq!(depth_0.len(), 2); // Two workflows
666
667 let depth_1 = registry.find_by_depth(1);
668 assert_eq!(depth_1.len(), 1); // One run
669 }
670}