apollo-errors-derive 0.4.0

Proc macro for deriving apollo-errors::Error trait
Documentation
//! Registry entry code generation

use proc_macro2::TokenStream;
use quote::{quote, quote_spanned};

use crate::codegen::helpers::{
    DEFAULT_JSONRPC_CODE, generate_downcast_attempts, generate_field_metadata, quote_option_str,
};
use crate::ir::{
    EnumDefinition, RegularVariantDefinition, TransparentVariantDefinition, VariantDefinition,
};

/// Generate variant metadata for a single variant
fn generate_variant_metadata(
    variant: &VariantDefinition,
    static_prefix: &str,
) -> (TokenStream, TokenStream) {
    match variant {
        VariantDefinition::Regular(regular) => {
            generate_regular_variant_metadata(regular, static_prefix)
        }
        VariantDefinition::Transparent(transparent) => {
            generate_transparent_variant_metadata(transparent)
        }
    }
}

/// Generate metadata for a regular variant
fn generate_regular_variant_metadata(
    variant: &RegularVariantDefinition,
    static_prefix: &str,
) -> (TokenStream, TokenStream) {
    let variant_name = variant.name.to_string();
    let message = &variant.error_message;
    let code = &variant.diagnostic_code;
    let http_status = variant.http_status.unwrap_or(500);
    let jsonrpc_code = variant.jsonrpc_code.unwrap_or(DEFAULT_JSONRPC_CODE);

    let help = quote_option_str(&variant.help_text);
    let url = quote_option_str(&variant.url);
    let severity = quote_option_str(&variant.severity);

    // Generate field metadata
    let field_metadata: Vec<_> = variant.fields.iter().map(generate_field_metadata).collect();

    // Create a static for the fields array
    let fields_static_name = syn::Ident::new(
        &format!("{}_FIELDS_{}", static_prefix, variant_name.to_uppercase()),
        variant.name.span(),
    );

    let fields_static = quote! {
        static #fields_static_name: &[::apollo_errors::private::FieldMetadata] = &[
            #(#field_metadata),*
        ];
    };

    let variant_metadata = quote! {
        ::apollo_errors::private::VariantMetadata::Regular(
            ::apollo_errors::private::RegularVariantMetadata {
                name: #variant_name,
                message: #message,
                code: #code,
                http_status: #http_status,
                jsonrpc_code: #jsonrpc_code,
                help: #help,
                url: #url,
                severity: #severity,
                fields: #fields_static_name,
            }
        )
    };

    (fields_static, variant_metadata)
}

