almost_enough/
lib.rs

1//! # almost-enough
2//!
3//! Batteries-included ergonomic extensions for the [`enough`] cooperative cancellation crate.
4//!
5//! This crate provides all the concrete implementations and helpers for working with
6//! stop tokens. It re-exports everything from `enough` for convenience.
7//!
8//! ## Quick Start
9//!
10//! ```rust
11//! use almost_enough::{Stopper, Stop};
12//!
13//! let stop = Stopper::new();
14//! let stop2 = stop.clone();  // Clone to share
15//!
16//! // Pass to operations
17//! assert!(!stop2.should_stop());
18//!
19//! // Any clone can cancel
20//! stop.cancel();
21//! assert!(stop2.should_stop());
22//! ```
23//!
24//! ## Type Overview
25//!
26//! | Type | Feature | Use Case |
27//! |------|---------|----------|
28//! | [`Unstoppable`] | core | Zero-cost "never stop" |
29//! | [`StopSource`] / [`StopRef`] | core | Stack-based, borrowed, zero-alloc |
30//! | [`FnStop`] | core | Wrap any closure |
31//! | [`OrStop`] | core | Combine multiple stops |
32//! | [`Stopper`] | alloc | **Default choice** - Arc-based, clone to share |
33//! | [`SyncStopper`] | alloc | Like Stopper with Acquire/Release ordering |
34//! | [`ChildStopper`] | alloc | Hierarchical parent-child cancellation |
35//! | [`BoxedStop`] | alloc | Type-erased dynamic dispatch |
36//! | [`WithTimeout`] | std | Add deadline to any `Stop` |
37//!
38//! ## StopExt Extension Trait
39//!
40//! The [`StopExt`] trait adds combinator methods to any [`Stop`] implementation:
41//!
42//! ```rust
43//! use almost_enough::{StopSource, Stop, StopExt};
44//!
45//! let timeout = StopSource::new();
46//! let cancel = StopSource::new();
47//!
48//! // Combine: stop if either stops
49//! let combined = timeout.as_ref().or(cancel.as_ref());
50//! assert!(!combined.should_stop());
51//!
52//! cancel.cancel();
53//! assert!(combined.should_stop());
54//! ```
55//!
56//! ## Type Erasure with `into_boxed()`
57//!
58//! Prevent monomorphization explosion at API boundaries:
59//!
60//! ```rust
61//! # #[cfg(feature = "alloc")]
62//! # fn main() {
63//! use almost_enough::{Stopper, BoxedStop, Stop, StopExt};
64//!
65//! fn outer(stop: impl Stop + 'static) {
66//!     // Erase the concrete type to avoid monomorphizing inner()
67//!     inner(stop.into_boxed());
68//! }
69//!
70//! fn inner(stop: BoxedStop) {
71//!     // Only one version of this function exists
72//!     while !stop.should_stop() {
73//!         break;
74//!     }
75//! }
76//!
77//! let stop = Stopper::new();
78//! outer(stop);
79//! # }
80//! # #[cfg(not(feature = "alloc"))]
81//! # fn main() {}
82//! ```
83//!
84//! ## Hierarchical Cancellation with `.child()`
85//!
86//! Create child stops that inherit cancellation from their parent:
87//!
88//! ```rust
89//! # #[cfg(feature = "alloc")]
90//! # fn main() {
91//! use almost_enough::{Stopper, Stop, StopExt};
92//!
93//! let parent = Stopper::new();
94//! let child = parent.child();
95//!
96//! // Child cancellation doesn't affect parent
97//! child.cancel();
98//! assert!(!parent.should_stop());
99//!
100//! // But parent cancellation propagates to children
101//! let child2 = parent.child();
102//! parent.cancel();
103//! assert!(child2.should_stop());
104//! # }
105//! # #[cfg(not(feature = "alloc"))]
106//! # fn main() {}
107//! ```
108//!
109//! ## Stop Guards (RAII Cancellation)
110//!
111//! Automatically stop on scope exit unless explicitly disarmed:
112//!
113//! ```rust
114//! # #[cfg(feature = "alloc")]
115//! # fn main() {
116//! use almost_enough::{Stopper, StopDropRoll};
117//!
118//! fn do_work(source: &Stopper) -> Result<(), &'static str> {
119//!     let guard = source.stop_on_drop();
120//!
121//!     // If we return early or panic, source is stopped
122//!     risky_operation()?;
123//!
124//!     // Success! Don't stop.
125//!     guard.disarm();
126//!     Ok(())
127//! }
128//!
129//! fn risky_operation() -> Result<(), &'static str> {
130//!     Ok(())
131//! }
132//!
133//! let source = Stopper::new();
134//! do_work(&source).unwrap();
135//! # }
136//! # #[cfg(not(feature = "alloc"))]
137//! # fn main() {}
138//! ```
139//!
140//! ## Feature Flags
141//!
142//! - **`std`** (default) - Full functionality including timeouts
143//! - **`alloc`** - Arc-based types, `into_boxed()`, `child()`, `StopDropRoll`
144//! - **None** - Core trait and stack-based types only
145
146#![cfg_attr(not(feature = "std"), no_std)]
147#![warn(missing_docs)]
148#![warn(clippy::all)]
149
150#[cfg(feature = "alloc")]
151extern crate alloc;
152
153// Re-export everything from enough
154#[allow(deprecated)]
155pub use enough::{Never, Stop, StopReason, Unstoppable};
156
157// Core modules (no_std, no alloc)
158mod func;
159mod or;
160mod source;
161
162pub use func::FnStop;
163pub use or::OrStop;
164pub use source::{StopRef, StopSource};
165
166// Alloc-dependent modules
167#[cfg(feature = "alloc")]
168mod boxed;
169#[cfg(feature = "alloc")]
170mod stopper;
171#[cfg(feature = "alloc")]
172mod sync_stopper;
173#[cfg(feature = "alloc")]
174mod tree;
175
176#[cfg(feature = "alloc")]
177pub use boxed::BoxedStop;
178#[cfg(feature = "alloc")]
179pub use stopper::Stopper;
180#[cfg(feature = "alloc")]
181pub use sync_stopper::SyncStopper;
182#[cfg(feature = "alloc")]
183pub use tree::ChildStopper;
184
185// Std-dependent modules
186#[cfg(feature = "std")]
187pub mod time;
188#[cfg(feature = "std")]
189pub use time::{TimeoutExt, WithTimeout};
190
191// Cancel guard module
192#[cfg(feature = "alloc")]
193mod guard;
194#[cfg(feature = "alloc")]
195pub use guard::{CancelGuard, Cancellable, StopDropRoll};
196
197/// Extension trait providing ergonomic combinators for [`Stop`] implementations.
198///
199/// This trait is automatically implemented for all `Stop + Sized` types.
200///
201/// # Example
202///
203/// ```rust
204/// use almost_enough::{StopSource, Stop, StopExt};
205///
206/// let source_a = StopSource::new();
207/// let source_b = StopSource::new();
208///
209/// // Combine with .or()
210/// let combined = source_a.as_ref().or(source_b.as_ref());
211///
212/// assert!(!combined.should_stop());
213///
214/// source_b.cancel();
215/// assert!(combined.should_stop());
216/// ```
217pub trait StopExt: Stop + Sized {
218    /// Combine this stop with another, stopping if either stops.
219    ///
220    /// This is equivalent to `OrStop::new(self, other)` but with a more
221    /// ergonomic method syntax that allows chaining.
222    ///
223    /// # Example
224    ///
225    /// ```rust
226    /// use almost_enough::{StopSource, Stop, StopExt};
227    ///
228    /// let timeout = StopSource::new();
229    /// let cancel = StopSource::new();
230    ///
231    /// let combined = timeout.as_ref().or(cancel.as_ref());
232    /// assert!(!combined.should_stop());
233    ///
234    /// cancel.cancel();
235    /// assert!(combined.should_stop());
236    /// ```
237    ///
238    /// # Chaining
239    ///
240    /// Multiple sources can be chained:
241    ///
242    /// ```rust
243    /// use almost_enough::{StopSource, Stop, StopExt};
244    ///
245    /// let a = StopSource::new();
246    /// let b = StopSource::new();
247    /// let c = StopSource::new();
248    ///
249    /// let combined = a.as_ref().or(b.as_ref()).or(c.as_ref());
250    ///
251    /// c.cancel();
252    /// assert!(combined.should_stop());
253    /// ```
254    #[inline]
255    fn or<S: Stop>(self, other: S) -> OrStop<Self, S> {
256        OrStop::new(self, other)
257    }
258
259    /// Convert this stop into a boxed trait object.
260    ///
261    /// This is useful for preventing monomorphization at API boundaries.
262    /// Instead of generating a new function for each `impl Stop` type,
263    /// you can erase the type to `BoxedStop` and have a single implementation.
264    ///
265    /// # Example
266    ///
267    /// ```rust
268    /// # #[cfg(feature = "alloc")]
269    /// # fn main() {
270    /// use almost_enough::{Stopper, BoxedStop, Stop, StopExt};
271    ///
272    /// // This function is monomorphized for each Stop type
273    /// fn process_generic(stop: impl Stop + 'static) {
274    ///     // Erase type at boundary
275    ///     process_concrete(stop.into_boxed());
276    /// }
277    ///
278    /// // This function has only one implementation
279    /// fn process_concrete(stop: BoxedStop) {
280    ///     while !stop.should_stop() {
281    ///         break;
282    ///     }
283    /// }
284    ///
285    /// let stop = Stopper::new();
286    /// process_generic(stop);
287    /// # }
288    /// # #[cfg(not(feature = "alloc"))]
289    /// # fn main() {}
290    /// ```
291    #[cfg(feature = "alloc")]
292    #[inline]
293    fn into_boxed(self) -> BoxedStop
294    where
295        Self: 'static,
296    {
297        BoxedStop::new(self)
298    }
299
300    /// Create a child stop that inherits cancellation from this stop.
301    ///
302    /// The returned [`ChildStopper`] will stop if:
303    /// - Its own `cancel()` is called
304    /// - This parent stop is cancelled
305    ///
306    /// Cancelling the child does NOT affect the parent.
307    ///
308    /// # Example
309    ///
310    /// ```rust
311    /// # #[cfg(feature = "alloc")]
312    /// # fn main() {
313    /// use almost_enough::{Stopper, Stop, StopExt};
314    ///
315    /// let parent = Stopper::new();
316    /// let child = parent.child();
317    ///
318    /// // Child cancellation is independent
319    /// child.cancel();
320    /// assert!(!parent.should_stop());
321    /// assert!(child.should_stop());
322    ///
323    /// // Parent cancellation propagates
324    /// let child2 = parent.child();
325    /// parent.cancel();
326    /// assert!(child2.should_stop());
327    /// # }
328    /// # #[cfg(not(feature = "alloc"))]
329    /// # fn main() {}
330    /// ```
331    #[cfg(feature = "alloc")]
332    #[inline]
333    fn child(&self) -> ChildStopper
334    where
335        Self: Clone + 'static,
336    {
337        ChildStopper::with_parent(self.clone())
338    }
339}
340
341// Blanket implementation for all Stop + Sized types
342impl<T: Stop + Sized> StopExt for T {}
343
344#[cfg(test)]
345mod tests {
346    use super::*;
347
348    #[test]
349    fn or_extension_works() {
350        let a = StopSource::new();
351        let b = StopSource::new();
352        let combined = a.as_ref().or(b.as_ref());
353
354        assert!(!combined.should_stop());
355
356        a.cancel();
357        assert!(combined.should_stop());
358    }
359
360    #[test]
361    fn or_chain_works() {
362        let a = StopSource::new();
363        let b = StopSource::new();
364        let c = StopSource::new();
365
366        let combined = a.as_ref().or(b.as_ref()).or(c.as_ref());
367
368        assert!(!combined.should_stop());
369
370        c.cancel();
371        assert!(combined.should_stop());
372    }
373
374    #[test]
375    fn or_with_unstoppable() {
376        let source = StopSource::new();
377        let combined = Unstoppable.or(source.as_ref());
378
379        assert!(!combined.should_stop());
380
381        source.cancel();
382        assert!(combined.should_stop());
383    }
384
385    #[test]
386    fn reexports_work() {
387        // Verify that re-exports from enough work
388        let _: StopReason = StopReason::Cancelled;
389        let _ = Unstoppable;
390        let source = StopSource::new();
391        let _ = source.as_ref();
392    }
393
394    #[cfg(feature = "alloc")]
395    #[test]
396    fn alloc_reexports_work() {
397        let stop = Stopper::new();
398        let _ = stop.clone();
399        let _ = BoxedStop::new(Unstoppable);
400    }
401
402    #[cfg(feature = "alloc")]
403    #[test]
404    fn into_boxed_works() {
405        let stop = Stopper::new();
406        let boxed: BoxedStop = stop.clone().into_boxed();
407
408        assert!(!boxed.should_stop());
409
410        stop.cancel();
411        assert!(boxed.should_stop());
412    }
413
414    #[cfg(feature = "alloc")]
415    #[test]
416    fn into_boxed_with_unstoppable() {
417        let boxed: BoxedStop = Unstoppable.into_boxed();
418        assert!(!boxed.should_stop());
419    }
420
421    #[cfg(feature = "alloc")]
422    #[test]
423    fn into_boxed_prevents_monomorphization() {
424        // This test verifies the pattern compiles correctly
425        fn outer(stop: impl Stop + 'static) {
426            inner(stop.into_boxed());
427        }
428
429        fn inner(stop: BoxedStop) {
430            let _ = stop.should_stop();
431        }
432
433        let stop = Stopper::new();
434        outer(stop);
435        outer(Unstoppable);
436    }
437
438    #[cfg(feature = "alloc")]
439    #[test]
440    fn child_extension_works() {
441        let parent = Stopper::new();
442        let child = parent.child();
443
444        assert!(!child.should_stop());
445
446        parent.cancel();
447        assert!(child.should_stop());
448    }
449
450    #[cfg(feature = "alloc")]
451    #[test]
452    fn child_independent_cancel() {
453        let parent = Stopper::new();
454        let child = parent.child();
455
456        child.cancel();
457
458        assert!(child.should_stop());
459        assert!(!parent.should_stop());
460    }
461
462    #[cfg(feature = "alloc")]
463    #[test]
464    fn child_chain() {
465        let grandparent = Stopper::new();
466        let parent = grandparent.child();
467        let child = parent.child();
468
469        grandparent.cancel();
470
471        assert!(parent.should_stop());
472        assert!(child.should_stop());
473    }
474}