Skip to main content

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/// `.classify::<W>()` — the verb that turns a foreign error into a local
181/// [`Classify`] wrapper at a one-off call site, so a function that does not
182/// itself return `W` can still enter the lanes through it:
183/// `conn.query(..).await.classify::<DbWrite>()?`.
184pub trait ClassifyResult<T, E> {
185    fn classify<W: Classify + From<E>>(self) -> Result<T, W>;
186}
187
188impl<T, E> ClassifyResult<T, E> for Result<T, E> {
189    fn classify<W: Classify + From<E>>(self) -> Result<T, W> {
190        self.map_err(W::from)
191    }
192}