dterror-derive 0.2.0

Proc-macros for the dterror CtxError and FromContext traits
Documentation
//! This module parses the container-level `#[context(derive(...))]` attribute.
//! The attribute selects which traits the derive adds to `{Error}Ctx` or `{Kind}Ctx`.

use proc_macro2::TokenStream as TokenStream2;
use quote::{ToTokens, quote};
use syn::{Attribute, DeriveInput, Path};

/// The traits that the derive adds to the generated Ctx type. By default, the list is empty.
#[derive(Default)]
pub(crate) struct CtxDerives {
    pub(crate) traits: Vec<Path>,
}

impl CtxDerives {
    /// Returns the `#[derive(...)]` attribute that the derive emits. If you do not select a trait,
    /// this function returns an empty token stream.
    pub(crate) fn derive_attr(&self) -> TokenStream2 {
        if self.traits.is_empty() {
            return TokenStream2::new();
        }
        let traits = &self.traits;
        quote! { #[derive(#(#traits),*)] }
    }
}

/// The derive parses the container-level `#[context(derive(Trait, ...))]` attribute.
///
/// The derive accepts at most one container-level `#[context(...)]` attribute.
/// The attribute may contain only the `derive` helper.
/// The list replaces the default (an empty list).
/// If you omit the attribute, the derive adds no traits.
/// `#[context(derive())]` has the same result as when you omit the attribute.
/// You can add trait paths that `#[derive(...)]` accepts.
/// The derive rejects duplicate names.
pub(crate) fn parse_ctx_derives(input: &DeriveInput) -> syn::Result<CtxDerives> {
    let attrs: Vec<&Attribute> = input
        .attrs
        .iter()
        .filter(|attr| attr.path().is_ident("context"))
        .collect();

    if let Some(duplicate) = attrs.get(1) {
        return Err(syn::Error::new_spanned(
            duplicate,
            "a type may carry at most one container-level `#[context(...)]` attribute",
        ));
    }

    let mut derives = CtxDerives::default();

    for attr in &attrs {
        attr.parse_nested_meta(|meta| {
            if !meta.path.is_ident("derive") {
                return Err(meta.error(
                    "unknown `#[context(...)]` helper at container level; expected `derive`",
                ));
            }
            meta.parse_nested_meta(|trait_meta| {
                let path = trait_meta.path.clone();
                if derives
                    .traits
                    .iter()
                    .any(|t| t.to_token_stream().to_string() == path.to_token_stream().to_string())
                {
                    let name = path
                        .segments
                        .last()
                        .map(|segment| segment.ident.to_string())
                        .unwrap_or_default();
                    return Err(trait_meta.error(format!(
                        "duplicate trait `{name}` in `#[context(derive(...))]`"
                    )));
                }
                derives.traits.push(path);
                Ok(())
            })
        })?;
    }

    Ok(derives)
}