foundation_utils/scoped.rs
1//! Scoped operations and callback patterns
2//!
3//! This module provides high-level APIs for scoped operations that guarantee
4//! setup and cleanup. Built on top of the RAII patterns but providing more
5//! ergonomic APIs for common use cases.
6//!
7//! ## Key Features
8//!
9//! - **Scoped Callbacks**: Execute code with guaranteed setup/cleanup
10//! - **Builder Pattern**: Fluent APIs for complex scoped operations
11//! - **Exception Safety**: Cleanup happens even during panics
12//! - **Return Values**: Scoped operations can return values from callbacks
13//! - **Composable**: Scoped operations can be nested and combined
14
15use crate::raii::Guard;
16
17/// Execute a function with a scoped context
18///
19/// This is the most common scoped operation pattern. It sets up a context,
20/// executes a function with that context, and automatically cleans up.
21///
22/// # Type Parameters
23/// - `T`: The type of context
24/// - `F`: The type of function to execute
25/// - `R`: The return type of the function
26///
27/// # Arguments
28/// - `context`: The context value to set up
29/// - `f`: The function to execute with the context
30///
31/// # Example
32/// ```rust
33/// use foundation_utils::scoped::with_context;
34///
35/// let result = with_context("my_context", |ctx| {
36/// println!("Working with context: {}", ctx);
37/// 42
38/// });
39/// assert_eq!(result, 42);
40/// ```
41pub fn with_context<T, F, R>(context: T, f: F) -> R
42where
43 F: FnOnce(&T) -> R,
44{
45 f(&context)
46}
47
48/// Execute a function with scoped setup and cleanup
49///
50/// This pattern provides explicit setup and cleanup functions that are
51/// guaranteed to be called even if the work function panics.
52///
53/// # Type Parameters
54/// - `S`: Setup function type
55/// - `W`: Work function type
56/// - `C`: Cleanup function type
57/// - `T`: Resource type returned by setup
58/// - `R`: Return type of work function
59///
60/// # Arguments
61/// - `setup`: Function to set up the resource
62/// - `work`: Function to do work with the resource
63/// - `cleanup`: Function to clean up the resource
64///
65/// # Example
66/// ```rust
67/// use foundation_utils::scoped::with_setup_cleanup;
68///
69/// let result = with_setup_cleanup(
70/// || "resource", // Setup
71/// |resource| format!("used: {}", resource), // Work
72/// |resource| println!("cleanup: {}", resource) // Cleanup
73/// );
74/// ```
75pub fn with_setup_cleanup<S, W, C, T, R>(setup: S, work: W, cleanup: C) -> R
76where
77 S: FnOnce() -> T,
78 W: FnOnce(&T) -> R,
79 C: FnOnce(T),
80{
81 let resource = setup();
82 let _guard = Guard::new(resource, cleanup);
83 work(_guard.resource())
84}
85
86/// Execute a function with optional scoped setup and cleanup
87///
88/// This is useful when setup/cleanup is conditional based on runtime conditions.
89///
90/// # Example
91/// ```rust
92/// use foundation_utils::scoped::with_optional_scope;
93///
94/// let should_setup = true;
95/// let result = with_optional_scope(
96/// should_setup,
97/// || "resource", // Setup (only if condition is true)
98/// |resource| format!("used: {:?}", resource), // Work
99/// |resource| println!("cleanup: {}", resource) // Cleanup
100/// );
101/// ```
102pub fn with_optional_scope<S, W, C, T, R>(condition: bool, setup: S, work: W, cleanup: C) -> R
103where
104 S: FnOnce() -> T,
105 W: FnOnce(Option<&T>) -> R,
106 C: FnOnce(T),
107{
108 if condition {
109 let resource = setup();
110 let _guard = Guard::new(resource, cleanup);
111 work(Some(_guard.resource()))
112 } else {
113 work(None)
114 }
115}
116
117/// Trait for scoped callback operations
118///
119/// This trait provides a standard interface for types that support
120/// scoped operations with automatic cleanup.
121pub trait ScopedCallback<T> {
122 /// Execute a function with scoped access to the resource
123 fn with_scope<F, R>(self, f: F) -> R
124 where
125 F: FnOnce(&T) -> R;
126}
127
128/// Builder for creating complex scoped operations
129///
130/// This builder allows you to compose multiple scoped operations
131/// with different setup/cleanup phases.
132///
133/// # Example
134/// ```rust
135/// use foundation_utils::scoped::ScopedBuilder;
136///
137/// let result = ScopedBuilder::new()
138/// .with_resource("resource1", |r| println!("cleanup: {}", r))
139/// .with_resource("resource2", |r| println!("cleanup: {}", r))
140/// .execute(|resources| {
141/// format!("used {} resources", resources.len())
142/// });
143/// ```
144pub struct ScopedBuilder<T> {
145 resources: Vec<T>,
146 cleanups: Vec<Box<dyn FnOnce(T)>>,
147}
148
149impl<T> ScopedBuilder<T> {
150 /// Create a new scoped builder
151 pub fn new() -> Self {
152 Self {
153 resources: Vec::new(),
154 cleanups: Vec::new(),
155 }
156 }
157
158 /// Add a resource with cleanup to the scoped operation
159 ///
160 /// # Arguments
161 /// - `resource`: The resource to manage
162 /// - `cleanup`: Function to clean up the resource when the scope ends
163 pub fn with_resource<F>(mut self, resource: T, cleanup: F) -> Self
164 where
165 F: FnOnce(T) + 'static,
166 {
167 self.resources.push(resource);
168 self.cleanups.push(Box::new(cleanup));
169 self
170 }
171
172 /// Execute the scoped operation with all resources
173 ///
174 /// All cleanup functions will be called in reverse order (LIFO)
175 /// even if the work function panics.
176 pub fn execute<F, R>(mut self, work: F) -> R
177 where
178 F: FnOnce(&[T]) -> R,
179 {
180 struct CleanupGuard<T> {
181 resources: Vec<T>,
182 cleanups: Vec<Box<dyn FnOnce(T)>>,
183 }
184
185 impl<T> Drop for CleanupGuard<T> {
186 fn drop(&mut self) {
187 while let (Some(resource), Some(cleanup)) =
188 (self.resources.pop(), self.cleanups.pop())
189 {
190 cleanup(resource);
191 }
192 }
193 }
194
195 let guard = CleanupGuard {
196 resources: std::mem::take(&mut self.resources),
197 cleanups: std::mem::take(&mut self.cleanups),
198 };
199
200 work(&guard.resources)
201 }
202}
203
204impl<T> Default for ScopedBuilder<T> {
205 fn default() -> Self {
206 Self::new()
207 }
208}
209
210/// Scoped operation that automatically manages multiple contexts
211///
212/// This is a convenience function for managing multiple contexts
213/// that need to be set up and torn down together.
214///
215/// # Example
216/// ```rust
217/// use foundation_utils::scoped::with_multiple_contexts;
218///
219/// let contexts = vec!["ctx1", "ctx2", "ctx3"];
220/// let result = with_multiple_contexts(
221/// contexts,
222/// |ctx| println!("setup: {}", ctx),
223/// |contexts| {
224/// println!("Working with {} contexts", contexts.len());
225/// 42
226/// },
227/// |ctx| println!("cleanup: {}", ctx)
228/// );
229/// ```
230pub fn with_multiple_contexts<T, S, W, C, R>(contexts: Vec<T>, setup: S, work: W, cleanup: C) -> R
231where
232 T: Clone,
233 S: Fn(&T),
234 W: FnOnce(&[T]) -> R,
235 C: Fn(&T),
236{
237 // Set up all contexts
238 for context in &contexts {
239 setup(context);
240 }
241
242 // Create a guard that will clean up all contexts in reverse order
243 let contexts_for_cleanup = contexts.clone();
244 let _guard = Guard::new(contexts_for_cleanup, move |contexts| {
245 for context in contexts.iter().rev() {
246 cleanup(context);
247 }
248 });
249
250 // Execute the work function
251 work(&contexts)
252}
253
254/// Macro for creating scoped operations
255///
256/// This macro provides a convenient syntax for common scoped patterns.
257///
258/// # Example
259/// ```rust
260/// use foundation_utils::scoped_operation;
261///
262/// let result = scoped_operation! {
263/// setup => || "resource",
264/// work => |resource| format!("used: {}", resource),
265/// cleanup => |resource| println!("cleanup: {}", resource)
266/// };
267/// ```
268#[macro_export]
269macro_rules! scoped_operation {
270 (
271 setup => $setup:expr_2021,
272 work => $work:expr_2021,
273 cleanup => $cleanup:expr_2021
274 ) => {
275 $crate::scoped::with_setup_cleanup($setup, $work, $cleanup)
276 };
277}
278
279/// Macro for creating context-based scoped operations
280///
281/// # Example
282/// ```rust
283/// use foundation_utils::with_scoped_context;
284///
285/// let result = with_scoped_context!("my_context", |ctx| {
286/// println!("Context: {}", ctx);
287/// 42
288/// });
289/// ```
290#[macro_export]
291macro_rules! with_scoped_context {
292 ($context:expr_2021, $work:expr_2021) => {
293 $crate::scoped::with_context($context, $work)
294 };
295}
296
297/// Helper trait for making types support scoped operations
298///
299/// Implement this trait to add scoped operation support to your types.
300pub trait IntoScoped {
301 type Resource;
302
303 /// Convert into a scoped resource
304 fn into_scoped(self) -> Self::Resource;
305
306 /// Execute a scoped operation with this resource
307 fn scoped<F, R>(self, f: F) -> R
308 where
309 Self: Sized,
310 F: FnOnce(&Self::Resource) -> R,
311 {
312 let resource = self.into_scoped();
313 f(&resource)
314 }
315}
316
317// Implement IntoScoped for common types
318impl<T> IntoScoped for T {
319 type Resource = T;
320
321 fn into_scoped(self) -> Self::Resource {
322 self
323 }
324}
325
326/// Convenience function for scope-aware error handling
327///
328/// This function allows you to handle errors within a scoped operation
329/// while still ensuring cleanup happens.
330///
331/// # Example
332/// ```rust
333/// use foundation_utils::scoped::with_error_scope;
334///
335/// let result = with_error_scope(
336/// || Ok("resource"),
337/// |resource| {
338/// // Work that might fail
339/// if resource.len() > 0 {
340/// Ok(format!("processed: {}", resource))
341/// } else {
342/// Err("empty resource")
343/// }
344/// },
345/// |resource| println!("cleanup: {}", resource)
346/// );
347/// ```
348pub fn with_error_scope<S, W, C, T, R, E>(setup: S, work: W, cleanup: C) -> Result<R, E>
349where
350 S: FnOnce() -> Result<T, E>,
351 W: FnOnce(&T) -> Result<R, E>,
352 C: FnOnce(T),
353{
354 let resource = setup()?;
355 let _guard = Guard::new(resource, cleanup);
356 work(_guard.resource())
357}
358
359#[cfg(test)]
360mod tests {
361 use super::*;
362 use std::sync::atomic::{AtomicUsize, Ordering};
363 use std::sync::{Arc, Mutex};
364
365 #[test]
366 fn test_with_context() {
367 let result = with_context(42, |ctx| *ctx + 10);
368 assert_eq!(result, 52);
369 }
370
371 #[test]
372 fn test_with_setup_cleanup() {
373 let cleanup_called = Arc::new(AtomicUsize::new(0));
374 let cleanup_called_clone = cleanup_called.clone();
375
376 let result = with_setup_cleanup(
377 || 42,
378 |resource| *resource + 10,
379 move |resource| {
380 cleanup_called_clone.store(resource, Ordering::Relaxed);
381 },
382 );
383
384 assert_eq!(result, 52);
385 assert_eq!(cleanup_called.load(Ordering::Relaxed), 42);
386 }
387
388 #[test]
389 fn test_with_optional_scope() {
390 // Test with condition true
391 let result = with_optional_scope(
392 true,
393 || 42,
394 |resource| resource.map(|r| *r + 10).unwrap_or(0),
395 |_| {}, // cleanup
396 );
397 assert_eq!(result, 52);
398
399 // Test with condition false
400 let result = with_optional_scope(
401 false,
402 || 42,
403 |resource| resource.map(|r| *r + 10).unwrap_or(0),
404 |_| {}, // cleanup
405 );
406 assert_eq!(result, 0);
407 }
408
409 #[test]
410 fn test_scoped_builder() {
411 let result = ScopedBuilder::new()
412 .with_resource("test1", |_| {})
413 .with_resource("test2", |_| {})
414 .execute(|resources| resources.len());
415
416 assert_eq!(result, 2);
417 }
418
419 #[test]
420 fn test_with_multiple_contexts() {
421 let setup_count = Arc::new(AtomicUsize::new(0));
422 let cleanup_count = Arc::new(AtomicUsize::new(0));
423
424 let setup_count_clone = setup_count.clone();
425 let cleanup_count_clone = cleanup_count.clone();
426
427 let contexts = vec![1, 2, 3];
428 let result = with_multiple_contexts(
429 contexts,
430 move |_| {
431 setup_count_clone.fetch_add(1, Ordering::Relaxed);
432 },
433 |contexts| contexts.len(),
434 move |_| {
435 cleanup_count_clone.fetch_add(1, Ordering::Relaxed);
436 },
437 );
438
439 assert_eq!(result, 3);
440 assert_eq!(setup_count.load(Ordering::Relaxed), 3);
441 assert_eq!(cleanup_count.load(Ordering::Relaxed), 3);
442 }
443
444 #[test]
445 fn test_with_error_scope() {
446 // Test success case
447 let result: Result<i32, &str> = with_error_scope(
448 || Ok(42),
449 |resource| Ok(*resource + 10),
450 |_| {}, // cleanup
451 );
452 assert_eq!(result, Ok(52));
453
454 // Test error in setup
455 let result: Result<i32, &str> = with_error_scope(
456 || Err("setup failed"),
457 |resource| Ok(*resource + 10),
458 |_: i32| {}, // cleanup
459 );
460 assert_eq!(result, Err("setup failed"));
461
462 // Test error in work
463 let result: Result<i32, &str> = with_error_scope(
464 || Ok(42),
465 |_| Err("work failed"),
466 |_| {}, // cleanup should still happen
467 );
468 assert_eq!(result, Err("work failed"));
469 }
470
471 #[test]
472 fn test_into_scoped() {
473 let result = 42.scoped(|resource| *resource + 10);
474 assert_eq!(result, 52);
475 }
476
477 #[test]
478 fn test_panic_safety_in_scoped_operations() {
479 let cleanup_called = Arc::new(AtomicUsize::new(0));
480 let cleanup_called_clone = cleanup_called.clone();
481
482 let result = std::panic::catch_unwind(|| {
483 with_setup_cleanup(
484 || 42,
485 |_| panic!("test panic"),
486 move |resource| {
487 cleanup_called_clone.store(resource, Ordering::Relaxed);
488 },
489 )
490 });
491
492 assert!(result.is_err());
493 assert_eq!(cleanup_called.load(Ordering::Relaxed), 42);
494 }
495
496 #[test]
497 fn test_scoped_builder_cleanups_called() {
498 let counter = Arc::new(AtomicUsize::new(0));
499 let c1 = counter.clone();
500 let c2 = counter.clone();
501 let c3 = counter.clone();
502
503 let result = ScopedBuilder::new()
504 .with_resource(10, move |_| {
505 c1.fetch_add(1, Ordering::SeqCst);
506 })
507 .with_resource(20, move |_| {
508 c2.fetch_add(1, Ordering::SeqCst);
509 })
510 .with_resource(30, move |_| {
511 c3.fetch_add(1, Ordering::SeqCst);
512 })
513 .execute(|resources| {
514 assert_eq!(resources, &[10, 20, 30]);
515 resources.iter().sum::<i32>()
516 });
517
518 assert_eq!(result, 60);
519 assert_eq!(counter.load(Ordering::SeqCst), 3);
520 }
521
522 #[test]
523 fn test_scoped_builder_lifo_cleanup_order() {
524 let order = Arc::new(Mutex::new(Vec::new()));
525 let o1 = order.clone();
526 let o2 = order.clone();
527 let o3 = order.clone();
528
529 ScopedBuilder::new()
530 .with_resource("first", move |r| {
531 o1.lock().unwrap().push(r);
532 })
533 .with_resource("second", move |r| {
534 o2.lock().unwrap().push(r);
535 })
536 .with_resource("third", move |r| {
537 o3.lock().unwrap().push(r);
538 })
539 .execute(|_| {});
540
541 let cleaned = order.lock().unwrap();
542 assert_eq!(&*cleaned, &["third", "second", "first"]);
543 }
544
545 #[test]
546 fn test_scoped_builder_panic_safety() {
547 let counter = Arc::new(AtomicUsize::new(0));
548 let c1 = counter.clone();
549 let c2 = counter.clone();
550
551 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
552 ScopedBuilder::new()
553 .with_resource(1, move |_| {
554 c1.fetch_add(1, Ordering::SeqCst);
555 })
556 .with_resource(2, move |_| {
557 c2.fetch_add(1, Ordering::SeqCst);
558 })
559 .execute(|_| panic!("work panicked"))
560 }));
561
562 assert!(result.is_err());
563 assert_eq!(counter.load(Ordering::SeqCst), 2);
564 }
565}