waddling-errors-macros 0.7.3

Procedural macros for structured error codes with compile-time validation and taxonomy enforcement
Documentation
//! Implementation of the `#[doc_gen(...)]` attribute macro
//!
//! This attribute wraps the `diag!` macro and generates automatic registration code
//! for specified documentation formats.

use proc_macro::TokenStream;
use proc_macro2::{Ident, Span};
use quote::quote;
use syn::{
    LitStr, Result, Token,
    parse::{Parse, ParseStream},
    punctuated::Punctuated,
};

/// Represents a diagnostic code parsed from token stream
/// Expected format: E.Component.Primary.SEQUENCE
struct DiagCode {
    parts: Vec<Ident>,
}

impl Parse for DiagCode {
    fn parse(input: ParseStream) -> Result<Self> {
        let mut parts = Vec::new();

        // Parse dot-separated identifiers until we hit a colon or end
        loop {
            // Parse an identifier
            parts.push(input.parse::<Ident>()?);

            // Check if there's a dot following
            if input.peek(Token![.]) {
                input.parse::<Token![.]>()?;
            } else {
                // No more dots, we're done
                break;
            }
        }

        Ok(DiagCode { parts })
    }
}

/// Parse #[doc_gen("json", "html", "custom")]
struct DocGenArgs {
    formats: Punctuated<LitStr, Token![,]>,
}

impl Parse for DocGenArgs {
    fn parse(input: ParseStream) -> Result<Self> {
        let formats = Punctuated::parse_terminated(input)?;
        Ok(DocGenArgs { formats })
    }
}

/// Extract diagnostic code from diag! macro invocation
/// Expects format: E.Component.Primary.SEQUENCE: { ... }
fn extract_diagnostic_code(tokens: &proc_macro2::TokenStream) -> Option<(String, Ident)> {
    // Use syn to parse the token stream as a DiagCode structure
    if let Ok(diag_code) = syn::parse2::<DiagCode>(tokens.clone()) {
        let parts = diag_code.parts;

        // We need at least 4 parts: E.Component.Primary.SEQUENCE
        if parts.len() >= 4 {
            // Reconstruct the code string from parsed identifiers
            let code = parts
                .iter()
                .map(|p| p.to_string())
                .collect::<Vec<_>>()
                .join(".");

            // Generate constant name: E_COMPONENT_PRIMARY_SEQUENCE_COMPLETE
            let const_name = format!(
                "{}_{}_{}_{}_COMPLETE",
                parts[0].to_string().to_uppercase(),
                parts[1].to_string().to_uppercase(),
                parts[2].to_string().to_uppercase(),
                parts[3].to_string().to_uppercase()
            );

            return Some((code, Ident::new(&const_name, Span::call_site())));
        }
    }

    None
}

pub fn expand(attr: TokenStream, item: TokenStream) -> TokenStream {
    // Parse the attribute arguments
    let args = syn::parse_macro_input!(attr as DocGenArgs);

    // Extract format names
    let format_names: Vec<String> = args.formats.iter().map(|lit| lit.value()).collect();

    // Convert TokenStream to TokenStream2 for quote!
    let item_tokens: proc_macro2::TokenStream = item.clone().into();

    // Try to extract diagnostic code and constant name
    let (diagnostic_code, const_name) = match extract_diagnostic_code(&item_tokens) {
        Some((code, name)) => (code, name),
        None => {
            // If we can't parse it, just generate the metadata without registration function
            let formats_array = format_names.iter().map(|s| s.as_str());
            let output = quote! {
                #item_tokens

                #[cfg(feature = "metadata")]
                pub mod __doc_gen_registry {
                    pub const REQUESTED_FORMATS: &[&str] = &[#(#formats_array),*];
                }
            };
            return TokenStream::from(output);
        }
    };

    // Generate format constant array
    let formats_array = format_names.iter().map(|s| s.as_str());

    // Generate registration function name (unique per diagnostic)
    let register_fn_name = Ident::new(
        &format!(
            "register_{}_for_doc_gen",
            const_name.to_string().to_lowercase()
        ),
        Span::call_site(),
    );

    let output = quote! {
        // Expand the diag! macro as-is
        #item_tokens

        // Generate registration metadata and function
        #[cfg(feature = "metadata")]
        pub mod __doc_gen_registry {
            /// Formats requested for this diagnostic
            pub const REQUESTED_FORMATS: &[&str] = &[#(#formats_array),*];

            /// Register this diagnostic with a DocRegistry
            pub fn register(registry: &mut ::waddling_errors::doc_generator::DocRegistry) {
                registry.register_diagnostic_complete(&super::#const_name);
            }
        }

        // Generate a module-level registration function
        #[cfg(feature = "metadata")]
        #[doc = concat!("Register ", #diagnostic_code, " for documentation generation")]
        pub fn #register_fn_name(registry: &mut ::waddling_errors::doc_generator::DocRegistry) {
            __doc_gen_registry::register(registry);
        }
    };

    TokenStream::from(output)
}