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}