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}