apollo-errors-derive 0.5.0

Proc macro for deriving apollo-errors::Error trait
Documentation
//! apollo_errors::Error trait implementation generation

use proc_macro2::TokenStream;
use quote::quote;

use crate::ir::{
    EnumDefinition, FieldDefinition, HttpStatusValue, RegularVariantDefinition, VariantDefinition,
};

use super::helpers::{generate_regular_match_arms, generate_transparent_match_arms};
use crate::codegen::helpers::{
    DEFAULT_JSONRPC_CODE, generate_code_expr, generate_field_key, generate_http_headers_body,
};

/// Generate code to serialize a field value.
/// Returns `Option<(&str, Value)>` — None if the field is Option and value is None.
fn generate_field_value(field: &FieldDefinition) -> TokenStream {
    let private = quote!(::apollo_errors::private);
    let serde_json_crate = quote!(#private::serde_json);

    let rust_name = &field.rust_name;
    let field_key = generate_field_key(field);

    if field.is_option {
        quote! {
            #rust_name.as_ref().map(|__apollo_inner| {
                (#field_key, #serde_json_crate::to_value(__apollo_inner).unwrap_or(#serde_json_crate::Value::Null))
            })
        }
    } else {
        quote! {
            ::std::option::Option::Some((#field_key, #serde_json_crate::to_value(#rust_name).unwrap_or(#serde_json_crate::Value::Null)))
        }
    }
}

/// Generate field values for extension fields in a variant
fn generate_extension_field_values(variant: &RegularVariantDefinition) -> Vec<TokenStream> {
    variant
        .fields
        .iter()
        .filter(|f| f.is_extension)
        .map(generate_field_value)
        .collect()
}

/// Generate the apollo_errors::Error trait implementation
pub fn generate_apollo_error_impl(ir: &EnumDefinition) -> TokenStream {
    // References to external crates
    let private = quote!(::apollo_errors::private);
    let serde_json_crate = quote!(#private::serde_json);

    let name = &ir.name;
    let (impl_generics, ty_generics, where_clause) = ir.generics.split_for_impl();

    let to_json_arms = generate_to_json_arms(&ir.variants);
    let to_html_arms = generate_to_html_arms(&ir.variants);
    let to_graphql_arms = generate_to_graphql_arms(&ir.variants);
    let to_text_arms = generate_to_text_arms(&ir.variants);
    let to_jsonrpc_arms = generate_to_jsonrpc_arms(&ir.variants);

    // Generate http_status() method for regular variants
    let http_status_regular_arms =
        generate_regular_match_arms(&ir.variants, |variant, _pattern| {
            match &variant.http_status {
                Some(HttpStatusValue::Code(code)) => {
                    quote! { ::apollo_errors::private::http_status_from_u16(#code) }
                }
                Some(HttpStatusValue::Path(path)) => quote! { #path },
                None => quote! { ::http::StatusCode::INTERNAL_SERVER_ERROR },
            }
        });

    // Generate http_status() method for transparent variants (delegates to inner)
    let http_status_transparent_arms =
        generate_transparent_match_arms(&ir.variants, |_variant, _pattern| {
            quote! { ::apollo_errors::Error::http_status(inner) }
        });

    // Generate http_headers() method for regular variants
    let http_headers_regular_arms =
        generate_regular_match_arms(&ir.variants, |variant, _pattern| {
            generate_http_headers_body(&variant.fields, false)
        });

    // Generate http_headers() method for transparent variants (delegates to inner)
    let http_headers_transparent_arms =
        generate_transparent_match_arms(&ir.variants, |_variant, _pattern| {
            quote! { ::apollo_errors::Error::http_headers(inner) }
        });

    quote! {
        #[automatically_derived]
        impl #impl_generics ::apollo_errors::Error for #name #ty_generics #where_clause {
            fn to_json(&self, config: ::apollo_errors::private::FormatConfig) -> ::std::result::Result<#serde_json_crate::Value, #serde_json_crate::Error> {
                match self {
                    #(#to_json_arms)*
                }
            }

            fn to_html(&self, config: ::apollo_errors::private::FormatConfig) -> ::std::string::String {
                match self {
                    #(#to_html_arms)*
                }
            }

            fn to_graphql(&self, config: ::apollo_errors::private::FormatConfig) -> ::std::result::Result<#serde_json_crate::Value, #serde_json_crate::Error> {
                match self {
                    #(#to_graphql_arms)*
                }
            }

            fn to_text(&self, config: ::apollo_errors::private::FormatConfig) -> ::std::string::String {
                match self {
                    #(#to_text_arms)*
                }
            }

            fn to_jsonrpc(&self, config: ::apollo_errors::private::FormatConfig) -> ::std::result::Result<#serde_json_crate::Value, #serde_json_crate::Error> {
                let __apollo_message = ::std::string::ToString::to_string(self);
                match self {
                    #(#to_jsonrpc_arms)*
                }
            }

            fn http_status(&self) -> ::apollo_errors::http::StatusCode {
                match self {
                    #(#http_status_regular_arms)*
                    #(#http_status_transparent_arms)*
                }
            }

            fn http_headers(&self) -> Vec<(::apollo_errors::http::HeaderName, ::apollo_errors::http::HeaderValue)> {
                match self {
                    #(#http_headers_regular_arms)*
                    #(#http_headers_transparent_arms)*
                }
            }
        }
    }
}

/// Generate match arms for to_json method (returns serde_json::Value)
fn generate_to_json_arms(variants: &[VariantDefinition]) -> Vec<TokenStream> {
    // References to external crates
    let private = quote!(::apollo_errors::private);
    let serde_json_crate = quote!(#private::serde_json);

    let regular_arms = generate_regular_match_arms(variants, |variant, _pattern| {
        let field_values = generate_extension_field_values(variant);
        let code_expr = generate_code_expr(&variant.diagnostic_code);

        if field_values.is_empty() {
            quote! {
                {
                    let __apollo_code = #code_expr;
                    let __apollo_message = ::std::string::ToString::to_string(self);
                    ::std::result::Result::Ok(#serde_json_crate::json!({
                        "error": __apollo_code,
                        "message": __apollo_message
                    }))
                }
            }
        } else {
            quote! {
                {
                    let __apollo_code = #code_expr;
                    let __apollo_message = ::std::string::ToString::to_string(self);
                    let mut __apollo_obj = #serde_json_crate::json!({
                        "error": __apollo_code,
                        "message": __apollo_message
                    });
                    if let #serde_json_crate::Value::Object(ref mut __apollo_map) = __apollo_obj {
                        #(
                            if let ::std::option::Option::Some((__apollo_key, __apollo_value)) = #field_values {
                                __apollo_map.insert(__apollo_key.to_string(), __apollo_value);
                            }
                        )*
                    }
                    ::std::result::Result::Ok(__apollo_obj)
                }
            }
        }
    });

    let transparent_arms = generate_transparent_match_arms(variants, |_variant, _pattern| {
        quote! { ::apollo_errors::Error::to_json(inner, config) }
    });

    regular_arms.into_iter().chain(transparent_arms).collect()
}

