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}