waddling-errors-macros 0.7.3

Procedural macros for structured error codes with compile-time validation and taxonomy enforcement
Documentation
//! Implementation of the `#[in_component(...)]` attribute macro
//!
//! This attribute marks modules or files as belonging to a specific component,
//! helping with discovery, IDE navigation, and documentation organization.
//!
//! # Use Cases
//!
//! 1. **Scattered Components**: Mark files in different folders as part of same component
//!    ```ignore
//!    // src/auth/mod.rs
//!    #[in_component(Auth)]
//!
//!    // src/api/middleware.rs (scattered Auth logic)
//!    #[in_component(Auth)]
//!    ```
//!
//! 2. **Discovery**: `grep -r "#\[in_component(Auth)\]"` finds all Auth-related files
//!
//! 3. **IDE Integration**: LSP can provide "jump to component" navigation
//!
//! 4. **Documentation**: Auto-group errors by component in generated docs
//!
//! 5. **Role-Based Security**: Control visibility of component locations
//!    ```ignore
//!    // Public documentation example
//!    #[in_component(Auth, role = public)]
//!
//!    // Internal implementation (default - secure!)
//!    #[in_component(Auth)]
//!    #[in_component(Auth, role = internal)]
//!
//!    // Developer utilities
//!    #[in_component(Auth, role = developer)]
//!    ```

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

/// Parse #[in_component(ComponentName)] or #[in_component(ComponentName, role = Role)]
struct InComponentArgs {
    component_name: Ident,
    role: Option<Ident>,
}

impl Parse for InComponentArgs {
    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(InComponentArgs {
            component_name,
            role,
        })
    }
}

pub fn expand(attr: TokenStream, item: TokenStream) -> TokenStream {
    // Parse the attribute arguments
    let args = match syn::parse::<InComponentArgs>(attr) {
        Ok(args) => args,
        Err(err) => {
            let err_msg = err.to_string();
            let item_tokens: proc_macro2::TokenStream = item.into();
            return TokenStream::from(quote! {
                compile_error!(#err_msg);
                #item_tokens
            });
        }
    };

    let component_name = args.component_name;
    // Preserve original case to match how component! macro registers components
    // component!(Auth) registers as "Auth", so #[in_component(Auth)] should too
    let component_str = component_name.to_string();

    // Parse role parameter with secure defaults
    // Supported roles: public, developer, internal (case-insensitive)
    // Default: internal (secure by default!)
    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
                );
                let item_tokens: proc_macro2::TokenStream = item.into();
                return TokenStream::from(quote! {
                    compile_error!(#err_msg);
                    #item_tokens
                });
            }
        }
    } else {
        // Default to Internal (SECURE BY DEFAULT)
        quote! { Some(::waddling_errors::Role::Internal) }
    };

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

    // Try to parse as ItemMod to inject metadata inside
    let item_mod: Option<syn::ItemMod> = syn::parse2(item_tokens.clone()).ok();

    if let Some(mut item_mod) = item_mod {
        // We have a module - inject metadata inside it
        if let Some((_, ref mut content)) = item_mod.content {
            // Create the metadata constants as items inside the module
            let metadata_items: proc_macro2::TokenStream = quote! {
                /// Component this module belongs to
                #[doc(hidden)]
                pub const __COMPONENT: &str = #component_str;

                /// Module path (filled by compiler)
                #[doc(hidden)]
                pub const __COMPONENT_MODULE_PATH: &str = module_path!();

                /// File path (filled by compiler)
                #[doc(hidden)]
                pub const __COMPONENT_FILE: &str = file!();

                /// Role visibility for this component location (defaults to Internal for security)
                #[doc(hidden)]
                pub const __COMPONENT_ROLE: Option<::waddling_errors::Role> = #role_enum;

                /// Register this component location with a DocRegistry
                ///
                /// Call this during documentation generation setup to enrich
                /// component metadata with location information. The role is automatically
                /// applied based on the #[in_component] attribute.
                ///
                /// # Example
                /// ```ignore
                /// let mut registry = DocRegistry::new("myapp", "1.0.0");
                /// auth::__register_component_location(&mut registry);
                /// ```
                #[doc(hidden)]
                #[cfg(feature = "metadata")]
                pub fn __register_component_location(registry: &mut ::waddling_errors::doc_generator::DocRegistry) {
                    registry.register_component_location_with_role(__COMPONENT, __COMPONENT_FILE, __COMPONENT_ROLE);
                }
            };

            // Parse the metadata as items and prepend to module content
            let metadata_parsed: Vec<syn::Item> = match syn::parse2(metadata_items) {
                Ok(syn::File { items, .. }) => items,
                Err(_) => vec![],
            };

            // Prepend metadata items
            for item in metadata_parsed.into_iter().rev() {
                content.insert(0, item);
            }
        }

        TokenStream::from(quote! { #item_mod })
    } else {
        // Not a module - just attach metadata as a sibling module
        // (for file-level attributes on non-mod items)
        let metadata_mod_name = Ident::new(
            &format!("__component_marker_{}", component_str.to_lowercase()),
            Span::call_site(),
        );

        let output = quote! {
            #item_tokens

            #[doc(hidden)]
            #[allow(non_snake_case)]
            pub mod #metadata_mod_name {
                pub const COMPONENT: &str = #component_str;
                pub const MODULE_PATH: &str = module_path!();
                pub const FILE: &str = file!();
                pub const ROLE: Option<::waddling_errors::Role> = #role_enum;

                /// Register this component location with a DocRegistry
                #[cfg(feature = "metadata")]
                pub fn register_location(registry: &mut ::waddling_errors::doc_generator::DocRegistry) {
                    registry.register_component_location_with_role(COMPONENT, FILE, ROLE);
                }
            }
        };

        TokenStream::from(output)
    }
}