ferrous_di/
scope_local.rs

1//! Scope-local context values for ergonomic per-run state.
2//!
3//! This module provides zero-boilerplate access to scoped context values that
4//! are shared across all services within a single agent run. Perfect for
5//! passing trace IDs, budgets, cancellation tokens, and other run-scoped data.
6
7use std::sync::Arc;
8use crate::ServiceCollection;
9
10/// Wrapper for scope-local values that are shared within a single scope.
11///
12/// `ScopeLocal<T>` provides ergonomic access to per-run context values like
13/// trace IDs, execution budgets, cancellation tokens, or any other data that
14/// should be shared across all services within a single agent run.
15///
16/// Unlike manually threading context through every service, `ScopeLocal<T>`
17/// makes context available anywhere within the scope with a simple
18/// `resolver.get_required::<ScopeLocal<T>>()` call.
19///
20/// # Thread Safety
21///
22/// The wrapped value is stored in an `Arc<T>`, making it cheaply clonable
23/// and safe to access from multiple threads within the same scope.
24///
25/// # Examples
26///
27/// ```
28/// use ferrous_di::{ServiceCollection, ScopeLocal, Resolver};
29/// use std::sync::Arc;
30///
31/// #[derive(Default)]
32/// struct RunContext {
33///     trace_id: String,
34///     max_steps: u32,
35///     budget_remaining: std::sync::atomic::AtomicU32,
36/// }
37///
38/// let mut services = ServiceCollection::new();
39/// 
40/// // Register scope-local context
41/// services.add_scope_local::<RunContext, _>(|_resolver| {
42///     Arc::new(RunContext {
43///         trace_id: "trace-12345".to_string(),
44///         max_steps: 50,
45///         budget_remaining: std::sync::atomic::AtomicU32::new(1000),
46///     })
47/// });
48///
49/// // Any service can access the context
50/// services.add_scoped_factory::<String, _>(|resolver| {
51///     let ctx = resolver.get_required::<ScopeLocal<RunContext>>();
52///     format!("Processing with trace: {}", ctx.trace_id)
53/// });
54///
55/// let provider = services.build();
56/// let scope1 = provider.create_scope();
57/// let scope2 = provider.create_scope();
58///
59/// // Each scope gets its own context instance
60/// let result1 = scope1.get_required::<String>();
61/// let result2 = scope2.get_required::<String>();
62/// // Different trace IDs in each scope
63/// ```
64pub struct ScopeLocal<T> {
65    value: Arc<T>,
66}
67
68impl<T> ScopeLocal<T> {
69    /// Creates a new scope-local wrapper around the given value.
70    pub fn new(value: T) -> Self {
71        Self {
72            value: Arc::new(value),
73        }
74    }
75
76    /// Creates a new scope-local wrapper from an existing Arc.
77    pub fn from_arc(value: Arc<T>) -> Self {
78        Self { value }
79    }
80
81    /// Gets a reference to the wrapped value.
82    pub fn get(&self) -> &T {
83        &self.value
84    }
85
86    /// Gets a clone of the underlying Arc.
87    ///
88    /// This is cheap since Arc cloning only increments a reference count.
89    pub fn arc(&self) -> Arc<T> {
90        self.value.clone()
91    }
92}
93
94impl<T> Clone for ScopeLocal<T> {
95    fn clone(&self) -> Self {
96        Self {
97            value: self.value.clone(),
98        }
99    }
100}
101
102impl<T> std::ops::Deref for ScopeLocal<T> {
103    type Target = T;
104
105    fn deref(&self) -> &Self::Target {
106        &self.value
107    }
108}
109
110impl<T: std::fmt::Debug> std::fmt::Debug for ScopeLocal<T> {
111    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
112        f.debug_struct("ScopeLocal")
113            .field("value", &self.value)
114            .finish()
115    }
116}
117
118impl ServiceCollection {
119    /// Registers a scope-local value factory.
120    ///
121    /// The factory is called once per scope to create a value that will be
122    /// shared across all services within that scope. This is perfect for
123    /// per-run context like trace IDs, execution budgets, cancellation tokens,
124    /// and other run-scoped state.
125    ///
126    /// The factory receives a resolver that can access other services, making
127    /// it possible to build context values that depend on configuration or
128    /// other services.
129    ///
130    /// # Type Safety
131    ///
132    /// The registered factory creates `ScopeLocal<T>` instances that can be
133    /// resolved from any service within the scope using
134    /// `resolver.get_required::<ScopeLocal<T>>()`.
135    ///
136    /// # Examples
137    ///
138    /// ```
139    /// use ferrous_di::{ServiceCollection, ScopeLocal, Resolver, Options};
140    /// use std::sync::Arc;
141    /// use std::sync::atomic::AtomicU32;
142    ///
143    /// #[derive(Default)]
144    /// struct AgentConfig {
145    ///     max_steps: u32,
146    ///     timeout_ms: u64,
147    /// }
148    ///
149    /// struct RunContext {
150    ///     trace_id: String,
151    ///     max_steps: u32,
152    ///     steps_remaining: AtomicU32,
153    /// }
154    ///
155    /// let mut services = ServiceCollection::new();
156    /// 
157    /// // Register configuration
158    /// services.add_options::<AgentConfig>()
159    ///     .configure(|_r, config| {
160    ///         config.max_steps = 100;
161    ///         config.timeout_ms = 30000;
162    ///     })
163    ///     .register();
164    ///
165    /// // Register scope-local context that uses config
166    /// services.add_scope_local::<RunContext, _>(|_resolver| {
167    ///     Arc::new(RunContext {
168    ///         trace_id: "trace-12345".to_string(),
169    ///         max_steps: 100,
170    ///         steps_remaining: AtomicU32::new(100),
171    ///     })
172    /// });
173    ///
174    /// // Services can access both config and context
175    /// services.add_scoped_factory::<String, _>(|resolver| {
176    ///     let ctx = resolver.get_required::<ScopeLocal<RunContext>>();
177    ///     let remaining = ctx.steps_remaining.load(std::sync::atomic::Ordering::Relaxed);
178    ///     format!("Trace {} has {} steps remaining", ctx.trace_id, remaining)
179    /// });
180    ///
181    /// let provider = services.build();
182    /// let scope = provider.create_scope();
183    /// let status = scope.get_required::<String>();
184    /// ```
185    ///
186    /// # Advanced Usage: Multiple Context Types
187    ///
188    /// You can register multiple scope-local context types for different concerns:
189    ///
190    /// ```
191    /// use ferrous_di::{ServiceCollection, ScopeLocal, Resolver};
192    /// use std::sync::Arc;
193    ///
194    /// struct TraceContext { trace_id: String }
195    /// struct BudgetContext { tokens_remaining: std::sync::atomic::AtomicU32 }
196    /// struct SecurityContext { user_id: String, permissions: Vec<String> }
197    ///
198    /// let mut services = ServiceCollection::new();
199    ///
200    /// services.add_scope_local::<TraceContext, _>(|_r| {
201    ///     Arc::new(TraceContext { 
202    ///         trace_id: "trace-12345".to_string() 
203    ///     })
204    /// });
205    ///
206    /// services.add_scope_local::<BudgetContext, _>(|_r| {
207    ///     Arc::new(BudgetContext {
208    ///         tokens_remaining: std::sync::atomic::AtomicU32::new(10000)
209    ///     })
210    /// });
211    ///
212    /// services.add_scope_local::<SecurityContext, _>(|_r| {
213    ///     Arc::new(SecurityContext {
214    ///         user_id: "agent-user-123".to_string(),
215    ///         permissions: vec!["read".to_string(), "write".to_string()],
216    ///     })
217    /// });
218    ///
219    /// // Each context type can be resolved independently
220    /// services.add_scoped_factory::<String, _>(|resolver| {
221    ///     let trace = resolver.get_required::<ScopeLocal<TraceContext>>();
222    ///     let budget = resolver.get_required::<ScopeLocal<BudgetContext>>();
223    ///     let security = resolver.get_required::<ScopeLocal<SecurityContext>>();
224    ///     
225    ///     format!("User {} (trace: {}) has {} tokens", 
226    ///         security.user_id,
227    ///         trace.trace_id,
228    ///         budget.tokens_remaining.load(std::sync::atomic::Ordering::Relaxed))
229    /// });
230    /// ```
231    pub fn add_scope_local<T, F>(&mut self, factory: F) -> &mut Self
232    where
233        T: Send + Sync + 'static,
234        F: Fn(&crate::provider::ResolverContext) -> Arc<T> + Send + Sync + 'static,
235    {
236        self.add_scoped_factory::<ScopeLocal<T>, _>(move |resolver| {
237            let value = factory(resolver);
238            ScopeLocal::from_arc(value)
239        });
240        self
241    }
242
243    /// Registers a workflow-specific scope-local context factory.
244    ///
245    /// This is a specialized version of `add_scope_local` that automatically
246    /// provides common workflow context features like run IDs, execution metadata,
247    /// and hierarchical scope information.
248    ///
249    /// Perfect for n8n-style workflow engines where each execution run needs
250    /// rich context information.
251    ///
252    /// # Examples
253    ///
254    /// ```
255    /// use ferrous_di::{ServiceCollection, ScopeLocal, WorkflowContext, Resolver};
256    /// use std::sync::Arc;
257    ///
258    /// let mut services = ServiceCollection::new();
259    /// 
260    /// // Register workflow context with auto-generated run ID
261    /// services.add_workflow_context::<WorkflowContext, _>(|_resolver| {
262    ///     Arc::new(WorkflowContext::new("user_registration_flow"))
263    /// });
264    ///
265    /// // Services can access rich workflow context
266    /// services.add_scoped_factory::<String, _>(|resolver| {
267    ///     let ctx = resolver.get_required::<ScopeLocal<WorkflowContext>>();
268    ///     format!("Executing {} (run: {})", ctx.workflow_name(), ctx.run_id())
269    /// });
270    ///
271    /// let provider = services.build();
272    /// let scope = provider.create_scope();
273    /// let status = scope.get_required::<String>();
274    /// ```
275    pub fn add_workflow_context<T, F>(&mut self, factory: F) -> &mut Self
276    where
277        T: Send + Sync + 'static,
278        F: Fn(&crate::provider::ResolverContext) -> Arc<T> + Send + Sync + 'static,
279    {
280        // This is essentially the same as add_scope_local but with workflow-specific naming
281        // and potential future enhancements for workflow metadata
282        self.add_scope_local(factory)
283    }
284
285    /// Registers multiple scope-local contexts in a batch.
286    ///
287    /// Convenient for workflow engines that need to register several context types
288    /// (security, tracing, budgets, etc.) in one operation.
289    ///
290    /// # Examples
291    ///
292    /// ```
293    /// use ferrous_di::{ServiceCollection, ScopeLocal, Resolver};
294    /// use std::sync::Arc;
295    ///
296    /// struct TraceContext { run_id: String }
297    /// struct SecurityContext { user_id: String }
298    /// struct BudgetContext { tokens: u32 }
299    ///
300    /// let mut services = ServiceCollection::new();
301    /// 
302    /// services.add_scope_locals()
303    ///     .add::<TraceContext, _>(|_| Arc::new(TraceContext { run_id: "run-123".into() }))
304    ///     .add::<SecurityContext, _>(|_| Arc::new(SecurityContext { user_id: "user-456".into() }))
305    ///     .add::<BudgetContext, _>(|_| Arc::new(BudgetContext { tokens: 1000 }))
306    ///     .register();
307    ///
308    /// let provider = services.build();
309    /// let scope = provider.create_scope();
310    /// 
311    /// // All contexts are available
312    /// let trace = scope.get_required::<ScopeLocal<TraceContext>>();
313    /// let security = scope.get_required::<ScopeLocal<SecurityContext>>();
314    /// let budget = scope.get_required::<ScopeLocal<BudgetContext>>();
315    /// ```
316    pub fn add_scope_locals(&mut self) -> ScopeLocalBuilder {
317        ScopeLocalBuilder::new(self)
318    }
319}
320
321/// Builder for registering multiple scope-local contexts.
322pub struct ScopeLocalBuilder<'a> {
323    collection: &'a mut ServiceCollection,
324}
325
326impl<'a> ScopeLocalBuilder<'a> {
327    fn new(collection: &'a mut ServiceCollection) -> Self {
328        Self { collection }
329    }
330
331    /// Adds a scope-local context type to the builder.
332    pub fn add<T, F>(self, factory: F) -> Self
333    where
334        T: Send + Sync + 'static,
335        F: Fn(&crate::provider::ResolverContext) -> Arc<T> + Send + Sync + 'static,
336    {
337        self.collection.add_scope_local(factory);
338        self
339    }
340
341    /// Completes the builder (for fluent API consistency).
342    pub fn register(self) {}
343}
344
345/// Standard workflow context for n8n-style execution engines.
346///
347/// Provides common workflow execution metadata that most workflow engines need.
348/// This is a convenience struct that workflow engines can use directly or extend.
349///
350/// # Examples
351///
352/// ```
353/// use ferrous_di::{ServiceCollection, ScopeLocal, WorkflowContext, Resolver};
354/// use std::sync::Arc;
355///
356/// let mut services = ServiceCollection::new();
357/// 
358/// services.add_scope_local::<WorkflowContext, _>(|_resolver| {
359///     Arc::new(WorkflowContext::new("user_onboarding"))
360/// });
361///
362/// services.add_scoped_factory::<String, _>(|resolver| {
363///     let ctx = resolver.get_required::<ScopeLocal<WorkflowContext>>();
364///     format!("Executing step in workflow '{}' (run: {})", 
365///         ctx.workflow_name(), 
366///         ctx.run_id())
367/// });
368///
369/// let provider = services.build();
370/// let scope = provider.create_scope();
371/// let status = scope.get_required::<String>();
372/// ```
373#[derive(Debug, Clone)]
374pub struct WorkflowContext {
375    /// Unique identifier for this workflow execution run
376    run_id: String,
377    /// Name/type of the workflow being executed
378    workflow_name: String,
379    /// Timestamp when this execution started
380    started_at: std::time::Instant,
381    /// Additional metadata for the workflow execution
382    metadata: std::collections::HashMap<String, String>,
383}
384
385impl WorkflowContext {
386    /// Creates a new workflow context with an auto-generated run ID.
387    pub fn new(workflow_name: impl Into<String>) -> Self {
388        Self {
389            run_id: Self::generate_run_id(),
390            workflow_name: workflow_name.into(),
391            started_at: std::time::Instant::now(),
392            metadata: std::collections::HashMap::new(),
393        }
394    }
395
396    /// Creates a new workflow context with a specific run ID.
397    pub fn with_run_id(workflow_name: impl Into<String>, run_id: impl Into<String>) -> Self {
398        Self {
399            run_id: run_id.into(),
400            workflow_name: workflow_name.into(),
401            started_at: std::time::Instant::now(),
402            metadata: std::collections::HashMap::new(),
403        }
404    }
405
406    /// Gets the run ID for this workflow execution.
407    pub fn run_id(&self) -> &str {
408        &self.run_id
409    }
410
411    /// Gets the workflow name.
412    pub fn workflow_name(&self) -> &str {
413        &self.workflow_name
414    }
415
416    /// Gets the elapsed time since this workflow execution started.
417    pub fn elapsed(&self) -> std::time::Duration {
418        self.started_at.elapsed()
419    }
420
421    /// Gets the start time for this workflow execution.
422    pub fn started_at(&self) -> std::time::Instant {
423        self.started_at
424    }
425
426    /// Adds metadata to the workflow context.
427    pub fn add_metadata(&mut self, key: impl Into<String>, value: impl Into<String>) {
428        self.metadata.insert(key.into(), value.into());
429    }
430
431    /// Gets metadata from the workflow context.
432    pub fn get_metadata(&self, key: &str) -> Option<&String> {
433        self.metadata.get(key)
434    }
435
436    /// Gets all metadata for the workflow context.
437    pub fn metadata(&self) -> &std::collections::HashMap<String, String> {
438        &self.metadata
439    }
440
441    /// Generates a unique run ID.
442    fn generate_run_id() -> String {
443        use std::collections::hash_map::DefaultHasher;
444        use std::hash::{Hash, Hasher};
445        use std::time::{SystemTime, UNIX_EPOCH};
446
447        let mut hasher = DefaultHasher::new();
448        SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos().hash(&mut hasher);
449        std::thread::current().id().hash(&mut hasher);
450        
451        format!("run_{:x}", hasher.finish())
452    }
453}
454
455impl Default for WorkflowContext {
456    fn default() -> Self {
457        Self::new("default_workflow")
458    }
459}
460
461/// Convenience macro for accessing scope-local values with less boilerplate.
462///
463/// This macro reduces the verbosity of accessing scope-local context values
464/// in workflow engines where context access is frequent.
465///
466/// # Examples
467///
468/// ```rust,no_run
469/// use ferrous_di::{ServiceCollection, ScopeLocal, WorkflowContext, scope_local, ScopedResolver, Resolver};
470/// use std::sync::Arc;
471///
472/// struct MyService;
473/// impl MyService {
474///     fn process(&self, resolver: &ScopedResolver) -> String {
475///         // Without macro:
476///         // let ctx = resolver.get_required::<ScopeLocal<WorkflowContext>>();
477///         
478///         // With macro:
479///         let ctx = scope_local!(resolver, WorkflowContext);
480///         format!("Processing in workflow: {}", ctx.workflow_name())
481///     }
482/// }
483///
484/// let mut services = ServiceCollection::new();
485/// services.add_scope_local::<WorkflowContext, _>(|_| {
486///     Arc::new(WorkflowContext::new("test_workflow"))
487/// });
488/// services.add_singleton(MyService);
489///
490/// let provider = services.build();
491/// let scope = provider.create_scope();
492/// let service = scope.get_required::<MyService>();
493/// // let result = service.process(&scope); // Would work with proper resolver
494/// ```
495#[macro_export]
496macro_rules! scope_local {
497    ($resolver:expr, $type:ty) => {
498        $resolver.get::<$crate::ScopeLocal<$type>>()
499            .unwrap_or_else(|e| panic!("Failed to resolve scope local {}: {:?}", std::any::type_name::<$crate::ScopeLocal<$type>>(), e))
500    };
501}
502
503/// Extensions for working with scope-local values in workflow contexts.
504pub mod workflow {
505    
506    
507
508    /// Standard security context for workflow engines.
509    #[derive(Debug, Clone)]
510    pub struct SecurityContext {
511        /// User or service account executing the workflow
512        pub user_id: String,
513        /// Permissions granted for this execution
514        pub permissions: Vec<String>,
515        /// Security tokens or credentials
516        pub tokens: std::collections::HashMap<String, String>,
517    }
518
519    impl SecurityContext {
520        pub fn new(user_id: impl Into<String>) -> Self {
521            Self {
522                user_id: user_id.into(),
523                permissions: Vec::new(),
524                tokens: std::collections::HashMap::new(),
525            }
526        }
527
528        pub fn with_permissions(mut self, permissions: Vec<String>) -> Self {
529            self.permissions = permissions;
530            self
531        }
532
533        pub fn add_token(&mut self, key: impl Into<String>, token: impl Into<String>) {
534            self.tokens.insert(key.into(), token.into());
535        }
536
537        pub fn has_permission(&self, permission: &str) -> bool {
538            self.permissions.contains(&permission.to_string())
539        }
540    }
541
542    /// Standard budget/quota context for workflow engines.
543    #[derive(Debug)]
544    pub struct BudgetContext {
545        /// Maximum tokens/credits available for this execution
546        pub max_tokens: std::sync::atomic::AtomicU32,
547        /// Tokens/credits remaining
548        pub tokens_remaining: std::sync::atomic::AtomicU32,
549        /// Maximum execution time allowed
550        pub max_duration: std::time::Duration,
551        /// When this execution started (for timeout calculation)
552        pub started_at: std::time::Instant,
553    }
554
555    impl BudgetContext {
556        pub fn new(max_tokens: u32, max_duration: std::time::Duration) -> Self {
557            Self {
558                max_tokens: std::sync::atomic::AtomicU32::new(max_tokens),
559                tokens_remaining: std::sync::atomic::AtomicU32::new(max_tokens),
560                max_duration,
561                started_at: std::time::Instant::now(),
562            }
563        }
564
565        pub fn consume_tokens(&self, amount: u32) -> bool {
566            let current = self.tokens_remaining.load(std::sync::atomic::Ordering::Relaxed);
567            if current >= amount {
568                self.tokens_remaining.fetch_sub(amount, std::sync::atomic::Ordering::Relaxed);
569                true
570            } else {
571                false
572            }
573        }
574
575        pub fn tokens_remaining(&self) -> u32 {
576            self.tokens_remaining.load(std::sync::atomic::Ordering::Relaxed)
577        }
578
579        pub fn time_remaining(&self) -> Option<std::time::Duration> {
580            let elapsed = self.started_at.elapsed();
581            if elapsed < self.max_duration {
582                Some(self.max_duration - elapsed)
583            } else {
584                None
585            }
586        }
587
588        pub fn is_expired(&self) -> bool {
589            self.time_remaining().is_none()
590        }
591    }
592}