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//! | [`Never`] | 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
154pub use enough::{Never, Stop, StopReason};
155
156// Core modules (no_std, no alloc)
157mod func;
158mod or;
159mod source;
160
161pub use func::FnStop;
162pub use or::OrStop;
163pub use source::{StopRef, StopSource};
164
165// Alloc-dependent modules
166#[cfg(feature = "alloc")]
167mod boxed;
168#[cfg(feature = "alloc")]
169mod stopper;
170#[cfg(feature = "alloc")]
171mod sync_stopper;
172#[cfg(feature = "alloc")]
173mod tree;
174
175#[cfg(feature = "alloc")]
176pub use boxed::BoxedStop;
177#[cfg(feature = "alloc")]
178pub use stopper::Stopper;
179#[cfg(feature = "alloc")]
180pub use sync_stopper::SyncStopper;
181#[cfg(feature = "alloc")]
182pub use tree::ChildStopper;
183
184// Std-dependent modules
185#[cfg(feature = "std")]
186pub mod time;
187#[cfg(feature = "std")]
188pub use time::{TimeoutExt, WithTimeout};
189
190// Cancel guard module
191#[cfg(feature = "alloc")]
192mod guard;
193#[cfg(feature = "alloc")]
194pub use guard::{CancelGuard, Cancellable, StopDropRoll};
195
196/// Extension trait providing ergonomic combinators for [`Stop`] implementations.
197///
198/// This trait is automatically implemented for all `Stop + Sized` types.
199///
200/// # Example
201///
202/// ```rust
203/// use almost_enough::{StopSource, Stop, StopExt};
204///
205/// let source_a = StopSource::new();
206/// let source_b = StopSource::new();
207///
208/// // Combine with .or()
209/// let combined = source_a.as_ref().or(source_b.as_ref());
210///
211/// assert!(!combined.should_stop());
212///
213/// source_b.cancel();
214/// assert!(combined.should_stop());
215/// ```
216pub trait StopExt: Stop + Sized {
217    /// Combine this stop with another, stopping if either stops.
218    ///
219    /// This is equivalent to `OrStop::new(self, other)` but with a more
220    /// ergonomic method syntax that allows chaining.
221    ///
222    /// # Example
223    ///
224    /// ```rust
225    /// use almost_enough::{StopSource, Stop, StopExt};
226    ///
227    /// let timeout = StopSource::new();
228    /// let cancel = StopSource::new();
229    ///
230    /// let combined = timeout.as_ref().or(cancel.as_ref());
231    /// assert!(!combined.should_stop());
232    ///
233    /// cancel.cancel();
234    /// assert!(combined.should_stop());
235    /// ```
236    ///
237    /// # Chaining
238    ///
239    /// Multiple sources can be chained:
240    ///
241    /// ```rust
242    /// use almost_enough::{StopSource, Stop, StopExt};
243    ///
244    /// let a = StopSource::new();
245    /// let b = StopSource::new();
246    /// let c = StopSource::new();
247    ///
248    /// let combined = a.as_ref().or(b.as_ref()).or(c.as_ref());
249    ///
250    /// c.cancel();
251    /// assert!(combined.should_stop());
252    /// ```
253    #[inline]
254    fn or<S: Stop>(self, other: S) -> OrStop<Self, S> {
255        OrStop::new(self, other)
256    }
257
258    /// Convert this stop into a boxed trait object.
259    ///
260    /// This is useful for preventing monomorphization at API boundaries.
261    /// Instead of generating a new function for each `impl Stop` type,
262    /// you can erase the type to `BoxedStop` and have a single implementation.
263    ///
264    /// # Example
265    ///
266    /// ```rust
267    /// # #[cfg(feature = "alloc")]
268    /// # fn main() {
269    /// use almost_enough::{Stopper, BoxedStop, Stop, StopExt};
270    ///
271    /// // This function is monomorphized for each Stop type
272    /// fn process_generic(stop: impl Stop + 'static) {
273    ///     // Erase type at boundary
274    ///     process_concrete(stop.into_boxed());
275    /// }
276    ///
277    /// // This function has only one implementation
278    /// fn process_concrete(stop: BoxedStop) {
279    ///     while !stop.should_stop() {
280    ///         break;
281    ///     }
282    /// }
283    ///
284    /// let stop = Stopper::new();
285    /// process_generic(stop);
286    /// # }
287    /// # #[cfg(not(feature = "alloc"))]
288    /// # fn main() {}
289    /// ```
290    #[cfg(feature = "alloc")]
291    #[inline]
292    fn into_boxed(self) -> BoxedStop
293    where
294        Self: 'static,
295    {
296        BoxedStop::new(self)
297    }
298
299    /// Create a child stop that inherits cancellation from this stop.
300    ///
301    /// The returned [`ChildStopper`] will stop if:
302    /// - Its own `cancel()` is called
303    /// - This parent stop is cancelled
304    ///
305    /// Cancelling the child does NOT affect the parent.
306    ///
307    /// # Example
308    ///
309    /// ```rust
310    /// # #[cfg(feature = "alloc")]
311    /// # fn main() {
312    /// use almost_enough::{Stopper, Stop, StopExt};
313    ///
314    /// let parent = Stopper::new();
315    /// let child = parent.child();
316    ///
317    /// // Child cancellation is independent
318    /// child.cancel();
319    /// assert!(!parent.should_stop());
320    /// assert!(child.should_stop());
321    ///
322    /// // Parent cancellation propagates
323    /// let child2 = parent.child();
324    /// parent.cancel();
325    /// assert!(child2.should_stop());
326    /// # }
327    /// # #[cfg(not(feature = "alloc"))]
328    /// # fn main() {}
329    /// ```
330    #[cfg(feature = "alloc")]
331    #[inline]
332    fn child(&self) -> ChildStopper
333    where
334        Self: Clone + 'static,
335    {
336        ChildStopper::with_parent(self.clone())
337    }
338}
339
340// Blanket implementation for all Stop + Sized types
341impl<T: Stop + Sized> StopExt for T {}
342
343#[cfg(test)]
344mod tests {
345    use super::*;
346
347    #[test]
348    fn or_extension_works() {
349        let a = StopSource::new();
350        let b = StopSource::new();
351        let combined = a.as_ref().or(b.as_ref());
352
353        assert!(!combined.should_stop());
354
355        a.cancel();
356        assert!(combined.should_stop());
357    }
358
359    #[test]
360    fn or_chain_works() {
361        let a = StopSource::new();
362        let b = StopSource::new();
363        let c = StopSource::new();
364
365        let combined = a.as_ref().or(b.as_ref()).or(c.as_ref());
366
367        assert!(!combined.should_stop());
368
369        c.cancel();
370        assert!(combined.should_stop());
371    }
372
373    #[test]
374    fn or_with_never() {
375        let source = StopSource::new();
376        let combined = Never.or(source.as_ref());
377
378        assert!(!combined.should_stop());
379
380        source.cancel();
381        assert!(combined.should_stop());
382    }
383
384    #[test]
385    fn reexports_work() {
386        // Verify that re-exports from enough work
387        let _: StopReason = StopReason::Cancelled;
388        let _ = Never;
389        let source = StopSource::new();
390        let _ = source.as_ref();
391    }
392
393    #[cfg(feature = "alloc")]
394    #[test]
395    fn alloc_reexports_work() {
396        let stop = Stopper::new();
397        let _ = stop.clone();
398        let _ = BoxedStop::new(Never);
399    }
400
401    #[cfg(feature = "alloc")]
402    #[test]
403    fn into_boxed_works() {
404        let stop = Stopper::new();
405        let boxed: BoxedStop = stop.clone().into_boxed();
406
407        assert!(!boxed.should_stop());
408
409        stop.cancel();
410        assert!(boxed.should_stop());
411    }
412
413    #[cfg(feature = "alloc")]
414    #[test]
415    fn into_boxed_with_never() {
416        let boxed: BoxedStop = Never.into_boxed();
417        assert!(!boxed.should_stop());
418    }
419
420    #[cfg(feature = "alloc")]
421    #[test]
422    fn into_boxed_prevents_monomorphization() {
423        // This test verifies the pattern compiles correctly
424        fn outer(stop: impl Stop + 'static) {
425            inner(stop.into_boxed());
426        }
427
428        fn inner(stop: BoxedStop) {
429            let _ = stop.should_stop();
430        }
431
432        let stop = Stopper::new();
433        outer(stop);
434        outer(Never);
435    }
436
437    #[cfg(feature = "alloc")]
438    #[test]
439    fn child_extension_works() {
440        let parent = Stopper::new();
441        let child = parent.child();
442
443        assert!(!child.should_stop());
444
445        parent.cancel();
446        assert!(child.should_stop());
447    }
448
449    #[cfg(feature = "alloc")]
450    #[test]
451    fn child_independent_cancel() {
452        let parent = Stopper::new();
453        let child = parent.child();
454
455        child.cancel();
456
457        assert!(child.should_stop());
458        assert!(!parent.should_stop());
459    }
460
461    #[cfg(feature = "alloc")]
462    #[test]
463    fn child_chain() {
464        let grandparent = Stopper::new();
465        let parent = grandparent.child();
466        let child = parent.child();
467
468        grandparent.cancel();
469
470        assert!(parent.should_stop());
471        assert!(child.should_stop());
472    }
473}