feat(status): add error context #70

Merged
alexion merged 1 commits from feat/status-context into main 2026-09-17 15:10:51 -04:00
3 changed files with 58 additions and 5 deletions

View File

@@ -1,5 +1,12 @@
pub type Result<T> = std::result::Result<T, Status>; pub type Result<T> = std::result::Result<T, Status>;
pub trait ResultExt<T> {
fn with_context<C, F>(self, context: F) -> Result<T>
where
C: Into<String>,
F: FnOnce() -> C;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Code { pub enum Code {
InvalidArgument, InvalidArgument,
@@ -48,6 +55,10 @@ impl Status {
self self
} }
pub fn with_context(self, context: impl Into<String>) -> Self {
Self::new(self.code, context).with_source(self)
}
pub fn code(&self) -> Code { pub fn code(&self) -> Code {
self.code self.code
} }
@@ -82,3 +93,16 @@ impl From<std::io::Error> for Status {
Self::new(code, error.to_string()).with_source(error) Self::new(code, error.to_string()).with_source(error)
} }
} }
impl<T, E> ResultExt<T> for std::result::Result<T, E>
where
E: Into<Status>,
{
fn with_context<C, F>(self, context: F) -> Result<T>
where
C: Into<String>,
F: FnOnce() -> C,
{
self.map_err(|error| error.into().with_context(context()))
}
}

View File

@@ -1,6 +1,6 @@
use std::error::Error; use std::error::Error;
use status::{Code, Status}; use status::{Code, ResultExt, Status};
#[test] #[test]
fn constructors_set_code_and_message() { fn constructors_set_code_and_message() {
@@ -34,6 +34,29 @@ fn with_source_preserves_source_error() {
assert_eq!("denied", status.source().unwrap().to_string()); assert_eq!("denied", status.source().unwrap().to_string());
} }
#[test]
fn with_context_preserves_code_and_wraps_status() {
let status = Status::not_found("missing input").with_context("failed to load config");
assert_eq!(Code::NotFound, status.code());
assert_eq!("failed to load config", status.message());
assert_eq!("NotFound: missing input", status.source().unwrap().to_string());
}
#[test]
fn result_ext_adds_lazy_context() {
let result: status::Result<()> = Err(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
"denied",
))
.with_context(|| "failed to read input");
let status = result.unwrap_err();
assert_eq!(Code::PermissionDenied, status.code());
assert_eq!("failed to read input", status.message());
assert_eq!("PermissionDenied: denied", status.source().unwrap().to_string());
}
#[test] #[test]
fn io_error_maps_not_found() { fn io_error_maps_not_found() {
let status = Status::from(std::io::Error::new( let status = Status::from(std::io::Error::new(

View File

@@ -1,4 +1,5 @@
use clap::Parser; use clap::Parser;
use status::ResultExt;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use xatu::SourceFile; use xatu::SourceFile;
@@ -26,7 +27,8 @@ fn is_excluded_dir(path: &Path) -> bool {
} }
fn collect_source_files(path: &Path) -> status::Result<Vec<SourceFile>> { fn collect_source_files(path: &Path) -> status::Result<Vec<SourceFile>> {
let metadata = std::fs::symlink_metadata(path)?; let metadata = std::fs::symlink_metadata(path)
.with_context(|| format!("failed to read metadata for {}", path.display()))?;
if metadata.file_type().is_dir() { if metadata.file_type().is_dir() {
if is_excluded_dir(path) { if is_excluded_dir(path) {
@@ -35,8 +37,11 @@ fn collect_source_files(path: &Path) -> status::Result<Vec<SourceFile>> {
let mut files = Vec::new(); let mut files = Vec::new();
for entry in std::fs::read_dir(path)? { for entry in std::fs::read_dir(path)
let entry = entry?; .with_context(|| format!("failed to read directory {}", path.display()))?
{
let entry =
entry.with_context(|| format!("failed to read entry in {}", path.display()))?;
files.extend(collect_source_files(&entry.path())?); files.extend(collect_source_files(&entry.path())?);
} }
@@ -46,7 +51,8 @@ fn collect_source_files(path: &Path) -> status::Result<Vec<SourceFile>> {
if metadata.file_type().is_file() && is_nix_file(path) { if metadata.file_type().is_file() && is_nix_file(path) {
return Ok(vec![SourceFile { return Ok(vec![SourceFile {
path: path.to_path_buf(), path: path.to_path_buf(),
source: std::fs::read_to_string(path)?, source: std::fs::read_to_string(path)
.with_context(|| format!("failed to read source file {}", path.display()))?,
}]); }]);
} }