Skip to main content

foundation_utils/
raii.rs

1//! RAII (Resource Acquisition Is Initialization) patterns
2//!
3//! This module provides generic RAII guard patterns that ensure automatic
4//! resource cleanup when guards go out of scope. Based on the proven patterns
5//! from observability_core but generalized for project-wide use.
6//!
7//! ## Key Features
8//!
9//! - **Automatic Cleanup**: Resources are cleaned up when guards drop
10//! - **Exception Safety**: Cleanup happens even during panics
11//! - **Zero Cost**: Pure compile-time abstractions
12//! - **Type Safe**: Compile-time guarantees of correctness
13//! - **WASM Compatible**: No external dependencies
14
15use std::fmt;
16use std::sync::{Arc, Mutex};
17
18/// Generic RAII guard that owns a resource and cleans it up on drop
19///
20/// This is the fundamental building block for all RAII patterns in the project.
21/// It ensures that resources are properly cleaned up even if panics occur.
22///
23/// # Type Parameters
24/// - `T`: The type of resource being managed
25/// - `F`: The type of cleanup function (must be `FnOnce(T)`)
26///
27/// # Example
28/// ```rust
29/// use foundation_utils::raii::Guard;
30///
31/// let _guard = Guard::new(42, |value| {
32///     println!("Cleaning up value: {}", value);
33/// });
34/// // Cleanup happens automatically when guard drops
35/// ```
36pub struct Guard<T, F>
37where
38    F: FnOnce(T),
39{
40    resource: Option<T>,
41    cleanup: Option<F>,
42}
43
44impl<T, F> Guard<T, F>
45where
46    F: FnOnce(T),
47{
48    /// Create a new RAII guard
49    ///
50    /// # Arguments
51    /// - `resource`: The resource to manage
52    /// - `cleanup`: Function to call when the guard drops
53    ///
54    /// # Example
55    /// ```rust
56    /// use foundation_utils::raii::Guard;
57    ///
58    /// let guard = Guard::new("resource", |r| println!("Cleanup: {}", r));
59    /// ```
60    pub fn new(resource: T, cleanup: F) -> Self {
61        Self {
62            resource: Some(resource),
63            cleanup: Some(cleanup),
64        }
65    }
66
67    /// Get a reference to the managed resource
68    ///
69    /// # Panics
70    /// Panics if the guard has already been consumed (should not happen in normal use)
71    pub fn resource(&self) -> &T {
72        self.resource
73            .as_ref()
74            .expect("Guard resource already consumed")
75    }
76
77    /// Get a mutable reference to the managed resource
78    ///
79    /// # Panics
80    /// Panics if the guard has already been consumed (should not happen in normal use)
81    pub fn resource_mut(&mut self) -> &mut T {
82        self.resource
83            .as_mut()
84            .expect("Guard resource already consumed")
85    }
86
87    /// Consume the guard and return the resource without cleanup
88    ///
89    /// This is useful when you want to transfer ownership without cleanup
90    pub fn into_inner(mut self) -> T {
91        let resource = self
92            .resource
93            .take()
94            .expect("Guard resource already consumed");
95        self.cleanup.take(); // Prevent cleanup from running
96        resource
97    }
98
99    /// Manually trigger cleanup now (guard becomes invalid after this)
100    ///
101    /// This allows explicit cleanup before the guard would naturally drop
102    pub fn cleanup_now(mut self) {
103        if let (Some(resource), Some(cleanup)) = (self.resource.take(), self.cleanup.take()) {
104            cleanup(resource);
105        }
106    }
107}
108
109impl<T, F> Drop for Guard<T, F>
110where
111    F: FnOnce(T),
112{
113    fn drop(&mut self) {
114        if let (Some(resource), Some(cleanup)) = (self.resource.take(), self.cleanup.take()) {
115            cleanup(resource);
116        }
117    }
118}
119
120impl<T, F> fmt::Debug for Guard<T, F>
121where
122    T: fmt::Debug,
123    F: FnOnce(T),
124{
125    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
126        f.debug_struct("Guard")
127            .field("resource", &self.resource)
128            .field("cleanup", &"<function>")
129            .finish()
130    }
131}
132
133/// Trait for types that can create scoped guards
134///
135/// This trait provides a standard interface for creating RAII guards
136/// for different types of resources.
137pub trait ScopedGuard {
138    type Resource;
139    type Guard;
140
141    /// Create a scoped guard for this resource
142    fn scoped(resource: Self::Resource) -> Self::Guard;
143}
144
145/// Specialized guard for managing shared state
146///
147/// This guard is useful for managing Arc<Mutex<T>> resources where
148/// you want to ensure proper locking and unlocking.
149pub struct SharedGuard<T> {
150    resource: Option<Arc<Mutex<T>>>,
151    was_locked: bool,
152}
153
154impl<T> SharedGuard<T> {
155    /// Create a new shared guard
156    ///
157    /// # Arguments
158    /// - `resource`: The Arc<Mutex<T>> to manage
159    ///
160    /// # Example
161    /// ```rust
162    /// use foundation_utils::raii::SharedGuard;
163    /// use std::sync::{Arc, Mutex};
164    ///
165    /// let shared_data = Arc::new(Mutex::new(42));
166    /// let _guard = SharedGuard::new(shared_data);
167    /// // Mutex is automatically handled
168    /// ```
169    pub fn new(resource: Arc<Mutex<T>>) -> Self {
170        Self {
171            resource: Some(resource),
172            was_locked: false,
173        }
174    }
175
176    /// Lock the mutex and get a reference to the data
177    ///
178    /// This is a convenience method that handles the lock/unlock pattern
179    pub fn lock(
180        &mut self,
181    ) -> Result<std::sync::MutexGuard<'_, T>, std::sync::PoisonError<std::sync::MutexGuard<'_, T>>>
182    {
183        if let Some(ref resource) = self.resource {
184            self.was_locked = true;
185            resource.lock()
186        } else {
187            panic!("SharedGuard resource already consumed")
188        }
189    }
190
191    /// Get the shared resource without locking
192    pub fn resource(&self) -> &Arc<Mutex<T>> {
193        self.resource
194            .as_ref()
195            .expect("SharedGuard resource already consumed")
196    }
197}
198
199impl<T> Drop for SharedGuard<T> {
200    fn drop(&mut self) {
201        // In this case, the MutexGuard handles unlocking automatically
202        // This guard is mainly for resource lifetime management
203        self.resource.take();
204    }
205}
206
207/// Context guard that manages thread-local or scoped context
208///
209/// This is a specialized guard for managing context that needs to be
210/// set and cleared in a scoped manner.
211pub struct ContextGuard<T, S, C>
212where
213    S: FnOnce(&T),
214    C: FnOnce(),
215{
216    context: Option<T>,
217    setter: Option<S>,
218    clearer: Option<C>,
219    was_set: bool,
220}
221
222impl<T, S, C> ContextGuard<T, S, C>
223where
224    S: FnOnce(&T),
225    C: FnOnce(),
226{
227    /// Create a new context guard
228    ///
229    /// # Arguments
230    /// - `context`: The context value to manage
231    /// - `setter`: Function to set the context
232    /// - `clearer`: Function to clear the context
233    ///
234    /// # Example
235    /// ```rust
236    /// use foundation_utils::raii::ContextGuard;
237    ///
238    /// let _guard = ContextGuard::new(
239    ///     "my_context",
240    ///     |ctx| println!("set: {}", ctx),
241    ///     || println!("cleared"),
242    /// );
243    /// // Context is automatically cleared when guard drops
244    /// ```
245    pub fn new(context: T, setter: S, clearer: C) -> Self {
246        let mut guard = Self {
247            context: Some(context),
248            setter: Some(setter),
249            clearer: Some(clearer),
250            was_set: false,
251        };
252
253        // Set the context immediately
254        if let (Some(context), Some(setter)) = (&guard.context, guard.setter.take()) {
255            setter(context);
256            guard.was_set = true;
257        }
258
259        guard
260    }
261
262    /// Get a reference to the context
263    pub fn context(&self) -> &T {
264        self.context
265            .as_ref()
266            .expect("ContextGuard context already consumed")
267    }
268}
269
270impl<T, S, C> Drop for ContextGuard<T, S, C>
271where
272    S: FnOnce(&T),
273    C: FnOnce(),
274{
275    fn drop(&mut self) {
276        if self.was_set {
277            if let Some(clearer) = self.clearer.take() {
278                clearer();
279            }
280        }
281        self.context.take();
282    }
283}
284
285/// No-op guard that does nothing
286///
287/// This is useful as a placeholder when guards are conditionally created
288/// but you still want to maintain the same interface.
289pub struct NoOpGuard;
290
291impl NoOpGuard {
292    /// Create a new no-op guard
293    pub fn new() -> Self {
294        Self
295    }
296}
297
298impl Default for NoOpGuard {
299    fn default() -> Self {
300        Self::new()
301    }
302}
303
304impl Drop for NoOpGuard {
305    fn drop(&mut self) {
306        // No-op
307    }
308}
309
310/// Macro to create a simple RAII guard
311///
312/// This macro provides a convenient way to create guards with inline cleanup logic.
313///
314/// # Example
315/// ```rust
316/// use foundation_utils::raii_guard;
317///
318/// let _guard = raii_guard!("my_resource", |r| {
319///     println!("Cleaning up: {}", r);
320/// });
321/// ```
322#[macro_export]
323macro_rules! raii_guard {
324    ($resource:expr_2021, $cleanup:expr_2021) => {
325        $crate::raii::Guard::new($resource, $cleanup)
326    };
327}
328
329/// Macro to create a context guard
330///
331/// This macro provides a convenient way to create context guards with
332/// setup and cleanup logic.
333///
334/// # Example
335/// ```rust
336/// use foundation_utils::context_guard;
337///
338/// let _guard = context_guard!(
339///     "my_context",
340///     |ctx| println!("set: {}", ctx),
341///     || println!("cleared")
342/// );
343/// ```
344#[macro_export]
345macro_rules! context_guard {
346    ($context:expr_2021, $setter:expr_2021, $clearer:expr_2021) => {
347        $crate::raii::ContextGuard::new($context, $setter, $clearer)
348    };
349}
350
351#[cfg(test)]
352mod tests {
353    use super::*;
354    use std::sync::atomic::{AtomicBool, Ordering};
355    use std::sync::{Arc, Mutex};
356
357    #[test]
358    fn test_basic_guard() {
359        let cleanup_called = Arc::new(AtomicBool::new(false));
360        let cleanup_called_clone = cleanup_called.clone();
361
362        {
363            let guard = Guard::new(42, move |value| {
364                assert_eq!(value, 42);
365                cleanup_called_clone.store(true, Ordering::Relaxed);
366            });
367
368            assert_eq!(*guard.resource(), 42);
369            assert!(!cleanup_called.load(Ordering::Relaxed));
370        } // Guard drops here
371
372        assert!(cleanup_called.load(Ordering::Relaxed));
373    }
374
375    #[test]
376    fn test_guard_into_inner() {
377        let cleanup_called = Arc::new(AtomicBool::new(false));
378        let cleanup_called_clone = cleanup_called.clone();
379
380        let guard = Guard::new(42, move |_| {
381            cleanup_called_clone.store(true, Ordering::Relaxed);
382        });
383
384        let value = guard.into_inner();
385        assert_eq!(value, 42);
386
387        // Cleanup should NOT have been called
388        assert!(!cleanup_called.load(Ordering::Relaxed));
389    }
390
391    #[test]
392    fn test_guard_manual_cleanup() {
393        let cleanup_called = Arc::new(AtomicBool::new(false));
394        let cleanup_called_clone = cleanup_called.clone();
395
396        let guard = Guard::new(42, move |_| {
397            cleanup_called_clone.store(true, Ordering::Relaxed);
398        });
399
400        guard.cleanup_now();
401
402        // Cleanup should have been called
403        assert!(cleanup_called.load(Ordering::Relaxed));
404    }
405
406    #[test]
407    fn test_shared_guard() {
408        let data = Arc::new(Mutex::new(42));
409        let mut guard = SharedGuard::new(data.clone());
410
411        {
412            let locked_data = guard.lock().unwrap();
413            assert_eq!(*locked_data, 42);
414        } // MutexGuard drops here, automatically unlocking
415
416        // Guard is still valid
417        let another_lock = guard.lock().unwrap();
418        assert_eq!(*another_lock, 42);
419    }
420
421    #[test]
422    fn test_context_guard() {
423        let context_set = Arc::new(AtomicBool::new(false));
424        let context_cleared = Arc::new(AtomicBool::new(false));
425
426        let context_set_clone = context_set.clone();
427        let context_cleared_clone = context_cleared.clone();
428
429        {
430            let _guard = ContextGuard::new(
431                "test_context",
432                move |_| {
433                    context_set_clone.store(true, Ordering::Relaxed);
434                },
435                move || {
436                    context_cleared_clone.store(true, Ordering::Relaxed);
437                },
438            );
439
440            // Context should be set
441            assert!(context_set.load(Ordering::Relaxed));
442            assert!(!context_cleared.load(Ordering::Relaxed));
443        } // Guard drops here
444
445        // Context should be cleared
446        assert!(context_cleared.load(Ordering::Relaxed));
447    }
448
449    #[test]
450    fn test_panic_safety() {
451        let cleanup_called = Arc::new(AtomicBool::new(false));
452        let cleanup_called_clone = cleanup_called.clone();
453
454        let result = std::panic::catch_unwind(|| {
455            let _guard = Guard::new(42, move |_| {
456                cleanup_called_clone.store(true, Ordering::Relaxed);
457            });
458
459            panic!("Test panic");
460        });
461
462        assert!(result.is_err());
463        // Cleanup should still have been called
464        assert!(cleanup_called.load(Ordering::Relaxed));
465    }
466
467    #[test]
468    fn test_no_op_guard() {
469        let _guard = NoOpGuard::new();
470        // Should not panic or do anything
471    }
472
473    #[test]
474    fn test_macros() {
475        let cleanup_called = Arc::new(AtomicBool::new(false));
476        let cleanup_called_clone = cleanup_called.clone();
477
478        {
479            let _guard = raii_guard!(42, move |_| {
480                cleanup_called_clone.store(true, Ordering::Relaxed);
481            });
482        }
483
484        assert!(cleanup_called.load(Ordering::Relaxed));
485    }
486}