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_*`, `classify`, and
3//! (under `tracing`) `record` are all the same act — relocating a `Result`'s
4//! error — so they live on one blanket-impl'd trait rather than one import
5//! per verb.
6
7use crate::{
8    classify::Classify,
9    fail::{Fail, 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, since only `Fail`
16/// carries a rejected lane to narrow away.
17#[doc(hidden)]
18#[diagnostic::on_unimplemented(
19    message = "`{Self}` has no rejected lane to narrow",
20    note = "`narrow_rejected` is for a `Fail<D, L>` whose rejected lane has no \
21            caller left to correct it; a `Fault<L>` has no rejected lane at all"
22)]
23pub trait NarrowRejectedLane: crate::fail::sealed::Sealed {
24    type Narrowed;
25    fn narrow_rejected(self) -> Self::Narrowed;
26}
27
28impl<D: Rejection, L: LaneProfile<Fatal = Fatal>> NarrowRejectedLane for Fail<D, L> {
29    type Narrowed = Fault<L>;
30    fn narrow_rejected(self) -> Fault<L> {
31        Fail::narrow_rejected(self)
32    }
33}
34
35/// Sealed. The error-level engine for [`ResultExt::narrow_denied`], shared by
36/// both carriers — `Fail` and `Fault` each still have a denied lane to
37/// narrow.
38#[doc(hidden)]
39#[diagnostic::on_unimplemented(
40    message = "`{Self}` has no denied lane to narrow",
41    note = "`narrow_denied` is only available on a profile whose `Denied` lane is enabled"
42)]
43pub trait NarrowDeniedLane: crate::fail::sealed::Sealed {
44    type Narrowed;
45    fn narrow_denied(self) -> Self::Narrowed;
46}
47
48// `L: LaneProfile<Denied = Denied>` (the concrete marker, not the generic
49// `Slot<Denied>`) is load-bearing: `NarrowDenied<F>` also has a blanket impl
50// for `Infallible` (a disabled slot), so bounding on `NarrowDenied` alone
51// would make this engine — and so `ResultExt::narrow_denied` — callable even
52// when the profile has no `Denied` lane at all, turning narrowing into a
53// silent identity on an already-absent lane instead of the compile error
54// decision 2 requires. The inherent `Fault::narrow_denied`/`Fail::narrow_denied`
55// keep the wider bound; only this engine is deliberately narrower.
56impl<D: Rejection, L> NarrowDeniedLane for Fail<D, L>
57where
58    L: LaneProfile<Denied = Denied>,
59    L::Denied: NarrowDenied<L::Fatal>,
60{
61    type Narrowed = Fail<D, WithoutDenied<L>>;
62    fn narrow_denied(self) -> Self::Narrowed {
63        Fail::narrow_denied(self)
64    }
65}
66
67impl<L> NarrowDeniedLane for Fault<L>
68where
69    L: LaneProfile<Denied = Denied>,
70    L::Denied: NarrowDenied<L::Fatal>,
71{
72    type Narrowed = Fault<WithoutDenied<L>>;
73    fn narrow_denied(self) -> Self::Narrowed {
74        Fault::narrow_denied(self)
75    }
76}
77
78/// The one trait a consumer imports to move a `Result` between error
79/// signatures: widen it to a bigger profile, narrow away a lane that is no
80/// longer live at this boundary, classify a foreign error into a local
81/// wrapper, or (under `tracing`) record it onto the current span.
82pub trait ResultExt<T, E>: Sized {
83    /// Target-inferred widening for `Fault` or `Fail` results.
84    ///
85    /// `Fault` results widen to `Fault`; `Fail` results widen to `Fail`,
86    /// converting the rejection through [`crate::Lift`]. Success values and
87    /// fault payloads are preserved. Widening can add lanes, but cannot
88    /// silently discard an enabled lane:
89    ///
90    /// ```compile_fail
91    /// use errlanes::{Fault, ResultExt, lanes};
92    /// fn discard_denied(value: Result<(), Fault<lanes!(Denied, Fatal)>>)
93    ///     -> Result<(), Fault<lanes!(Fatal)>>
94    /// {
95    ///     value.widen()
96    /// }
97    /// ```
98    fn widen<E2>(self) -> Result<T, E2>
99    where
100        Self: WidenResult<T, E2>;
101
102    /// Narrows away the `Rejected` lane: a rejection with no caller left to
103    /// correct it becomes `Fatal(Invariant)`, carrying the rejection as its
104    /// source. Only available where `E` is a `Fail` — a `Fault` has no
105    /// rejected lane to narrow, and that is a compile error, not an
106    /// identity:
107    ///
108    /// ```compile_fail
109    /// use errlanes::{Fault, ResultExt, lanes};
110    /// fn narrow(value: Result<(), Fault<lanes!(Fatal)>>)
111    ///     -> Result<(), Fault<lanes!(Fatal)>>
112    /// {
113    ///     value.narrow_rejected()
114    /// }
115    /// ```
116    fn narrow_rejected(self) -> Result<T, E::Narrowed>
117    where
118        E: NarrowRejectedLane;
119
120    /// Narrows away the `Transient` lane: a caller-owned retry loop hands
121    /// back the narrowed profile once it stops retrying, so it stops
122    /// offering `Transient` to its own callers. `attempts` is what the loop
123    /// counted; an exhausted transient becomes `Fatal(Exhausted)` with the
124    /// last transient as its source.
125    fn narrow_transient(self, attempts: u32) -> Result<T, E::WithoutTransient>
126    where
127        E: Laned;
128
129    /// Narrows away the `Denied` lane: a denial at a boundary with no
130    /// subject (code running as the system) becomes `Fatal(Denied)`. Only
131    /// available where `E`'s profile enables `Denied`:
132    ///
133    /// ```compile_fail
134    /// use errlanes::{Fault, ResultExt, lanes};
135    /// fn narrow(value: Result<(), Fault<lanes!(Transient, Fatal)>>)
136    ///     -> Result<(), Fault<lanes!(Transient, Fatal)>>
137    /// {
138    ///     value.narrow_denied()
139    /// }
140    /// ```
141    fn narrow_denied(self) -> Result<T, E::Narrowed>
142    where
143        E: NarrowDeniedLane;
144
145    /// `.classify::<W>()` — the verb that turns a foreign error into a local
146    /// [`Classify`] wrapper at a one-off call site, so a function that does
147    /// not itself return `W` can still enter the lanes through it:
148    /// `conn.query(..).await.classify::<DbWrite>()?`.
149    fn classify<W: Classify + From<E>>(self) -> Result<T, W>;
150
151    /// Records onto the current span, then hands the result straight back —
152    /// for a call site that must record and keep going rather than
153    /// propagate (`?`), such as a batch dispatcher writing its own
154    /// `conclusion` after the fact.
155    #[cfg(feature = "tracing")]
156    fn record(self) -> Self
157    where
158        E: Laned;
159}
160
161impl<T, E> ResultExt<T, E> for Result<T, E> {
162    fn widen<E2>(self) -> Result<T, E2>
163    where
164        Self: WidenResult<T, E2>,
165    {
166        WidenResult::widen(self)
167    }
168
169    fn narrow_rejected(self) -> Result<T, E::Narrowed>
170    where
171        E: NarrowRejectedLane,
172    {
173        self.map_err(NarrowRejectedLane::narrow_rejected)
174    }
175
176    fn narrow_transient(self, attempts: u32) -> Result<T, E::WithoutTransient>
177    where
178        E: Laned,
179    {
180        self.map_err(|e| e.narrow_transient(attempts))
181    }
182
183    fn narrow_denied(self) -> Result<T, E::Narrowed>
184    where
185        E: NarrowDeniedLane,
186    {
187        self.map_err(NarrowDeniedLane::narrow_denied)
188    }
189
190    fn classify<W: Classify + From<E>>(self) -> Result<T, W> {
191        self.map_err(W::from)
192    }
193
194    #[cfg(feature = "tracing")]
195    fn record(self) -> Self
196    where
197        E: Laned,
198    {
199        if let Err(e) = &self {
200            e.record(&tracing::Span::current());
201        }
202        self
203    }
204}