apollo-errors-derive 0.5.0

Proc macro for deriving apollo-errors::Error trait
Documentation
//! Diagnostic attribute parsing

use syn::{Attribute, Result};

/// Holds all parsed diagnostic attributes for a variant
pub(crate) struct DiagnosticAttrs {
    pub(crate) code: Option<String>,
    pub(crate) help: Option<String>,
    pub(crate) url: Option<String>,
    pub(crate) severity: Option<String>,
}

/// Validate and format error code.
///
/// Error codes must follow the format `<component>::<verb?>::<error>` where:
/// - At least 2 segments separated by `::`
/// - All segments must be lowercase snake_case
/// - Verb is optional
///
/// Examples:
/// - `config::invalid` ✓
/// - `cache::auth::invalid_credentials` ✓
/// - `APOLLO_ERROR` ✗ (must use `::` separators and lowercase)
/// - `error` ✗ (must have at least 2 segments)
fn validate_error_code(path: &syn::Path) -> Result<String> {
    let segments: Vec<String> = path.segments.iter().map(|s| s.ident.to_string()).collect();

    // Must have at least 2 segments: namespace::code
    if segments.len() < 2 {
        return Err(syn::Error::new_spanned(
            path,
            format!(
                "error code must have at least 2 segments (namespace::code), got {} segment(s): `{}`",
                segments.len(),
                segments.join("::")
            ),
        ));
    }

    // All segments must be lowercase
    for segment in &segments {
        if segment != &segment.to_lowercase() {
            return Err(syn::Error::new_spanned(
                path,
                format!(
                    "error code segments must be lowercase, found `{}` in `{}`",
                    segment,
                    segments.join("::")
                ),
            ));
        }
    }

    Ok(segments.join("::"))
}

/// Parse all #[diagnostic(...)] attributes in a single pass
pub(crate) fn parse_diagnostic_attrs(attrs: &[Attribute]) -> Result<DiagnosticAttrs> {
    let mut diagnostic_attrs = DiagnosticAttrs {
        code: None,
        help: None,
        url: None,
        severity: None,
    };

    for attr in attrs {
        if attr.path().is_ident("diagnostic") {
            attr.parse_nested_meta(|meta| {
                if meta.path.is_ident("code") {
                    let content;
                    syn::parenthesized!(content in meta.input);
                    let path: syn::Path = content.parse()?;

                    // Validate and format the error code
                    let code = validate_error_code(&path)?;

                    diagnostic_attrs.code = Some(code);
                    Ok(())
                } else if meta.path.is_ident("help") {
                    let content;
                    syn::parenthesized!(content in meta.input);
                    let lit: syn::LitStr = content.parse()?;
                    diagnostic_attrs.help = Some(lit.value());
                    Ok(())
                } else if meta.path.is_ident("url") {
                    let content;
                    syn::parenthesized!(content in meta.input);
                    let lit: syn::LitStr = content.parse()?;
                    diagnostic_attrs.url = Some(lit.value());
                    Ok(())
                } else if meta.path.is_ident("severity") {
                    let content;
                    syn::parenthesized!(content in meta.input);
                    let ident: syn::Ident = content.parse()?;
                    diagnostic_attrs.severity = Some(ident.to_string());
                    Ok(())
                } else {
                    Err(meta.error(
                        "unknown diagnostic attribute; expected one of: code, help, url, severity",
                    ))
                }
            })?;
        }
    }

    Ok(diagnostic_attrs)
}