Skip to main content

errlanes/
result_ext.rs

1//! `ResultExt` — the one trait a consumer imports to move a `Result` from one
2//! error signature to another. `widen`, the three `narrow_*`, `rejected`,
3//! `classify`, and (under `tracing`) `record` are all the same act —
4//! relocating a `Result`'s error — so they live on one blanket-impl'd trait
5//! rather than one import per verb.
6
7use crate::{
8    classify::Classify,
9    fail::{Fail, Failure, Fault, Laned, Rejection, WidenResult},
10    lane::{Denied, Fatal},
11    profile::{LaneProfile, NarrowDenied, WithoutDenied},
12};
13
14/// Sealed. The error-level engine for [`ResultExt::narrow_rejected`] — keyed
15/// on the source shape the same way [`WidenResult`] is: a `Fail` narrows to
16/// the `Fault` of its own profile, a bare `Rejection` to the bare `Fatal`.
17/// Both outputs are associated types, read off the source, so
18/// `.narrow_rejected()?` never needs a destination named at the call site.
19#[doc(hidden)]
20#[diagnostic::on_unimplemented(
21    message = "`{Self}` has no rejected lane to narrow",
22    note = "`narrow_rejected` is for a `Fail<D, L>`, or a bare `Rejection`, with no \
23            caller left to correct the rejection; a `Fault<L>` has no rejected lane at all"
24)]
25pub trait NarrowRejectedLane: narrow_sealed::Sealed {
26    type Narrowed;
27    fn narrow_rejected(self) -> Self::Narrowed;
28}
29
30/// Its own seal rather than `fail::sealed`: that one is blanket-implemented
31/// for every `Failure`, and a downstream type may be both a `Failure` carrier
32/// and a `Rejection`, so a second blanket over `R: Rejection` would overlap
33/// there. Here the two impls are `Fail<D, L>` and `R: Rejection`, which
34/// coherence can tell apart — `Fail` is local and never a `Rejection`.
35mod narrow_sealed {
36    use crate::{
37        fail::{Fail, Rejection},
38        profile::LaneProfile,
39    };
40    pub trait Sealed {}
41    impl<D: Rejection, L: LaneProfile> Sealed for Fail<D, L> {}
42    impl<R: Rejection> Sealed for R {}
43}
44
45impl<D: Rejection, L: LaneProfile<Fatal = Fatal>> NarrowRejectedLane for Fail<D, L> {
46    type Narrowed = Fault<L>;
47    fn narrow_rejected(self) -> Fault<L> {
48        Fail::narrow_rejected(self)
49    }
50}
51
52/// A bare rejection has no profile to keep, so narrowing it yields the bare
53/// payload: `Fatal(Invariant)` with the rejection as its (opaque) source. `?`
54/// then carries that into any `Fault<L>` or `Fail<D, L>` whose `Fatal` lane
55/// is enabled — the destination profile is read off the function signature,
56/// never named at the call site.
57impl<R: Rejection> NarrowRejectedLane for R {
58    type Narrowed = Fatal;
59    fn narrow_rejected(self) -> Fatal {
60        crate::fail::invariant_from_rejection(self)
61    }
62}
63
64/// Sealed. The error-level engine for [`ResultExt::narrow_denied`], shared by
65/// both carriers — `Fail` and `Fault` each still have a denied lane to
66/// narrow.
67#[doc(hidden)]
68#[diagnostic::on_unimplemented(
69    message = "`{Self}` has no denied lane to narrow",
70    note = "`narrow_denied` is only available on a profile whose `Denied` lane is enabled"
71)]
72pub trait NarrowDeniedLane: crate::fail::sealed::Sealed {
73    type Narrowed;
74    fn narrow_denied(self) -> Self::Narrowed;
75}
76
77// `L: LaneProfile<Denied = Denied>` (the concrete marker, not the generic
78// `Slot<Denied>`) is load-bearing: `NarrowDenied<F>` also has a blanket impl
79// for `Infallible` (a disabled slot), so bounding on `NarrowDenied` alone
80// would make this engine — and so `ResultExt::narrow_denied` — callable even
81// when the profile has no `Denied` lane at all, turning narrowing into a
82// silent identity on an already-absent lane instead of the compile error
83// decision 2 requires. The inherent `Fault::narrow_denied`/`Fail::narrow_denied`
84// keep the wider bound; only this engine is deliberately narrower.
85impl<D: Rejection, L> NarrowDeniedLane for Fail<D, L>
86where
87    L: LaneProfile<Denied = Denied>,
88    L::Denied: NarrowDenied<L::Fatal>,
89{
90    type Narrowed = Fail<D, WithoutDenied<L>>;
91    fn narrow_denied(self) -> Self::Narrowed {
92        Fail::narrow_denied(self)
93    }
94}
95
96impl<L> NarrowDeniedLane for Fault<L>
97where
98    L: LaneProfile<Denied = Denied>,
99    L::Denied: NarrowDenied<L::Fatal>,
100{
101    type Narrowed = Fault<WithoutDenied<L>>;
102    fn narrow_denied(self) -> Self::Narrowed {
103        Fault::narrow_denied(self)
104    }
105}
106
107/// The one trait a consumer imports to move a `Result` between error
108/// signatures: widen it to a bigger profile, narrow away a lane that is no
109/// longer live at this boundary, hand the rejection to the caller as a value,
110/// classify a foreign error into a local wrapper, or (under `tracing`) record
111/// it onto the current span.
112pub trait ResultExt<T, E>: Sized {
113    /// Target-inferred widening for `Fault` or `Fail` results.
114    ///
115    /// `Fault` results widen to `Fault`; `Fail` results widen to `Fail`,
116    /// converting the rejection through [`crate::Lift`]. Success values and
117    /// fault payloads are preserved. Widening can add lanes, but cannot
118    /// silently discard an enabled lane:
119    ///
120    /// ```compile_fail
121    /// use errlanes::{Fault, ResultExt, lanes};
122    /// fn discard_denied(value: Result<(), Fault<lanes!(Denied, Fatal)>>)
123    ///     -> Result<(), Fault<lanes!(Fatal)>>
124    /// {
125    ///     value.widen()
126    /// }
127    /// ```
128    fn widen<E2>(self) -> Result<T, E2>
129    where
130        Self: WidenResult<T, E2>;
131
132    /// Narrows away the `Rejected` lane: a rejection with no caller left to
133    /// correct it becomes `Fatal(Invariant)`, carrying the rejection as its
134    /// source. On a `Fail<D, L>` the result is `Fault<L>`. On a *bare*
135    /// `Rejection` — a public method that returns just `R` because its
136    /// caller can act on it, consumed by an internal frame that already
137    /// proved the precondition — the result is the bare `Fatal`, and `?`
138    /// carries it into whatever `Fault`/`Fail` the function returns, so the
139    /// destination profile is never named at the call site:
140    ///
141    /// ```
142    /// use errlanes::{Fault, ResultExt, lanes};
143    ///
144    /// #[derive(Debug, errlanes::Rejection)]
145    /// #[rejection(code = "LANE_DISABLED")]
146    /// struct LaneDisabled;
147    ///
148    /// fn listen() -> Result<(), LaneDisabled> {
149    ///     Err(LaneDisabled)
150    /// }
151    ///
152    /// // The lane was required at registration: by now, off is an invariant.
153    /// fn run() -> Result<(), Fault<lanes!(Transient, Fatal)>> {
154    ///     listen().narrow_rejected()?;
155    ///     Ok(())
156    /// }
157    ///
158    /// assert!(matches!(run(), Err(Fault::Fatal(_))));
159    /// ```
160    ///
161    /// A `Fault` has no rejected lane to narrow, and that is a compile
162    /// error, not an identity:
163    ///
164    /// ```compile_fail
165    /// use errlanes::{Fault, ResultExt, lanes};
166    /// fn narrow(value: Result<(), Fault<lanes!(Fatal)>>)
167    ///     -> Result<(), Fault<lanes!(Fatal)>>
168    /// {
169    ///     value.narrow_rejected()
170    /// }
171    /// ```
172    fn narrow_rejected(self) -> Result<T, E::Narrowed>
173    where
174        E: NarrowRejectedLane;
175
176    /// Narrows away the `Transient` lane: a caller-owned retry loop hands
177    /// back the narrowed profile once it stops retrying, so it stops
178    /// offering `Transient` to its own callers. `attempts` is what the loop
179    /// counted; an exhausted transient becomes `Fatal(Exhausted)` with the
180    /// last transient as its source.
181    fn narrow_transient(self, attempts: u32) -> Result<T, E::WithoutTransient>
182    where
183        E: Laned;
184
185    /// Narrows away the `Denied` lane: a denial at a boundary with no
186    /// subject (code running as the system) becomes `Fatal(Denied)`. Only
187    /// available where `E`'s profile enables `Denied`:
188    ///
189    /// ```compile_fail
190    /// use errlanes::{Fault, ResultExt, lanes};
191    /// fn narrow(value: Result<(), Fault<lanes!(Transient, Fatal)>>)
192    ///     -> Result<(), Fault<lanes!(Transient, Fatal)>>
193    /// {
194    ///     value.narrow_denied()
195    /// }
196    /// ```
197    fn narrow_denied(self) -> Result<T, E::Narrowed>
198    where
199        E: NarrowDeniedLane;
200
201    /// Hands the rejection to the caller as a value and keeps the faults
202    /// propagating: `Result<T, Fail<D, L>>` becomes
203    /// `Result<Result<T, D>, Fault<L>>`, the `Result`-level form of
204    /// [`Fail::rejected`]. The outer `?` carries the faults on into any
205    /// enclosing carrier; the inner `Result` is the domain outcome, with the
206    /// rejection as its `Err`, matched right where it occurred:
207    ///
208    /// ```
209    /// use errlanes::{Fail, Fault, ResultExt, lanes};
210    ///
211    /// #[derive(Debug, errlanes::Rejection)]
212    /// #[rejection(code = "TIMED_OUT")]
213    /// struct TimedOut;
214    ///
215    /// fn await_completion() -> Result<u64, Fail<TimedOut, lanes!(Transient, Fatal)>> {
216    ///     Err(Fail::Rejected(TimedOut))
217    /// }
218    ///
219    /// fn poll_once() -> Result<Option<u64>, Fault<lanes!(Transient, Fatal)>> {
220    ///     match await_completion().rejected()? {
221    ///         Ok(outcome) => Ok(Some(outcome)),
222    ///         Err(TimedOut) => Ok(None),
223    ///     }
224    /// }
225    ///
226    /// assert!(poll_once().unwrap().is_none());
227    /// ```
228    ///
229    /// This is the dual of [`narrow_rejected`](Self::narrow_rejected): that
230    /// one is for a boundary with no caller left to correct the rejection,
231    /// this one for the call site that is going to. There is deliberately no
232    /// `Option`-returning accessor on a `Result`: an `as_rejected()` that
233    /// answered `None` for both `Ok` and a fault would be the one place a
234    /// lane could be dropped without naming it. Only available where `E`
235    /// carries a rejected lane; a `Fault` has none:
236    ///
237    /// ```compile_fail
238    /// use errlanes::{Fault, ResultExt, lanes};
239    /// fn split(value: Result<(), Fault<lanes!(Fatal)>>) {
240    ///     let _ = value.rejected();
241    /// }
242    /// ```
243    fn rejected(self) -> Result<Result<T, E::Rejection>, Fault<E::Lanes>>
244    where
245        E: Failure;
246
247    /// Maps the rejection with a closure and leaves every other lane as it
248    /// is: the `Result`-level form of [`Fail::map_rejected`]. For a
249    /// type-level remap use [`widen`](Self::widen); this is for enriching a
250    /// rejection with data only the call site has, such as the input that
251    /// was attempted: `repo.create(new).await.map_rejected(|r| r.with_attempted(id))?`.
252    fn map_rejected<D2>(self, f: impl FnOnce(E::Rejection) -> D2) -> Result<T, Fail<D2, E::Lanes>>
253    where
254        E: Failure;
255
256    /// `.classify::<W>()` — the verb that turns a foreign error into a local
257    /// [`Classify`] wrapper at a one-off call site, so a function that does
258    /// not itself return `W` can still enter the lanes through it:
259    /// `conn.query(..).await.classify::<DbWrite>()?`.
260    fn classify<W: Classify + From<E>>(self) -> Result<T, W>;
261
262    /// Records onto the current span, then hands the result straight back —
263    /// for a call site that must record and keep going rather than
264    /// propagate (`?`), such as a batch dispatcher writing its own
265    /// `conclusion` after the fact.
266    #[cfg(feature = "tracing")]
267    fn record(self) -> Self
268    where
269        E: Laned;
270}
271
272impl<T, E> ResultExt<T, E> for Result<T, E> {
273    fn widen<E2>(self) -> Result<T, E2>
274    where
275        Self: WidenResult<T, E2>,
276    {
277        WidenResult::widen(self)
278    }
279
280    fn narrow_rejected(self) -> Result<T, E::Narrowed>
281    where
282        E: NarrowRejectedLane,
283    {
284        self.map_err(NarrowRejectedLane::narrow_rejected)
285    }
286
287    fn narrow_transient(self, attempts: u32) -> Result<T, E::WithoutTransient>
288    where
289        E: Laned,
290    {
291        self.map_err(|e| e.narrow_transient(attempts))
292    }
293
294    fn narrow_denied(self) -> Result<T, E::Narrowed>
295    where
296        E: NarrowDeniedLane,
297    {
298        self.map_err(NarrowDeniedLane::narrow_denied)
299    }
300
301    fn rejected(self) -> Result<Result<T, E::Rejection>, Fault<E::Lanes>>
302    where
303        E: Failure,
304    {
305        match self {
306            Ok(value) => Ok(Ok(value)),
307            Err(e) => e.into_fail().rejected().map(Err),
308        }
309    }
310
311    fn map_rejected<D2>(self, f: impl FnOnce(E::Rejection) -> D2) -> Result<T, Fail<D2, E::Lanes>>
312    where
313        E: Failure,
314    {
315        self.map_err(|e| e.into_fail().map_rejected(f))
316    }
317
318    fn classify<W: Classify + From<E>>(self) -> Result<T, W> {
319        self.map_err(W::from)
320    }
321
322    #[cfg(feature = "tracing")]
323    fn record(self) -> Self
324    where
325        E: Laned,
326    {
327        if let Err(e) = &self {
328            e.record(&tracing::Span::current());
329        }
330        self
331    }
332}