apollo-errors-derive 0.4.0

Proc macro for deriving apollo-errors::Error trait
Documentation
//! Parsing - Convert syn AST into our IR

mod diagnostic;
mod field;
mod http_header;
mod http_status;
mod jsonrpc_code;
mod struct_parse;
mod variant;

use syn::{Data, DeriveInput, Result};

use crate::ir::{Definition, EnumDefinition};

use struct_parse::parse_struct;
use variant::parse_variant;

/// Parse a DeriveInput into a Definition (enum or struct)
pub(crate) fn parse_error_derive(input: DeriveInput) -> Result<Definition> {
    match input.data {
        Data::Enum(data_enum) => {
            // Parse each variant
            let mut variants = Vec::new();
            for variant in data_enum.variants {
                let variant_def = parse_variant(variant)?;
                variants.push(variant_def);
            }

            let error_def = EnumDefinition {
                name: input.ident,
                generics: input.generics,
                variants,
            };

            // Validate the error definition
            error_def.validate()?;

            Ok(Definition::Enum(error_def))
        }
        Data::Struct(data_struct) => {
            let struct_def = parse_struct(input.ident, input.generics, &input.attrs, data_struct)?;

            // Validate the struct definition
            struct_def.validate()?;

            Ok(Definition::Struct(struct_def))
        }
        Data::Union(_) => Err(syn::Error::new_spanned(
            input,
            "#[derive(Error)] cannot be used on unions",
        )),
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::ir::VariantDefinition;
    use syn::parse_quote;

    #[test]
    fn test_parse_simple_enum() {
        let parsed = parse_quote! {
            enum MyError {
                #[error("Something went wrong")]
                #[diagnostic(code(errors::simple))]
                Simple,
            }
        };

        let result = parse_error_derive(parsed);
        assert!(result.is_ok());

        let Definition::Enum(error_def) = result.unwrap() else {
            panic!("Expected Enum definition");
        };
        assert_eq!(error_def.variants.len(), 1);

        let VariantDefinition::Regular(variant) = &error_def.variants[0] else {
            panic!("Expected Regular variant");
        };
        assert_eq!(variant.error_message, "Something went wrong");
        assert_eq!(variant.diagnostic_code, "errors::simple");
    }

    #[test]
    fn test_parse_with_fields() {
        let parsed = parse_quote! {
            enum MyError {
                #[error("Invalid port")]
                #[diagnostic(code(config::invalid_port))]
                InvalidPort {
                    #[extension]
                    port: u16,

                    #[extension]
                    config_file: String,
                },
            }
        };

        let result = parse_error_derive(parsed);
        assert!(result.is_ok());

        let Definition::Enum(error_def) = result.unwrap() else {
            panic!("Expected Enum definition");
        };
        let VariantDefinition::Regular(variant) = &error_def.variants[0] else {
            panic!("Expected Regular variant");
        };
        assert_eq!(variant.fields.len(), 2);

        let port_field = &variant.fields[0];
        assert_eq!(port_field.rust_name, "port");
        assert_eq!(port_field.output_name, "port");
        assert!(port_field.is_extension);

        let config_field = &variant.fields[1];
        assert_eq!(config_field.rust_name, "config_file");
        assert_eq!(config_field.output_name, "config_file");
        assert!(config_field.is_extension);
    }

    #[test]
    fn test_parse_struct() {
        let parsed = parse_quote! {
            #[error("Something went wrong")]
            #[diagnostic(code(errors::struct_error))]
            struct MyError {
                #[extension]
                field: String,
            }
        };

        let result = parse_error_derive(parsed);
        assert!(result.is_ok(), "Parsing failed: {result:?}");

        let Definition::Struct(struct_def) = result.unwrap() else {
            panic!("Expected Struct definition");
        };
        assert_eq!(struct_def.name, "MyError");
        assert_eq!(struct_def.error_message, "Something went wrong");
        assert_eq!(struct_def.diagnostic_code, "errors::struct_error");
        assert_eq!(struct_def.fields.len(), 1);
        assert_eq!(struct_def.fields[0].rust_name, "field");
    }

    #[test]
    fn test_parse_consolidated_diagnostic_attrs() {
        let parsed = parse_quote! {
            enum MyError {
                #[error("Invalid configuration")]
                #[diagnostic(code(config::invalid), help("Check your config"), url("https://example.com"))]
                InvalidConfig,
            }
        };

        let result = parse_error_derive(parsed);
        assert!(result.is_ok(), "Parsing failed: {result:?}");

        let Definition::Enum(error_def) = result.unwrap() else {
            panic!("Expected Enum definition");
        };
        let VariantDefinition::Regular(variant) = &error_def.variants[0] else {
            panic!("Expected Regular variant");
        };
        assert_eq!(variant.diagnostic_code, "config::invalid");
        assert_eq!(variant.help_text, Some("Check your config".to_string()));
        assert_eq!(variant.url, Some("https://example.com".to_string()));
    }

    #[test]
    fn test_parse_transparent_variant() {
        let parsed = parse_quote! {
            enum MyError {
                #[error("Something went wrong")]
                #[diagnostic(code(errors::regular))]
                Regular,

                #[diagnostic(transparent)]
                Inner(std::io::Error),
            }
        };

        let result = parse_error_derive(parsed);
        assert!(result.is_ok(), "Parsing failed: {result:?}");

        let Definition::Enum(error_def) = result.unwrap() else {
            panic!("Expected Enum definition");
        };
        assert_eq!(error_def.variants.len(), 2);

        let VariantDefinition::Regular(regular) = &error_def.variants[0] else {
            panic!("Expected Regular variant");
        };
        assert_eq!(regular.error_message, "Something went wrong");

        let VariantDefinition::Transparent(transparent) = &error_def.variants[1] else {
            panic!("Expected Transparent variant");
        };
        assert_eq!(transparent.name, "Inner");
    }

    #[test]
    fn test_error_code_validation_too_few_segments() {
        let parsed = parse_quote! {
            enum MyError {
                #[error("Bad code")]
                #[diagnostic(code(just_one))]
                Bad,
            }
        };

        let result = parse_error_derive(parsed);
        assert!(result.is_err());
        let err = result.unwrap_err().to_string();
        assert!(err.contains("at least 2 segments"));
    }

    #[test]
    fn test_error_code_validation_uppercase_rejected() {
        let parsed = parse_quote! {
            enum MyError {
                #[error("Bad code")]
                #[diagnostic(code(TEST::UPPERCASE))]
                Bad,
            }
        };

        let result = parse_error_derive(parsed);
        assert!(result.is_err());
        let err = result.unwrap_err().to_string();
        assert!(err.contains("lowercase"));
    }
}