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}