Skip to main content

apollo_errors/
catalog.rs

1//! Error catalog with expanded transparent variants
2//!
3//! This module provides functions to build an error catalog where transparent variants
4//! are expanded to show the actual variants they forward to.
5
6use crate::metadata::{ErrorMetadata, FieldMetadata, RegularVariantMetadata, VariantMetadata};
7use crate::registry::ERROR_REGISTRY;
8use serde::{Serialize, Serializer};
9
10/// Serialize StatusCode as its u16 value for JSON output
11fn serialize_status_code<S: Serializer>(
12    status: &http::StatusCode,
13    serializer: S,
14) -> Result<S::Ok, S::Error> {
15    serializer.serialize_u16(status.as_u16())
16}
17
18/// An error type entry in the expanded catalog
19#[derive(Debug, Clone, Serialize)]
20#[serde(rename_all = "camelCase")]
21pub struct CatalogErrorEntry {
22    /// The Rust type name
23    pub type_name: &'static str,
24
25    /// All variants (with transparent variants expanded)
26    pub variants: Vec<CatalogVariantEntry>,
27}
28
29/// A variant entry in the expanded catalog
30///
31/// This is always a "regular" variant - transparent variants are expanded
32/// to include the variants from the type they forward to.
33#[derive(Debug, Clone, Serialize)]
34#[serde(rename_all = "camelCase")]
35pub struct CatalogVariantEntry {
36    /// The variant name
37    pub name: &'static str,
38
39    /// Error message template
40    pub message: &'static str,
41
42    /// Error code (e.g., "AUTH_FAILED")
43    pub code: &'static str,
44
45    /// HTTP status code
46    #[serde(serialize_with = "serialize_status_code")]
47    pub http_status: http::StatusCode,
48
49    /// Optional help text
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub help: Option<&'static str>,
52
53    /// Optional documentation URL
54    #[serde(skip_serializing_if = "Option::is_none")]
55    pub url: Option<&'static str>,
56
57    /// Optional severity
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub severity: Option<&'static str>,
60
61    /// Fields in this variant
62    pub fields: &'static [FieldMetadata],
63}
64
65impl From<&RegularVariantMetadata> for CatalogVariantEntry {
66    fn from(v: &RegularVariantMetadata) -> Self {
67        Self {
68            name: v.name,
69            message: v.message,
70            code: v.code,
71            http_status: http::StatusCode::from_u16(v.http_status)
72                .expect("validated at compile-time by the derive macro"),
73            help: v.help,
74            url: v.url,
75            severity: v.severity,
76            fields: v.fields,
77        }
78    }
79}
80
81/// Find error metadata by type name
82fn find_error_by_type_name(type_name: &str) -> Option<&'static ErrorMetadata> {
83    ERROR_REGISTRY
84        .iter()
85        .find(|entry| entry.metadata.type_name == type_name)
86        .map(|entry| entry.metadata)
87}
88
89/// Expand variants, resolving transparent variants to their inner type's variants
90fn expand_variants(variants: &[VariantMetadata]) -> Vec<CatalogVariantEntry> {
91    let mut result = Vec::new();
92
93    for variant in variants {
94        match variant {
95            VariantMetadata::Regular(regular) => {
96                result.push(CatalogVariantEntry::from(regular));
97            }
98            VariantMetadata::Transparent(transparent) => {
99                // Look up the forwarded type and include its variants
100                if let Some(inner_metadata) = find_error_by_type_name(transparent.forward_to) {
101                    // Recursively expand in case the inner type also has transparent variants
102                    result.extend(expand_variants(inner_metadata.variants));
103                }
104                // If the type isn't found in the registry, we skip it
105                // (it might not be registered, e.g., a generic type)
106            }
107        }
108    }
109
110    result
111}
112
113/// Build an expanded error catalog
114///
115/// Returns a list of all registered error types with their variants.
116/// Transparent variants are expanded to show the actual variants from
117/// the type they forward to.
118///
119/// # Example
120///
121/// ```rust,ignore
122/// use apollo_errors::error_catalog;
123///
124/// let catalog = error_catalog();
125/// let json = serde_json::to_string_pretty(&catalog).unwrap();
126/// println!("{}", json);
127/// ```
128pub fn error_catalog() -> Vec<CatalogErrorEntry> {
129    ERROR_REGISTRY
130        .iter()
131        .map(|entry| {
132            let metadata = entry.metadata;
133            CatalogErrorEntry {
134                type_name: metadata.type_name,
135                variants: expand_variants(metadata.variants),
136            }
137        })
138        .collect()
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144
145    #[test]
146    fn test_catalog_returns_entries() {
147        let catalog = error_catalog();
148        // We can't test specific entries without having error types registered,
149        // but we can verify the function works
150        assert!(catalog.is_empty() || !catalog.is_empty());
151    }
152}