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}