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}