use thiserror::Error;
use crate::diagnostics::Diagnostic;
#[derive(Error, Debug)]
pub enum AigentError {
#[error("parse error: {message}")]
Parse {
message: String,
},
#[error("{}", format_validation_errors(errors))]
Validation {
errors: Vec<Diagnostic>,
},
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("YAML error: {0}")]
Yaml(#[from] serde_yaml_ng::Error),
#[error("build error: {message}")]
Build {
message: String,
},
#[error("already exists: {}", path.display())]
AlreadyExists {
path: std::path::PathBuf,
},
}
fn format_validation_errors(errors: &[Diagnostic]) -> String {
match errors.len() {
0 => "Validation failed: no details".to_string(),
1 => errors[0].to_string(),
_ => {
let bullets: String = errors.iter().map(|e| format!("\n - {e}")).collect();
format!("Validation failed:{bullets}")
}
}
}
pub type Result<T> = std::result::Result<T, AigentError>;
#[cfg(test)]
mod tests {
use super::*;
use crate::diagnostics::{Severity, E001, E002};
fn diag(severity: Severity, code: &'static str, msg: &str) -> Diagnostic {
Diagnostic::new(severity, code, msg)
}
#[test]
fn parse_display() {
let err = AigentError::Parse {
message: "bad yaml".to_string(),
};
assert_eq!(err.to_string(), "parse error: bad yaml");
}
#[test]
fn validation_single_error_display() {
let err = AigentError::Validation {
errors: vec![diag(Severity::Error, E001, "name too long")],
};
assert_eq!(err.to_string(), "name too long");
}
#[test]
fn validation_multiple_errors_display() {
let err = AigentError::Validation {
errors: vec![
diag(Severity::Error, E001, "bad name"),
diag(Severity::Error, E002, "too long"),
],
};
assert_eq!(
err.to_string(),
"Validation failed:\n - bad name\n - too long"
);
}
#[test]
fn validation_empty_errors_display() {
let err = AigentError::Validation { errors: vec![] };
assert_eq!(err.to_string(), "Validation failed: no details");
}
#[test]
fn build_display() {
let err = AigentError::Build {
message: "LLM unavailable".to_string(),
};
assert_eq!(err.to_string(), "build error: LLM unavailable");
}
#[test]
fn io_error_converts_via_from() {
fn trigger() -> Result<()> {
let _f = std::fs::File::open("/nonexistent/path/that/does/not/exist")?;
Ok(())
}
let err = trigger().unwrap_err();
assert!(matches!(err, AigentError::Io(_)));
assert!(err.to_string().starts_with("IO error:"));
}
#[test]
fn yaml_error_converts_via_from() {
fn trigger() -> Result<()> {
let _: serde_yaml_ng::Value = serde_yaml_ng::from_str(":\n :\n :")?;
Ok(())
}
let err = trigger().unwrap_err();
assert!(matches!(err, AigentError::Yaml(_)));
assert!(err.to_string().starts_with("YAML error:"));
}
#[test]
fn validation_errors_accessible_via_match() {
let err = AigentError::Validation {
errors: vec![
diag(Severity::Error, E001, "err1"),
diag(Severity::Error, E001, "err2"),
diag(Severity::Error, E001, "err3"),
],
};
match err {
AigentError::Validation { errors } => {
assert_eq!(errors.len(), 3);
assert_eq!(errors[0].message, "err1");
assert_eq!(errors[2].message, "err3");
}
_ => panic!("expected Validation variant"),
}
}
#[test]
fn parse_message_accessible_via_match() {
let err = AigentError::Parse {
message: "unexpected EOF".to_string(),
};
match err {
AigentError::Parse { message } => assert_eq!(message, "unexpected EOF"),
_ => panic!("expected Parse variant"),
}
}
#[test]
fn aigent_error_implements_std_error() {
let err = AigentError::Parse {
message: "test".to_string(),
};
let _: &dyn std::error::Error = &err;
}
#[test]
fn result_alias_works_with_question_mark() {
fn ok_path() -> Result<i32> {
Ok(42)
}
fn err_path() -> Result<i32> {
Err(AigentError::Build {
message: "fail".to_string(),
})
}
assert_eq!(ok_path().unwrap(), 42);
assert!(err_path().is_err());
}
}