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}