Skip to main content

enough/
lib.rs

1//! # enough
2//!
3//! Minimal cooperative cancellation trait. Zero dependencies, `no_std` compatible.
4//!
5//! ## Which Crate?
6//!
7//! - **Library authors**: Use this crate (`enough`) - minimal, zero deps
8//! - **Application code**: Use [`almost-enough`](https://docs.rs/almost-enough) for concrete types
9//!
10//! ## For Library Authors
11//!
12//! Accept `impl Stop` as the last parameter. Re-export `Unstoppable` for callers who don't need cancellation:
13//!
14//! ```rust
15//! use enough::{Stop, StopReason};
16//!
17//! pub fn decode(data: &[u8], stop: impl Stop) -> Result<Vec<u8>, DecodeError> {
18//!     let mut output = Vec::new();
19//!     for (i, chunk) in data.chunks(1024).enumerate() {
20//!         // Check periodically in hot loops
21//!         if i % 16 == 0 {
22//!             stop.check()?;
23//!         }
24//!         // process chunk...
25//!         output.extend_from_slice(chunk);
26//!     }
27//!     Ok(output)
28//! }
29//!
30//! #[derive(Debug)]
31//! pub enum DecodeError {
32//!     Stopped(StopReason),
33//!     InvalidData,
34//! }
35//!
36//! impl From<StopReason> for DecodeError {
37//!     fn from(r: StopReason) -> Self { DecodeError::Stopped(r) }
38//! }
39//! ```
40//!
41//! ## Zero-Cost When Not Needed
42//!
43//! Use [`Unstoppable`] when you don't need cancellation:
44//!
45//! ```rust
46//! use enough::Unstoppable;
47//!
48//! // Compiles to nothing - zero runtime cost
49//! // let result = my_codec::decode(&data, Unstoppable);
50//! ```
51//!
52//! ## Implementations
53//!
54//! This crate provides only the trait and a zero-cost `Unstoppable` implementation.
55//! For concrete cancellation primitives (`Stopper`, `StopSource`, timeouts, etc.),
56//! see the [`almost-enough`](https://docs.rs/almost-enough) crate.
57//!
58//! ## Feature Flags
59//!
60//! - **None (default)** - Core trait only, `no_std` compatible
61//! - **`std`** - Implies `alloc` (kept for downstream compatibility)
62
63#![cfg_attr(not(feature = "std"), no_std)]
64#![warn(missing_docs)]
65#![warn(clippy::all)]
66
67#[cfg(feature = "alloc")]
68extern crate alloc;
69
70mod reason;
71
72pub use reason::StopReason;
73
74/// Cooperative cancellation check.
75///
76/// Implement this trait for custom cancellation sources. The implementation
77/// must be thread-safe (`Send + Sync`) to support parallel processing and
78/// async runtimes.
79///
80/// # Example Implementation
81///
82/// ```rust
83/// use enough::{Stop, StopReason};
84/// use core::sync::atomic::{AtomicBool, Ordering};
85///
86/// pub struct MyStop<'a> {
87///     cancelled: &'a AtomicBool,
88/// }
89///
90/// impl Stop for MyStop<'_> {
91///     fn check(&self) -> Result<(), StopReason> {
92///         if self.cancelled.load(Ordering::Relaxed) {
93///             Err(StopReason::Cancelled)
94///         } else {
95///             Ok(())
96///         }
97///     }
98/// }
99/// ```
100pub trait Stop: Send + Sync {
101    /// Check if the operation should stop.
102    ///
103    /// Returns `Ok(())` to continue, `Err(StopReason)` to stop.
104    ///
105    /// Call this periodically in long-running loops. The frequency depends
106    /// on your workload - typically every 16-1000 iterations is reasonable.
107    fn check(&self) -> Result<(), StopReason>;
108
109    /// Returns `true` if the operation should stop.
110    ///
111    /// Convenience method for when you want to handle stopping yourself
112    /// rather than using the `?` operator.
113    #[inline]
114    fn should_stop(&self) -> bool {
115        self.check().is_err()
116    }
117}
118
119/// A [`Stop`] implementation that never stops (no cooperative cancellation).
120///
121/// This is a zero-cost type for callers who don't need cancellation support.
122/// All methods are inlined and optimized away.
123///
124/// The name `Unstoppable` clearly communicates that this operation cannot be
125/// cooperatively cancelled - there is no cancellation token to check.
126///
127/// # Example
128///
129/// ```rust
130/// use enough::{Stop, Unstoppable};
131///
132/// fn process(data: &[u8], stop: impl Stop) -> Vec<u8> {
133///     // ...
134///     # vec![]
135/// }
136///
137/// // Caller doesn't need cancellation
138/// let data = [1u8, 2, 3];
139/// let result = process(&data, Unstoppable);
140/// ```
141#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
142pub struct Unstoppable;
143
144/// Type alias for backwards compatibility.
145///
146/// New code should use [`Unstoppable`] instead, which more clearly
147/// communicates that cooperative cancellation is not possible.
148#[deprecated(since = "0.3.0", note = "Use `Unstoppable` instead for clarity")]
149pub type Never = Unstoppable;
150
151impl Stop for Unstoppable {
152    #[inline(always)]
153    fn check(&self) -> Result<(), StopReason> {
154        Ok(())
155    }
156
157    #[inline(always)]
158    fn should_stop(&self) -> bool {
159        false
160    }
161}
162
163// Blanket impl: &T where T: Stop
164impl<T: Stop + ?Sized> Stop for &T {
165    #[inline]
166    fn check(&self) -> Result<(), StopReason> {
167        (**self).check()
168    }
169
170    #[inline]
171    fn should_stop(&self) -> bool {
172        (**self).should_stop()
173    }
174}
175
176// Blanket impl: &mut T where T: Stop
177impl<T: Stop + ?Sized> Stop for &mut T {
178    #[inline]
179    fn check(&self) -> Result<(), StopReason> {
180        (**self).check()
181    }
182
183    #[inline]
184    fn should_stop(&self) -> bool {
185        (**self).should_stop()
186    }
187}
188
189#[cfg(feature = "alloc")]
190impl<T: Stop + ?Sized> Stop for alloc::boxed::Box<T> {
191    #[inline]
192    fn check(&self) -> Result<(), StopReason> {
193        (**self).check()
194    }
195
196    #[inline]
197    fn should_stop(&self) -> bool {
198        (**self).should_stop()
199    }
200}
201
202#[cfg(feature = "alloc")]
203impl<T: Stop + ?Sized> Stop for alloc::sync::Arc<T> {
204    #[inline]
205    fn check(&self) -> Result<(), StopReason> {
206        (**self).check()
207    }
208
209    #[inline]
210    fn should_stop(&self) -> bool {
211        (**self).should_stop()
212    }
213}
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218
219    #[test]
220    fn unstoppable_does_not_stop() {
221        assert!(!Unstoppable.should_stop());
222        assert!(Unstoppable.check().is_ok());
223    }
224
225    #[test]
226    fn unstoppable_is_copy() {
227        let a = Unstoppable;
228        let b = a; // Copy
229        let _ = a; // Still valid
230        let _ = b;
231    }
232
233    #[test]
234    fn unstoppable_is_default() {
235        let _: Unstoppable = Default::default();
236    }
237
238    #[test]
239    fn reference_impl_works() {
240        let unstoppable = Unstoppable;
241        let reference: &dyn Stop = &unstoppable;
242        assert!(!reference.should_stop());
243    }
244
245    #[test]
246    #[allow(deprecated)]
247    fn never_alias_works() {
248        // Backwards compatibility
249        let stop: Never = Unstoppable;
250        assert!(!stop.should_stop());
251    }
252
253    #[test]
254    fn stop_reason_from_impl() {
255        // Test that From<StopReason> pattern works
256        #[derive(Debug, PartialEq)]
257        #[allow(dead_code)]
258        enum TestError {
259            Stopped(StopReason),
260            Other,
261        }
262
263        impl From<StopReason> for TestError {
264            fn from(r: StopReason) -> Self {
265                TestError::Stopped(r)
266            }
267        }
268
269        fn might_stop(stop: impl Stop) -> Result<(), TestError> {
270            stop.check()?;
271            Ok(())
272        }
273
274        assert!(might_stop(Unstoppable).is_ok());
275    }
276
277    #[test]
278    fn dyn_stop_works() {
279        fn process(stop: &dyn Stop) -> bool {
280            stop.should_stop()
281        }
282
283        let unstoppable = Unstoppable;
284        assert!(!process(&unstoppable));
285    }
286}