errlanes 0.18.0

Four error lanes (Rejected/Denied/Transient/Fatal) carried in the type, not re-derived at every layer
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
//! `ResultExt` — the one trait a consumer imports to move a `Result` from one
//! error signature to another. `lift`, `into_fault`, `into_fail`, the three
//! `narrow_*`, `rejected`, `classify`, and (under `tracing`) `record` all
//! relocate a `Result`'s error, so they live on one blanket-implemented trait
//! rather than one import per verb.

use std::convert::Infallible;

use crate::{
    carrier::{Carrier, IntoLanes, fail_into_fault, kind},
    classify::Classify,
    fail::{Fail, Fault, Laned, Lift, Rejection, UnmappedInto},
    lane::{Denied, Fatal},
    profile::{LaneProfile, NarrowDenied, WithoutDenied},
};

/// Sealed. The error-level engine for [`ResultExt::narrow_rejected`] — keyed
/// on the source shape: a `Fail` narrows to
/// the `Fault` of its own profile, a bare `Rejection` to the bare `Fatal`, a
/// carrier through its built-in. Both outputs are associated types, read off
/// the source, so `.narrow_rejected()?` never needs a destination named at the
/// call site.
///
/// One impl over `S: IntoLanes`, handing off to [`NarrowRejectedBy`] on the
/// concrete shape type. A separate `C: Carrier` impl would overlap the
/// `R: Rejection` one (generic against generic).
#[doc(hidden)]
#[diagnostic::on_unimplemented(
    message = "`{Self}` has no rejected lane to narrow",
    note = "`narrow_rejected` is for a `Fail<D, L>`, or a bare `Rejection`, with no \
            caller left to correct the rejection; a `Fault<L>` has no rejected lane at all"
)]
pub trait NarrowRejectedLane: narrow_sealed::Sealed {
    type Narrowed;
    fn narrow_rejected(self) -> Self::Narrowed;
}

mod narrow_sealed {
    use crate::carrier::IntoLanes;
    pub trait Sealed {}
    impl<S: IntoLanes> Sealed for S {}
}

impl<S: IntoLanes> NarrowRejectedLane for S
where
    S::Shape: NarrowRejectedBy<S>,
{
    type Narrowed = <S::Shape as NarrowRejectedBy<S>>::Narrowed;
    fn narrow_rejected(self) -> Self::Narrowed {
        <S::Shape as NarrowRejectedBy<S>>::narrow(self)
    }
}

#[doc(hidden)]
pub trait NarrowRejectedBy<S> {
    type Narrowed;
    fn narrow(s: S) -> Self::Narrowed;
}

impl<D: Rejection, L: LaneProfile<Fatal = Fatal>> NarrowRejectedBy<Fail<D, L>> for kind::Fail {
    type Narrowed = Fault<L>;
    fn narrow(s: Fail<D, L>) -> Fault<L> {
        Fail::narrow_rejected(s)
    }
}

/// A bare rejection has no profile to keep, so narrowing it yields the bare
/// payload: `Fatal(Invariant)` with the rejection as its (opaque) source. `?`
/// then carries that into any `Fault<L>` or `Fail<D, L>` whose `Fatal` lane
/// is enabled — the destination profile is read off the function signature,
/// never named at the call site.
impl<R: Rejection> NarrowRejectedBy<R> for kind::Source {
    type Narrowed = Fatal;
    fn narrow(s: R) -> Fatal {
        crate::fail::invariant_from_rejection(s)
    }
}

impl<C: Carrier> NarrowRejectedBy<C> for kind::CarrierShape
where
    C::Repr: NarrowRejectedLane,
{
    type Narrowed = <C::Repr as NarrowRejectedLane>::Narrowed;
    fn narrow(s: C) -> Self::Narrowed {
        s.into_repr().narrow_rejected()
    }
}

/// Sealed. The error-level engine for [`ResultExt::narrow_denied`], shared by
/// both carriers — `Fail` and `Fault` each still have a denied lane to
/// narrow.
#[doc(hidden)]
#[diagnostic::on_unimplemented(
    message = "`{Self}` has no denied lane to narrow",
    note = "`narrow_denied` is only available on a profile whose `Denied` lane is enabled"
)]
pub trait NarrowDeniedLane: crate::fail::sealed::Sealed {
    type Narrowed;
    fn narrow_denied(self) -> Self::Narrowed;
}

