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}