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///
87/// An enum arm whose innermost case is written `Source::Variant { .. }` binds
88/// nothing and drops the payload: the destination is a unit variant, or a
89/// variant holding one unit leaf. It combines with `to = Variant` but not with
90/// `with`, `into`, `field` or `when`. Naming fields, `Source::Variant { a, .. }`,
91/// builds the destination from them: a variant with exactly those named fields,
92/// or one field whose struct type has exactly those fields.
93#[proc_macro_derive(Lift, attributes(lift))]
94pub fn derive_lift(input: TokenStream) -> TokenStream {
95    expand(input, |ast| lift::derive(ast).map_err(darling::Error::from))
96}
97
98/// Compose rejection families from a list of sources: `Source` imports every
99/// case under its original variant name, `Source as Prefix` under a prefixed
100/// one. Collisions between unprefixed sources require a local
101/// `#[compose(merge)]` variant; use `#[compose(merge(SourceA::Case,
102/// SourceB::OtherCase))]` to select participants explicitly. Merged variants
103/// own their metadata through an explicit rejection code or payload
104/// delegation. Every source gets a total lift.
105#[proc_macro_attribute]
106pub fn compose(args: TokenStream, input: TokenStream) -> TokenStream {
107    match syn::parse::<syn::ItemEnum>(input)
108        .and_then(|item| composition::compose(args.into(), item))
109    {
110        Ok(t) => resolve_runtime(t).into(),
111        Err(e) => e.to_compile_error().into(),
112    }
113}
114
115#[doc(hidden)]
116#[proc_macro]
117pub fn __compose_rejection(input: TokenStream) -> TokenStream {
118    match composition::callback(input.into()) {
119        Ok(t) => resolve_runtime(t).into(),
120        Err(e) => e.to_compile_error().into(),
121    }
122}
123
124#[proc_macro_attribute]
125pub fn instrument(args: TokenStream, input: TokenStream) -> TokenStream {
126    match instrument::expand(args.into(), input.into()) {
127        Ok(t) => resolve_runtime(t).into(),
128        Err(e) => e.to_compile_error().into(),
129    }
130}