Skip to main content

base_message_macros/
lib.rs

1// Released under the MIT License.
2// Copyright, 2026, by Samuel Williams.
3
4//! The derive macro for `base-message` field sections. Use it through `base_message::FieldSection`, rather than depending on this crate directly.
5
6use proc_macro::TokenStream;
7use proc_macro2::{Span, TokenStream as TokenStream2};
8use quote::{quote, quote_spanned};
9use std::collections::BTreeMap;
10use syn::parse::ParseStream;
11use syn::spanned::Spanned;
12use syn::visit_mut::VisitMut;
13use syn::{Data, DeriveInput, Error, GenericParam, Ident, Lifetime, LitStr, Token, Type};
14
15/// Derive the interpretation of a field section from a struct's declarations.
16///
17/// Each member of the struct is declared with `#[field("name")]`, naming a field in lowercase, or `#[fields(flatten)]`, embedding another section. A declared member's type says how its instances combine: `Option<T>` for an optional singleton, `T` for a required one, and `Vec<T>` for a list or separate field. `#[field("name", invalid = "ignore")]` overrides the field type's invalid-value policy, except that a required field always treats an invalid value as an error. On the struct, `#[fields(forbid("name", ...))]` forbids fields, and `#[fields(unknown = "reject")]` rejects fields it neither declares nor forbids, which are otherwise ignored. A section may have one lifetime parameter, for values it borrows from the input.
18#[proc_macro_derive(FieldSection, attributes(field, fields))]
19pub fn derive_field_section(input: TokenStream) -> TokenStream {
20    // A derive's input always parses as an item, so errors come from the declarations.
21    syn::parse::<DeriveInput>(input)
22        .and_then(|input| expand(&input))
23        .unwrap_or_else(Error::into_compile_error)
24        .into()
25}
26
27/// The struct-level declarations.
28struct Section {
29    /// `Some(true)` for `unknown = "reject"`, `Some(false)` for an explicit `unknown = "ignore"`, with the setting's literal.
30    unknown: Option<(bool, LitStr)>,
31    forbidden: Vec<LitStr>,
32}
33
34/// Whether a declared member may be absent, must be present, or collects several values.
35#[derive(PartialEq)]
36enum Container {
37    Optional,
38    Required,
39    Many,
40}
41
42struct Declared {
43    member: Ident,
44    ty: Type,
45    name: LitStr,
46    invalid: Option<Ident>,
47    container: Container,
48}
49
50struct Flattened {
51    member: Ident,
52    ty: Type,
53    /// The type with the section's lifetime as `'static`, for checks evaluated outside the impl.
54    identity: Type,
55}
56
57/// What one member declares.
58enum Member {
59    Declared(LitStr, Option<Ident>),
60    Flattened,
61}
62
63impl Declared {
64    /// The accumulator type: `Option<T>` for a required `T`, or the declared container, spanned on the declared type so that type errors point at it.
65    fn accumulator(&self) -> TokenStream2 {
66        let ty = &self.ty;
67        match self.container {
68            Container::Required => quote_spanned!(ty.span() => ::core::option::Option<#ty>),
69            Container::Optional | Container::Many => quote!(#ty),
70        }
71    }
72
73    /// The generated call that adds one instance, spanned on the declared type so that type errors point at it.
74    fn add(&self, lifetime: &Lifetime) -> TokenStream2 {
75        let (member, name, accumulator) = (&self.member, &self.name, self.accumulator());
76        let invalid = match (&self.invalid, &self.container) {
77            (_, Container::Required) => quote!(::base_message::InvalidValue::Error),
78            (Some(invalid), _) => quote!(::base_message::InvalidValue::#invalid),
79            (None, _) => {
80                quote!(<#accumulator as ::base_message::__private::Accumulate<#lifetime>>::INVALID)
81            }
82        };
83        quote_spanned!(self.ty.span() => <#accumulator as ::base_message::__private::Accumulate<#lifetime>>::add(&mut __builder.#member, __value, __position, #name, #invalid)?)
84    }
85}
86
87/// Replaces every lifetime with `'static`. In a member's type, the only lifetime other than `'static` that can appear is the section's own.
88struct StaticLifetimes;
89
90impl VisitMut for StaticLifetimes {
91    fn visit_lifetime_mut(&mut self, lifetime: &mut Lifetime) {
92        *lifetime = Lifetime::new("'static", lifetime.span());
93    }
94}
95
96fn expand(input: &DeriveInput) -> syn::Result<TokenStream2> {
97    let Data::Struct(data) = &input.data else {
98        return Err(Error::new_spanned(
99            input,
100            "FieldSection can only be derived for a struct with named fields",
101        ));
102    };
103    let own_lifetime = section_lifetime(input)?;
104    let lifetime = own_lifetime
105        .clone()
106        .unwrap_or_else(|| Lifetime::new("'__field", Span::call_site()));
107    let to_static = |ty: &Type| {
108        let mut ty = ty.clone();
109        StaticLifetimes.visit_type_mut(&mut ty);
110        ty
111    };
112    let section = struct_section(input)?;
113    let mut declared: Vec<Declared> = Vec::new();
114    let mut flattened: Vec<Flattened> = Vec::new();
115    for field in &data.fields {
116        let member = field.ident.clone().ok_or_else(|| {
117            Error::new_spanned(
118                field,
119                "FieldSection can only be derived for a struct with named fields",
120            )
121        })?;
122        let mut kind = None;
123        for attribute in &field.attrs {
124            let is_field = attribute.path().is_ident("field");
125            if !is_field && !attribute.path().is_ident("fields") {
126                continue;
127            }
128            if kind.is_some() {
129                return Err(Error::new_spanned(
130                    attribute,
131                    "a member takes one `#[field(\"name\")]` or `#[fields(flatten)]` declaration",
132                ));
133            }
134            kind = Some(if is_field {
135                let (name, invalid) = attribute.parse_args_with(parse_field)?;
136                check_name(&name)?;
137                if declared
138                    .iter()
139                    .any(|existing| existing.name.value() == name.value())
140                {
141                    return Err(Error::new_spanned(
142                        &name,
143                        "this field name is already declared",
144                    ));
145                }
146                Member::Declared(name, invalid)
147            } else {
148                let word: Ident = attribute.parse_args()?;
149                if word == "other" {
150                    return Err(Error::new_spanned(
151                        word,
152                        "sections do not keep unknown fields; use `partition` to collect the fields a section does not take",
153                    ));
154                }
155                if word != "flatten" {
156                    return Err(Error::new_spanned(word, "expected `flatten`"));
157                }
158                Member::Flattened
159            });
160        }
161        match kind {
162            Some(Member::Declared(name, invalid)) => {
163                let container = container(&field.ty);
164                if let (Container::Required, Some(invalid)) = (&container, &invalid) {
165                    if invalid == "Ignore" {
166                        return Err(Error::new(
167                            invalid.span(),
168                            "a required field always treats an invalid value as an error; declare it as `Option<T>` to make it optional",
169                        ));
170                    }
171                }
172                declared.push(Declared {
173                    container,
174                    member,
175                    ty: field.ty.clone(),
176                    name,
177                    invalid,
178                });
179            }
180            Some(Member::Flattened) => flattened.push(Flattened {
181                member,
182                identity: to_static(&field.ty),
183                ty: field.ty.clone(),
184            }),
185            None => {
186                return Err(Error::new_spanned(
187                    field,
188                    "declare this member with `#[field(\"name\")]` or `#[fields(flatten)]`",
189                ));
190            }
191        }
192    }
193    for forbidden in &section.forbidden {
194        if declared
195            .iter()
196            .any(|declared| declared.name.value() == forbidden.value())
197        {
198            return Err(Error::new_spanned(
199                forbidden,
200                "this field name is declared, so it cannot also be forbidden",
201            ));
202        }
203    }
204
205    let ident = &input.ident;
206    let (section_type, identity) = match &own_lifetime {
207        Some(own) => (quote!(#ident<#own>), quote!(#ident<'static>)),
208        None => (quote!(#ident), quote!(#ident)),
209    };
210
211    let builder_fields = declared.iter().map(|declared| {
212        let (member, accumulator) = (&declared.member, declared.accumulator());
213        quote!(#member: ::base_message::__private::Slot<#accumulator>)
214    });
215    let flattened_builders = flattened.iter().map(|Flattened { member, ty, .. }| quote!(#member: <#ty as ::base_message::FieldSection<#lifetime>>::Builder));
216    let builder_members = declared.iter().map(|declared| &declared.member);
217    let flattened_members = flattened.iter().map(|Flattened { member, ty, .. }| quote!(#member: <#ty as ::base_message::FieldSection<#lifetime>>::builder()));
218
219    // Name dispatch jumps on the name's length and lowercased first byte, then compares the few names that share both. Forbidden names join the same jump, so refusing them costs nothing extra.
220    let mut groups: BTreeMap<(usize, u8), Vec<TokenStream2>> = BTreeMap::new();
221    for declared in &declared {
222        let (name, add) = (&declared.name, declared.add(&lifetime));
223        groups.entry(group(name)).or_default().push(quote! {
224            if __name.eq_ignore_ascii_case(#name) {
225                #add;
226                return ::core::result::Result::Ok(true);
227            }
228        });
229    }
230    for forbidden in &section.forbidden {
231        groups.entry(group(forbidden)).or_default().push(quote! {
232            if __name.eq_ignore_ascii_case(#forbidden) {
233                return ::core::result::Result::Err(::base_message::__private::forbidden(#forbidden, __position));
234            }
235        });
236    }
237    let arms = groups
238        .iter()
239        .map(|((length, first), checks)| quote!((#length, #first) => { #(#checks)* }));
240    let flattened_accepts = flattened.iter().map(|Flattened { member, ty, .. }| {
241        quote! {
242            if <#ty as ::base_message::FieldSection<#lifetime>>::accept(&mut __builder.#member, __name, __value, __position)? {
243                return ::core::result::Result::Ok(true);
244            }
245        }
246    });
247
248    let accept_declaration = accept_declaration(&declared, &flattened, &lifetime);
249
250    let finishes = declared.iter().map(|declared| {
251        let (member, name, accumulator) = (&declared.member, &declared.name, declared.accumulator());
252        let finish = quote_spanned!(declared.ty.span() => <#accumulator as ::base_message::__private::Accumulate<#lifetime>>::finish(__builder.#member));
253        match declared.container {
254            Container::Required => quote!(#member: #finish.ok_or_else(|| ::base_message::__private::missing(#name))?),
255            Container::Optional | Container::Many => quote!(#member: #finish),
256        }
257    });
258    let flattened_finishes = flattened.iter().map(|Flattened { member, ty, .. }| quote!(#member: <#ty as ::base_message::FieldSection<#lifetime>>::finish(__builder.#member)?));
259    let names = declared.iter().map(|declared| &declared.name);
260    let forbidden = &section.forbidden;
261    let flattened_names = flattened.iter().map(|Flattened { identity, .. }| quote!(&<#identity as ::base_message::FieldSection<'static>>::NAMES));
262    let rejects_unknown = matches!(section.unknown, Some((true, _)));
263    let checks = checks(&declared, &flattened, &section);
264
265    Ok(quote! {
266        const _: () = {
267            #[doc(hidden)]
268            pub struct __FieldSectionBuilder<#lifetime> {
269                #(#builder_fields,)*
270                #(#flattened_builders,)*
271                __lifetime: ::core::marker::PhantomData<&#lifetime ()>,
272            }
273
274            #[automatically_derived]
275            impl<#lifetime> ::base_message::FieldSection<#lifetime> for #section_type {
276                const NAMES: ::base_message::FieldNames = ::base_message::FieldNames::new(&[#(#names),*], &[#(#forbidden),*], &[#(#flattened_names),*], #rejects_unknown);
277
278                type Identity = #identity;
279
280                type Builder = __FieldSectionBuilder<#lifetime>;
281
282                // Not `Default`, which requires the builder to be `Sized`, so that a mismatched declaration's error refers to this crate's requirements rather than the standard library's.
283                fn builder() -> Self::Builder {
284                    __FieldSectionBuilder {
285                        #(#builder_members: ::core::default::Default::default(),)*
286                        #(#flattened_members,)*
287                        __lifetime: ::core::marker::PhantomData,
288                    }
289                }
290
291                #[inline]
292                fn accept(__builder: &mut Self::Builder, __name: &str, __value: &#lifetime [u8], __position: usize) -> ::core::result::Result<bool, ::base_message::FieldError> {
293                    if let ::core::option::Option::Some(&__first) = __name.as_bytes().first() {
294                        match (__name.len(), __first.to_ascii_lowercase()) {
295                            #(#arms)*
296                            _ => {}
297                        }
298                    }
299                    #(#flattened_accepts)*
300                    ::core::result::Result::Ok(false)
301                }
302
303                #[inline]
304                fn accept_declaration(__builder: &mut Self::Builder, __declaration: usize, __value: &#lifetime [u8], __position: usize) -> ::core::result::Result<(), ::base_message::FieldError> {
305                    #accept_declaration
306                }
307
308                fn finish(__builder: Self::Builder) -> ::core::result::Result<Self, ::base_message::FieldError> {
309                    ::core::result::Result::Ok(Self {
310                        #(#finishes,)*
311                        #(#flattened_finishes,)*
312                    })
313                }
314            }
315
316            #checks
317        };
318    })
319}
320
321/// The body of `accept_declaration`. Declaration ids number this section's own declarations in order, then each flattened section's.
322///
323/// A static table classifies only a section's own declarations, so an id the section does not have means a broken invariant, such as a hand-written `FieldSection` whose identity names another section; it panics rather than giving the value to another declaration.
324fn accept_declaration(
325    declared: &[Declared],
326    flattened: &[Flattened],
327    lifetime: &Lifetime,
328) -> TokenStream2 {
329    let own_count = declared.len();
330    let arms = declared.iter().enumerate().map(|(id, declared)| {
331        let add = declared.add(lifetime);
332        quote!(#id => { #add; ::core::result::Result::Ok(()) })
333    });
334    let routes = flattened.iter().map(|Flattened { member, ty, identity }| {
335        quote! {
336            let __count = const { <#identity as ::base_message::FieldSection<'static>>::NAMES.count() };
337            if __declaration < __count {
338                return <#ty as ::base_message::FieldSection<#lifetime>>::accept_declaration(&mut __builder.#member, __declaration, __value, __position);
339            }
340            let __declaration = __declaration - __count;
341        }
342    });
343    quote! {
344        match __declaration {
345            #(#arms)*
346            _ => {
347                let __declaration = __declaration - #own_count;
348                #(#routes)*
349                ::core::unreachable!("a static table classified a declaration id past this section's declarations")
350            }
351        }
352    }
353}
354
355/// Compile-time checks: no name is declared or forbidden both by this section and by a section it flattens, or by two flattened sections, and an explicit `unknown = "ignore"` does not contradict a flattened section that rejects unknown fields.
356fn checks(declared: &[Declared], flattened: &[Flattened], section: &Section) -> TokenStream2 {
357    let mut checks = Vec::new();
358    let names = declared
359        .iter()
360        .map(|declared| &declared.name)
361        .chain(&section.forbidden);
362    for name in names {
363        for Flattened {
364            member, identity, ..
365        } in flattened
366        {
367            let message = format!(
368                "the field name {:?} is also declared or forbidden by the section flattened as `{member}`, so the two would disagree about it",
369                name.value()
370            );
371            checks.push(quote_spanned! {name.span() =>
372                const _: () = ::core::assert!(!<#identity as ::base_message::FieldSection<'static>>::NAMES.names(#name), #message);
373            });
374        }
375    }
376    for (index, second) in flattened.iter().enumerate() {
377        for first in &flattened[..index] {
378            let message = format!(
379                "the sections flattened as `{}` and `{}` both declare or forbid a field name, so they would disagree about it",
380                first.member, second.member
381            );
382            let (left, right) = (&first.identity, &second.identity);
383            checks.push(quote_spanned! {second.member.span() =>
384                const _: () = ::core::assert!(!<#left as ::base_message::FieldSection<'static>>::NAMES.shares_names_with(&<#right as ::base_message::FieldSection<'static>>::NAMES), #message);
385            });
386        }
387    }
388    if let Some((false, setting)) = &section.unknown {
389        for Flattened {
390            member, identity, ..
391        } in flattened
392        {
393            let message = format!(
394                "the section flattened as `{member}` rejects unknown fields, so this section cannot ignore them"
395            );
396            checks.push(quote_spanned! {setting.span() =>
397                const _: () = ::core::assert!(!<#identity as ::base_message::FieldSection<'static>>::NAMES.rejects_unknown(), #message);
398            });
399        }
400    }
401    quote!(#(#checks)*)
402}
403
404/// A name's dispatch group: its length and first byte. Names are lowercase, so the first byte needs no conversion.
405fn group(name: &LitStr) -> (usize, u8) {
406    let text = name.value();
407    (text.len(), text.as_bytes()[0])
408}
409
410/// The section's lifetime parameter, if it has one. A section may borrow from its input through one lifetime, and has no type or const parameters, which the fields' interpretation could not choose.
411fn section_lifetime(input: &DeriveInput) -> syn::Result<Option<Lifetime>> {
412    let generics = &input.generics;
413    let message = "a field section may have one lifetime parameter, for values it borrows from the input, and no other generic parameters, bounds, or where clause";
414    if generics.params.len() > 1 || generics.where_clause.is_some() {
415        return Err(Error::new_spanned(generics, message));
416    }
417    match generics.params.first() {
418        None => Ok(None),
419        Some(GenericParam::Lifetime(parameter))
420            if parameter.bounds.is_empty() && parameter.attrs.is_empty() =>
421        {
422            Ok(Some(parameter.lifetime.clone()))
423        }
424        Some(parameter) => Err(Error::new_spanned(parameter, message)),
425    }
426}
427
428fn parse_field(input: ParseStream) -> syn::Result<(LitStr, Option<Ident>)> {
429    let name: LitStr = input.parse()?;
430    let mut invalid = None;
431    while !input.is_empty() {
432        input.parse::<Token![,]>()?;
433        if input.is_empty() {
434            break;
435        }
436        let key: Ident = input.parse()?;
437        if key != "invalid" {
438            return Err(Error::new_spanned(
439                key,
440                "expected `invalid = \"error\"` or `invalid = \"ignore\"`",
441            ));
442        }
443        if invalid.is_some() {
444            return Err(Error::new_spanned(key, "`invalid` is already given"));
445        }
446        input.parse::<Token![=]>()?;
447        let value: LitStr = input.parse()?;
448        invalid = Some(match value.value().as_str() {
449            "error" => Ident::new("Error", value.span()),
450            "ignore" => Ident::new("Ignore", value.span()),
451            _ => {
452                return Err(Error::new_spanned(
453                    value,
454                    "`invalid` must be \"error\" or \"ignore\"",
455                ));
456            }
457        });
458    }
459    Ok((name, invalid))
460}
461
462/// The struct-level `#[fields(unknown = "reject" | "ignore", forbid("name", ...))]` declarations; ignoring unknown fields is the default.
463fn struct_section(input: &DeriveInput) -> syn::Result<Section> {
464    let mut section = Section {
465        unknown: None,
466        forbidden: Vec::new(),
467    };
468    for attribute in &input.attrs {
469        if attribute.path().is_ident("field") {
470            return Err(Error::new_spanned(
471                attribute,
472                "`#[field(...)]` declares a member; on the struct, use `#[fields(...)]`",
473            ));
474        }
475        if !attribute.path().is_ident("fields") {
476            continue;
477        }
478        attribute.parse_args_with(|stream: ParseStream| {
479            while !stream.is_empty() {
480                let key: Ident = stream.parse()?;
481                if key == "unknown" {
482                    if section.unknown.is_some() {
483                        return Err(Error::new_spanned(key, "`unknown` is already given"));
484                    }
485                    stream.parse::<Token![=]>()?;
486                    let value: LitStr = stream.parse()?;
487                    let rejects = match value.value().as_str() {
488                        "reject" => true,
489                        "ignore" => false,
490                        _ => return Err(Error::new_spanned(value, "`unknown` must be \"reject\" or \"ignore\"; use `partition` to collect the fields a section does not take")),
491                    };
492                    section.unknown = Some((rejects, value));
493                } else if key == "forbid" {
494                    let content;
495                    syn::parenthesized!(content in stream);
496                    for name in content.parse_terminated(|input: ParseStream| input.parse::<LitStr>(), Token![,])? {
497                        check_name(&name)?;
498                        if section.forbidden.iter().any(|existing| existing.value() == name.value()) {
499                            return Err(Error::new_spanned(&name, "this field name is already forbidden"));
500                        }
501                        section.forbidden.push(name);
502                    }
503                } else {
504                    return Err(Error::new_spanned(key, "expected `unknown = \"...\"` or `forbid(\"name\", ...)`"));
505                }
506                if !stream.is_empty() {
507                    stream.parse::<Token![,]>()?;
508                }
509            }
510            Ok(())
511        })?;
512    }
513    Ok(section)
514}
515
516/// Field names are matched case-insensitively, so they are declared once, in lowercase, as tokens.
517fn check_name(name: &LitStr) -> syn::Result<()> {
518    let text = name.value();
519    let valid = !text.is_empty()
520        && text.bytes().all(|byte| {
521            byte.is_ascii_lowercase() || byte.is_ascii_digit() || b"!#$%&'*+-.^_`|~".contains(&byte)
522        });
523    if valid {
524        Ok(())
525    } else {
526        Err(Error::new_spanned(
527            name,
528            "a field name must be a lowercase token, such as \"content-type\"",
529        ))
530    }
531}
532
533/// `Option<T>` may be absent and `Vec<T>` collects values; any other type is a required singleton.
534fn container(ty: &Type) -> Container {
535    let Type::Path(path) = ty else {
536        return Container::Required;
537    };
538    match path
539        .path
540        .segments
541        .last()
542        .map(|segment| segment.ident.to_string())
543        .as_deref()
544    {
545        Some("Option") => Container::Optional,
546        Some("Vec") => Container::Many,
547        _ => Container::Required,
548    }
549}