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 + '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}