apollo-errors 0.7.0

Structured error handling with automatic format conversion
Documentation
//! Error catalog with expanded transparent variants
//!
//! This module provides functions to build an error catalog where transparent variants
//! are expanded to show the actual variants they forward to.

use crate::metadata::{
    CodeMetadata, ErrorMetadata, FieldMetadata, RegularVariantMetadata, VariantMetadata,
};
use crate::registry::ERROR_REGISTRY;
use serde::{Serialize, Serializer};

/// Serialize StatusCode as its u16 value for JSON output
fn serialize_status_code<S: Serializer>(
    status: &http::StatusCode,
    serializer: S,
) -> Result<S::Ok, S::Error> {
    serializer.serialize_u16(status.as_u16())
}

/// An error type entry in the expanded catalog
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CatalogErrorEntry {
    /// The Rust type name
    pub type_name: &'static str,

    /// All variants (with transparent variants expanded)
    pub variants: Vec<CatalogVariantEntry>,
}

/// A variant entry in the expanded catalog
///
/// This is always a "regular" variant - transparent variants are expanded
/// to include the variants from the type they forward to.
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CatalogVariantEntry {
    /// The variant name
    pub name: &'static str,

    /// Error message template
    pub message: &'static str,

    /// Error code variants
    pub code: CodeMetadata,

    /// HTTP status code
    #[serde(serialize_with = "serialize_status_code")]
    pub http_status: http::StatusCode,

    /// Optional help text
    #[serde(skip_serializing_if = "Option::is_none")]
    pub help: Option<&'static str>,

    /// Optional documentation URL
    #[serde(skip_serializing_if = "Option::is_none")]
    pub url: Option<&'static str>,

    /// Optional severity
    #[serde(skip_serializing_if = "Option::is_none")]
    pub severity: Option<&'static str>,

    /// Fields in this variant
    pub fields: &'static [FieldMetadata],
}

impl From<&RegularVariantMetadata> for CatalogVariantEntry {
    fn from(v: &RegularVariantMetadata) -> Self {
        Self {
            name: v.name,
            message: v.message,
            code: CodeMetadata {
                default: v.code,
                screaming_snake: v.code_screaming_snake,
                camel: v.code_camel,
                pascal: v.code_pascal,
                kebab: v.code_kebab,
            },
            http_status: crate::private::http_status_from_u16(v.http_status),
            help: v.help,
            url: v.url,
            severity: v.severity,
            fields: v.fields,
        }
    }
}

/// Find error metadata by type name
fn find_error_by_type_name(type_name: &str) -> Option<&'static ErrorMetadata> {
    ERROR_REGISTRY
        .iter()
        .find(|entry| entry.metadata.type_name == type_name)
        .map(|entry| entry.metadata)
}

/// Expand variants, resolving transparent variants to their inner type's variants
fn expand_variants(variants: &[VariantMetadata]) -> Vec<CatalogVariantEntry> {
    let mut result = Vec::new();

    for variant in variants {
        match variant {
            VariantMetadata::Regular(regular) => {
                result.push(CatalogVariantEntry::from(regular));
            }
            VariantMetadata::Transparent(transparent) => {
                // Look up the forwarded type and include its variants
                if let Some(inner_metadata) = find_error_by_type_name(transparent.forward_to) {
                    // Recursively expand in case the inner type also has transparent variants
                    result.extend(expand_variants(inner_metadata.variants));
                }
                // If the type isn't found in the registry, we skip it
                // (it might not be registered, e.g., a generic type)
            }
        }
    }

    result
}

/// Build an expanded error catalog
///
/// Returns a list of all registered error types with their variants.
/// Transparent variants are expanded to show the actual variants from
/// the type they forward to.
///
/// # Example
///
/// ```rust,ignore
/// use apollo_errors::error_catalog;
///
/// let catalog = error_catalog();
/// let json = serde_json::to_string_pretty(&catalog).unwrap();
/// println!("{}", json);
/// ```
pub fn error_catalog() -> Vec<CatalogErrorEntry> {
    ERROR_REGISTRY
        .iter()
        .map(|entry| {
            let metadata = entry.metadata;
            CatalogErrorEntry {
                type_name: metadata.type_name,
                variants: expand_variants(metadata.variants),
            }
        })
        .collect()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_catalog_returns_entries() {
        let catalog = error_catalog();
        // We can't test specific entries without having error types registered,
        // but we can verify the function works
        assert!(catalog.is_empty() || !catalog.is_empty());
    }
}