Skip to main content

errlanes_derive/
lib.rs

1//! Proc macros for `errlanes`: `#[derive(Rejection)]`, `#[derive(Lift)]`, `#[derive(Carrier)]`,
2//! `#[compose]`, and `#[instrument]`.
3
4mod carrier;
5mod classify;
6mod composition;
7mod instrument;
8mod lift;
9mod rejection;
10mod std_error;
11
12use proc_macro::TokenStream;
13use syn::{DeriveInput, parse_macro_input};
14
15// Resolve the runtime where the macro is invoked, including dependency aliases
16// and EsRepo consumers that use es_entity's reexport without a direct dependency.
17fn runtime_path() -> proc_macro2::TokenStream {
18    use proc_macro_crate::{FoundCrate, crate_name};
19    match crate_name("errlanes") {
20        Ok(FoundCrate::Name(name)) => {
21            let name = quote::format_ident!("{name}");
22            quote::quote!(#name)
23        }
24        Ok(FoundCrate::Itself) => quote::quote!(errlanes),
25        Err(_) => match crate_name("es-entity") {
26            Ok(FoundCrate::Name(name)) => {
27                let name = quote::format_ident!("{name}");
28                quote::quote!(#name::errlanes)
29            }
30            Ok(FoundCrate::Itself) => quote::quote!(es_entity::errlanes),
31            Err(_) => quote::quote!(errlanes),
32        },
33    }
34}
35
36fn resolve_runtime(tokens: proc_macro2::TokenStream) -> proc_macro2::TokenStream {
37    use proc_macro2::{Group, TokenTree};
38    let mut output = proc_macro2::TokenStream::new();
39    let mut input = tokens.into_iter().peekable();
40    let mut qualified = false;
41    while let Some(token) = input.next() {
42        let was_qualified = qualified;
43        qualified = matches!(&token, TokenTree::Punct(p) if p.as_char() == ':');
44        match token {
45            TokenTree::Ident(ref name)
46                if name == "errlanes"
47                    && !was_qualified
48                    && matches!(input.peek(), Some(TokenTree::Punct(p)) if p.as_char() == ':') =>
49            {
50                output.extend(runtime_path());
51            }
52            TokenTree::Group(group) => {
53                let mut rewritten = Group::new(group.delimiter(), resolve_runtime(group.stream()));
54                rewritten.set_span(group.span());
55                output.extend([TokenTree::Group(rewritten)]);
56            }
57            other => output.extend([other]),
58        }
59    }
60    output
61}
62
63fn expand(
64    input: TokenStream,
65    f: impl FnOnce(&DeriveInput) -> darling::Result<proc_macro2::TokenStream>,
66) -> TokenStream {
67    let ast = parse_macro_input!(input as DeriveInput);
68    match f(&ast) {
69        Ok(tokens) => resolve_runtime(tokens).into(),
70        Err(e) => e.write_errors().into(),
71    }
72}
73
74#[proc_macro_derive(Rejection, attributes(rejection, lift, error, source, from))]
75pub fn derive_rejection(input: TokenStream) -> TokenStream {
76    expand(input, rejection::derive)
77}
78
79#[proc_macro_derive(Classify, attributes(classify, rejection, lift, error, source, from))]
80pub fn derive_classify(input: TokenStream) -> TokenStream {
81    expand(input, classify::derive)
82}
83
84/// Derive error conversion mappings. A single-field tuple struct can project
85/// a source struct field with `#[lift(Source, field = name)]`, generating
86/// `From<Source>` independently of rejection metadata.
87#[proc_macro_derive(Lift, attributes(lift))]
88pub fn derive_lift(input: TokenStream) -> TokenStream {
89    expand(input, |ast| lift::derive(ast).map_err(darling::Error::from))
90}
91
92/// Derive a [carrier](https://docs.rs/errlanes/latest/errlanes/trait.Carrier.html): a
93/// crate-local lane enum that stands in for `Fault<lanes!(..)>` or `Fail<R, lanes!(..)>`.
94///
95/// ```ignore
96/// #[derive(Debug, errlanes::Carrier)]
97/// #[carrier(from(effects::HostFault))]
98/// pub enum PartyFault {
99///     /// the subject may not do this
100///     Denied(errlanes::Denied),
101///     Transient(errlanes::Transient),
102///     Fatal(errlanes::Fatal),
103/// }
104///
105/// #[derive(Debug, errlanes::Carrier)]
106/// pub enum WriteError<R> {
107///     Rejected(R),
108///     Transient(errlanes::Transient),
109///     Fatal(errlanes::Fatal),
110/// }
111/// ```
112///
113/// The variant names are the profile: each is a one-field tuple variant named from
114/// `Rejected` / `Denied` / `Transient` / `Fatal`, at most one of each. `Rejected(T)` makes the
115/// carrier `Fail`-like with rejection `T` (a concrete type or one of the enum's own generic
116/// parameters); without it the carrier is `Fault`-like and needs at least one lane.
117/// `#[carrier(from(Up, ..))]` lists other carriers that convert into this one by `?`.
118/// `Debug` is yours to derive (`Error` needs it).
119#[proc_macro_derive(Carrier, attributes(carrier))]
120pub fn derive_carrier(input: TokenStream) -> TokenStream {
121    let ast = parse_macro_input!(input as DeriveInput);
122    match carrier::derive(&ast) {
123        Ok(tokens) => resolve_runtime(tokens).into(),
124        Err(e) => e.to_compile_error().into(),
125    }
126}
127
128/// Compose rejection families from a list of sources: `Source` imports every
129/// case under its original variant name, `Source as Prefix` under a prefixed
130/// one. Collisions between unprefixed sources require a local
131/// `#[compose(merge)]` variant; use `#[compose(merge(SourceA::Case,
132/// SourceB::OtherCase))]` to select participants explicitly. Merged variants
133/// own their metadata through an explicit rejection code or payload
134/// delegation. Every source gets a total lift.
135#[proc_macro_attribute]
136pub fn compose(args: TokenStream, input: TokenStream) -> TokenStream {
137    match syn::parse::<syn::ItemEnum>(input)
138        .and_then(|item| composition::compose(args.into(), item))
139    {
140        Ok(t) => resolve_runtime(t).into(),
141        Err(e) => e.to_compile_error().into(),
142    }
143}
144
145#[doc(hidden)]
146#[proc_macro]
147pub fn __compose_rejection(input: TokenStream) -> TokenStream {
148    match composition::callback(input.into()) {
149        Ok(t) => resolve_runtime(t).into(),
150        Err(e) => e.to_compile_error().into(),
151    }
152}
153
154#[proc_macro_attribute]
155pub fn instrument(args: TokenStream, input: TokenStream) -> TokenStream {
156    match instrument::expand(args.into(), input.into()) {
157        Ok(t) => resolve_runtime(t).into(),
158        Err(e) => e.to_compile_error().into(),
159    }
160}