Skip to main content

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}