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;
pub(crate) fn parse_error_derive(input: DeriveInput) -> Result<Definition> {
match input.data {
Data::Enum(data_enum) => {
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,
};
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)?;
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"));
}
}