waddling-errors 0.7.3

Structured, secure-by-default diagnostic codes for distributed systems with no_std and role-based documentation
Documentation
//! JSON generation for error documentation

#[cfg(feature = "std")]
use std::{collections::HashMap, fs, path::Path, string::String, vec::Vec};

use super::types::{ComponentMeta, ErrorDoc, PrimaryMeta, SequenceMeta};

use crate::traits::Role;

/// Generate JSON documentation
#[cfg(feature = "serde")]
#[allow(clippy::too_many_arguments)]
pub(super) fn generate_json(
    path: impl AsRef<Path>,
    project_name: &str,
    version: &str,
    errors: &[&ErrorDoc],
    components: &HashMap<String, ComponentMeta>,
    primaries: &HashMap<String, PrimaryMeta>,
    sequences: &HashMap<u32, SequenceMeta>,
    filter_role: Option<Role>,
) -> std::io::Result<()> {
    use serde_json::json;

    // First filter errors by role (entire error visibility)
    let role_filtered_errors: Vec<&ErrorDoc> = if let Some(role) = filter_role {
        errors
            .iter()
            .filter(|e| super::html::is_visible_at_role(e.role.as_deref(), role))
            .copied()
            .collect()
    } else {
        errors.to_vec()
    };

    // Collect visible error codes for filtering component/primary metadata
    let visible_codes: std::collections::HashSet<&str> = role_filtered_errors
        .iter()
        .map(|e| e.code.as_str())
        .collect();

    // Then create JSON as HashMap keyed by code for O(1) lookups
    let target_role = filter_role.unwrap_or(crate::traits::Role::Internal);
    let filtered_errors: HashMap<String, serde_json::Value> = role_filtered_errors
        .iter()
        .map(|e| {
            #[cfg(feature = "runtime-hash")]
            let error_json = json!({
                "code": e.code,
                "severity": e.severity,
                "component": e.component,
                "primary": e.primary,
                "sequence": e.sequence,
                "description": e.description,
                "message": e.message,
                "fields": e.fields,
                "hash": e.hash,
                "namespace": e.namespace,
                "namespace_hash": e.namespace_hash,
                "hints": e.hints_for_role(target_role),
                "tags": e.tags_for_role(target_role),
                "introduced": e.introduced,
                "deprecated": e.deprecated,
                "docs_url": e.docs_url,
                "related_codes": e.related_codes_for_role(target_role),
                "see_also": e.see_also_for_role(target_role),
                "role": e.role,
                "code_snippets": e.code_snippets,
            });
            #[cfg(not(feature = "runtime-hash"))]
            let error_json = json!({
                "code": e.code,
                "severity": e.severity,
                "component": e.component,
                "primary": e.primary,
                "sequence": e.sequence,
                "description": e.description,
                "message": e.message,
                "fields": e.fields,
                "namespace": e.namespace,
                "hints": e.hints_for_role(target_role),
                "tags": e.tags_for_role(target_role),
                "introduced": e.introduced,
                "deprecated": e.deprecated,
                "docs_url": e.docs_url,
                "related_codes": e.related_codes_for_role(target_role),
                "see_also": e.see_also_for_role(target_role),
                "role": e.role,
                "code_snippets": e.code_snippets,
            });
            (e.code.clone(), error_json)
        })
        .collect();

    // Group by component
    let mut by_component: HashMap<String, Vec<String>> = HashMap::new();
    for error in &role_filtered_errors {
        by_component
            .entry(error.component.clone())
            .or_default()
            .push(error.code.clone());
    }

    // Group by primary
    let mut by_primary: HashMap<String, Vec<String>> = HashMap::new();
    for error in &role_filtered_errors {
        by_primary
            .entry(error.primary.clone())
            .or_default()
            .push(error.code.clone());
    }

    // Group by severity
    let mut by_severity: HashMap<String, Vec<String>> = HashMap::new();
    for error in &role_filtered_errors {
        by_severity
            .entry(error.severity.clone())
            .or_default()
            .push(error.code.clone());
    }

    // Create reverse hash lookup map (hash → code) for O(1) search by compact ID
    #[cfg(feature = "runtime-hash")]
    let hash_lookup: HashMap<String, String> = role_filtered_errors
        .iter()
        .filter_map(|e| e.hash.as_ref().map(|h| (h.clone(), e.code.clone())))
        .collect();

    // Filter components to only include errors that are visible
    let filtered_components: HashMap<String, serde_json::Value> = components
        .iter()
        .map(|(name, comp)| {
            let mut filtered_errors: Vec<&str> = comp
                .errors
                .iter()
                .filter(|code| visible_codes.contains(code.as_str()))
                .map(|s| s.as_str())
                .collect();
            // Ensure deterministic output
            filtered_errors.sort_unstable();
            filtered_errors.dedup();

            // Sort locations for deterministic output (registration order is non-deterministic)
            let mut sorted_locations = comp.locations.clone();
            sorted_locations
                .sort_unstable_by(|a, b| a.path.cmp(&b.path).then_with(|| a.role.cmp(&b.role)));

            (
                name.clone(),
                json!({
                    "name": comp.name,
                    "description": comp.description,
                    "examples": comp.examples,
                    "tags": comp.tags,
                    "errors": filtered_errors,
                    "error_count": filtered_errors.len(),
                    "locations": sorted_locations,
                }),
            )
        })
        .collect();

    // Filter primaries to only include errors that are visible
    let filtered_primaries: HashMap<String, serde_json::Value> = primaries
        .iter()
        .map(|(name, prim)| {
            let mut filtered_errors: Vec<&str> = prim
                .errors
                .iter()
                .filter(|code| visible_codes.contains(code.as_str()))
                .map(|s| s.as_str())
                .collect();
            // Ensure deterministic output
            filtered_errors.sort_unstable();
            filtered_errors.dedup();
            (
                name.clone(),
                json!({
                    "name": prim.name,
                    "description": prim.description,
                    "examples": prim.examples,
                    "related": prim.related,
                    "errors": filtered_errors,
                    "error_count": filtered_errors.len(),
                }),
            )
        })
        .collect();

    // Build output JSON with optional hash lookup
    #[cfg(feature = "runtime-hash")]
    let output = json!({
        "_format_version": "2.0-hashmap",
        "project": project_name,
        "version": version,
        "total_errors": filtered_errors.len(),
        "errors": filtered_errors,
        "components": filtered_components,
        "primaries": filtered_primaries,
        "sequences": sequences,
        "hash_lookup": hash_lookup,
        "search_patterns": {
            "by_component": by_component,
            "by_primary": by_primary,
            "by_severity": by_severity,
        },
        "role_filter": match filter_role {
            Some(Role::Public) => "public",
            Some(Role::Developer) => "developer",
            Some(Role::Internal) => "internal",
            None => "all",
        }
    });

    #[cfg(not(feature = "runtime-hash"))]
    let output = json!({
        "_format_version": "2.0-hashmap",
        "project": project_name,
        "version": version,
        "total_errors": filtered_errors.len(),
        "errors": filtered_errors,
        "components": filtered_components,
        "primaries": filtered_primaries,
        "sequences": sequences,
        "search_patterns": {
            "by_component": by_component,
            "by_primary": by_primary,
            "by_severity": by_severity,
        },
        "role_filter": match filter_role {
            Some(Role::Public) => "public",
            Some(Role::Developer) => "developer",
            Some(Role::Internal) => "internal",
            None => "all",
        }
    });

    if let Some(parent) = path.as_ref().parent() {
        fs::create_dir_all(parent)?;
    }

    let json_string = serde_json::to_string_pretty(&output)
        .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
    fs::write(path, json_string)
}