waddling-errors-macros 0.7.3

Procedural macros for structured error codes with compile-time validation and taxonomy enforcement
Documentation
//! Test: End-to-end `#[in_component]` to JSON documentation generation
//!
//! This test verifies that:
//! 1. `#[in_component]` properly registers component locations
//! 2. Component names are uppercased to match `diag!` normalization
//! 3. Locations appear correctly in generated JSON documentation
//!
//! Run with:
//! ```bash
//! cargo run --example in_component_doc_generation --features metadata,doc-gen
//! ```
//!
//! This test will fail if locations are not properly included in the output.

#![allow(dead_code)]

// Compile-time feature check
#[cfg(not(all(feature = "metadata", feature = "doc-gen")))]
compile_error!(
    "\n\n\
    ❌ This example requires both 'metadata' and 'doc-gen' features!\n\
    \n\
    Run with:\n\
    cargo run --example in_component_doc_generation --features metadata,doc-gen\n\
    "
);

#[cfg(all(feature = "metadata", feature = "doc-gen"))]
use std::fs;
#[cfg(all(feature = "metadata", feature = "doc-gen"))]
use waddling_errors_macros::{diag, in_component, setup};

// ============================================================================
// Setup: Components, Primaries, Sequences
// ============================================================================

#[cfg(all(feature = "metadata", feature = "doc-gen"))]
pub mod components {
    use waddling_errors_macros::component;

    component! {
        pub enum Component {
            Auth {
                docs: "Authentication and authorization",
            },
            Api {
                docs: "External API integrations",
            },
        }
    }

    pub use Component::*;
}

#[cfg(all(feature = "metadata", feature = "doc-gen"))]
pub mod primaries {
    use waddling_errors_macros::primary;

    primary! {
        pub enum Primary {
            Token {
                docs: "Token-related errors",
            },
            Connection {
                docs: "Connection errors",
            },
            Session {
                docs: "Session errors",
            },
        }
    }
}

#[cfg(all(feature = "metadata", feature = "doc-gen"))]
pub mod sequences {
    use waddling_errors_macros::sequence;

    sequence! {
        EXPIRED(1) {
            description: "Resource has expired",
        },
        INVALID(2) {
            description: "Resource is invalid",
        },
        FAILED(3) {
            description: "Operation failed",
        },
    }
}

#[cfg(all(feature = "metadata", feature = "doc-gen"))]
setup! {
    components = crate::components,
    primaries = crate::primaries,
    sequences = crate::sequences,
}

// ============================================================================
// Modules with #[in_component] - Each marks a file location for a component
// ============================================================================

/// Auth component - public documentation example
#[cfg(all(feature = "metadata", feature = "doc-gen"))]
#[in_component(Auth, role = public)]
mod auth_public_example {
    use super::*;

    /// Public auth example - shows how to use JWT tokens
    pub fn example_usage() {
        println!("Example: Using JWT authentication");
    }

    diag! {
        strict(component, primary, sequence, naming, duplicates, sequence_values, string_values),
        E.Auth.Token.EXPIRED: {
            message: "JWT token has expired",
            hints: ["Request a new token"],
        },
    }
}

/// Auth component - internal implementation
#[cfg(all(feature = "metadata", feature = "doc-gen"))]
#[in_component(Auth, role = internal)]
mod auth_internal {
    use super::*;

    /// Internal auth implementation
    pub fn verify_token() {
        println!("Internal: Verifying token signature");
    }

    diag! {
        strict(component, primary, sequence, naming, duplicates, sequence_values, string_values),
        E.Auth.Token.INVALID: {
            message: "JWT token signature invalid",
            hints: ["Check signing key"],
        },
    }
}

/// Auth component - developer debugging utilities
#[cfg(all(feature = "metadata", feature = "doc-gen"))]
#[in_component(Auth, role = developer)]
mod auth_debug {
    use super::*;

    /// Developer utility for inspecting tokens
    pub fn inspect_claims() {
        println!("Debug: Inspecting JWT claims");
    }