/// Generate metadata for a transparent variant
fn generate_transparent_variant_metadata(
    variant: &TransparentVariantDefinition,
) -> (TokenStream, TokenStream) {
    let variant_name = variant.name.to_string();
    let inner_type = &variant.field.ty;
    let forward_to = quote!(#inner_type).to_string();

    // Transparent variants don't have field statics
    let fields_static = quote! {};

    let variant_metadata = quote! {
        ::apollo_errors::private::VariantMetadata::Transparent(
            ::apollo_errors::private::TransparentVariantMetadata {
                name: #variant_name,
                forward_to: #forward_to,
            }
        )
    };

    (fields_static, variant_metadata)
}

/// Generate the registry entry for this error type
pub fn generate_registry_entry(ir: &EnumDefinition) -> TokenStream {
    // References to external crates
    let private = quote!(::apollo_errors::private);
    let linkme_crate = quote!(#private::linkme);
    let serde_json_crate = quote!(#private::serde_json);

    let name = &ir.name;
    let name_str = name.to_string();

    // Create a unique static name based on type name
    let static_prefix = format!("_APOLLO_ERROR_{}", name_str.to_uppercase());
    let static_name = syn::Ident::new(&format!("{static_prefix}_REGISTRY_ENTRY"), name.span());

    // Only generate for non-generic types (generics would need special handling)
    if !ir.generics.params.is_empty() {
        let error_msg = format!(
            "Generic error type `{name_str}` cannot be registered in the global error registry. \
             Consider using a concrete type or removing generic parameters."
        );
        return quote_spanned! {name.span()=>
            compile_error!(#error_msg);
        };
    }

    // Generate variant metadata
    let mut field_statics = Vec::new();
    let mut variant_metadata = Vec::new();

    for variant in &ir.variants {
        let (fields_static, variant_meta) = generate_variant_metadata(variant, &static_prefix);
        field_statics.push(fields_static);
        variant_metadata.push(variant_meta);
    }

    // Create static for variants array
    let variants_static_name = syn::Ident::new(&format!("{static_prefix}_VARIANTS"), name.span());

    // Create static for error metadata
    let metadata_static_name = syn::Ident::new(&format!("{static_prefix}_METADATA"), name.span());

    // Generate function names for render functions (to avoid closure lifetime issues)
    let render_json_fn = syn::Ident::new(
        &format!("{}_render_json", static_prefix.to_lowercase()),
        name.span(),
    );
    let render_debug_fn = syn::Ident::new(
        &format!("{}_render_debug", static_prefix.to_lowercase()),
        name.span(),
    );
    let render_html_fn = syn::Ident::new(
        &format!("{}_render_html", static_prefix.to_lowercase()),
        name.span(),
    );
    let render_graphql_fn = syn::Ident::new(
        &format!("{}_render_graphql", static_prefix.to_lowercase()),
        name.span(),
    );
    let render_text_fn = syn::Ident::new(
        &format!("{}_render_text", static_prefix.to_lowercase()),
        name.span(),
    );
    let http_status_fn = syn::Ident::new(
        &format!("{}_http_status", static_prefix.to_lowercase()),
        name.span(),
    );
    let render_jsonrpc_fn = syn::Ident::new(
        &format!("{}_render_jsonrpc", static_prefix.to_lowercase()),
        name.span(),
    );
    let http_headers_fn = syn::Ident::new(
        &format!("{}_http_headers", static_prefix.to_lowercase()),
        name.span(),
    );

    // Generate downcast bodies for each render function
    let json_body = generate_downcast_attempts(name, "to_json");
    let debug_body = generate_downcast_attempts(name, "to_debug");
    let html_body = generate_downcast_attempts(name, "to_html");
    let graphql_body = generate_downcast_attempts(name, "to_graphql");
    let text_body = generate_downcast_attempts(name, "to_text");
    let http_status_body = generate_downcast_attempts(name, "http_status");
    let jsonrpc_body = generate_downcast_attempts(name, "to_jsonrpc");
    let http_headers_body = generate_downcast_attempts(name, "http_headers");

    quote! {
        // Field metadata statics
        #(#field_statics)*

        // Variants array
        static #variants_static_name: &[::apollo_errors::private::VariantMetadata] = &[
            #(#variant_metadata),*
        ];

        // Error metadata
        static #metadata_static_name: ::apollo_errors::private::ErrorMetadata =
            ::apollo_errors::private::ErrorMetadata {
                type_name: #name_str,
                variants: #variants_static_name,
            };

        // Render functions (defined as module-level functions to avoid closure lifetime issues)
        fn #render_json_fn(error: &(dyn std::error::Error + 'static)) -> Option<::std::result::Result<#serde_json_crate::Value, #serde_json_crate::Error>> {
            #json_body
        }

        fn #render_debug_fn(error: &(dyn std::error::Error + 'static)) -> Option<String> {
            #debug_body
        }

        fn #render_html_fn(error: &(dyn std::error::Error + 'static)) -> Option<String> {
            #html_body
        }

        fn #render_graphql_fn(error: &(dyn std::error::Error + 'static)) -> Option<::std::result::Result<#serde_json_crate::Value, #serde_json_crate::Error>> {
            #graphql_body
        }

        fn #render_text_fn(error: &(dyn std::error::Error + 'static)) -> Option<String> {
            #text_body
        }

        fn #http_status_fn(error: &(dyn std::error::Error + 'static)) -> Option<::apollo_errors::http::StatusCode> {
            #http_status_body
        }

        fn #render_jsonrpc_fn(error: &(dyn std::error::Error + 'static)) -> Option<::std::result::Result<#serde_json_crate::Value, #serde_json_crate::Error>> {
            #jsonrpc_body
        }

        fn #http_headers_fn(error: &(dyn std::error::Error + 'static)) -> Option<Vec<(::apollo_errors::http::HeaderName, ::apollo_errors::http::HeaderValue)>> {
            #http_headers_body
        }

        #[#linkme_crate::distributed_slice(::apollo_errors::private::ERROR_REGISTRY)]
        #[linkme(crate = #linkme_crate)]
        #[doc(hidden)]
        static #static_name: ::apollo_errors::private::ErrorRegistryEntry =
            ::apollo_errors::private::ErrorRegistryEntry {
                type_id: std::any::TypeId::of::<#name>(),
                type_name: #name_str,
                metadata: &#metadata_static_name,
                render_json: #render_json_fn,
                render_debug: #render_debug_fn,
                render_html: #render_html_fn,
                render_graphql: #render_graphql_fn,
                render_text: #render_text_fn,
                http_status: #http_status_fn,
                render_jsonrpc: #render_jsonrpc_fn,
                http_headers: #http_headers_fn,
            };
    }
}