enough/
lib.rs

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