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}