waddling-errors-macros 0.7.3

Procedural macros for structured error codes with compile-time validation and taxonomy enforcement
Documentation
//! Implementation of the `component_location!` macro
//!
//! A standalone macro for registering component locations without needing to attach
//! to a module. This is more flexible than `#[in_component]` and supports:
//!
//! - Multiple component locations per file
//! - File-level usage without wrapping code
//! - Folder-level markers (in mod.rs)
//!
//! # Examples
//!
//! ```ignore
//! // Single component location
//! component_location!(Auth);  // Defaults to internal role
//!
//! // With explicit role
//! component_location!(Auth, role = public);
//!
//! // Multiple components in same file
//! component_location!(Api, role = public);
//! component_location!(Auth, role = developer);
//!
//! // In mod.rs to mark a folder
//! component_location!(Database);  // Marks this folder as Database component
//! ```

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

/// Arguments for component_location! macro
/// Supports: component_location!(ComponentName) or component_location!(ComponentName, role = Role)
struct ComponentLocationArgs {
    component_name: Ident,
    role: Option<Ident>,
}

impl Parse for ComponentLocationArgs {
    fn parse(input: ParseStream) -> Result<Self> {
        let component_name = input.parse()?;

        let mut role = None;

        // Check for optional role parameter
        if input.peek(Token![,]) {
            let _ = input.parse::<Token![,]>()?;

            // Check if there's a role = ... pattern
            if input.peek(Ident) {
                let key: Ident = input.parse()?;
                if key == "role" {
                    let _ = input.parse::<Token![=]>()?;
                    role = Some(input.parse()?);

                    // Allow optional trailing comma
                    if input.peek(Token![,]) {
                        let _ = input.parse::<Token![,]>()?;
                    }
                }
            }
        }

        Ok(ComponentLocationArgs {
            component_name,
            role,
        })
    }
}

pub fn expand(input: TokenStream) -> TokenStream {
    // Parse the macro arguments
    let args = match syn::parse::<ComponentLocationArgs>(input) {
        Ok(args) => args,
        Err(err) => {
            let err_msg = err.to_string();
            return TokenStream::from(quote! {
                compile_error!(#err_msg);
            });
        }
    };

    let component_name = args.component_name;
    // Preserve original case to match how component! macro registers components
    // component!(Auth) registers as "Auth", so component_location!(Auth) should too
    let component_str = component_name.to_string();
    // For the module name, use lowercase to ensure valid Rust identifier
    let component_str_lower = component_str.to_lowercase();

    // Parse role parameter with secure defaults
    let role_enum = if let Some(role_ident) = args.role {
        let role_str = role_ident.to_string().to_lowercase();
        match role_str.as_str() {
            "public" => quote! { Some(::waddling_errors::Role::Public) },
            "developer" => quote! { Some(::waddling_errors::Role::Developer) },
            "internal" => quote! { Some(::waddling_errors::Role::Internal) },
            _ => {
                let err_msg = format!(
                    "Invalid role '{}'. Expected: public, developer, or internal",
                    role_str
                );
                return TokenStream::from(quote! {
                    compile_error!(#err_msg);
                });
            }
        }
    } else {
        // Default to Internal (SECURE BY DEFAULT)
        quote! { Some(::waddling_errors::Role::Internal) }
    };

    // Generate module name based on component name (lowercase for valid Rust identifier).
    // This is intentionally NOT unique per-invocation: calling component_location!(Auth)
    // twice in the same file will cause a "duplicate module" compile error, which is
    // correct feedback - marking the same component twice in one file is redundant.
    // Different components (e.g., Auth and Api) will get different module names.
    let marker_mod_name = Ident::new(
        &format!("__component_loc_{}", component_str_lower),
        Span::call_site(),
    );

    // Generate function name for ctor auto-registration.
    // Same intentional collision behavior as the module name above.
    let register_fn_name = Ident::new(
        &format!("__register_component_loc_{}", component_str_lower),
        Span::call_site(),
    );

    // Generate the marker module with metadata and auto-registration
    let output = quote! {
        #[doc(hidden)]
        #[allow(non_snake_case, dead_code)]
        pub mod #marker_mod_name {
            /// Component name (preserves case, matches component! registration)
            pub const COMPONENT: &str = #component_str;

            /// File path where this component is located
            pub const FILE: &str = file!();

            /// Module path
            pub const MODULE_PATH: &str = module_path!();

            /// Role visibility for this component location
            pub const ROLE: Option<::waddling_errors::Role> = #role_enum;

            /// Register this component location with a DocRegistry
            ///
            /// This is called automatically via ctor when auto-register feature is enabled.
            /// You can also call it manually if needed.
            #[cfg(feature = "metadata")]
            pub fn register(registry: &mut ::waddling_errors::doc_generator::DocRegistry) {
                registry.register_component_location_with_role(COMPONENT, FILE, ROLE);
            }

            // Auto-register via ctor when auto-register feature is enabled
            #[cfg(all(feature = "auto-register", feature = "metadata"))]
            #[::ctor::ctor]
            fn #register_fn_name() {
                ::waddling_errors::registry::register_component_location(
                    COMPONENT,
                    FILE,
                    ROLE,
                );
            }
        }
    };

    TokenStream::from(output)
}