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:
dailz
2026-04-10 21:18:20 +08:00
parent 8e977af52c
commit 843b3db081

View File

@@ -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"
);
}
}