how_far/completion.rs
1//! Transfer the final work result without converting or consuming its error.
2use crate::{AsStopReason, Outcome};
3
4/// An owner that can record a result without changing the operation's result.
5/// Reporting problems are diagnostic evidence, never replacement work errors.
6pub trait Complete: Sized {
7 /// Record an explicitly chosen outcome, resolving unfinished observations.
8 /// Join workers before calling this. Started but uncompleted children are
9 /// abandoned; untouched children are skipped on success, not run on error.
10 fn complete_as(self, outcome: Outcome);
11
12 /// Classify the error with [`AsStopReason`], record completion, and return the
13 /// original result.
14 ///
15 /// Forgetting `AsStopReason` for a library's error is reported as that trait
16 /// being unimplemented, not as a type mismatch elsewhere:
17 ///
18 /// ```compile_fail,E0277
19 /// use how_far::{prelude::*, PhaseSpec, Stages, StopReason, Total};
20 ///
21 /// enum Error {
22 /// Stopped(StopReason),
23 /// }
24 /// impl From<StopReason> for Error {
25 /// fn from(reason: StopReason) -> Self {
26 /// Self::Stopped(reason)
27 /// }
28 /// }
29 /// fn encode(pulse: &dyn Pulse) -> Result<(), Error> {
30 /// Stages::new(pulse, &[PhaseSpec::new("work", 1, Total::Unknown)])
31 /// .complete_with(|stages| stages.run(|stage| Ok(stage.check()?)))
32 /// }
33 /// ```
34 fn complete<T, E>(self, result: Result<T, E>) -> Result<T, E>
35 where
36 E: AsStopReason,
37 {
38 self.complete_classified(result, |error| error.as_stop_reason().is_some())
39 }
40
41 /// Complete with an explicit classifier, for a foreign error type that
42 /// Rust's orphan rules keep from implementing [`AsStopReason`].
43 fn complete_classified<T, E>(
44 self,
45 result: Result<T, E>,
46 is_stop: impl FnOnce(&E) -> bool,
47 ) -> Result<T, E> {
48 self.complete_as(Outcome::from_result(&result, is_stop));
49 result
50 }
51
52 /// Run a multi-step `body` with this owner, then complete the owner with
53 /// the body's result and return that result unchanged.
54 ///
55 /// An early `?` inside `body` still reaches the handoff, which a `?` placed
56 /// before a separate `complete` call would bypass. A panic in `body` drops
57 /// the owner, recording abandonment as usual.
58 ///
59 /// ```
60 /// use how_far::{prelude::*, PhaseSpec, Stages, StopReason, Total};
61 ///
62 /// fn convert(pulse: &dyn Pulse) -> Result<u64, StopReason> {
63 /// Stages::new(pulse, &[
64 /// PhaseSpec::new("decode", 1, Total::Exact(1)),
65 /// PhaseSpec::new("sharpen", 1, Total::Exact(1)),
66 /// ])
67 /// .complete_with(|stages| {
68 /// let pixels = stages.run(|stage| stage.step(1).map(|()| 7))?;
69 /// // An untouched "sharpen" stage is resolved as Skipped.
70 /// Ok(pixels)
71 /// })
72 /// }
73 /// assert_eq!(convert(&how_far::NoPulse), Ok(7));
74 /// ```
75 fn complete_with<T, E>(mut self, body: impl FnOnce(&mut Self) -> Result<T, E>) -> Result<T, E>
76 where
77 E: AsStopReason,
78 {
79 let result = body(&mut self);
80 self.complete(result)
81 }
82
83 /// [`complete_with`](Self::complete_with) with an explicit classifier, for
84 /// a foreign error type that cannot implement [`AsStopReason`], such as an error
85 /// wrapped with its source location.
86 fn complete_with_classified<T, E>(
87 mut self,
88 is_stop: impl FnOnce(&E) -> bool,
89 body: impl FnOnce(&mut Self) -> Result<T, E>,
90 ) -> Result<T, E> {
91 let result = body(&mut self);
92 self.complete_classified(result, is_stop)
93 }
94}
95
96/// The explicit boundary where a normal Rust result completes its phase owner.
97pub trait ResultExt<T, E>: Sized {
98 /// Record completion and return this result unchanged. This is not invoked
99 /// by `?`, `map`, or Drop automatically.
100 fn finish_phase(self, owner: impl Complete) -> Result<T, E>
101 where
102 E: AsStopReason;
103}
104impl<T, E> ResultExt<T, E> for Result<T, E> {
105 fn finish_phase(self, owner: impl Complete) -> Result<T, E>
106 where
107 E: AsStopReason,
108 {
109 owner.complete(self)
110 }
111}