Skip to main content

buffa_reflect_derive/
lib.rs

1//! Proc-macro derive for `buffa_reflect::ReflectMessage`.
2//!
3//! See the `buffa-reflect` crate documentation for the full reflection API.
4//! This crate is exposed transparently through the default `derive` feature
5//! of `buffa-reflect`.
6//!
7//! # Recognized attributes
8//!
9//! The macro reads `#[buffa_reflect(...)]` attributes on the annotated type.
10//! Exactly one of the two descriptor-binding keys must appear:
11//!
12//! | key                         | value                                                    |
13//! | --------------------------- | -------------------------------------------------------- |
14//! | `descriptor_pool`           | Rust expression yielding `&buffa_reflect::DescriptorPool`|
15//! | `file_descriptor_set_bytes` | Rust expression yielding `&[u8]` (a serialized FDS)      |
16//! | `message_name`              | Fully-qualified proto name, e.g. `"acme.api.v1.User"`    |
17//!
18//! `buffa-reflect-build` injects all three on every generated message; you
19//! rarely need to write them by hand.
20
21use proc_macro::TokenStream;
22use proc_macro2::TokenStream as TokenStream2;
23use quote::quote;
24use syn::{Attribute, DeriveInput, Error, Expr, ExprLit, Lit, LitStr, parse_macro_input};
25
26/// Derive `buffa_reflect::ReflectMessage` for the annotated message struct.
27///
28/// See the crate-level documentation for the recognized attribute shapes.
29#[proc_macro_derive(ReflectMessage, attributes(buffa_reflect))]
30pub fn derive_reflect_message(input: TokenStream) -> TokenStream {
31    let input = parse_macro_input!(input as DeriveInput);
32    expand(&input)
33        .unwrap_or_else(Error::into_compile_error)
34        .into()
35}
36
37fn expand(input: &DeriveInput) -> Result<TokenStream2, Error> {
38    let parsed = parse_attributes(&input.attrs)?;
39    let ident = &input.ident;
40    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
41
42    let message_name = parsed.message_name.ok_or_else(|| {
43        Error::new_spanned(
44            input,
45            "missing `#[buffa_reflect(message_name = \"...\")]` attribute",
46        )
47    })?;
48    let message_name_lit = LitStr::new(&message_name.value(), message_name.span());
49
50    let body = match (parsed.pool_expr, parsed.bytes_expr) {
51        (None, None) => {
52            return Err(Error::new_spanned(
53                input,
54                "missing `#[buffa_reflect(...)]` binding: provide either `descriptor_pool = \
55                 \"...\"` or `file_descriptor_set_bytes = \"...\"`",
56            ));
57        }
58        (Some(_), Some(_)) => {
59            return Err(Error::new_spanned(
60                input,
61                "conflicting `#[buffa_reflect(...)]` bindings: provide exactly one of \
62                 `descriptor_pool` / `file_descriptor_set_bytes`",
63            ));
64        }
65        (Some(pool), None) => {
66            let pool_expr: Expr = pool.parse()?;
67            quote! {
68                let __pool: &::buffa_reflect::DescriptorPool = &#pool_expr;
69                __pool
70                    .get_message_by_name(#message_name_lit)
71                    .expect(concat!(
72                        "buffa-reflect: descriptor for `",
73                        #message_name_lit,
74                        "` not found",
75                    ))
76            }
77        }
78        (None, Some(bytes)) => {
79            let bytes_expr: Expr = bytes.parse()?;
80            quote! {
81                static __INIT: ::std::sync::OnceLock<::buffa_reflect::DescriptorPool> =
82                    ::std::sync::OnceLock::new();
83                let __pool = __INIT.get_or_init(|| {
84                    ::buffa_reflect::DescriptorPool::decode(#bytes_expr)
85                        .expect("buffa-reflect: invalid FileDescriptorSet")
86                });
87                __pool
88                    .get_message_by_name(#message_name_lit)
89                    .expect(concat!(
90                        "buffa-reflect: descriptor for `",
91                        #message_name_lit,
92                        "` not found",
93                    ))
94            }
95        }
96    };
97
98    Ok(quote! {
99        #[automatically_derived]
100        impl #impl_generics ::buffa_reflect::ReflectMessage for #ident #ty_generics #where_clause {
101            fn descriptor(&self) -> ::buffa_reflect::MessageDescriptor {
102                #body
103            }
104        }
105    })
106}
107
108#[derive(Default)]
109struct ParsedAttrs {
110    message_name: Option<LitStr>,
111    pool_expr: Option<LitStr>,
112    bytes_expr: Option<LitStr>,
113}
114
115fn parse_attributes(attrs: &[Attribute]) -> Result<ParsedAttrs, Error> {
116    let mut out = ParsedAttrs::default();
117    // `buffa-reflect-build` keys per-message attributes by FQN, but the
118    // codegen prefix-match also picks up parent-FQN attributes on nested
119    // messages. So we may legitimately see multiple `message_name` values
120    // here — keep the longest (= most specific FQN).
121    let mut all_names: Vec<LitStr> = Vec::new();
122    for attr in attrs {
123        if !attr.path().is_ident("buffa_reflect") {
124            continue;
125        }
126        attr.parse_nested_meta(|meta| {
127            let key = meta
128                .path
129                .get_ident()
130                .ok_or_else(|| meta.error("expected a key like `message_name = \"...\"`"))?
131                .to_string();
132            let value: Expr = meta.value()?.parse()?;
133            let lit = match value {
134                Expr::Lit(ExprLit {
135                    lit: Lit::Str(s), ..
136                }) => s,
137                other => {
138                    return Err(Error::new_spanned(other, "expected a string literal value"));
139                }
140            };
141            match key.as_str() {
142                "message_name" => all_names.push(lit),
143                "descriptor_pool" => set_once(&mut out.pool_expr, lit, "descriptor_pool")?,
144                "file_descriptor_set_bytes" => {
145                    set_once(&mut out.bytes_expr, lit, "file_descriptor_set_bytes")?;
146                }
147                other => {
148                    return Err(meta.error(format!(
149                        "unknown `buffa_reflect` key `{other}` (expected `message_name`, \
150                         `descriptor_pool`, or `file_descriptor_set_bytes`)"
151                    )));
152                }
153            }
154            Ok(())
155        })?;
156    }
157    out.message_name = all_names.into_iter().max_by_key(|lit| lit.value().len());
158    Ok(out)
159}
160
161fn set_once(slot: &mut Option<LitStr>, value: LitStr, key: &str) -> Result<(), Error> {
162    if slot.is_some() {
163        return Err(Error::new(
164            value.span(),
165            format!("duplicate `{key}` in `#[buffa_reflect(...)]`"),
166        ));
167    }
168    *slot = Some(value);
169    Ok(())
170}