nyx/src/errors.rs
Eli Peter 1f2bfe76c1
docs: Enhance module documentation across various files for clarity a… (#62)
* docs: Enhance module documentation across various files for clarity and completeness

* fix: Remove unnecessary blank line in build.rs for cleaner code

* docs: Update documentation to improve clarity and consistency in code comments
2026-05-02 17:46:45 -04:00

227 lines
6.4 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Error types used throughout the scanner.
//!
//! [`NyxError`] wraps I/O, TOML parse, SQLite, tree-sitter, and connection-pool
//! errors into a single enum. [`NyxResult<T>`] is the standard return type alias.
//!
//! [`ConfigError`] and [`ConfigErrorKind`] carry structured config-validation
//! diagnostics (section, field, message, kind) so callers can format them
//! consistently without ad-hoc string matching.
use serde::Serialize;
use serde::de::StdError;
use std::fmt;
use std::sync::PoisonError;
use thiserror::Error;
pub type NyxResult<T, E = NyxError> = Result<T, E>;
// ─── Config validation ──────────────────────────────────────────────────────
/// A single config validation error with structured metadata.
#[derive(Debug, Clone, Serialize)]
pub struct ConfigError {
pub section: String,
pub field: String,
pub message: String,
pub kind: ConfigErrorKind,
}
impl fmt::Display for ConfigError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "[{}.{}] {}", self.section, self.field, self.message)
}
}
/// Category of config validation error.
#[derive(Debug, Clone, Serialize)]
pub enum ConfigErrorKind {
OutOfRange,
InvalidValue,
EmptyRequired,
Conflict,
}
#[derive(Debug, Error)]
pub enum NyxError {
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("TOML parse error: {0}")]
Toml(#[from] toml::de::Error),
#[error("SQLite error: {0}")]
Sql(#[from] rusqlite::Error),
#[error("tree-sitter error: {0}")]
TreeSitter(#[from] tree_sitter::LanguageError),
#[error("connection-pool error: {0}")]
Pool(#[from] r2d2::Error),
#[error("time error: {0}")]
Time(#[from] std::time::SystemTimeError),
#[error("poisoned lock: {0}")]
Poison(String),
#[error(transparent)]
Other(#[from] Box<dyn StdError + Send + Sync + 'static>),
#[error("{0}")]
Msg(String),
#[error("config validation failed:\n{}", .0.iter().map(|e| format!(" - {e}")).collect::<Vec<_>>().join("\n"))]
ConfigValidation(Vec<ConfigError>),
}
impl<T> From<PoisonError<T>> for NyxError
where
T: fmt::Debug,
{
fn from(err: PoisonError<T>) -> Self {
NyxError::Poison(err.to_string())
}
}
impl From<&str> for NyxError {
fn from(s: &str) -> Self {
NyxError::Msg(s.to_owned())
}
}
impl From<String> for NyxError {
fn from(s: String) -> Self {
NyxError::Msg(s)
}
}
impl From<Box<dyn std::error::Error>> for NyxError {
fn from(err: Box<dyn std::error::Error>) -> Self {
NyxError::Msg(err.to_string())
}
}
#[test]
fn io_conversion_retains_message() {
let e = std::io::Error::other("boom!");
let n: NyxError = e.into();
assert!(matches!(n, NyxError::Io(_)));
assert!(n.to_string().contains("boom"));
}
#[test]
fn poison_conversion_maps_correct_variant() {
let lock = std::sync::Arc::new(std::sync::Mutex::new(()));
{
let lock2 = std::sync::Arc::clone(&lock);
std::thread::spawn(move || {
let _guard = lock2.lock().unwrap();
panic!("intentional poison the mutex");
})
.join()
.ok();
}
let poison = lock.lock().unwrap_err();
let nyx: NyxError = poison.into();
assert!(matches!(nyx, NyxError::Poison(_)));
}
#[test]
fn simple_string_into_msg() {
let nyx: NyxError = "plain msg".into();
assert!(matches!(nyx, NyxError::Msg(s) if s == "plain msg"));
}
#[test]
fn string_owned_into_msg() {
let s = String::from("owned message");
let nyx: NyxError = s.into();
assert!(matches!(nyx, NyxError::Msg(ref m) if m == "owned message"));
assert!(nyx.to_string().contains("owned message"));
}
#[test]
fn box_dyn_error_into_msg() {
let boxed: Box<dyn std::error::Error> = Box::new(std::io::Error::other("inner error"));
let nyx: NyxError = boxed.into();
// The From<Box<dyn std::error::Error>> impl wraps as Msg
assert!(matches!(nyx, NyxError::Msg(_)));
assert!(nyx.to_string().contains("inner error"));
}
#[test]
fn config_error_display_includes_section_field_and_message() {
let err = ConfigError {
section: "server".to_string(),
field: "port".to_string(),
message: "must be non-zero".to_string(),
kind: ConfigErrorKind::OutOfRange,
};
let s = err.to_string();
assert!(s.contains("server"), "should mention section: {s}");
assert!(s.contains("port"), "should mention field: {s}");
assert!(
s.contains("must be non-zero"),
"should mention message: {s}"
);
}
#[test]
fn config_error_kind_debug_names() {
let kinds = [
ConfigErrorKind::OutOfRange,
ConfigErrorKind::InvalidValue,
ConfigErrorKind::EmptyRequired,
ConfigErrorKind::Conflict,
];
let names = ["OutOfRange", "InvalidValue", "EmptyRequired", "Conflict"];
for (kind, name) in kinds.iter().zip(names.iter()) {
assert!(format!("{kind:?}").contains(name));
}
}
#[test]
fn nyx_error_config_validation_display_lists_all_errors() {
let errs = vec![
ConfigError {
section: "scanner".to_string(),
field: "threads".to_string(),
message: "must be > 0".to_string(),
kind: ConfigErrorKind::OutOfRange,
},
ConfigError {
section: "output".to_string(),
field: "format".to_string(),
message: "unrecognised value".to_string(),
kind: ConfigErrorKind::InvalidValue,
},
];
let nyx = NyxError::ConfigValidation(errs);
let s = nyx.to_string();
assert!(s.contains("scanner"), "should list first error: {s}");
assert!(s.contains("output"), "should list second error: {s}");
assert!(s.contains("must be > 0"), "should include message: {s}");
}
#[test]
fn nyx_result_ok_variant_propagates_value() {
let value = 42;
let result: NyxResult<u32> = Ok(value);
match result {
Ok(actual) => assert_eq!(actual, value),
Err(err) => panic!("expected Ok result, got {err}"),
}
}
#[test]
fn nyx_result_err_variant_contains_error() {
let message = "oops".to_string();
let result: NyxResult<u32> = Err(NyxError::Msg(message.clone()));
match result {
Ok(value) => panic!("expected Err result, got Ok({value})"),
Err(err) => assert!(err.to_string().contains(&message)),
}
}