feat(core): define error types with thiserror
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -1 +1,164 @@
|
||||
// Task 3 will implement error types
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum CoreError {
|
||||
#[error("I/O error: {context}: {source}")]
|
||||
Io {
|
||||
#[source]
|
||||
source: std::io::Error,
|
||||
context: String,
|
||||
},
|
||||
|
||||
#[error("parse error at line {line}: {source}")]
|
||||
Parse {
|
||||
#[source]
|
||||
source: serde_json::Error,
|
||||
line: u64,
|
||||
},
|
||||
|
||||
#[error("invalid search pattern '{pattern}': {reason}")]
|
||||
Search { pattern: String, reason: String },
|
||||
|
||||
#[error("index error: {message}")]
|
||||
Index { message: String },
|
||||
|
||||
#[error("config error: {source}")]
|
||||
Config {
|
||||
#[source]
|
||||
source: toml::de::Error,
|
||||
},
|
||||
|
||||
#[error("encoding error at line {line}: {bytes:?}")]
|
||||
Encoding { line: u64, bytes: Vec<u8> },
|
||||
|
||||
#[error("file watch error: {0}")]
|
||||
Watch(String),
|
||||
|
||||
#[error("file not found: {path:?}")]
|
||||
FileNotFound { path: PathBuf },
|
||||
|
||||
#[error("{0}")]
|
||||
Other(String),
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, CoreError>;
|
||||
|
||||
impl From<std::io::Error> for CoreError {
|
||||
fn from(err: std::io::Error) -> Self {
|
||||
CoreError::Io {
|
||||
source: err,
|
||||
context: "I/O operation".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for CoreError {
|
||||
fn from(err: serde_json::Error) -> Self {
|
||||
CoreError::Parse {
|
||||
source: err,
|
||||
line: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<toml::de::Error> for CoreError {
|
||||
fn from(err: toml::de::Error) -> Self {
|
||||
CoreError::Config { source: err }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<notify::Error> for CoreError {
|
||||
fn from(err: notify::Error) -> Self {
|
||||
CoreError::Watch(err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_io_error_display() {
|
||||
let err = CoreError::Io {
|
||||
source: std::io::Error::new(std::io::ErrorKind::NotFound, "file missing"),
|
||||
context: "reading config".into(),
|
||||
};
|
||||
let msg = err.to_string();
|
||||
assert!(
|
||||
msg.contains("reading config"),
|
||||
"display should contain context"
|
||||
);
|
||||
assert!(
|
||||
msg.contains("file missing"),
|
||||
"display should contain source message"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_error_display() {
|
||||
let json_err: serde_json::Error =
|
||||
serde_json::from_str::<serde_json::Value>("{bad}").unwrap_err();
|
||||
let err = CoreError::Parse {
|
||||
source: json_err,
|
||||
line: 42,
|
||||
};
|
||||
let msg = err.to_string();
|
||||
assert!(msg.contains("42"), "display should contain line number");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_search_error_display() {
|
||||
let err = CoreError::Search {
|
||||
pattern: "[invalid".into(),
|
||||
reason: "unclosed bracket".into(),
|
||||
};
|
||||
let msg = err.to_string();
|
||||
assert!(msg.contains("[invalid"), "display should contain pattern");
|
||||
assert!(
|
||||
msg.contains("unclosed bracket"),
|
||||
"display should contain reason"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_io_error() {
|
||||
let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "access denied");
|
||||
let core_err: CoreError = io_err.into();
|
||||
match core_err {
|
||||
CoreError::Io { context, .. } => assert_eq!(context, "I/O operation"),
|
||||
other => panic!("expected Io variant, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_notify_error() {
|
||||
let notify_err = notify::Error::watch_not_found();
|
||||
let core_err: CoreError = notify_err.into();
|
||||
match core_err {
|
||||
CoreError::Watch(msg) => assert!(!msg.is_empty()),
|
||||
other => panic!("expected Watch variant, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_result_type_alias() {
|
||||
let ok: Result<i32> = Ok(42);
|
||||
let err: Result<i32> = Err(CoreError::Other("something went wrong".into()));
|
||||
assert!(ok.is_ok());
|
||||
assert!(err.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_source_chain() {
|
||||
let io_err = std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "unexpected eof");
|
||||
let core_err = CoreError::Io {
|
||||
source: io_err,
|
||||
context: "test".into(),
|
||||
};
|
||||
let source = std::error::Error::source(&core_err);
|
||||
assert!(
|
||||
source.is_some(),
|
||||
"source() should return the inner io::Error"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user