Skip to main content

errlanes/
fail.rs

1use std::{error::Error, fmt};
2
3use crate::profile::{AllLanes, LaneProfile, NarrowDenied, NarrowTransient};
4
5use crate::lane::{Denied, Exhausted, Fatal, Lane, Transient};
6
7/// Operator level, independent of any tracing dependency in the core API.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
9pub enum Level {
10    Trace,
11    Debug,
12    Info,
13    Warn,
14    Error,
15}
16
17#[cfg(feature = "tracing")]
18impl From<Level> for tracing::Level {
19    fn from(level: Level) -> Self {
20        match level {
21            Level::Trace => tracing::Level::TRACE,
22            Level::Debug => tracing::Level::DEBUG,
23            Level::Info => tracing::Level::INFO,
24            Level::Warn => tracing::Level::WARN,
25            Level::Error => tracing::Level::ERROR,
26        }
27    }
28}
29
30/// A pure, caller-correctable domain outcome. Implemented by hand or via
31/// `#[derive(errlanes::Rejection)]`, which emits `Display`/`Error` itself
32/// (defaulting `Display` to the code) unless `#[rejection(error = manual)]`
33/// hands that to `thiserror` or a hand-written impl.
34pub trait Rejection: Error + Send + Sync + 'static {
35    /// A stable, typed, wire-safe identity for this outcome.
36    type Code: Copy
37        + Eq
38        + std::hash::Hash
39        + fmt::Debug
40        + fmt::Display
41        + Into<&'static str>
42        + Send
43        + Sync
44        + 'static;
45
46    fn code(&self) -> Self::Code;
47
48    /// Operator level for this outcome. Rejections default to `Info`.
49    fn level(&self) -> Level {
50        Level::Info
51    }
52}
53
54/// Consuming mapping. `#[derive(Lift)]` with `#[lift(Source)]` generates an exhaustive
55/// mapping with `Unmapped = Infallible` and a total `From<Source>` conversion.
56/// `#[lift(Source, unhandled = fatal)]` returns the original unmapped source;
57/// [`Fail::widen`] wraps it as a fatal invariant with its source intact.
58/// The derive does not require or implement [`Rejection`]. Derive `Rejection`
59/// separately to provide codes and levels; simple lift mappings forward that
60/// metadata by default unless the destination declares its own.
61pub trait Lift<X>: Sized {
62    type Unmapped;
63    fn lift(x: X) -> Result<Self, Self::Unmapped>;
64}
65
66/// A total `From` counts as a strict lift, so one call-site method can require
67/// only `Lift` and still cover both mapping modes. Bounded to `X: Rejection`
68/// (rather than any `X`) so it does not overlap the `Lift<Infallible>`
69/// blanket below — both are needed, since a [`crate::Classify`] wrapper's
70/// rejected slot is either a `Rejection` or `Infallible` (never anything
71/// else, by `RejectedSlot`'s own two impls).
72impl<X: Rejection, P: From<X>> Lift<X> for P {
73    type Unmapped = core::convert::Infallible;
74    fn lift(x: X) -> Result<Self, Self::Unmapped> {
75        Ok(P::from(x))
76    }
77}
78
79/// Every destination lifts an `Infallible` rejected slot trivially — this is
80/// what lets a fault-only [`crate::Classify`] wrapper satisfy the `Fail`
81/// blanket for *any* `D`, with no `Lift`/`From` declared at all.
82impl<P> Lift<core::convert::Infallible> for P {
83    type Unmapped = core::convert::Infallible;
84    fn lift(x: core::convert::Infallible) -> Result<Self, Self::Unmapped> {
85        match x {}
86    }
87}
88
89/// Conversion of an unmapped value into an enabled fatal slot. Strict lifts
90/// have no unmapped values and therefore work with any destination profile.
91#[doc(hidden)]
92pub trait UnmappedInto<F> {
93    fn unmapped_into(self) -> F;
94}
95impl<F> UnmappedInto<F> for core::convert::Infallible {
96    fn unmapped_into(self) -> F {
97        match self {}
98    }
99}
100impl<R: Rejection> UnmappedInto<Fatal> for R {
101    fn unmapped_into(self) -> Fatal {
102        // `with_opaque_source`: `self` is a `Rejection`, whose `Display` may
103        // embed caller-supplied input (see the trait's display discipline);
104        // `source()` still returns it for a handler or test to
105        // `downcast_ref`, but `message_chain` must not walk into it.
106        Fatal::from_error(crate::FatalKind::Invariant, self)
107            .with_context("unhandled rejection at partial lift")
108            .with_opaque_source()
109    }
110}
111
112/// `Fail` minus the `Rejected` lane: what an operation that cannot reject
113/// (nothing about it is the caller's to correct) returns. Reads return
114/// `Fault<L>`; writes return `Fail<{Entity}ConstraintViolation, L>` — the type
115/// itself says whether a call can ever hand back a domain outcome.
116#[derive(Debug, Clone)]
117pub enum Fault<L: LaneProfile = AllLanes> {
118    Denied(L::Denied),
119    Transient(L::Transient),
120    Fatal(L::Fatal),
121}
122
123impl<L: LaneProfile> Fault<L> {
124    pub fn lane(&self) -> Lane {
125        match self {
126            Fault::Denied(_) => Lane::Denied,
127            Fault::Transient(_) => Lane::Transient,
128            Fault::Fatal(_) => Lane::Fatal,
129        }
130    }
131
132    pub fn is_transient(&self) -> bool {
133        matches!(self, Fault::Transient(_))
134    }
135
136    pub fn is_fatal(&self) -> bool {
137        matches!(self, Fault::Fatal(_))
138    }
139
140    pub fn is_denied(&self) -> bool {
141        matches!(self, Fault::Denied(_))
142    }
143
144    /// The operator-safe one-line text for this failure — exactly what
145    /// `record` writes to `exception.message`, for a boundary that must
146    /// persist it rather than (or as well as) record it. `Transient`/
147    /// `Fatal`: the whole `source()` chain joined with `": "`. `Denied`: its
148    /// `Display`.
149    pub fn message(&self) -> String {
150        match self {
151            Fault::Denied(d) => d.to_string(),
152            Fault::Transient(t) => crate::dynamic::message_chain(t),
153            Fault::Fatal(x) => crate::dynamic::message_chain(x),
154        }
155    }
156
157    /// Consumes the `Transient` lane, yielding the same profile with its
158    /// transient slot disabled. `attempts` is what the retry loop counted; an
159    /// exhausted transient becomes `Fatal(Exhausted)` with the last transient
160    /// as its source.
161    pub fn narrow_transient(self, attempts: u32) -> Fault<crate::profile::WithoutTransient<L>>
162    where
163        L::Transient: NarrowTransient<L::Fatal>,
164    {
165        match self {
166            Fault::Denied(d) => Fault::Denied(d),
167            Fault::Transient(last) => Fault::Fatal(last.narrow(attempts)),
168            Fault::Fatal(f) => Fault::Fatal(f),
169        }
170    }
171
172    /// Narrows away the `Denied` lane: a denial at a boundary with no
173    /// subject (code running as the system) becomes `Fatal(Denied)` with
174    /// the `Denied` as its source.
175    pub fn narrow_denied(self) -> Fault<crate::profile::WithoutDenied<L>>
176    where
177        L::Denied: NarrowDenied<L::Fatal>,
178    {
179        match self {
180            Fault::Denied(d) => Fault::Fatal(d.narrow()),
181            Fault::Transient(t) => Fault::Transient(t),
182            Fault::Fatal(f) => Fault::Fatal(f),
183        }
184    }
185}
186
187/// Borrowed lane accessors. `min_exhaustive_patterns` lets a *by-value* match
188/// name only the lanes a profile enables, but a borrowed match still demands an
189/// arm for every variant. These accessors are the borrowed form, so no caller
190/// ever has to write a `match *never {}` arm. Each is available only when the
191/// profile enables that lane, so `e.as_denied()` on a no-denial profile is a
192/// compile error rather than a permanent `None`.
193impl<L: LaneProfile<Denied = Denied>> Fault<L> {
194    pub fn as_denied(&self) -> Option<&Denied> {
195        match self {
196            Fault::Denied(d) => Some(d),
197            _ => None,
198        }
199    }
200}
201
202impl<L: LaneProfile<Transient = Transient>> Fault<L> {
203    pub fn as_transient(&self) -> Option<&Transient> {
204        match self {
205            Fault::Transient(t) => Some(t),
206            _ => None,
207        }
208    }
209
210    pub fn is_congestion(&self) -> bool {
211        self.as_transient().is_some_and(Transient::is_congestion)
212    }
213
214    /// See [`TransientKind::is_contention`].
215    pub fn is_contention(&self) -> bool {
216        self.as_transient().is_some_and(Transient::is_contention)
217    }
218}
219
220impl<L: LaneProfile<Fatal = Fatal>> Fault<L> {
221    pub fn as_fatal(&self) -> Option<&Fatal> {
222        match self {
223            Fault::Fatal(f) => Some(f),
224            _ => None,
225        }
226    }
227}
228
229impl<D, L: LaneProfile<Denied = Denied>> Fail<D, L> {
230    pub fn as_denied(&self) -> Option<&Denied> {
231        match self {
232            Fail::Denied(d) => Some(d),
233            _ => None,
234        }
235    }
236}
237
238impl<D, L: LaneProfile<Transient = Transient>> Fail<D, L> {
239    pub fn as_transient(&self) -> Option<&Transient> {
240        match self {
241            Fail::Transient(t) => Some(t),
242            _ => None,
243        }
244    }
245
246    pub fn is_congestion(&self) -> bool {
247        self.as_transient().is_some_and(Transient::is_congestion)
248    }
249
250    /// See [`TransientKind::is_contention`].
251    pub fn is_contention(&self) -> bool {
252        self.as_transient().is_some_and(Transient::is_contention)
253    }
254}
255
256impl<D, L: LaneProfile<Fatal = Fatal>> Fail<D, L> {
257    pub fn as_fatal(&self) -> Option<&Fatal> {
258        match self {
259            Fail::Fatal(f) => Some(f),
260            _ => None,
261        }
262    }
263}
264
265impl<L: LaneProfile> fmt::Display for Fault<L> {
266    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
267        match self {
268            Fault::Denied(d) => write!(f, "{d}"),
269            Fault::Transient(t) => write!(f, "{t}"),
270            Fault::Fatal(x) => write!(f, "{x}"),
271        }
272    }
273}
274
275impl<L: LaneProfile> Error for Fault<L> {
276    fn source(&self) -> Option<&(dyn Error + 'static)> {
277        // Same contract as `Fail::source` (pitfall 2): the lane payload
278        // itself, so `lane_of` can downcast it.
279        match self {
280            Fault::Denied(d) => Some(d),
281            Fault::Transient(t) => Some(t),
282            Fault::Fatal(x) => Some(x),
283        }
284    }
285}
286
287impl<L: LaneProfile> From<Transient> for Fault<L>
288where
289    L: LaneProfile<Transient = Transient>,
290{
291    fn from(t: Transient) -> Self {
292        Fault::Transient(t)
293    }
294}
295
296impl<L: LaneProfile> From<Fatal> for Fault<L>
297where
298    L: LaneProfile<Fatal = Fatal>,
299{
300    fn from(f: Fatal) -> Self {
301        Fault::Fatal(f)
302    }
303}
304
305impl<L: LaneProfile> From<Denied> for Fault<L>
306where
307    L: LaneProfile<Denied = Denied>,
308{
309    fn from(d: Denied) -> Self {
310        Fault::Denied(d)
311    }
312}
313
314impl<L: LaneProfile> From<Exhausted> for Fault<L>
315where
316    L: LaneProfile<Fatal = Fatal>,
317{
318    fn from(e: Exhausted) -> Self {
319        Fault::Fatal(Fatal::from_error(crate::lane::FatalKind::Exhausted, e))
320    }
321}
322
323/// `Infallible` is what a disabled lane slot is, so a value proven never to
324/// exist converts trivially — `match e {}`.
325impl<L: LaneProfile> From<core::convert::Infallible> for Fault<L> {
326    fn from(e: core::convert::Infallible) -> Self {
327        match e {}
328    }
329}
330
331/// The generic view over a domain rejection `D`, before retries have run.
332///
333/// **Display discipline**: [`Display`](fmt::Display)'s `Rejected` arm keeps
334/// its `rejected: {d}` prefix for logs, but no boundary may build a
335/// user-facing message from `to_string()` — a rejection's message may embed
336/// caller-supplied input. Use [`as_rejected`](Fail::as_rejected) and
337/// [`Rejection::code`] instead: `record_fail` keys `error.code` off the code,
338/// never the message, and a GraphQL boundary should do the same for its error
339/// extension.
340#[derive(Debug, Clone)]
341pub enum Fail<D, L: LaneProfile = AllLanes> {
342    Rejected(D),
343    Denied(L::Denied),
344    Transient(L::Transient),
345    Fatal(L::Fatal),
346}
347
348impl<D, L: LaneProfile> Fail<D, L> {
349    pub fn lane(&self) -> Lane {
350        match self {
351            Fail::Rejected(_) => Lane::Rejected,
352            Fail::Denied(_) => Lane::Denied,
353            Fail::Transient(_) => Lane::Transient,
354            Fail::Fatal(_) => Lane::Fatal,
355        }
356    }
357
358    /// Widen the rejection and the lane profile without changing any
359    /// classification. The value-level form of [`WidenResult::widen`], with
360    /// the same single rule: `P: Lift<D>` is satisfied by a total `From<D>`
361    /// (through errlanes' blanket, `Unmapped = Infallible`) and by a partial
362    /// `#[lift(Source, unhandled = fatal)]` mapping (`Unmapped = Source`),
363    /// whose unmapped cases demote to `Fatal(Invariant)` with the rejection
364    /// as their source — which is why a partial mapping requires the
365    /// destination to admit `Fatal`. The strict/partial choice is declared
366    /// once on the destination enum; the call site does not repeat it.
367    pub fn widen<P: Lift<D>, M: LaneProfile>(self) -> Fail<P, M>
368    where
369        L::Denied: Into<M::Denied>,
370        L::Transient: Into<M::Transient>,
371        L::Fatal: Into<M::Fatal>,
372        P::Unmapped: UnmappedInto<M::Fatal>,
373    {
374        match self {
375            Self::Rejected(d) => match P::lift(d) {
376                Ok(mapped) => Fail::Rejected(mapped),
377                Err(unmapped) => Fail::Fatal(unmapped.unmapped_into()),
378            },
379            Self::Denied(d) => Fail::Denied(d.into()),
380            Self::Transient(t) => Fail::Transient(t.into()),
381            Self::Fatal(f) => Fail::Fatal(f.into()),
382        }
383    }
384
385    pub fn widen_with<P, M: LaneProfile<Fatal = Fatal>>(
386        self,
387        f: impl FnOnce(D) -> Result<P, Fatal>,
388    ) -> Fail<P, M>
389    where
390        L::Denied: Into<M::Denied>,
391        L::Transient: Into<M::Transient>,
392        L::Fatal: Into<M::Fatal>,
393    {
394        match self {
395            Self::Rejected(d) => match f(d) {
396                Ok(p) => Fail::Rejected(p),
397                Err(e) => Fail::Fatal(e),
398            },
399            Self::Denied(d) => Fail::Denied(d.into()),
400            Self::Transient(t) => Fail::Transient(t.into()),
401            Self::Fatal(f) => Fail::Fatal(f.into()),
402        }
403    }
404
405    pub fn map_rejected<D2>(self, f: impl FnOnce(D) -> D2) -> Fail<D2, L> {
406        match self {
407            Fail::Rejected(d) => Fail::Rejected(f(d)),
408            Fail::Denied(d) => Fail::Denied(d),
409            Fail::Transient(t) => Fail::Transient(t),
410            Fail::Fatal(fatal) => Fail::Fatal(fatal),
411        }
412    }
413
414    /// Narrows to the domain outcome, or the non-domain fault. `let d =
415    /// e.rejected()?;` propagates the fault into any enclosing `Fail<_, L>` (or
416    /// a `Failure` carrier) via the blanket `From<Fault<L>>`.
417    pub fn rejected(self) -> Result<D, Fault<L>> {
418        match self {
419            Fail::Rejected(d) => Ok(d),
420            Fail::Denied(d) => Err(Fault::Denied(d)),
421            Fail::Transient(t) => Err(Fault::Transient(t)),
422            Fail::Fatal(f) => Err(Fault::Fatal(f)),
423        }
424    }
425
426    pub fn as_rejected(&self) -> Option<&D> {
427        match self {
428            Fail::Rejected(d) => Some(d),
429            _ => None,
430        }
431    }
432
433    pub fn is_transient(&self) -> bool {
434        matches!(self, Fail::Transient(_))
435    }
436
437    pub fn is_fatal(&self) -> bool {
438        matches!(self, Fail::Fatal(_))
439    }
440
441    pub fn is_denied(&self) -> bool {
442        matches!(self, Fail::Denied(_))
443    }
444
445    /// The operator-safe one-line text for this failure — exactly what
446    /// `record` writes to `exception.message`, for a boundary that must
447    /// persist it rather than (or as well as) record it. `Transient`/
448    /// `Fatal`: the whole `source()` chain joined with `": "`. `Denied`: its
449    /// `Display`. `Rejected`: the rejection's `code`, never its message
450    /// (display discipline — a rejection's message may embed caller-supplied
451    /// input).
452    pub fn message(&self) -> String
453    where
454        D: Rejection,
455    {
456        match self {
457            Fail::Rejected(d) => d.code().to_string(),
458            Fail::Denied(d) => d.to_string(),
459            Fail::Transient(t) => crate::dynamic::message_chain(t),
460            Fail::Fatal(x) => crate::dynamic::message_chain(x),
461        }
462    }
463
464    /// Consumes the `Transient` lane, yielding the same rejection over `L`
465    /// with its transient slot disabled. An exhausted transient becomes
466    /// `Fatal(Exhausted)`, carrying `attempts` and the last transient as its
467    /// source.
468    pub fn narrow_transient(self, attempts: u32) -> Fail<D, crate::profile::WithoutTransient<L>>
469    where
470        L::Transient: NarrowTransient<L::Fatal>,
471    {
472        match self {
473            Fail::Rejected(d) => Fail::Rejected(d),
474            Fail::Denied(d) => Fail::Denied(d),
475            Fail::Transient(last) => Fail::Fatal(last.narrow(attempts)),
476            Fail::Fatal(f) => Fail::Fatal(f),
477        }
478    }
479
480    /// Narrows away the `Denied` lane: a denial at a boundary with no
481    /// subject (code running as the system) becomes `Fatal(Denied)` with
482    /// the `Denied` as its source.
483    pub fn narrow_denied(self) -> Fail<D, crate::profile::WithoutDenied<L>>
484    where
485        L::Denied: NarrowDenied<L::Fatal>,
486    {
487        match self {
488            Fail::Rejected(d) => Fail::Rejected(d),
489            Fail::Denied(d) => Fail::Fatal(d.narrow()),
490            Fail::Transient(t) => Fail::Transient(t),
491            Fail::Fatal(f) => Fail::Fatal(f),
492        }
493    }
494
495    /// Narrows away the `Rejected` lane: a rejection at a boundary with no
496    /// caller to correct it is an invariant violation, and becomes
497    /// `Fatal(Invariant)` with the rejection as its source — the same rule
498    /// a partial `#[lift(.., unhandled = fatal)]` applies. Nothing is left
499    /// to reject, so the result is a `Fault`. `with_opaque_source`: `d`'s
500    /// `Display` may embed caller-supplied input (the same display
501    /// discipline `Rejection` documents everywhere else), so `source()`
502    /// still returns it for a handler or test to `downcast_ref`, but
503    /// `message_chain` must not walk into it.
504    pub fn narrow_rejected(self) -> Fault<L>
505    where
506        D: Rejection,
507        L: LaneProfile<Fatal = Fatal>,
508    {
509        match self {
510            Fail::Rejected(d) => Fault::Fatal(invariant_from_rejection(d)),
511            Fail::Denied(d) => Fault::Denied(d),
512            Fail::Transient(t) => Fault::Transient(t),
513            Fail::Fatal(f) => Fault::Fatal(f),
514        }
515    }
516}
517
518/// The one construction site for "a rejection with no caller left to correct
519/// it": `Fatal(Invariant)` carrying the rejection as its source. Shared by
520/// [`Fail::narrow_rejected`] and the bare-rejection `narrow_rejected` in
521/// `result_ext`, so both apply the same context and the same
522/// `with_opaque_source` discipline (`d`'s `Display` may embed caller input).
523pub(crate) fn invariant_from_rejection<D: Rejection>(d: D) -> Fatal {
524    Fatal::from_error(crate::FatalKind::Invariant, d)
525        .with_context("rejected with no caller to correct")
526        .with_opaque_source()
527}
528
529/// The real blanket the `Infallible` encoding could not have: `Fault<L>` is not
530/// `Fail`, so this does not overlap `From<T> for T`.
531impl<D, S: LaneProfile, L: LaneProfile> From<Fault<S>> for Fail<D, L>
532where
533    S::Denied: Into<L::Denied>,
534    S::Transient: Into<L::Transient>,
535    S::Fatal: Into<L::Fatal>,
536{
537    fn from(f: Fault<S>) -> Self {
538        match f {
539            Fault::Denied(d) => Fail::Denied(d.into()),
540            Fault::Transient(t) => Fail::Transient(t.into()),
541            Fault::Fatal(x) => Fail::Fatal(x.into()),
542        }
543    }
544}
545
546impl<D, L: LaneProfile> From<Transient> for Fail<D, L>
547where
548    L: LaneProfile<Transient = Transient>,
549{
550    fn from(t: Transient) -> Self {
551        Fail::Transient(t)
552    }
553}
554
555impl<D, L: LaneProfile> From<Fatal> for Fail<D, L>
556where
557    L: LaneProfile<Fatal = Fatal>,
558{
559    fn from(f: Fatal) -> Self {
560        Fail::Fatal(f)
561    }
562}
563
564impl<D, L: LaneProfile> From<Denied> for Fail<D, L>
565where
566    L: LaneProfile<Denied = Denied>,
567{
568    fn from(d: Denied) -> Self {
569        Fail::Denied(d)
570    }
571}
572
573impl<D, L: LaneProfile> From<Exhausted> for Fail<D, L>
574where
575    L: LaneProfile<Fatal = Fatal>,
576{
577    fn from(e: Exhausted) -> Self {
578        Fail::Fatal(Fatal::from_error(crate::lane::FatalKind::Exhausted, e))
579    }
580}
581
582impl<D: fmt::Display, L: LaneProfile> fmt::Display for Fail<D, L> {
583    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
584        match self {
585            Fail::Rejected(d) => write!(f, "rejected: {d}"),
586            Fail::Denied(d) => write!(f, "{d}"),
587            Fail::Transient(t) => write!(f, "{t}"),
588            Fail::Fatal(x) => write!(f, "{x}"),
589        }
590    }
591}
592
593impl<D: Error + 'static, L: LaneProfile> Error for Fail<D, L> {
594    fn source(&self) -> Option<&(dyn Error + 'static)> {
595        match self {
596            // The lane payload itself must be the *next* link in the chain
597            // so that `lane_of` can find it by downcasting.
598            Fail::Rejected(d) => Some(d),
599            Fail::Denied(d) => Some(d),
600            Fail::Transient(t) => Some(t),
601            Fail::Fatal(x) => Some(x),
602        }
603    }
604}
605
606/// Implemented by per-crate carrier newtypes, typically via
607/// `#[derive(errlanes::Failure)]`. Generic machinery (retry, boundary
608/// recorders) is written against this, not against `Fail<D, L>` directly.
609pub trait Failure: Error + Send + Sync + Sized + 'static {
610    type Rejection: Rejection;
611    type Lanes: LaneProfile;
612
613    fn into_fail(self) -> Fail<Self::Rejection, Self::Lanes>;
614    fn from_fail(f: Fail<Self::Rejection, Self::Lanes>) -> Self;
615    fn as_fail(&self) -> &Fail<Self::Rejection, Self::Lanes>;
616
617    fn lane(&self) -> Lane {
618        self.as_fail().lane()
619    }
620
621    fn is_transient(&self) -> bool {
622        matches!(self.lane(), Lane::Transient)
623    }
624}
625
626impl<D: Rejection, L: LaneProfile> Failure for Fail<D, L> {
627    type Rejection = D;
628    type Lanes = L;
629
630    fn into_fail(self) -> Fail<Self::Rejection, Self::Lanes> {
631        self
632    }
633
634    fn from_fail(f: Fail<Self::Rejection, Self::Lanes>) -> Self {
635        f
636    }
637
638    fn as_fail(&self) -> &Fail<Self::Rejection, Self::Lanes> {
639        self
640    }
641}
642
643pub(crate) mod sealed {
644    pub trait Sealed {}
645    impl<F: super::Failure> Sealed for F {}
646    impl<L: super::LaneProfile> Sealed for super::Fault<L> {}
647}
648
649/// Sealed. The thing `retry`/`record` need from any error they are handed:
650/// its lane, and how to narrow it. Implemented by every [`Failure`] (which
651/// covers `Fail<D, L>` itself and every carrier) and by [`Fault<L>`] — the two
652/// shapes a generated repo op can return.
653pub trait Laned: sealed::Sealed + Error + Send + Sync + 'static + Sized {
654    type WithoutTransient: Error + Send + Sync + 'static;
655
656    fn lane(&self) -> Lane;
657
658    fn is_transient(&self) -> bool {
659        self.lane() == Lane::Transient
660    }
661
662    fn is_fatal(&self) -> bool {
663        self.lane() == Lane::Fatal
664    }
665
666    fn is_denied(&self) -> bool {
667        self.lane() == Lane::Denied
668    }
669
670    fn narrow_transient(self, attempts: u32) -> Self::WithoutTransient;
671
672    /// The operator-safe one-line text for this failure — exactly what
673    /// `record` writes to `exception.message`, for a boundary that must
674    /// persist it rather than (or as well as) record it. `Transient`/
675    /// `Fatal`: the whole `source()` chain joined with `": "`. `Denied`: its
676    /// `Display`. `Rejected`: the rejection's `code`, never its message
677    /// (display discipline — a rejection's message may embed caller-supplied
678    /// input).
679    fn message(&self) -> String;
680
681    #[cfg(feature = "tracing")]
682    fn record(&self, span: &tracing::Span);
683}
684
685/// Note the `NarrowTransient` bound: a profile that admits `Transient` but
686/// not `Fatal` is not `Laned`, so `retry` cannot be handed one. Retrying an
687/// operation that claims it can never fail permanently is exactly the
688/// contradiction the bound rules out.
689impl<F: Failure> Laned for F
690where
691    <F::Lanes as LaneProfile>::Transient: NarrowTransient<<F::Lanes as LaneProfile>::Fatal>,
692{
693    type WithoutTransient = Fail<F::Rejection, crate::profile::WithoutTransient<F::Lanes>>;
694
695    fn lane(&self) -> Lane {
696        Failure::lane(self)
697    }
698
699    fn narrow_transient(self, attempts: u32) -> Self::WithoutTransient {
700        self.into_fail().narrow_transient(attempts)
701    }
702
703    fn message(&self) -> String {
704        self.as_fail().message()
705    }
706
707    #[cfg(feature = "tracing")]
708    fn record(&self, span: &tracing::Span) {
709        crate::record::record_fail(span, self.as_fail());
710    }
711}
712
713impl<L: LaneProfile> Laned for Fault<L>
714where
715    L::Transient: NarrowTransient<L::Fatal>,
716{
717    type WithoutTransient = Fault<crate::profile::WithoutTransient<L>>;
718
719    fn lane(&self) -> Lane {
720        Fault::lane(self)
721    }
722
723    fn narrow_transient(self, attempts: u32) -> Self::WithoutTransient {
724        Fault::narrow_transient(self, attempts)
725    }
726
727    fn message(&self) -> String {
728        Fault::message(self)
729    }
730
731    #[cfg(feature = "tracing")]
732    fn record(&self, span: &tracing::Span) {
733        crate::record::record_fault(span, self);
734    }
735}
736
737/// Dispatch engine for [`crate::ResultExt::widen`], keyed on the source
738/// shape so each carrier (`Fault`, `Fail`, a bare [`crate::Classify`] source)
739/// gets its own body. Hidden: a consumer calls `.widen()` on a `Result`,
740/// never this trait directly. See `ResultExt::widen` for the full doc,
741/// including the `compile_fail` example of a lane widening cannot discard.
742#[doc(hidden)]
743pub trait WidenResult<T, E>: Sized {
744    fn widen(self) -> Result<T, E>;
745}
746
747impl<T, L: LaneProfile, M: LaneProfile> WidenResult<T, Fault<M>> for Result<T, Fault<L>>
748where
749    L::Denied: Into<M::Denied>,
750    L::Transient: Into<M::Transient>,
751    L::Fatal: Into<M::Fatal>,
752{
753    fn widen(self) -> Result<T, Fault<M>> {
754        self.map_err(Fault::widen)
755    }
756}
757
758/// One rule for every rejection remapping. `P: Lift<R>` is satisfied by a total
759/// `From<R>` (through errlanes' blanket, `Unmapped = Infallible`) and by a
760/// partial `#[lift(Source, unhandled = fatal)]` mapping (`Unmapped = Source`).
761/// The `UnmappedInto` bound then enforces, per destination, exactly what each
762/// mode needs: a total mapping works into any profile, while a partial one
763/// requires the destination to admit `Fatal`. The strict/partial choice is
764/// declared once on the destination enum, so the call site does not repeat it.
765impl<T, R, P: Lift<R>, L: LaneProfile, M: LaneProfile> WidenResult<T, Fail<P, M>>
766    for Result<T, Fail<R, L>>
767where
768    L::Denied: Into<M::Denied>,
769    L::Transient: Into<M::Transient>,
770    L::Fatal: Into<M::Fatal>,
771    P::Unmapped: UnmappedInto<M::Fatal>,
772{
773    fn widen(self) -> Result<T, Fail<P, M>> {
774        self.map_err(Fail::widen)
775    }
776}
777
778// The bare-source impl for `Result<T, C>` (a rejection, a fault wrapper, or a
779// mixed wrapper alike) lives in `classify.rs`, generalised from `C: Rejection`
780// to `C: Classify` — a bare rejection is the `Rejected = C, Lanes = NoLanes`
781// case of that.
782
783impl<L: LaneProfile> Fault<L> {
784    pub fn widen<M: LaneProfile>(self) -> Fault<M>
785    where
786        L::Denied: Into<M::Denied>,
787        L::Transient: Into<M::Transient>,
788        L::Fatal: Into<M::Fatal>,
789    {
790        match self {
791            Self::Denied(d) => Fault::Denied(d.into()),
792            Self::Transient(t) => Fault::Transient(t.into()),
793            Self::Fatal(f) => Fault::Fatal(f.into()),
794        }
795    }
796}
797
798/// Compile-time field metadata used by the rejection composition protocol.
799#[doc(hidden)]
800pub trait RejectionField<const VARIANT: u64, const FIELD: usize> {
801    type Type;
802}
803
804/// Borrowed metadata protocol; forwarding does not clone or reconstruct errors.
805#[doc(hidden)]
806pub trait RejectionMetadata<const VARIANT: u64>: Rejection {
807    type Fields<'a>;
808    fn field_code(fields: Self::Fields<'_>) -> Self::Code;
809    fn field_level(fields: Self::Fields<'_>) -> Level;
810}
811
812#[cfg(test)]
813mod tests {
814    use super::*;
815    use crate::lane::{FatalKind, TransientKind};
816
817    #[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
818    struct SmallCode;
819    impl fmt::Display for SmallCode {
820        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
821            f.write_str("SMALL")
822        }
823    }
824    impl From<SmallCode> for &'static str {
825        fn from(_: SmallCode) -> Self {
826            "SMALL"
827        }
828    }
829
830    #[derive(Debug, Clone, PartialEq, Eq)]
831    struct Small;
832    impl fmt::Display for Small {
833        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
834            f.write_str("small")
835        }
836    }
837    impl Error for Small {}
838    impl Rejection for Small {
839        type Code = SmallCode;
840        fn code(&self) -> SmallCode {
841            SmallCode
842        }
843    }
844
845    #[derive(Debug, Clone, PartialEq, Eq)]
846    struct Big(Small);
847    impl fmt::Display for Big {
848        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
849            write!(f, "big({})", self.0)
850        }
851    }
852    impl Error for Big {}
853    impl From<Small> for Big {
854        fn from(s: Small) -> Self {
855            Big(s)
856        }
857    }
858    impl Rejection for Big {
859        type Code = SmallCode;
860        fn code(&self) -> SmallCode {
861            self.0.code()
862        }
863    }
864
865    #[test]
866    fn widen_preserves_the_lane() {
867        let f: Fail<Small> = Fail::Rejected(Small);
868        let widened: Fail<Big> = f.widen();
869        assert_eq!(widened.lane(), Lane::Rejected);
870        assert_eq!(widened.as_rejected(), Some(&Big(Small)));
871
872        let t: Fail<Small> = Transient::new(TransientKind::Deadlock).into();
873        let widened: Fail<Big> = t.widen();
874        assert_eq!(widened.lane(), Lane::Transient);
875    }
876
877    #[test]
878    fn widen_with_demotes_an_unmapped_rejection_to_fatal() {
879        let f: Fail<Small> = Fail::Rejected(Small);
880        let widened: Fail<Big> = f.widen_with(|_| Err(Fatal::invariant("unmapped")));
881        assert_eq!(widened.lane(), Lane::Fatal);
882
883        let f: Fail<Small> = Fail::Rejected(Small);
884        let widened: Fail<Big> = f.widen_with(|s| Ok(Big(s)));
885        assert_eq!(widened.lane(), Lane::Rejected);
886    }
887
888    /// Regression: `UnmappedInto<Fatal> for R: Rejection` (the partial-lift
889    /// path) must keep the unmapped rejection reachable by `downcast_ref`
890    /// for a handler or test, but never let its `Display` -- which may embed
891    /// caller-supplied input -- reach an operator-facing message.
892    #[test]
893    fn unmapped_rejection_demoted_to_fatal_does_not_leak_its_display() {
894        let fatal: Fatal = Small.unmapped_into();
895        assert_eq!(fatal.kind, FatalKind::Invariant);
896        assert!(
897            fatal.source().unwrap().downcast_ref::<Small>().is_some(),
898            "the rejection must still be reachable for a handler or test to downcast"
899        );
900        let message = crate::dynamic::message_chain(&fatal);
901        assert!(
902            !message.contains("small"),
903            "a partial lift's unmapped rejection's Display must never reach an \
904             operator-facing message; got {message:?}"
905        );
906    }
907
908    /// Same regression as `unmapped_rejection_demoted_to_fatal_does_not_leak_its_display`,
909    /// for `narrow_rejected`'s own construction site.
910    #[test]
911    fn narrow_rejected_does_not_leak_the_rejections_display() {
912        let f: Fail<Small> = Fail::Rejected(Small);
913        let narrowed = f.narrow_rejected();
914        match &narrowed {
915            Fault::Fatal(fatal) => {
916                assert_eq!(fatal.kind, FatalKind::Invariant);
917                assert!(fatal.source().unwrap().downcast_ref::<Small>().is_some());
918            }
919            other => panic!("expected Fatal(Invariant), got {other:?}"),
920        }
921        let message = narrowed.message();
922        assert!(
923            !message.contains("small"),
924            "narrow_rejected's rejection Display must never reach an operator-facing \
925             message; got {message:?}"
926        );
927    }
928
929    #[test]
930    fn narrow_transient_turns_transient_into_exhausted_and_leaves_everything_else_alone() {
931        let t: Fail<Small> = Transient::new(TransientKind::Deadlock).into();
932        match t.narrow_transient(3) {
933            Fail::Fatal(f) => {
934                assert_eq!(f.kind, FatalKind::Exhausted);
935                let e = Error::source(&f)
936                    .and_then(|s| s.downcast_ref::<Exhausted>())
937                    .expect("exhaustion is the fatal's source");
938                assert_eq!(e.attempts, 3);
939                assert_eq!(e.last.kind, TransientKind::Deadlock);
940            }
941            other => panic!("expected Fatal(Exhausted), got {other:?}"),
942        }
943
944        let r: Fail<Small> = Fail::Rejected(Small);
945        assert!(matches!(r.narrow_transient(1), Fail::Rejected(Small)));
946
947        let fatal: Fail<Small> = Fatal::new(FatalKind::Config).into();
948        assert!(matches!(fatal.narrow_transient(1), Fail::Fatal(_)));
949    }
950
951    #[test]
952    fn fault_converts_into_any_fail_via_the_real_blanket() {
953        let fault: Fault = Fatal::new(FatalKind::CorruptState).into();
954        let widened: Fail<Small> = fault.into();
955        assert_eq!(widened.lane(), Lane::Fatal);
956    }
957
958    #[test]
959    fn rejected_narrows_or_returns_the_fault() {
960        let f: Fail<Small> = Fail::Rejected(Small);
961        assert_eq!(f.rejected().unwrap(), Small);
962
963        let t: Fail<Small> = Transient::new(TransientKind::Deadlock).into();
964        assert!(t.rejected().is_err());
965    }
966
967    #[test]
968    fn fail_source_is_the_lane_payload_itself_so_lane_of_can_downcast_it() {
969        let t: Fail<Small> = Transient::new(TransientKind::Deadlock).into();
970        let source = std::error::Error::source(&t).expect("source present");
971        assert!(source.is::<Transient>());
972    }
973
974    #[test]
975    fn fault_source_is_the_lane_payload_itself_so_lane_of_can_downcast_it() {
976        let t: Fault = Transient::new(TransientKind::Deadlock).into();
977        let source = std::error::Error::source(&t).expect("source present");
978        assert!(source.is::<Transient>());
979    }
980
981    #[test]
982    fn laned_narrow_transient_agrees_between_fail_and_fault() {
983        fn exhausted_attempts(e: &(dyn Error + 'static)) -> u32 {
984            e.source()
985                .and_then(|s| s.downcast_ref::<Exhausted>())
986                .expect("exhaustion is the fatal's source")
987                .attempts
988        }
989
990        let t: Fail<Small> = Transient::new(TransientKind::Deadlock).into();
991        let narrowed: Fail<Small, crate::profile::WithoutTransient<AllLanes>> =
992            Laned::narrow_transient(t, 2);
993        assert_eq!(narrowed.lane(), Lane::Fatal);
994        assert_eq!(exhausted_attempts(narrowed.as_fatal().unwrap()), 2);
995
996        let t: Fault = Transient::new(TransientKind::Deadlock).into();
997        let narrowed: Fault<crate::profile::WithoutTransient<AllLanes>> =
998            Laned::narrow_transient(t, 2);
999        assert_eq!(narrowed.lane(), Lane::Fatal);
1000        assert_eq!(exhausted_attempts(narrowed.as_fatal().unwrap()), 2);
1001    }
1002}