/// Generate match arms for to_graphql method (returns serde_json::Value)
fn generate_to_graphql_arms(variants: &[VariantDefinition]) -> Vec<TokenStream> {
    // References to external crates
    let private = quote!(::apollo_errors::private);
    let serde_json_crate = quote!(#private::serde_json);

    let regular_arms = generate_regular_match_arms(variants, |variant, _pattern| {
        let extension_fields = generate_extension_field_values(variant);
        let code_expr = generate_code_expr(&variant.diagnostic_code);

        if extension_fields.is_empty() {
            quote! {
                {
                    let __apollo_code = #code_expr;
                    let __apollo_message = ::std::string::ToString::to_string(self);
                    ::std::result::Result::Ok(#serde_json_crate::json!({
                        "message": __apollo_message,
                        "extensions": {
                            "code": __apollo_code
                        }
                    }))
                }
            }
        } else {
            quote! {
                {
                    let __apollo_code = #code_expr;
                    let __apollo_message = ::std::string::ToString::to_string(self);
                    let mut __apollo_extensions = #serde_json_crate::json!({
                        "code": __apollo_code
                    });
                    if let #serde_json_crate::Value::Object(ref mut __apollo_map) = __apollo_extensions {
                        #(
                            if let ::std::option::Option::Some((__apollo_key, __apollo_value)) = #extension_fields {
                                __apollo_map.insert(__apollo_key.to_string(), __apollo_value);
                            }
                        )*
                    }
                    ::std::result::Result::Ok(#serde_json_crate::json!({
                        "message": __apollo_message,
                        "extensions": __apollo_extensions
                    }))
                }
            }
        }
    });

    let transparent_arms = generate_transparent_match_arms(variants, |_variant, _pattern| {
        quote! { ::apollo_errors::Error::to_graphql(inner, config) }
    });

    regular_arms.into_iter().chain(transparent_arms).collect()
}

/// Generate match arms for to_text method
fn generate_to_text_arms(variants: &[VariantDefinition]) -> Vec<TokenStream> {
    let regular_arms = generate_regular_match_arms(variants, |variant, _pattern| {
        let code_expr = generate_code_expr(&variant.diagnostic_code);
        quote! {
            {
                let __apollo_code = #code_expr;
                let __apollo_message = ::std::string::ToString::to_string(self);
                format!("[{__apollo_code}] {__apollo_message}")
            }
        }
    });

    let transparent_arms = generate_transparent_match_arms(variants, |_variant, _pattern| {
        quote! { ::apollo_errors::Error::to_text(inner, config) }
    });

    regular_arms.into_iter().chain(transparent_arms).collect()
}

