errlanes 0.18.1

Four error lanes (Rejected/Denied/Transient/Fatal) carried in the type, not re-derived at every layer
Documentation
use errlanes::{Fail, FatalKind, Lift, Rejection, ResultExt, Transient, TransientKind, lanes};
use std::{error::Error, time::Duration};

#[derive(Debug, errlanes::Rejection)]
enum SubscriptionRejection {
    CaughtUpTimeout {
        checkpoint: u64,
        target: u64,
        waited: Duration,
    },
    NoSuchJob {
        key: String,
    },
}

#[derive(Debug, PartialEq, errlanes::Rejection, errlanes::Lift)]
#[rejection(code = "EC_CAUGHT_UP_TIMEOUT")]
#[lift(SubscriptionRejection, variant = CaughtUpTimeout, unhandled = fatal)]
struct EcCaughtUpTimeout {
    #[lift(from = checkpoint)]
    applied: u64,
    #[lift(from = target)]
    frontier: u64,
    waited: Duration,
}

#[test]
fn selected_variant_moves_renamed_fields_and_keeps_destination_metadata() {
    let waited = Duration::from_secs(2);
    let result: Result<(), Fail<EcCaughtUpTimeout, lanes!(Fatal)>> =
        Err(SubscriptionRejection::CaughtUpTimeout {
            checkpoint: 3,
            target: 8,
            waited,
        })
        .lift();
    let Fail::Rejected(timeout) = result.unwrap_err() else {
        panic!("timeout rejection")
    };
    assert_eq!(
        timeout,
        EcCaughtUpTimeout {
            applied: 3,
            frontier: 8,
            waited
        }
    );
    assert_eq!(
        Into::<&'static str>::into(timeout.code()),
        "EC_CAUGHT_UP_TIMEOUT"
    );
}

#[test]
fn unhandled_variant_is_preserved_and_expands_to_an_invariant_fault() {
    let source = EcCaughtUpTimeout::lift(SubscriptionRejection::NoSuchJob {
        key: "missing".into(),
    })
    .unwrap_err();
    assert!(matches!(&source, SubscriptionRejection::NoSuchJob { key } if key == "missing"));
    let result: Result<(), Fail<EcCaughtUpTimeout, lanes!(Fatal)>> = Err(source).lift();
    let Fail::Fatal(fatal) = result.unwrap_err() else {
        panic!("invariant fault")
    };
    assert_eq!(fatal.kind, FatalKind::Invariant);
    assert!(matches!(
        fatal.source().unwrap().downcast_ref::<SubscriptionRejection>(),
        Some(SubscriptionRejection::NoSuchJob { key }) if key == "missing"
    ));
}

#[test]
fn lane_expansion_carriers_preserves_success_and_fault_payloads() {
    type Inner = Fail<SubscriptionRejection, lanes!(Transient, Fatal)>;
    type Outer = Fail<EcCaughtUpTimeout, lanes!(Transient, Fatal)>;
    let success: Result<_, Outer> = Ok::<_, Inner>(42).lift();
    assert_eq!(success.unwrap(), 42);
    let transient =
        Transient::new(TransientKind::Deadlock).with_source(std::io::Error::other("retry"));
    let source = transient.source_arc().unwrap().clone();
    let result: Result<(), Outer> = Err::<(), Inner>(transient.into()).lift();
    let Fail::Transient(transient) = result.unwrap_err() else {
        panic!("transient")
    };
    assert!(std::sync::Arc::ptr_eq(
        &source,
        transient.source_arc().unwrap()
    ));
}

#[derive(Debug, errlanes::Rejection)]
enum Only {
    Value { payload: String },
}

#[derive(Debug, errlanes::Rejection, errlanes::Lift)]
#[rejection(code = "TOTAL")]
#[lift(Only, variant = Value)]
struct Total {
    #[lift(from = payload)]
    value: String,
}

#[test]
fn strict_struct_lifts_generate_from_and_need_no_fatal_lane() {
    let value = Total::from(Only::Value {
        payload: "moved".into(),
    });
    assert_eq!(value.value, "moved");
    let result: Result<(), Fail<Total, lanes!()>> = Err(Only::Value {
        payload: "rejected".into(),
    })
    .map_err(Into::into);
    let Fail::Rejected(value) = result.unwrap_err();
    assert_eq!(value.value, "rejected");
}

mod generic_source {
    pub enum Source<'a, T> {
        Value { item: T, label: &'a str },
    }
}

#[derive(errlanes::Lift)]
#[lift(generic_source::Source<'a, T>, strict, variant = Value,)]
struct Generic<'a, T>
where
    T: Eq,
{
    #[lift(from = item,)]
    value: T,
    label: &'a str,
}

#[test]
fn strict_mappings_preserve_generics_lifetimes_and_where_clauses() {
    let label = String::from("borrowed");
    let result = Generic::from(generic_source::Source::Value {
        item: String::from("owned"),
        label: &label,
    });
    assert_eq!(result.value, "owned");
    assert_eq!(result.label, "borrowed");
}

enum Overlap {
    Value {
        left: String,
        right: String,
        r#type: String,
    },
}

#[derive(errlanes::Lift)]
#[lift(Overlap, variant = Value)]
struct Swapped {
    #[lift(from = right)]
    left: String,
    #[lift(from = left)]
    right: String,
    #[lift(from = r#type)]
    kind: String,
}

#[test]
fn renames_do_not_capture_other_source_fields() {
    let result = Swapped::from(Overlap::Value {
        left: "left".into(),
        right: "right".into(),
        r#type: "kind".into(),
    });
    assert_eq!(result.left, "right");
    assert_eq!(result.right, "left");
    assert_eq!(result.kind, "kind");
}