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. `lift`, `into_fault`, `into_fail`, the three
3//! `narrow_*`, `rejected`, `classify`, and (under `tracing`) `record` all
4//! relocate a `Result`'s error, so they live on one blanket-implemented trait
5//! rather than one import per verb.
6
7use std::convert::Infallible;
8
9use crate::{
10    carrier::{Carrier, IntoLanes, fail_into_fault, kind},
11    classify::Classify,
12    fail::{Fail, Fault, Laned, Lift, Rejection, UnmappedInto},
13    lane::{Denied, Fatal},
14    profile::{LaneProfile, NarrowDenied, WithoutDenied},
15};
16
17/// Sealed. The error-level engine for [`ResultExt::narrow_rejected`] — keyed
18/// on the source shape: a `Fail` narrows to
19/// the `Fault` of its own profile, a bare `Rejection` to the bare `Fatal`, a
20/// carrier through its built-in. Both outputs are associated types, read off
21/// the source, so `.narrow_rejected()?` never needs a destination named at the
22/// call site.
23///
24/// One impl over `S: IntoLanes`, handing off to [`NarrowRejectedBy`] on the
25/// concrete shape type. A separate `C: Carrier` impl would overlap the
26/// `R: Rejection` one (generic against generic).
27#[doc(hidden)]
28#[diagnostic::on_unimplemented(
29    message = "`{Self}` has no rejected lane to narrow",
30    note = "`narrow_rejected` is for a `Fail<D, L>`, or a bare `Rejection`, with no \
31            caller left to correct the rejection; a `Fault<L>` has no rejected lane at all"
32)]
33pub trait NarrowRejectedLane: narrow_sealed::Sealed {
34    type Narrowed;
35    fn narrow_rejected(self) -> Self::Narrowed;
36}
37
38mod narrow_sealed {
39    use crate::carrier::IntoLanes;
40    pub trait Sealed {}
41    impl<S: IntoLanes> Sealed for S {}
42}
43
44impl<S: IntoLanes> NarrowRejectedLane for S
45where
46    S::Shape: NarrowRejectedBy<S>,
47{
48    type Narrowed = <S::Shape as NarrowRejectedBy<S>>::Narrowed;
49    fn narrow_rejected(self) -> Self::Narrowed {
50        <S::Shape as NarrowRejectedBy<S>>::narrow(self)
51    }
52}
53
54#[doc(hidden)]
55pub trait NarrowRejectedBy<S> {
56    type Narrowed;
57    fn narrow(s: S) -> Self::Narrowed;
58}
59
60impl<D: Rejection, L: LaneProfile<Fatal = Fatal>> NarrowRejectedBy<Fail<D, L>> for kind::Fail {
61    type Narrowed = Fault<L>;
62    fn narrow(s: Fail<D, L>) -> Fault<L> {
63        Fail::narrow_rejected(s)
64    }
65}
66
67/// A bare rejection has no profile to keep, so narrowing it yields the bare
68/// payload: `Fatal(Invariant)` with the rejection as its (opaque) source. `?`
69/// then carries that into any `Fault<L>` or `Fail<D, L>` whose `Fatal` lane
70/// is enabled — the destination profile is read off the function signature,
71/// never named at the call site.
72impl<R: Rejection> NarrowRejectedBy<R> for kind::Source {
73    type Narrowed = Fatal;
74    fn narrow(s: R) -> Fatal {
75        crate::fail::invariant_from_rejection(s)
76    }
77}
78
79impl<C: Carrier> NarrowRejectedBy<C> for kind::CarrierShape
80where
81    C::Repr: NarrowRejectedLane,
82{
83    type Narrowed = <C::Repr as NarrowRejectedLane>::Narrowed;
84    fn narrow(s: C) -> Self::Narrowed {
85        s.into_repr().narrow_rejected()
86    }
87}
88
89/// Sealed. The error-level engine for [`ResultExt::narrow_denied`], shared by
90/// both carriers — `Fail` and `Fault` each still have a denied lane to
91/// narrow.
92#[doc(hidden)]
93#[diagnostic::on_unimplemented(
94    message = "`{Self}` has no denied lane to narrow",
95    note = "`narrow_denied` is only available on a profile whose `Denied` lane is enabled"
96)]
97pub trait NarrowDeniedLane: crate::fail::sealed::Sealed {
98    type Narrowed;
99    fn narrow_denied(self) -> Self::Narrowed;
100}
101
102// `L: LaneProfile<Denied = Denied>` (the concrete marker, not the generic
103// `Slot<Denied>`) is load-bearing: `NarrowDenied<F>` also has a blanket impl
104// for `Infallible` (a disabled slot), so bounding on `NarrowDenied` alone
105// would make this engine — and so `ResultExt::narrow_denied` — callable even
106// when the profile has no `Denied` lane at all, turning narrowing into a
107// silent identity on an already-absent lane instead of the compile error
108// decision 2 requires. The inherent `Fault::narrow_denied`/`Fail::narrow_denied`
109// keep the wider bound; only this engine is deliberately narrower.
110impl<D: Rejection, L> NarrowDeniedLane for Fail<D, L>
111where
112    L: LaneProfile<Denied = Denied>,
113    L::Denied: NarrowDenied<L::Fatal>,
114{
115    type Narrowed = Fail<D, WithoutDenied<L>>;
116    fn narrow_denied(self) -> Self::Narrowed {
117        Fail::narrow_denied(self)
118    }
119}
120
121impl<L> NarrowDeniedLane for Fault<L>
122where
123    L: LaneProfile<Denied = Denied>,
124    L::Denied: NarrowDenied<L::Fatal>,
125{
126    type Narrowed = Fault<WithoutDenied<L>>;
127    fn narrow_denied(self) -> Self::Narrowed {
128        Fault::narrow_denied(self)
129    }
130}
131
132/// A carrier narrows through its built-in, so its own lane set governs
133/// availability: a carrier with no `Denied` lane has a `Repr` with a disabled
134/// `Denied` slot, and `narrow_denied` is not callable on it. The result is
135/// the narrowed *built-in*, which `?` carries on.
136impl<C: Carrier> NarrowDeniedLane for C
137where
138    C::Repr: NarrowDeniedLane,
139{
140    type Narrowed = <C::Repr as NarrowDeniedLane>::Narrowed;
141    fn narrow_denied(self) -> Self::Narrowed {
142        self.into_repr().narrow_denied()
143    }
144}
145
146/// A source that can enter its own `Fault` without losing a rejection.
147#[doc(hidden)]
148#[diagnostic::on_unimplemented(
149    message = "`{Self}` can reject and cannot enter `Fault`",
150    note = "handle the rejection, call `.narrow_rejected()` when justified, or use `.into_fail()`"
151)]
152pub trait FaultSource: IntoLanes {
153    fn own_fault(self) -> Fault<Self::Lanes>;
154}
155impl<W: IntoLanes<Rejected = Infallible>> FaultSource for W {
156    fn own_fault(self) -> Fault<Self::Lanes> {
157        fail_into_fault(self.into_lanes())
158    }
159}
160
161/// A source whose built-in has a rejected lane.
162#[doc(hidden)]
163#[diagnostic::on_unimplemented(
164    message = "`{Self}` never rejects",
165    note = "use `.into_fault()` instead"
166)]
167pub trait FailSource: IntoLanes {
168    fn own_fail(self) -> Fail<Self::Rejected, Self::Lanes>;
169}
170impl<W: IntoLanes> FailSource for W
171where
172    W::Rejected: Rejection,
173{
174    fn own_fail(self) -> Fail<Self::Rejected, Self::Lanes> {
175        self.into_lanes()
176    }
177}
178
179/// The one trait a consumer imports to move a `Result` between error
180/// signatures: lift its rejection, narrow away a lane that is no
181/// longer live at this boundary, hand the rejection to the caller as a value,
182/// classify a foreign error into a local wrapper, or (under `tracing`) record
183/// it onto the current span.
184pub trait ResultExt<T, E>: Sized {
185    /// Lift a rejection into a new vocabulary and enable `Fatal` for any
186    /// unmapped case. `?` subsequently expands the resulting lanes.
187    fn lift<P>(self) -> Result<T, Fail<P, <E::Lanes as LaneProfile>::WithFatal>>
188    where
189        E: IntoLanes,
190        E::Rejected: Rejection,
191        P: Rejection + Lift<E::Rejected>,
192        <E::Lanes as LaneProfile>::Fatal: Into<Fatal>,
193        P::Unmapped: UnmappedInto<Fatal>;
194
195    /// Narrows away the `Rejected` lane: a rejection with no caller left to
196    /// correct it becomes `Fatal(Invariant)`, carrying the rejection as its
197    /// source. On a `Fail<D, L>` the result is `Fault<L>`. On a *bare*
198    /// `Rejection` — a public method that returns just `R` because its
199    /// caller can act on it, consumed by an internal frame that already
200    /// proved the precondition — the result is the bare `Fatal`, and `?`
201    /// carries it into whatever `Fault`/`Fail` the function returns, so the
202    /// destination profile is never named at the call site:
203    ///
204    /// ```
205    /// use errlanes::{Fault, ResultExt, lanes};
206    ///
207    /// #[derive(Debug, errlanes::Rejection)]
208    /// #[rejection(code = "LANE_DISABLED")]
209    /// struct LaneDisabled;
210    ///
211    /// fn listen() -> Result<(), LaneDisabled> {
212    ///     Err(LaneDisabled)
213    /// }
214    ///
215    /// // The lane was required at registration: by now, off is an invariant.
216    /// fn run() -> Result<(), Fault<lanes!(Transient, Fatal)>> {
217    ///     listen().narrow_rejected()?;
218    ///     Ok(())
219    /// }
220    ///
221    /// assert!(matches!(run(), Err(Fault::Fatal(_))));
222    /// ```
223    ///
224    /// A `Fault` has no rejected lane to narrow, and that is a compile
225    /// error, not an identity:
226    ///
227    /// ```compile_fail
228    /// use errlanes::{Fault, ResultExt, lanes};
229    /// fn narrow(value: Result<(), Fault<lanes!(Fatal)>>)
230    ///     -> Result<(), Fault<lanes!(Fatal)>>
231    /// {
232    ///     value.narrow_rejected()
233    /// }
234    /// ```
235    fn narrow_rejected(self) -> Result<T, E::Narrowed>
236    where
237        E: NarrowRejectedLane;
238
239    /// Narrows away the `Transient` lane: a caller-owned retry loop hands
240    /// back the narrowed profile once it stops retrying, so it stops
241    /// offering `Transient` to its own callers. `attempts` is what the loop
242    /// counted; an exhausted transient becomes `Fatal(Exhausted)` with the
243    /// last transient as its source.
244    fn narrow_transient(self, attempts: u32) -> Result<T, E::WithoutTransient>
245    where
246        E: Laned;
247
248    /// Narrows away the `Denied` lane: a denial at a boundary with no
249    /// subject (code running as the system) becomes `Fatal(Denied)`. Only
250    /// available where `E`'s profile enables `Denied`:
251    ///
252    /// ```compile_fail
253    /// use errlanes::{Fault, ResultExt, lanes};
254    /// fn narrow(value: Result<(), Fault<lanes!(Transient, Fatal)>>)
255    ///     -> Result<(), Fault<lanes!(Transient, Fatal)>>
256    /// {
257    ///     value.narrow_denied()
258    /// }
259    /// ```
260    fn narrow_denied(self) -> Result<T, E::Narrowed>
261    where
262        E: NarrowDeniedLane;
263
264    /// Hands the rejection to the caller as a value and keeps the faults
265    /// propagating: `Result<T, Fail<D, L>>` becomes
266    /// `Result<Result<T, D>, Fault<L>>`, the `Result`-level form of
267    /// [`Fail::rejected`]. The outer `?` carries the faults on into any
268    /// enclosing carrier; the inner `Result` is the domain outcome, with the
269    /// rejection as its `Err`, matched right where it occurred:
270    ///
271    /// ```
272    /// use errlanes::{Fail, Fault, ResultExt, lanes};
273    ///
274    /// #[derive(Debug, errlanes::Rejection)]
275    /// #[rejection(code = "TIMED_OUT")]
276    /// struct TimedOut;
277    ///
278    /// fn await_completion() -> Result<u64, Fail<TimedOut, lanes!(Transient, Fatal)>> {
279    ///     Err(Fail::Rejected(TimedOut))
280    /// }
281    ///
282    /// fn poll_once() -> Result<Option<u64>, Fault<lanes!(Transient, Fatal)>> {
283    ///     match await_completion().rejected()? {
284    ///         Ok(outcome) => Ok(Some(outcome)),
285    ///         Err(TimedOut) => Ok(None),
286    ///     }
287    /// }
288    ///
289    /// assert!(poll_once().unwrap().is_none());
290    /// ```
291    ///
292    /// This is the dual of [`narrow_rejected`](Self::narrow_rejected): that
293    /// one is for a boundary with no caller left to correct the rejection,
294    /// this one for the call site that is going to. There is deliberately no
295    /// `Option`-returning accessor on a `Result`: an `as_rejected()` that
296    /// answered `None` for both `Ok` and a fault would be the one place a
297    /// lane could be dropped without naming it. Only available where `E`
298    /// carries a rejected lane; a `Fault` has none:
299    ///
300    /// ```compile_fail
301    /// use errlanes::{Fault, ResultExt, lanes};
302    /// fn split(value: Result<(), Fault<lanes!(Fatal)>>) {
303    ///     let _ = value.rejected();
304    /// }
305    /// ```
306    fn rejected(self) -> Result<Result<T, E::Rejected>, Fault<E::Lanes>>
307    where
308        E: IntoLanes,
309        E::Rejected: Rejection;
310
311    /// Maps the rejection with a closure and leaves every other lane as it
312    /// is: the `Result`-level form of [`Fail::map_rejected`]. For a
313    /// type-level remap use [`lift`](Self::lift); this is for enriching a
314    /// rejection with data only the call site has, such as the input that
315    /// was attempted: `repo.create(new).await.map_rejected(|r| r.with_attempted(id))?`.
316    fn map_rejected<D2>(self, f: impl FnOnce(E::Rejected) -> D2) -> Result<T, Fail<D2, E::Lanes>>
317    where
318        E: IntoLanes,
319        E::Rejected: Rejection;
320
321    /// Convert a never-rejecting source to its own `Fault` at a box or carrier boundary.
322    fn into_fault(self) -> Result<T, Fault<E::Lanes>>
323    where
324        E: FaultSource;
325
326    /// Convert a rejecting source to its own `Fail` at a foreign carrier boundary.
327    fn into_fail(self) -> Result<T, Fail<E::Rejected, E::Lanes>>
328    where
329        E: FailSource;
330
331    /// `.classify::<W>()` — the verb that turns a foreign error into a local
332    /// [`Classify`] wrapper at a one-off call site, so a function that does
333    /// not itself return `W` can still enter the lanes through it:
334    /// `conn.query(..).await.classify::<DbWrite>()?`.
335    fn classify<W: Classify + From<E>>(self) -> Result<T, W>;
336
337    /// Records onto the current span, then hands the result straight back —
338    /// for a call site that must record and keep going rather than
339    /// propagate (`?`), such as a batch dispatcher writing its own
340    /// `conclusion` after the fact.
341    #[cfg(feature = "tracing")]
342    fn record(self) -> Self
343    where
344        E: Laned;
345}
346
347impl<T, E> ResultExt<T, E> for Result<T, E> {
348    fn lift<P>(self) -> Result<T, Fail<P, <E::Lanes as LaneProfile>::WithFatal>>
349    where
350        E: IntoLanes,
351        E::Rejected: Rejection,
352        P: Rejection + Lift<E::Rejected>,
353        <E::Lanes as LaneProfile>::Fatal: Into<Fatal>,
354        P::Unmapped: UnmappedInto<Fatal>,
355    {
356        self.map_err(|e| e.into_lanes().lift())
357    }
358
359    fn narrow_rejected(self) -> Result<T, E::Narrowed>
360    where
361        E: NarrowRejectedLane,
362    {
363        self.map_err(NarrowRejectedLane::narrow_rejected)
364    }
365
366    fn narrow_transient(self, attempts: u32) -> Result<T, E::WithoutTransient>
367    where
368        E: Laned,
369    {
370        self.map_err(|e| e.narrow_transient(attempts))
371    }
372
373    fn narrow_denied(self) -> Result<T, E::Narrowed>
374    where
375        E: NarrowDeniedLane,
376    {
377        self.map_err(NarrowDeniedLane::narrow_denied)
378    }
379
380    fn rejected(self) -> Result<Result<T, E::Rejected>, Fault<E::Lanes>>
381    where
382        E: IntoLanes,
383        E::Rejected: Rejection,
384    {
385        match self {
386            Ok(value) => Ok(Ok(value)),
387            Err(e) => e.into_lanes().rejected().map(Err),
388        }
389    }
390
391    fn map_rejected<D2>(self, f: impl FnOnce(E::Rejected) -> D2) -> Result<T, Fail<D2, E::Lanes>>
392    where
393        E: IntoLanes,
394        E::Rejected: Rejection,
395    {
396        self.map_err(|e| e.into_lanes().map_rejected(f))
397    }
398
399    fn into_fault(self) -> Result<T, Fault<E::Lanes>>
400    where
401        E: FaultSource,
402    {
403        self.map_err(FaultSource::own_fault)
404    }
405
406    fn into_fail(self) -> Result<T, Fail<E::Rejected, E::Lanes>>
407    where
408        E: FailSource,
409    {
410        self.map_err(FailSource::own_fail)
411    }
412
413    fn classify<W: Classify + From<E>>(self) -> Result<T, W> {
414        self.map_err(W::from)
415    }
416
417    #[cfg(feature = "tracing")]
418    fn record(self) -> Self
419    where
420        E: Laned,
421    {
422        if let Err(e) = &self {
423            e.record(&tracing::Span::current());
424        }
425        self
426    }
427}