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 + 'static` in your public API. Internally,
13//! use [`StopToken`](https://docs.rs/almost-enough/latest/almost_enough/struct.StopToken.html)
14//! from `almost-enough` — it handles the `Unstoppable` optimization
15//! automatically and is the fastest option for real stop types:
16//!
17//! ```rust
18//! use enough::{Stop, StopReason};
19//!
20//! pub fn decode(data: &[u8], stop: impl Stop + 'static) -> Result<Vec<u8>, DecodeError> {
21//! // Internally: StopToken::new(stop) erases the type and optimizes
22//! // Unstoppable to a no-op. See almost-enough docs for details.
23//! let mut output = Vec::new();
24//! for (i, chunk) in data.chunks(1024).enumerate() {
25//! if i % 16 == 0 {
26//! stop.check()?;
27//! }
28//! output.extend_from_slice(chunk);
29//! }
30//! Ok(output)
31//! }
32//!
33//! #[derive(Debug)]
34//! pub enum DecodeError {
35//! Stopped(StopReason),
36//! InvalidData,
37//! }
38//!
39//! impl From<StopReason> for DecodeError {
40//! fn from(r: StopReason) -> Self { DecodeError::Stopped(r) }
41//! }
42//! ```
43//!
44//! ## Zero-Cost When Not Needed
45//!
46//! Use [`Unstoppable`] when you don't need cancellation:
47//!
48//! ```rust
49//! use enough::Unstoppable;
50//!
51//! // Compiles to nothing - zero runtime cost
52//! // let result = my_codec::decode(&data, Unstoppable);
53//! ```
54//!
55//! ## Implementations
56//!
57//! This crate provides only the trait and a zero-cost `Unstoppable` implementation.
58//! For concrete cancellation primitives (`Stopper`, `StopSource`, timeouts, etc.),
59//! see the [`almost-enough`](https://docs.rs/almost-enough) crate.
60//!
61//! ## Feature Flags
62//!
63//! - **None (default)** - Core trait only, `no_std` compatible
64//! - **`std`** - Implies `alloc` (kept for downstream compatibility)
65
66#![cfg_attr(not(feature = "std"), no_std)]
67#![forbid(unsafe_code)]
68#![warn(missing_docs)]
69#![warn(clippy::all)]
70
71#[cfg(feature = "alloc")]
72extern crate alloc;
73
74mod reason;
75
76pub use reason::StopReason;
77
78/// Cooperative cancellation check.
79///
80/// Implement this trait for custom cancellation sources. The implementation
81/// must be thread-safe (`Send + Sync`) to support parallel processing and
82/// async runtimes.
83///
84/// # Example Implementation
85///
86/// ```rust
87/// use enough::{Stop, StopReason};
88/// use core::sync::atomic::{AtomicBool, Ordering};
89///
90/// pub struct MyStop<'a> {
91/// cancelled: &'a AtomicBool,
92/// }
93///
94/// impl Stop for MyStop<'_> {
95/// fn check(&self) -> Result<(), StopReason> {
96/// if self.cancelled.load(Ordering::Relaxed) {
97/// Err(StopReason::Cancelled)
98/// } else {
99/// Ok(())
100/// }
101/// }
102/// }
103/// ```
104pub trait Stop: Send + Sync {
105 /// Check if the operation should stop.
106 ///
107 /// Returns `Ok(())` to continue, `Err(StopReason)` to stop.
108 ///
109 /// Call this periodically in long-running loops. The frequency depends
110 /// on your workload - typically every 16-1000 iterations is reasonable.
111 fn check(&self) -> Result<(), StopReason>;
112
113 /// Returns `true` if the operation should stop.
114 ///
115 /// Convenience method for when you want to handle stopping yourself
116 /// rather than using the `?` operator.
117 #[inline]
118 fn should_stop(&self) -> bool {
119 self.check().is_err()
120 }
121
122 /// Returns `true` if this stop can ever signal a stop.
123 ///
124 /// [`Unstoppable`] returns `false`. Wrapper types delegate to their
125 /// inner stop. The default is `true` (conservative — always check).
126 ///
127 /// Use this with `impl Stop for Option<T>` to skip checks in hot loops
128 /// behind `dyn Stop`:
129 ///
130 /// ```rust
131 /// use enough::{Stop, StopReason, Unstoppable};
132 ///
133 /// fn process(stop: &dyn Stop) -> Result<(), StopReason> {
134 /// let stop = stop.may_stop().then_some(stop);
135 /// // stop is Option<&dyn Stop>, which impl Stop:
136 /// // None → check() always returns Ok(()), Some → delegates
137 /// for i in 0..100 {
138 /// stop.check()?;
139 /// }
140 /// Ok(())
141 /// }
142 ///
143 /// // Unstoppable: may_stop() returns false, so stop is None
144 /// assert!(process(&Unstoppable).is_ok());
145 /// ```
146 ///
147 /// In generic code (`impl Stop`), this is unnecessary — the compiler
148 /// already optimizes `Unstoppable::check()` to nothing via inlining.
149 /// Use `may_stop()` only when accepting `&dyn Stop`.
150 #[inline]
151 fn may_stop(&self) -> bool {
152 true
153 }
154}
155
156/// A [`Stop`] implementation that never stops (no cooperative cancellation).
157///
158/// This is a zero-cost type for callers who don't need cancellation support.
159/// All methods are inlined and optimized away.
160///
161/// The name `Unstoppable` clearly communicates that this operation cannot be
162/// cooperatively cancelled - there is no cancellation token to check.
163///
164/// # Example
165///
166/// ```rust
167/// use enough::{Stop, Unstoppable};
168///
169/// fn process(data: &[u8], stop: impl Stop) -> Vec<u8> {
170/// // ...
171/// # vec![]
172/// }
173///
174/// // Caller doesn't need cancellation
175/// let data = [1u8, 2, 3];
176/// let result = process(&data, Unstoppable);
177/// ```
178#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
179pub struct Unstoppable;
180
181/// Type alias for backwards compatibility.
182///
183/// New code should use [`Unstoppable`] instead, which more clearly
184/// communicates that cooperative cancellation is not possible.
185#[deprecated(since = "0.3.0", note = "Use `Unstoppable` instead for clarity")]
186pub type Never = Unstoppable;
187
188impl Stop for Unstoppable {
189 #[inline(always)]
190 fn check(&self) -> Result<(), StopReason> {
191 Ok(())
192 }
193
194 #[inline(always)]
195 fn should_stop(&self) -> bool {
196 false
197 }
198
199 #[inline(always)]
200 fn may_stop(&self) -> bool {
201 false
202 }
203}
204
205// Blanket impl: &T where T: Stop
206impl<T: Stop + ?Sized> Stop for &T {
207 #[inline]
208 fn check(&self) -> Result<(), StopReason> {
209 (**self).check()
210 }
211
212 #[inline]
213 fn should_stop(&self) -> bool {
214 (**self).should_stop()
215 }
216
217 #[inline]
218 fn may_stop(&self) -> bool {
219 (**self).may_stop()
220 }
221}
222
223// Blanket impl: &mut T where T: Stop
224impl<T: Stop + ?Sized> Stop for &mut T {
225 #[inline]
226 fn check(&self) -> Result<(), StopReason> {
227 (**self).check()
228 }
229
230 #[inline]
231 fn should_stop(&self) -> bool {
232 (**self).should_stop()
233 }
234
235 #[inline]
236 fn may_stop(&self) -> bool {
237 (**self).may_stop()
238 }
239}
240
241#[cfg(feature = "alloc")]
242impl<T: Stop + ?Sized> Stop for alloc::boxed::Box<T> {
243 #[inline]
244 fn check(&self) -> Result<(), StopReason> {
245 (**self).check()
246 }
247
248 #[inline]
249 fn should_stop(&self) -> bool {
250 (**self).should_stop()
251 }
252
253 #[inline]
254 fn may_stop(&self) -> bool {
255 (**self).may_stop()
256 }
257}
258
259#[cfg(feature = "alloc")]
260impl<T: Stop + ?Sized> Stop for alloc::sync::Arc<T> {
261 #[inline]
262 fn check(&self) -> Result<(), StopReason> {
263 (**self).check()
264 }
265
266 #[inline]
267 fn should_stop(&self) -> bool {
268 (**self).should_stop()
269 }
270
271 #[inline]
272 fn may_stop(&self) -> bool {
273 (**self).may_stop()
274 }
275}
276
277/// `Option<T>` implements `Stop`: `None` is a no-op (always `Ok(())`),
278/// `Some(inner)` delegates to the inner stop.
279///
280/// This enables the [`may_stop()`](Stop::may_stop) pattern for hot loops:
281///
282/// ```rust
283/// use enough::{Stop, StopReason, Unstoppable};
284///
285/// fn hot_loop(stop: &dyn Stop) -> Result<(), StopReason> {
286/// let stop = stop.may_stop().then_some(stop);
287/// for i in 0..1000 {
288/// stop.check()?; // None → Ok(()), Some → delegates
289/// }
290/// Ok(())
291/// }
292///
293/// assert!(hot_loop(&Unstoppable).is_ok());
294/// ```
295impl<T: Stop> Stop for Option<T> {
296 #[inline]
297 fn check(&self) -> Result<(), StopReason> {
298 match self {
299 Some(s) => s.check(),
300 None => Ok(()),
301 }
302 }
303
304 #[inline]
305 fn should_stop(&self) -> bool {
306 match self {
307 Some(s) => s.should_stop(),
308 None => false,
309 }
310 }
311
312 #[inline]
313 fn may_stop(&self) -> bool {
314 match self {
315 Some(s) => s.may_stop(),
316 None => false,
317 }
318 }
319}
320
321#[cfg(test)]
322mod tests {
323 use super::*;
324
325 #[test]
326 fn unstoppable_does_not_stop() {
327 assert!(!Unstoppable.should_stop());
328 assert!(Unstoppable.check().is_ok());
329 }
330
331 #[test]
332 fn unstoppable_is_copy() {
333 let a = Unstoppable;
334 let b = a; // Copy
335 let _ = a; // Still valid
336 let _ = b;
337 }
338
339 #[test]
340 fn unstoppable_is_default() {
341 let _: Unstoppable = Default::default();
342 }
343
344 #[test]
345 fn reference_impl_works() {
346 let unstoppable = Unstoppable;
347 let reference: &dyn Stop = &unstoppable;
348 assert!(!reference.should_stop());
349 }
350
351 #[test]
352 #[allow(deprecated)]
353 fn never_alias_works() {
354 // Backwards compatibility
355 let stop: Never = Unstoppable;
356 assert!(!stop.should_stop());
357 }
358
359 #[test]
360 fn stop_reason_from_impl() {
361 // Test that From<StopReason> pattern works
362 #[derive(Debug, PartialEq)]
363 #[allow(dead_code)]
364 enum TestError {
365 Stopped(StopReason),
366 Other,
367 }
368
369 impl From<StopReason> for TestError {
370 fn from(r: StopReason) -> Self {
371 TestError::Stopped(r)
372 }
373 }
374
375 fn might_stop(stop: impl Stop) -> Result<(), TestError> {
376 stop.check()?;
377 Ok(())
378 }
379
380 assert!(might_stop(Unstoppable).is_ok());
381 }
382
383 #[test]
384 fn dyn_stop_works() {
385 fn process(stop: &dyn Stop) -> bool {
386 stop.should_stop()
387 }
388
389 let unstoppable = Unstoppable;
390 assert!(!process(&unstoppable));
391 }
392
393 #[test]
394 fn unstoppable_may_not_stop() {
395 assert!(!Unstoppable.may_stop());
396 }
397
398 #[test]
399 fn dyn_unstoppable_may_not_stop() {
400 let stop: &dyn Stop = &Unstoppable;
401 assert!(!stop.may_stop());
402 }
403
404 #[test]
405 fn may_stop_via_reference() {
406 let stop = &Unstoppable;
407 assert!(!stop.may_stop());
408 }
409
410 #[test]
411 fn option_none_is_noop() {
412 let stop: Option<&dyn Stop> = None;
413 assert!(stop.check().is_ok());
414 assert!(!stop.should_stop());
415 assert!(!stop.may_stop());
416 }
417
418 #[test]
419 fn option_some_delegates() {
420 use core::sync::atomic::{AtomicBool, Ordering};
421
422 struct TestStop(AtomicBool);
423 impl Stop for TestStop {
424 fn check(&self) -> Result<(), StopReason> {
425 if self.0.load(Ordering::Relaxed) {
426 Err(StopReason::Cancelled)
427 } else {
428 Ok(())
429 }
430 }
431 }
432
433 let inner = TestStop(AtomicBool::new(false));
434 let stop: Option<&dyn Stop> = Some(&inner);
435 assert!(stop.check().is_ok());
436 assert!(!stop.should_stop());
437 assert!(stop.may_stop());
438
439 inner.0.store(true, Ordering::Relaxed);
440 assert!(stop.should_stop());
441 assert_eq!(stop.check(), Err(StopReason::Cancelled));
442 }
443
444 #[test]
445 fn may_stop_hot_loop_pattern() {
446 fn process(stop: &dyn Stop) -> Result<(), StopReason> {
447 let stop = stop.may_stop().then_some(stop);
448 for _ in 0..100 {
449 stop.check()?;
450 }
451 Ok(())
452 }
453
454 assert!(process(&Unstoppable).is_ok());
455 }
456}