almost_enough/
guard.rs

1//! RAII guard for automatic cancellation on drop.
2//!
3//! This module provides [`CancelGuard`], which cancels a source when dropped
4//! unless explicitly disarmed. This is useful for ensuring cleanup happens
5//! on error paths or panics.
6//!
7//! # Example
8//!
9//! ```rust
10//! use almost_enough::{Stopper, StopDropRoll};
11//!
12//! fn process(source: &Stopper) -> Result<(), &'static str> {
13//!     // Guard will cancel on drop unless disarmed
14//!     let guard = source.stop_on_drop();
15//!
16//!     // Do work that might fail...
17//!     do_risky_work()?;
18//!
19//!     // Success - don't cancel
20//!     guard.disarm();
21//!     Ok(())
22//! }
23//!
24//! fn do_risky_work() -> Result<(), &'static str> {
25//!     Ok(())
26//! }
27//!
28//! let source = Stopper::new();
29//! process(&source).unwrap();
30//! assert!(!source.is_cancelled()); // Not cancelled because we disarmed
31//! ```
32
33use crate::{ChildStopper, Stopper};
34
35/// Trait for types that can be stopped/cancelled.
36///
37/// This is implemented for [`Stopper`] and [`ChildStopper`] to allow
38/// creating [`CancelGuard`]s via the [`StopDropRoll`] trait.
39///
40/// The method is named `stop()` to align with the [`Stop`](crate::Stop) trait
41/// and avoid conflicts with inherent `cancel()` methods.
42pub trait Cancellable: Clone + Send {
43    /// Request stop/cancellation.
44    fn stop(&self);
45}
46
47impl Cancellable for Stopper {
48    #[inline]
49    fn stop(&self) {
50        self.cancel();
51    }
52}
53
54impl Cancellable for ChildStopper {
55    #[inline]
56    fn stop(&self) {
57        self.cancel();
58    }
59}
60
61/// A guard that cancels a source when dropped, unless disarmed.
62///
63/// This provides RAII-style cancellation for cleanup on error paths or panics.
64/// Create one via the [`StopDropRoll`] trait.
65///
66/// # Example
67///
68/// ```rust
69/// use almost_enough::{Stopper, StopDropRoll};
70///
71/// let source = Stopper::new();
72///
73/// {
74///     let guard = source.stop_on_drop();
75///     // guard dropped here, source gets cancelled
76/// }
77///
78/// assert!(source.is_cancelled());
79/// ```
80///
81/// # Disarming
82///
83/// Call [`disarm()`](Self::disarm) to prevent cancellation:
84///
85/// ```rust
86/// use almost_enough::{Stopper, StopDropRoll};
87///
88/// let source = Stopper::new();
89///
90/// {
91///     let guard = source.stop_on_drop();
92///     guard.disarm(); // Prevents cancellation
93/// }
94///
95/// assert!(!source.is_cancelled());
96/// ```
97#[derive(Debug)]
98pub struct CancelGuard<C: Cancellable> {
99    source: Option<C>,
100}
101
102impl<C: Cancellable> CancelGuard<C> {
103    /// Create a new guard that will cancel the source on drop.
104    ///
105    /// Prefer using [`StopDropRoll::stop_on_drop()`] instead.
106    #[inline]
107    pub fn new(source: C) -> Self {
108        Self {
109            source: Some(source),
110        }
111    }
112
113    /// Disarm the guard, preventing cancellation on drop.
114    ///
115    /// Call this when the guarded operation succeeds and you don't
116    /// want to cancel.
117    ///
118    /// # Example
119    ///
120    /// ```rust
121    /// use almost_enough::{Stopper, StopDropRoll};
122    ///
123    /// let source = Stopper::new();
124    /// let guard = source.stop_on_drop();
125    ///
126    /// // Operation succeeded, don't cancel
127    /// guard.disarm();  // Consumes guard, preventing cancellation
128    ///
129    /// assert!(!source.is_cancelled());
130    /// ```
131    #[inline]
132    pub fn disarm(mut self) {
133        self.source = None;
134    }
135
136    /// Check if this guard is still armed (will cancel on drop).
137    #[inline]
138    pub fn is_armed(&self) -> bool {
139        self.source.is_some()
140    }
141
142    /// Get a reference to the underlying source, if still armed.
143    #[inline]
144    pub fn source(&self) -> Option<&C> {
145        self.source.as_ref()
146    }
147}
148
149impl<C: Cancellable> Drop for CancelGuard<C> {
150    fn drop(&mut self) {
151        if let Some(source) = self.source.take() {
152            source.stop();
153        }
154    }
155}
156
157/// Extension trait for creating [`CancelGuard`]s.
158///
159/// This trait is implemented for types that support cancellation,
160/// allowing you to create RAII guards that stop on drop.
161///
162/// # Supported Types
163///
164/// - [`Stopper`] - Stops all clones
165/// - [`ChildStopper`] - Stops just this node (not siblings or parent)
166///
167/// # Example
168///
169/// ```rust
170/// use almost_enough::{Stopper, StopDropRoll};
171///
172/// fn fallible_work(source: &Stopper) -> Result<i32, &'static str> {
173///     let guard = source.stop_on_drop();
174///
175///     // If we return Err or panic, source is stopped
176///     let result = compute()?;
177///
178///     // Success - don't stop
179///     guard.disarm();
180///     Ok(result)
181/// }
182///
183/// fn compute() -> Result<i32, &'static str> {
184///     Ok(42)
185/// }
186///
187/// let source = Stopper::new();
188/// assert_eq!(fallible_work(&source), Ok(42));
189/// assert!(!source.is_cancelled());
190/// ```
191///
192/// # With ChildStopper
193///
194/// ```rust
195/// use almost_enough::{Stopper, ChildStopper, StopDropRoll, Stop, StopExt};
196///
197/// let parent = Stopper::new();
198/// let child = parent.child();
199///
200/// {
201///     let guard = child.stop_on_drop();
202///     // guard dropped, child is stopped
203/// }
204///
205/// assert!(child.is_cancelled());
206/// assert!(!parent.is_cancelled()); // Parent is NOT affected
207/// ```
208pub trait StopDropRoll: Cancellable {
209    /// Create a guard that will stop this source on drop.
210    ///
211    /// The guard can be disarmed via [`CancelGuard::disarm()`] to
212    /// prevent stopping.
213    fn stop_on_drop(&self) -> CancelGuard<Self>;
214}
215
216impl<C: Cancellable> StopDropRoll for C {
217    #[inline]
218    fn stop_on_drop(&self) -> CancelGuard<Self> {
219        CancelGuard::new(self.clone())
220    }
221}
222
223#[cfg(test)]
224mod tests {
225    use super::*;
226    use crate::{Stop, StopExt};
227
228    #[test]
229    fn guard_cancels_on_drop() {
230        let source = Stopper::new();
231        assert!(!source.is_cancelled());
232
233        {
234            let _guard = source.stop_on_drop();
235        } // guard dropped here
236
237        assert!(source.is_cancelled());
238    }
239
240    #[test]
241    fn guard_disarm_prevents_cancel() {
242        let source = Stopper::new();
243
244        {
245            let guard = source.stop_on_drop();
246            guard.disarm();
247        }
248
249        assert!(!source.is_cancelled());
250    }
251
252    #[test]
253    fn guard_is_armed() {
254        let source = Stopper::new();
255        let guard = source.stop_on_drop();
256
257        assert!(guard.is_armed());
258        guard.disarm();
259        // After disarm, guard is consumed, so we can't check is_armed
260    }
261
262    #[test]
263    fn guard_source_accessor() {
264        let source = Stopper::new();
265        let guard = source.stop_on_drop();
266
267        assert!(guard.source().is_some());
268    }
269
270    #[test]
271    fn guard_pattern_success() {
272        fn work(source: &Stopper) -> Result<i32, &'static str> {
273            let guard = source.stop_on_drop();
274            let result = Ok(42);
275            guard.disarm();
276            result
277        }
278
279        let source = Stopper::new();
280        assert_eq!(work(&source), Ok(42));
281        assert!(!source.is_cancelled());
282    }
283
284    #[test]
285    fn guard_pattern_failure() {
286        fn work(source: &Stopper) -> Result<i32, &'static str> {
287            let _guard = source.stop_on_drop();
288            Err("failed")
289            // guard dropped, source cancelled
290        }
291
292        let source = Stopper::new();
293        assert_eq!(work(&source), Err("failed"));
294        assert!(source.is_cancelled());
295    }
296
297    #[test]
298    fn guard_multiple_clones() {
299        let source = Stopper::new();
300        let source2 = source.clone();
301
302        {
303            let _guard = source.stop_on_drop();
304        }
305
306        // Both clones see the cancellation
307        assert!(source.is_cancelled());
308        assert!(source2.is_cancelled());
309    }
310
311    #[test]
312    fn guard_with_clone() {
313        let source = Stopper::new();
314        let clone = source.clone();
315
316        assert!(!clone.should_stop());
317
318        {
319            let _guard = source.stop_on_drop();
320        }
321
322        assert!(clone.should_stop());
323    }
324
325    #[test]
326    fn guard_tree_stopper() {
327        let parent = Stopper::new();
328        let child = parent.child();
329
330        {
331            let _guard = child.stop_on_drop();
332        }
333
334        // Child is cancelled
335        assert!(child.is_cancelled());
336        // Parent is NOT cancelled
337        assert!(!parent.is_cancelled());
338    }
339
340    #[test]
341    fn guard_tree_stopper_disarm() {
342        let parent = Stopper::new();
343        let child = parent.child();
344
345        {
346            let guard = child.stop_on_drop();
347            guard.disarm();
348        }
349
350        assert!(!child.is_cancelled());
351        assert!(!parent.is_cancelled());
352    }
353}