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}