Skip to main content

errlanes_derive/
lib.rs

1//! Proc macros for `errlanes`: `#[derive(Rejection)]`, `#[derive(Lift)]`, `#[compose]`, and
2//! `#[instrument]`.
3
4mod classify;
5mod composition;
6mod instrument;
7mod lift;
8mod rejection;
9mod std_error;
10
11use proc_macro::TokenStream;
12use syn::{DeriveInput, parse_macro_input};
13
14// Resolve the runtime where the macro is invoked, including dependency aliases
15// and EsRepo consumers that use es_entity's reexport without a direct dependency.
16fn runtime_path() -> proc_macro2::TokenStream {
17    use proc_macro_crate::{FoundCrate, crate_name};
18    match crate_name("errlanes") {
19        Ok(FoundCrate::Name(name)) => {
20            let name = quote::format_ident!("{name}");
21            quote::quote!(#name)
22        }
23        Ok(FoundCrate::Itself) => quote::quote!(errlanes),
24        Err(_) => match crate_name("es-entity") {
25            Ok(FoundCrate::Name(name)) => {
26                let name = quote::format_ident!("{name}");
27                quote::quote!(#name::errlanes)
28            }
29            Ok(FoundCrate::Itself) => quote::quote!(es_entity::errlanes),
30            Err(_) => quote::quote!(errlanes),
31        },
32    }
33}
34
35fn resolve_runtime(tokens: proc_macro2::TokenStream) -> proc_macro2::TokenStream {
36    use proc_macro2::{Group, TokenTree};
37    let mut output = proc_macro2::TokenStream::new();
38    let mut input = tokens.into_iter().peekable();
39    let mut qualified = false;
40    while let Some(token) = input.next() {
41        let was_qualified = qualified;
42        qualified = matches!(&token, TokenTree::Punct(p) if p.as_char() == ':');
43        match token {
44            TokenTree::Ident(ref name)
45                if name == "errlanes"
46                    && !was_qualified
47                    && matches!(input.peek(), Some(TokenTree::Punct(p)) if p.as_char() == ':') =>
48            {
49                output.extend(runtime_path());
50            }
51            TokenTree::Group(group) => {
52                let mut rewritten = Group::new(group.delimiter(), resolve_runtime(group.stream()));
53                rewritten.set_span(group.span());
54                output.extend([TokenTree::Group(rewritten)]);
55            }
56            other => output.extend([other]),
57        }
58    }
59    output
60}
61
62fn expand(
63    input: TokenStream,
64    f: impl FnOnce(&DeriveInput) -> darling::Result<proc_macro2::TokenStream>,
65) -> TokenStream {
66    let ast = parse_macro_input!(input as DeriveInput);
67    match f(&ast) {
68        Ok(tokens) => resolve_runtime(tokens).into(),
69        Err(e) => e.write_errors().into(),
70    }
71}
72
73#[proc_macro_derive(Rejection, attributes(rejection, lift, error, source, from))]
74pub fn derive_rejection(input: TokenStream) -> TokenStream {
75    expand(input, rejection::derive)
76}
77
78#[proc_macro_derive(Classify, attributes(classify, rejection, lift, error, source, from))]
79pub fn derive_classify(input: TokenStream) -> TokenStream {
80    expand(input, classify::derive)
81}
82
83/// Derive error conversion mappings. A single-field tuple struct can project
84/// a source struct field with `#[lift(Source, field = name)]`, generating
85/// `From<Source>` independently of rejection metadata.
86#[proc_macro_derive(Lift, attributes(lift))]
87pub fn derive_lift(input: TokenStream) -> TokenStream {
88    expand(input, |ast| lift::derive(ast).map_err(darling::Error::from))
89}
90
91/// Compose rejection families from a list of sources: `Source` imports every
92/// case under its original variant name, `Source as Prefix` under a prefixed
93/// one. Collisions between unprefixed sources require a local
94/// `#[compose(merge)]` variant; use `#[compose(merge(SourceA::Case,
95/// SourceB::OtherCase))]` to select participants explicitly. Merged variants
96/// own their metadata through an explicit rejection code or payload
97/// delegation. Every source gets a total lift.
98#[proc_macro_attribute]
99pub fn compose(args: TokenStream, input: TokenStream) -> TokenStream {
100    match syn::parse::<syn::ItemEnum>(input)
101        .and_then(|item| composition::compose(args.into(), item))
102    {
103        Ok(t) => resolve_runtime(t).into(),
104        Err(e) => e.to_compile_error().into(),
105    }
106}
107
108#[doc(hidden)]
109#[proc_macro]
110pub fn __compose_rejection(input: TokenStream) -> TokenStream {
111    match composition::callback(input.into()) {
112        Ok(t) => resolve_runtime(t).into(),
113        Err(e) => e.to_compile_error().into(),
114    }
115}
116
117#[proc_macro_attribute]
118pub fn instrument(args: TokenStream, input: TokenStream) -> TokenStream {
119    match instrument::expand(args.into(), input.into()) {
120        Ok(t) => resolve_runtime(t).into(),
121        Err(e) => e.to_compile_error().into(),
122    }
123}