/// Generate match arms for to_html method
fn generate_to_html_arms(variants: &[VariantDefinition]) -> Vec<TokenStream> {
    let regular_arms = generate_regular_match_arms(variants, |variant, _pattern| {
        let extension_fields = generate_extension_field_html(variant);
        let code_expr = generate_code_expr(&variant.diagnostic_code);

        quote! {
            {
                let __apollo_code = #code_expr;
                let __apollo_message = ::std::string::ToString::to_string(self);
                let __apollo_help_html = ::apollo_errors::private::diagnostic_help(self)
                    .map(|h| format!("<p class=\"error-help\">{}</p>", ::apollo_errors::private::HtmlEscaped(h)))
                    .unwrap_or_default();
                let __apollo_extensions_html = #extension_fields;
                format!(
                    "<div class=\"error\">\n<h3 class=\"error-code\">{}</h3>\n<p class=\"error-message\">{}</p>\n{}{}\n</div>",
                    ::apollo_errors::private::HtmlEscaped(__apollo_code),
                    ::apollo_errors::private::HtmlEscaped(&__apollo_message),
                    __apollo_help_html,
                    __apollo_extensions_html
                )
            }
        }
    });

    let transparent_arms = generate_transparent_match_arms(variants, |_variant, _pattern| {
        quote! { ::apollo_errors::Error::to_html(inner, config) }
    });

    regular_arms.into_iter().chain(transparent_arms).collect()
}

fn generate_extension_field_html(variant: &RegularVariantDefinition) -> TokenStream {
    let field_items: Vec<_> = variant
        .fields
        .iter()
        .filter(|f| f.is_extension)
        .map(|field| {
            let rust_name = &field.rust_name;
            let field_key = generate_field_key(field);

            if field.is_option {
                quote! {
                    #rust_name.as_ref().map(|__apollo_inner| {
                        format!("<div class=\"error-field\"><span class=\"field-name\">{}:</span> <span class=\"field-value\">{}</span></div>",
                            #field_key,
                            ::apollo_errors::private::HtmlEscaped(__apollo_inner)
                        )
                    })
                }
            } else {
                quote! {
                    ::std::option::Option::Some(format!("<div class=\"error-field\"><span class=\"field-name\">{}:</span> <span class=\"field-value\">{}</span></div>",
                        #field_key,
                        ::apollo_errors::private::HtmlEscaped(#rust_name)
                    ))
                }
            }
        })
        .collect();

    if field_items.is_empty() {
        quote! { ::std::string::String::new() }
    } else {
        quote! {
            {
                let mut __apollo_fields: Vec<::std::string::String> = Vec::new();
                #(
                    if let ::std::option::Option::Some(__apollo_field_html) = #field_items {
                        __apollo_fields.push(__apollo_field_html);
                    }
                )*
                if __apollo_fields.is_empty() {
                    ::std::string::String::new()
                } else {
                    format!("\n<div class=\"error-extensions\">\n{}\n</div>", __apollo_fields.join("\n"))
                }
            }
        }
    }
}

/// Generate match arms for to_jsonrpc method (returns serde_json::Value)
fn generate_to_jsonrpc_arms(variants: &[VariantDefinition]) -> Vec<TokenStream> {
    // References to external crates
    let private = quote!(::apollo_errors::private);
    let serde_json_crate = quote!(#private::serde_json);

    let regular_arms = generate_regular_match_arms(variants, |variant, _pattern| {
        let extension_fields = generate_extension_field_values(variant);
        let jsonrpc_code = variant.jsonrpc_code.unwrap_or(DEFAULT_JSONRPC_CODE);
        let code_expr = generate_code_expr(&variant.diagnostic_code);

        quote! {
            {
                let __apollo_diagnostic_code = #code_expr;
                let mut __apollo_data = #serde_json_crate::json!({
                    "diagnostic_code": __apollo_diagnostic_code
                });
                if let #serde_json_crate::Value::Object(ref mut __apollo_map) = __apollo_data {
                    #(
                        if let ::std::option::Option::Some((__apollo_key, __apollo_value)) = #extension_fields {
                            __apollo_map.insert(__apollo_key.to_string(), __apollo_value);
                        }
                    )*
                }
                ::std::result::Result::Ok(#serde_json_crate::json!({
                    "code": #jsonrpc_code,
                    "message": __apollo_message,
                    "data": __apollo_data
                }))
            }
        }
    });

    let transparent_arms = generate_transparent_match_arms(variants, |_variant, _pattern| {
        quote! { ::apollo_errors::Error::to_jsonrpc(inner, config) }
    });

    regular_arms.into_iter().chain(transparent_arms).collect()
}