errlanes/classify.rs
1//! How a local error enters the lanes: which part is a typed domain outcome
2//! ([`Fail::Rejected`]) and which fault lanes the rest can take.
3//!
4//! A foreign error (`sqlx::Error`, `serde_json::Error`, `reqwest::Error`, …)
5//! never implements [`Classify`] itself unless errlanes blesses it behind a
6//! `classify-*` feature (`sqlx.rs`, `serde_json.rs`, `reqwest.rs`) — a
7//! consumer wraps it in a local type that does, via
8//! `#[derive(errlanes::Classify)]` or by hand, and enters with
9//! `.classify::<Wrapper>()?`.
10
11use std::{convert::Infallible, error::Error};
12
13use crate::{
14 fail::{Fail, Fault, Lift, Rejection, UnmappedInto},
15 lane::{Fatal, Transient},
16 profile::{LaneProfile, Profile},
17};
18
19/// The rejected slot of a [`Classify`] type: a [`Rejection`], or `Infallible`
20/// for a type that never rejects — the same encoding a disabled lane slot
21/// uses.
22pub trait RejectedSlot: Send + Sync + 'static {}
23impl RejectedSlot for Infallible {}
24impl<R: Rejection> RejectedSlot for R {}
25
26/// The pairwise union of two rejected slots — `derive(Classify)` folds a
27/// mixed wrapper's `Rejected` this way across its `delegate` variants,
28/// order-independent: at most one of the two may be a genuine `Rejection`
29/// (the other `Infallible`), so there is always exactly one sensible `Out`.
30/// Two genuine, *different* `Rejection`s have no impl here at all — that is
31/// the "a mixed wrapper rejects through one type; compose them" rule,
32/// enforced by the type system rather than by the derive trying to read
33/// ahead across fields it cannot resolve the types of.
34#[doc(hidden)]
35pub trait RejectedUnion<Other: RejectedSlot>: RejectedSlot {
36 type Out: RejectedSlot;
37}
38impl RejectedUnion<Infallible> for Infallible {
39 type Out = Infallible;
40}
41impl<R: Rejection> RejectedUnion<Infallible> for R {
42 type Out = R;
43}
44impl<R: Rejection> RejectedUnion<R> for Infallible {
45 type Out = R;
46}
47impl<R: Rejection> RejectedUnion<R> for R {
48 type Out = R;
49}
50
51/// How a local error enters the lanes: which part is a typed domain outcome
52/// that a caller can correct, and which fault lanes the rest can take.
53///
54/// A pure [`Rejection`] gets this for free (the blanket below): all of it is
55/// rejected, no lanes. A fault wrapper or a mixed wrapper implements it
56/// directly, by hand or via `#[derive(errlanes::Classify)]`. A type is one or
57/// the other, never both — `Rejection` and a direct `Classify` impl on the
58/// same type conflict (`E0119`).
59#[diagnostic::on_unimplemented(
60 message = "`{Self}` does not say how it enters the lanes",
61 note = "derive `errlanes::Rejection` if a caller can correct it, or `errlanes::Classify` \
62 with a lane (`#[classify(fatal(Kind))]`, …); a foreign error is wrapped in a \
63 local type first"
64)]
65pub trait Classify: Error + Send + Sync + 'static {
66 type Rejected: RejectedSlot;
67 type Lanes: LaneProfile;
68
69 fn classify(self) -> Fail<Self::Rejected, Self::Lanes>;
70}
71
72/// A [`Rejection`] is the special case of [`Classify`]: all of it is
73/// rejected, no lanes.
74impl<R: Rejection> Classify for R {
75 type Rejected = R;
76 type Lanes = crate::profile::NoLanes;
77
78 fn classify(self) -> Fail<R, Self::Lanes> {
79 Fail::Rejected(self)
80 }
81}
82
83/// `?` into a [`Fail`]: the rejected part must be absorbed totally by `D` (no
84/// decision left for the call site to make with `.widen()`); the wrapper's
85/// lanes must fit the destination's profile. Replaces the narrower
86/// `impl<C: Rejection, D: From<C>, L> From<C> for Fail<D, L>` — a bare
87/// rejection is the `W::Rejected = W, W::Lanes = NoLanes` case of this.
88impl<W: Classify, D: Lift<W::Rejected, Unmapped = Infallible>, M: LaneProfile> From<W>
89 for Fail<D, M>
90where
91 <W::Lanes as LaneProfile>::Denied: Into<M::Denied>,
92 <W::Lanes as LaneProfile>::Transient: Into<M::Transient>,
93 <W::Lanes as LaneProfile>::Fatal: Into<M::Fatal>,
94{
95 fn from(w: W) -> Self {
96 match w.classify() {
97 Fail::Rejected(r) => match D::lift(r) {
98 Ok(d) => Fail::Rejected(d),
99 Err(never) => match never {},
100 },
101 Fail::Denied(d) => Fail::Denied(d.into()),
102 Fail::Transient(t) => Fail::Transient(t.into()),
103 Fail::Fatal(x) => Fail::Fatal(x.into()),
104 }
105 }
106}
107
108/// `?` into a [`Fault`]: only for a wrapper that never rejects. A rejection or
109/// a mixed wrapper entering a `Fault` function stays an explicit narrowing
110/// (`.map_err(Fail::narrow_rejected)`), never a silent `?`.
111impl<W: Classify<Rejected = Infallible>, M: LaneProfile> From<W> for Fault<M>
112where
113 <W::Lanes as LaneProfile>::Denied: Into<M::Denied>,
114 <W::Lanes as LaneProfile>::Transient: Into<M::Transient>,
115 <W::Lanes as LaneProfile>::Fatal: Into<M::Fatal>,
116{
117 fn from(w: W) -> Self {
118 match w.classify() {
119 Fail::Rejected(never) => match never {},
120 Fail::Denied(d) => Fault::Denied(d.into()),
121 Fail::Transient(t) => Fault::Transient(t.into()),
122 Fail::Fatal(x) => Fault::Fatal(x.into()),
123 }
124 }
125}
126
127/// A wrapper that only ever ends in the fatal lane converts into that bare
128/// payload directly, so `-> Result<T, Fatal>` is a legal, narrowest
129/// signature for a fault-only wrapper.
130impl<W: Classify<Rejected = Infallible, Lanes = Profile<false, false, true>>> From<W> for Fatal {
131 fn from(w: W) -> Self {
132 match w.classify() {
133 Fail::Rejected(never) => match never {},
134 Fail::Denied(never) => match never {},
135 Fail::Transient(never) => match never {},
136 Fail::Fatal(x) => x,
137 }
138 }
139}
140
141/// As above, for a wrapper whose only lane is transient.
142impl<W: Classify<Rejected = Infallible, Lanes = Profile<false, true, false>>> From<W>
143 for Transient
144{
145 fn from(w: W) -> Self {
146 match w.classify() {
147 Fail::Rejected(never) => match never {},
148 Fail::Denied(never) => match never {},
149 Fail::Transient(t) => t,
150 Fail::Fatal(never) => match never {},
151 }
152 }
153}
154
155/// The partial-absorption form for any [`Classify`] source — a bare
156/// rejection, a fault wrapper, or a mixed wrapper alike. `?` is the total
157/// form (the `From` impls above); `.widen()?` is this one, for when the
158/// destination's rejection only partially lifts the wrapper's rejected part.
159impl<T, C: Classify, P: Lift<C::Rejected>, M: LaneProfile> crate::fail::WidenResult<T, Fail<P, M>>
160 for Result<T, C>
161where
162 <C::Lanes as LaneProfile>::Denied: Into<M::Denied>,
163 <C::Lanes as LaneProfile>::Transient: Into<M::Transient>,
164 <C::Lanes as LaneProfile>::Fatal: Into<M::Fatal>,
165 P::Unmapped: UnmappedInto<M::Fatal>,
166{
167 fn widen(self) -> Result<T, Fail<P, M>> {
168 self.map_err(|c| match c.classify() {
169 Fail::Rejected(r) => match P::lift(r) {
170 Ok(p) => Fail::Rejected(p),
171 Err(u) => Fail::Fatal(u.unmapped_into()),
172 },
173 Fail::Denied(d) => Fail::Denied(d.into()),
174 Fail::Transient(t) => Fail::Transient(t.into()),
175 Fail::Fatal(x) => Fail::Fatal(x.into()),
176 })
177 }
178}
179
180/// The same act for a destination with no rejected lane: a wrapper that never
181/// rejects widens straight into a [`Fault<M>`]. `?` already covers the case
182/// where the destination is the function's own return type (the `From` impl
183/// above); this is for the call site that must name the destination because
184/// nothing else will infer it — above all a `Box<dyn Error>` boundary, where
185/// `?` alone would box the wrapper *unlaned* and the receiving
186/// [`Fault::classify`] would then walk past it to whatever foreign error it
187/// wraps, reverting the very classification the wrapper exists to override.
188///
189/// ```
190/// use errlanes::{Fault, ResultExt, lanes};
191///
192/// #[derive(Debug, errlanes::Classify)]
193/// #[classify(fatal(CorruptState))]
194/// #[error("could not decode stored state")]
195/// struct Stored(#[source] std::io::Error);
196///
197/// fn decode() -> Result<u8, Stored> {
198/// Err(Stored(std::io::Error::other("bad bytes")))
199/// }
200///
201/// // A boxed boundary: the destination carrier is named, then boxed.
202/// fn boundary() -> Result<u8, Box<dyn std::error::Error + Send + Sync>> {
203/// Ok(decode().widen::<Fault<lanes!(Transient, Fatal)>>()?)
204/// }
205///
206/// let fault = errlanes::Fault::classify(&*boundary().unwrap_err());
207/// assert!(matches!(fault, Fault::Fatal(f) if f.kind == errlanes::FatalKind::CorruptState));
208/// ```
209impl<T, C: Classify<Rejected = Infallible>, M: LaneProfile> crate::fail::WidenResult<T, Fault<M>>
210 for Result<T, C>
211where
212 <C::Lanes as LaneProfile>::Denied: Into<M::Denied>,
213 <C::Lanes as LaneProfile>::Transient: Into<M::Transient>,
214 <C::Lanes as LaneProfile>::Fatal: Into<M::Fatal>,
215{
216 fn widen(self) -> Result<T, Fault<M>> {
217 self.map_err(Fault::from)
218 }
219}