    diag! {
        strict(component, primary, sequence, naming, duplicates, sequence_values, string_values),
        W.Auth.Session.INVALID: {
            message: "Session validation warning",
            hints: ["Check session store"],
        },
    }
}

/// Api component - public example
#[cfg(all(feature = "metadata", feature = "doc-gen"))]
#[in_component(Api, role = public)]
mod api_example {
    use super::*;

    /// Public API usage example
    pub fn call_api() {
        println!("Example: Calling external API");
    }

    diag! {
        strict(component, primary, sequence, naming, duplicates, sequence_values, string_values),
        E.Api.Connection.FAILED: {
            message: "API connection failed",
            hints: ["Check network connectivity"],
        },
    }
}

/// Api component - internal implementation  
#[cfg(all(feature = "metadata", feature = "doc-gen"))]
#[in_component(Api)] // Defaults to internal
mod api_internal {
    use super::*;

    /// Internal API retry logic
    pub fn retry_with_backoff() {
        println!("Internal: Retry with exponential backoff");
    }

    diag! {
        strict(component, primary, sequence, naming, duplicates, sequence_values, string_values),
        E.Api.Connection.INVALID: {
            message: "API endpoint URL invalid",
            hints: ["Verify API configuration"],
        },
    }
}

// ============================================================================
// Main: Generate documentation and verify output
// ============================================================================

#[cfg(all(feature = "metadata", feature = "doc-gen"))]
fn main() {
    use waddling_errors::doc_generator::{DocRegistry, JsonRenderer};

    println!("🦆 In-Component Documentation Generation Test");
    println!("==============================================\n");

    // Step 1: Create registry and register diagnostics
    let mut registry = DocRegistry::new("in_component_test", "1.0.0");

    println!("📝 Registering diagnostics...");
    registry.register_diagnostic_runtime(&auth_public_example::E_AUTH_TOKEN_EXPIRED);
    registry.register_diagnostic_runtime(&auth_internal::E_AUTH_TOKEN_INVALID);
    registry.register_diagnostic_runtime(&auth_debug::W_AUTH_SESSION_INVALID);
    registry.register_diagnostic_runtime(&api_example::E_API_CONNECTION_FAILED);
    registry.register_diagnostic_runtime(&api_internal::E_API_CONNECTION_INVALID);
    println!("   ✓ Registered 5 diagnostics");

    // Step 2: Register component locations from #[in_component] modules
    println!("\n📍 Registering component locations...");

    // These are the auto-generated functions from #[in_component]
    auth_public_example::__register_component_location(&mut registry);
    auth_internal::__register_component_location(&mut registry);
    auth_debug::__register_component_location(&mut registry);
    api_example::__register_component_location(&mut registry);
    api_internal::__register_component_location(&mut registry);

    println!("   ✓ Registered 5 component locations");
    println!("     - Auth: 3 locations (public, developer, internal)");
    println!("     - Api: 2 locations (public, internal)");

    // Step 3: Verify component constants preserve case (matching component! definitions)
    println!("\n🔍 Verifying component name case preservation...");
    assert_eq!(
        auth_public_example::__COMPONENT,
        "Auth",
        "Expected PascalCase Auth"
    );
    assert_eq!(
        auth_internal::__COMPONENT,
        "Auth",
        "Expected PascalCase Auth"
    );
    assert_eq!(auth_debug::__COMPONENT, "Auth", "Expected PascalCase Auth");
    assert_eq!(api_example::__COMPONENT, "Api", "Expected PascalCase Api");
    assert_eq!(api_internal::__COMPONENT, "Api", "Expected PascalCase Api");
    println!("   ✓ All component names preserve PascalCase");

    // Step 4: Generate JSON documentation for all roles
    let output_dir = "target/doc/in_component_test";
    println!("\n📚 Generating documentation to {}/...", output_dir);

    match registry.render_all_roles(vec![Box::new(JsonRenderer)], output_dir) {
        Ok(_) => println!("   ✓ Documentation generated successfully"),
        Err(e) => {
            eprintln!("   ❌ Failed to generate documentation: {}", e);
            std::process::exit(1);
        }
    }

    // Step 5: Parse and verify the generated JSON
    println!("\n🔬 Verifying generated JSON output...\n");

    // Check internal JSON (should have all locations)
    let int_json_path = format!("{}/in_component_test-int.json", output_dir);
    verify_json_locations(
        &int_json_path,
        "Internal",
        &[
            ("Auth", 3, vec!["Public", "Developer", "Internal"]),
            ("Api", 2, vec!["Public", "Internal"]),
        ],
    );

    // Check developer JSON (should have public + developer locations)
    let dev_json_path = format!("{}/in_component_test-dev.json", output_dir);
    verify_json_locations(
        &dev_json_path,
        "Developer",
        &[
            ("Auth", 2, vec!["Public", "Developer"]),
            ("Api", 1, vec!["Public"]),
        ],
    );

    // Check public JSON (should only have public locations)
    let pub_json_path = format!("{}/in_component_test-pub.json", output_dir);
    verify_json_locations(
        &pub_json_path,
        "Public",
        &[("Auth", 1, vec!["Public"]), ("Api", 1, vec!["Public"])],
    );

    println!("\n✅ All verifications passed!");
    println!("\n📋 Summary:");
    println!("   - #[in_component] correctly preserves PascalCase component names");
    println!("   - Component locations are registered in DocRegistry");
    println!("   - Role-based filtering works for locations");
    println!("   - JSON output includes component locations");
    println!();
    println!("🎉 The in_component macro is working correctly!");
}