// `L: LaneProfile<Denied = Denied>` (the concrete marker, not the generic
// `Slot<Denied>`) is load-bearing: `NarrowDenied<F>` also has a blanket impl
// for `Infallible` (a disabled slot), so bounding on `NarrowDenied` alone
// would make this engine — and so `ResultExt::narrow_denied` — callable even
// when the profile has no `Denied` lane at all, turning narrowing into a
// silent identity on an already-absent lane instead of the compile error
// decision 2 requires. The inherent `Fault::narrow_denied`/`Fail::narrow_denied`
// keep the wider bound; only this engine is deliberately narrower.
impl<D: Rejection, L> NarrowDeniedLane for Fail<D, L>
where
    L: LaneProfile<Denied = Denied>,
    L::Denied: NarrowDenied<L::Fatal>,
{
    type Narrowed = Fail<D, WithoutDenied<L>>;
    fn narrow_denied(self) -> Self::Narrowed {
        Fail::narrow_denied(self)
    }
}

impl<L> NarrowDeniedLane for Fault<L>
where
    L: LaneProfile<Denied = Denied>,
    L::Denied: NarrowDenied<L::Fatal>,
{
    type Narrowed = Fault<WithoutDenied<L>>;
    fn narrow_denied(self) -> Self::Narrowed {
        Fault::narrow_denied(self)
    }
}

/// A carrier narrows through its built-in, so its own lane set governs
/// availability: a carrier with no `Denied` lane has a `Repr` with a disabled
/// `Denied` slot, and `narrow_denied` is not callable on it. The result is
/// the narrowed *built-in*, which `?` carries on.
impl<C: Carrier> NarrowDeniedLane for C
where
    C::Repr: NarrowDeniedLane,
{
    type Narrowed = <C::Repr as NarrowDeniedLane>::Narrowed;
    fn narrow_denied(self) -> Self::Narrowed {
        self.into_repr().narrow_denied()
    }
}

/// A source that can enter its own `Fault` without losing a rejection.
#[doc(hidden)]
#[diagnostic::on_unimplemented(
    message = "`{Self}` can reject and cannot enter `Fault`",
    note = "handle the rejection, call `.narrow_rejected()` when justified, or use `.into_fail()`"
)]
pub trait FaultSource: IntoLanes {
    fn own_fault(self) -> Fault<Self::Lanes>;
}
impl<W: IntoLanes<Rejected = Infallible>> FaultSource for W {
    fn own_fault(self) -> Fault<Self::Lanes> {
        fail_into_fault(self.into_lanes())
    }
}

/// A source whose built-in has a rejected lane.
#[doc(hidden)]
#[diagnostic::on_unimplemented(
    message = "`{Self}` never rejects",
    note = "use `.into_fault()` instead"
)]
pub trait FailSource: IntoLanes {
    fn own_fail(self) -> Fail<Self::Rejected, Self::Lanes>;
}
impl<W: IntoLanes> FailSource for W
where
    W::Rejected: Rejection,
{
    fn own_fail(self) -> Fail<Self::Rejected, Self::Lanes> {
        self.into_lanes()
    }
}

/// The one trait a consumer imports to move a `Result` between error
/// signatures: lift its rejection, narrow away a lane that is no
/// longer live at this boundary, hand the rejection to the caller as a value,
/// classify a foreign error into a local wrapper, or (under `tracing`) record
/// it onto the current span.
pub trait ResultExt<T, E>: Sized {
    /// Lift a rejection into a new vocabulary and enable `Fatal` for any
    /// unmapped case. `?` subsequently expands the resulting lanes.
    fn lift<P>(self) -> Result<T, Fail<P, <E::Lanes as LaneProfile>::WithFatal>>
    where
        E: IntoLanes,
        E::Rejected: Rejection,
        P: Rejection + Lift<E::Rejected>,
        <E::Lanes as LaneProfile>::Fatal: Into<Fatal>,
        P::Unmapped: UnmappedInto<Fatal>;