/// Verify that the generated JSON contains expected component locations
#[cfg(all(feature = "metadata", feature = "doc-gen"))]
fn verify_json_locations(json_path: &str, role_name: &str, expected: &[(&str, usize, Vec<&str>)]) {
    println!("   Checking {} JSON ({})...", role_name, json_path);

    let content = fs::read_to_string(json_path).unwrap_or_else(|e| {
        panic!("Failed to read {}: {}", json_path, e);
    });

    for (component_name, expected_count, expected_roles) in expected {
        // Simple string-based verification (no JSON parser needed)
        // Find the component section
        let component_marker = format!("\"{}\":", component_name);
        if !content.contains(&component_marker) {
            panic!(
                "Component '{}' not found in {} JSON",
                component_name, role_name
            );
        }

        // Verify expected roles are present
        for role in expected_roles {
            let role_pattern = format!("\"role\": \"{}\"", role);
            if !content.contains(&role_pattern) {
                panic!(
                    "Role '{}' not found in {} JSON for component {}",
                    role, role_name, component_name
                );
            }
        }

        // Also count total location entries for this component
        // by finding "path": patterns after the component
        let component_section_start = content.find(&component_marker).unwrap();
        let component_section = &content[component_section_start..];

        // Find the locations array for this component
        if let Some(locations_start) = component_section.find("\"locations\": [") {
            let locations_section = &component_section[locations_start..];
            // Find the closing bracket
            if let Some(locations_end) = locations_section.find(']') {
                let locations_content = &locations_section[..locations_end];
                // Count "path": occurrences
                let path_count = locations_content.matches("\"path\":").count();

                if path_count != *expected_count {
                    // Print the locations content for debugging
                    println!(
                        "      Locations content for {}: {}",
                        component_name, locations_content
                    );
                    panic!(
                        "Component '{}' in {} JSON: expected {} locations, got {}",
                        component_name, role_name, expected_count, path_count
                    );
                }

                println!("{} has {} locations", component_name, path_count);
            }
        } else {
            panic!(
                "No locations array found for component '{}'",
                component_name
            );
        }
    }
}

// Dummy main when features not enabled
#[cfg(not(all(feature = "metadata", feature = "doc-gen")))]
fn main() {}