    /// Narrows away the `Rejected` lane: a rejection with no caller left to
    /// correct it becomes `Fatal(Invariant)`, carrying the rejection as its
    /// source. On a `Fail<D, L>` the result is `Fault<L>`. On a *bare*
    /// `Rejection` — a public method that returns just `R` because its
    /// caller can act on it, consumed by an internal frame that already
    /// proved the precondition — the result is the bare `Fatal`, and `?`
    /// carries it into whatever `Fault`/`Fail` the function returns, so the
    /// destination profile is never named at the call site:
    ///
    /// ```
    /// use errlanes::{Fault, ResultExt, lanes};
    ///
    /// #[derive(Debug, errlanes::Rejection)]
    /// #[rejection(code = "LANE_DISABLED")]
    /// struct LaneDisabled;
    ///
    /// fn listen() -> Result<(), LaneDisabled> {
    ///     Err(LaneDisabled)
    /// }
    ///
    /// // The lane was required at registration: by now, off is an invariant.
    /// fn run() -> Result<(), Fault<lanes!(Transient, Fatal)>> {
    ///     listen().narrow_rejected()?;
    ///     Ok(())
    /// }
    ///
    /// assert!(matches!(run(), Err(Fault::Fatal(_))));
    /// ```
    ///
    /// A `Fault` has no rejected lane to narrow, and that is a compile
    /// error, not an identity:
    ///
    /// ```compile_fail
    /// use errlanes::{Fault, ResultExt, lanes};
    /// fn narrow(value: Result<(), Fault<lanes!(Fatal)>>)
    ///     -> Result<(), Fault<lanes!(Fatal)>>
    /// {
    ///     value.narrow_rejected()
    /// }
    /// ```
    fn narrow_rejected(self) -> Result<T, E::Narrowed>
    where
        E: NarrowRejectedLane;

    /// Narrows away the `Transient` lane: a caller-owned retry loop hands
    /// back the narrowed profile once it stops retrying, so it stops
    /// offering `Transient` to its own callers. `attempts` is what the loop
    /// counted; an exhausted transient becomes `Fatal(Exhausted)` with the
    /// last transient as its source.
    fn narrow_transient(self, attempts: u32) -> Result<T, E::WithoutTransient>
    where
        E: Laned;

    /// Narrows away the `Denied` lane: a denial at a boundary with no
    /// subject (code running as the system) becomes `Fatal(Denied)`. Only
    /// available where `E`'s profile enables `Denied`:
    ///
    /// ```compile_fail
    /// use errlanes::{Fault, ResultExt, lanes};
    /// fn narrow(value: Result<(), Fault<lanes!(Transient, Fatal)>>)
    ///     -> Result<(), Fault<lanes!(Transient, Fatal)>>
    /// {
    ///     value.narrow_denied()
    /// }
    /// ```
    fn narrow_denied(self) -> Result<T, E::Narrowed>
    where
        E: NarrowDeniedLane;

    /// Hands the rejection to the caller as a value and keeps the faults
    /// propagating: `Result<T, Fail<D, L>>` becomes
    /// `Result<Result<T, D>, Fault<L>>`, the `Result`-level form of
    /// [`Fail::rejected`]. The outer `?` carries the faults on into any
    /// enclosing carrier; the inner `Result` is the domain outcome, with the
    /// rejection as its `Err`, matched right where it occurred:
    ///
    /// ```
    /// use errlanes::{Fail, Fault, ResultExt, lanes};
    ///
    /// #[derive(Debug, errlanes::Rejection)]
    /// #[rejection(code = "TIMED_OUT")]
    /// struct TimedOut;
    ///
    /// fn await_completion() -> Result<u64, Fail<TimedOut, lanes!(Transient, Fatal)>> {
    ///     Err(Fail::Rejected(TimedOut))
    /// }
    ///
    /// fn poll_once() -> Result<Option<u64>, Fault<lanes!(Transient, Fatal)>> {
    ///     match await_completion().rejected()? {
    ///         Ok(outcome) => Ok(Some(outcome)),
    ///         Err(TimedOut) => Ok(None),
    ///     }
    /// }
    ///
    /// assert!(poll_once().unwrap().is_none());
    /// ```
    ///
    /// This is the dual of [`narrow_rejected`](Self::narrow_rejected): that
    /// one is for a boundary with no caller left to correct the rejection,
    /// this one for the call site that is going to. There is deliberately no
    /// `Option`-returning accessor on a `Result`: an `as_rejected()` that
    /// answered `None` for both `Ok` and a fault would be the one place a
    /// lane could be dropped without naming it. Only available where `E`
    /// carries a rejected lane; a `Fault` has none:
    ///
    /// ```compile_fail
    /// use errlanes::{Fault, ResultExt, lanes};
    /// fn split(value: Result<(), Fault<lanes!(Fatal)>>) {
    ///     let _ = value.rejected();
    /// }
    /// ```
    fn rejected(self) -> Result<Result<T, E::Rejected>, Fault<E::Lanes>>
    where
        E: IntoLanes,
        E::Rejected: Rejection;

    /// Maps the rejection with a closure and leaves every other lane as it
    /// is: the `Result`-level form of [`Fail::map_rejected`]. For a
    /// type-level remap use [`lift`](Self::lift); this is for enriching a
    /// rejection with data only the call site has, such as the input that
    /// was attempted: `repo.create(new).await.map_rejected(|r| r.with_attempted(id))?`.
    fn map_rejected<D2>(self, f: impl FnOnce(E::Rejected) -> D2) -> Result<T, Fail<D2, E::Lanes>>
    where
        E: IntoLanes,
        E::Rejected: Rejection;

    /// Convert a never-rejecting source to its own `Fault` at a box or carrier boundary.
    fn into_fault(self) -> Result<T, Fault<E::Lanes>>
    where
        E: FaultSource;

    /// Convert a rejecting source to its own `Fail` at a foreign carrier boundary.
    fn into_fail(self) -> Result<T, Fail<E::Rejected, E::Lanes>>
    where
        E: FailSource;

    /// `.classify::<W>()` — the verb that turns a foreign error into a local
    /// [`Classify`] wrapper at a one-off call site, so a function that does
    /// not itself return `W` can still enter the lanes through it:
    /// `conn.query(..).await.classify::<DbWrite>()?`.
    fn classify<W: Classify + From<E>>(self) -> Result<T, W>;

    /// Records onto the current span, then hands the result straight back —
    /// for a call site that must record and keep going rather than
    /// propagate (`?`), such as a batch dispatcher writing its own
    /// `conclusion` after the fact.
    #[cfg(feature = "tracing")]
    fn record(self) -> Self
    where
        E: Laned;
}

impl<T, E> ResultExt<T, E> for Result<T, E> {
    fn lift<P>(self) -> Result<T, Fail<P, <E::Lanes as LaneProfile>::WithFatal>>
    where
        E: IntoLanes,
        E::Rejected: Rejection,
        P: Rejection + Lift<E::Rejected>,
        <E::Lanes as LaneProfile>::Fatal: Into<Fatal>,
        P::Unmapped: UnmappedInto<Fatal>,
    {
        self.map_err(|e| e.into_lanes().lift())
    }

    fn narrow_rejected(self) -> Result<T, E::Narrowed>
    where
        E: NarrowRejectedLane,
    {
        self.map_err(NarrowRejectedLane::narrow_rejected)
    }

    fn narrow_transient(self, attempts: u32) -> Result<T, E::WithoutTransient>
    where
        E: Laned,
    {
        self.map_err(|e| e.narrow_transient(attempts))
    }

    fn narrow_denied(self) -> Result<T, E::Narrowed>
    where
        E: NarrowDeniedLane,
    {
        self.map_err(NarrowDeniedLane::narrow_denied)
    }

    fn rejected(self) -> Result<Result<T, E::Rejected>, Fault<E::Lanes>>
    where
        E: IntoLanes,
        E::Rejected: Rejection,
    {
        match self {
            Ok(value) => Ok(Ok(value)),
            Err(e) => e.into_lanes().rejected().map(Err),
        }
    }

    fn map_rejected<D2>(self, f: impl FnOnce(E::Rejected) -> D2) -> Result<T, Fail<D2, E::Lanes>>
    where
        E: IntoLanes,
        E::Rejected: Rejection,
    {
        self.map_err(|e| e.into_lanes().map_rejected(f))
    }

    fn into_fault(self) -> Result<T, Fault<E::Lanes>>
    where
        E: FaultSource,
    {
        self.map_err(FaultSource::own_fault)
    }

    fn into_fail(self) -> Result<T, Fail<E::Rejected, E::Lanes>>
    where
        E: FailSource,
    {
        self.map_err(FailSource::own_fail)
    }

    fn classify<W: Classify + From<E>>(self) -> Result<T, W> {
        self.map_err(W::from)
    }

    #[cfg(feature = "tracing")]
    fn record(self) -> Self
    where
        E: Laned,
    {
        if let Err(e) = &self {
            e.record(&tracing::Span::current());
        }
        self
    }
}