use crate::HedlError;
use std::fmt;
pub trait HedlResultExt<T> {
fn context<C>(self, context: C) -> Result<T, HedlError>
where
C: fmt::Display;
fn with_context<C, F>(self, f: F) -> Result<T, HedlError>
where
C: fmt::Display,
F: FnOnce() -> C;
fn map_err_to_hedl<F>(self, f: F) -> Result<T, HedlError>
where
F: FnOnce(Self::ErrorType) -> HedlError,
Self: Sized;
type ErrorType;
}
impl<T> HedlResultExt<T> for Result<T, HedlError> {
type ErrorType = HedlError;
fn context<C>(self, context: C) -> Result<T, HedlError>
where
C: fmt::Display,
{
self.map_err(|e| add_context_to_error(e, context.to_string()))
}
fn with_context<C, F>(self, f: F) -> Result<T, HedlError>
where
C: fmt::Display,
F: FnOnce() -> C,
{
self.map_err(|e| add_context_to_error(e, f().to_string()))
}
fn map_err_to_hedl<F>(self, _f: F) -> Result<T, HedlError>
where
F: FnOnce(Self::ErrorType) -> HedlError,
{
self
}
}
impl<T> HedlResultExt<T> for Result<T, std::io::Error> {
type ErrorType = std::io::Error;
fn context<C>(self, context: C) -> Result<T, HedlError>
where
C: fmt::Display,
{
self.map_err(|e| {
let mut err = HedlError::io(e.to_string());
err.context = Some(context.to_string());
err
})
}
fn with_context<C, F>(self, f: F) -> Result<T, HedlError>
where
C: fmt::Display,
F: FnOnce() -> C,
{
self.map_err(|e| {
let mut err = HedlError::io(e.to_string());
err.context = Some(f().to_string());
err
})
}
fn map_err_to_hedl<F>(self, f: F) -> Result<T, HedlError>
where
F: FnOnce(Self::ErrorType) -> HedlError,
{
self.map_err(f)
}
}
fn add_context_to_error(mut error: HedlError, new_context: String) -> HedlError {
if new_context.is_empty() {
return error;
}
error.context = Some(match error.context {
Some(existing) => {
format!("{new_context}; {existing}")
}
None => new_context,
});
error
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{parse, HedlErrorKind};
#[cfg(feature = "serde")]
impl<T> HedlResultExt<T> for Result<T, serde_json::Error> {
type ErrorType = serde_json::Error;
fn context<C>(self, context: C) -> Result<T, HedlError>
where
C: std::fmt::Display,
{
self.map_err(|e| {
let mut err = HedlError::conversion(e.to_string());
err.context = Some(context.to_string());
err
})
}
fn with_context<C, F>(self, f: F) -> Result<T, HedlError>
where
C: std::fmt::Display,
F: FnOnce() -> C,
{
self.map_err(|e| {
let mut err = HedlError::conversion(e.to_string());
err.context = Some(f().to_string());
err
})
}
fn map_err_to_hedl<F>(self, f: F) -> Result<T, HedlError>
where
F: FnOnce(Self::ErrorType) -> HedlError,
{
self.map_err(f)
}
}
#[test]
fn test_context_on_error() {
let result: Result<(), HedlError> = Err(HedlError::syntax("bad token", 5));
let err = result.context("in function foo").unwrap_err();
assert_eq!(err.context, Some("in function foo".to_string()));
assert_eq!(err.line, 5);
assert_eq!(err.kind, HedlErrorKind::Syntax);
}
#[test]
fn test_context_on_ok() {
let result: Result<i32, HedlError> = Ok(42);
let value = result.context("this should not be evaluated").unwrap();
assert_eq!(value, 42);
}
#[test]
fn test_context_chaining() {
let result: Result<(), HedlError> = Err(HedlError::reference("unresolved @User:1", 10));
let err = result
.context("in section users")
.context("while validating document")
.unwrap_err();
let ctx = err.context.unwrap();
assert!(ctx.contains("while validating document"));
assert!(ctx.contains("in section users"));
}
#[test]
fn test_context_with_format() {
let user_id = "alice";
let result: Result<(), HedlError> = Err(HedlError::collision("duplicate ID", 7));
let err = result.context(format!("for user {user_id}")).unwrap_err();
assert_eq!(err.context, Some("for user alice".to_string()));
}
#[test]
fn test_context_preserves_error_fields() {
let original = HedlError::schema("type mismatch", 15).with_column(20);
let result: Result<(), HedlError> = Err(original);
let err = result.context("additional info").unwrap_err();
assert_eq!(err.line, 15);
assert_eq!(err.column, Some(20));
assert_eq!(err.kind, HedlErrorKind::Schema);
assert_eq!(err.message, "type mismatch");
}
#[test]
fn test_context_empty_string() {
let result: Result<(), HedlError> = Err(HedlError::syntax("error", 1));
let err = result.context("").unwrap_err();
assert_eq!(err.context, None);
}
#[test]
fn test_with_context_lazy_evaluation() {
let mut evaluated = false;
let result: Result<i32, HedlError> = Ok(42);
let value = result
.with_context(|| {
evaluated = true;
"expensive computation"
})
.unwrap();
assert_eq!(value, 42);
assert!(!evaluated, "Context should not be evaluated on Ok");
}
#[test]
fn test_with_context_on_error() {
let mut evaluated = false;
let result: Result<(), HedlError> = Err(HedlError::alias("duplicate alias", 3));
let err = result
.with_context(|| {
evaluated = true;
"this should be evaluated"
})
.unwrap_err();
assert!(evaluated, "Context should be evaluated on Err");
assert_eq!(err.context, Some("this should be evaluated".to_string()));
}
#[test]
fn test_with_context_closure_captures() {
let filename = "config.hedl";
let line_count = 42;
let result: Result<(), HedlError> = Err(HedlError::semantic("invalid value", 20));
let err = result
.with_context(|| format!("in file {} at line {}/{}", filename, 20, line_count))
.unwrap_err();
let ctx = err.context.as_ref().unwrap();
assert!(ctx.contains("config.hedl"));
assert!(ctx.contains("42"));
}
#[test]
fn test_with_context_chaining() {
let result: Result<(), HedlError> = Err(HedlError::shape("wrong column count", 8));
let err = result
.with_context(|| "in matrix list")
.with_context(|| "while parsing data section")
.unwrap_err();
let ctx = err.context.unwrap();
assert!(ctx.contains("while parsing data section"));
assert!(ctx.contains("in matrix list"));
}
#[test]
fn test_map_err_to_hedl_io_error() {
let io_result: Result<String, std::io::Error> = Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
"file not found",
));
let hedl_result = io_result.map_err_to_hedl(|e: std::io::Error| {
HedlError::io(format!("Failed to read config: {e}"))
});
let err = hedl_result.unwrap_err();
assert_eq!(err.kind, HedlErrorKind::IO);
assert!(err.message.contains("Failed to read config"));
assert!(err.message.contains("file not found"));
}
#[test]
fn test_map_err_to_hedl_preserves_ok() {
let io_result: Result<String, std::io::Error> = Ok("content".to_string());
let hedl_result =
io_result.map_err_to_hedl(|e: std::io::Error| HedlError::io(e.to_string()));
assert_eq!(hedl_result.unwrap(), "content");
}
#[cfg(feature = "serde")]
#[test]
fn test_map_err_to_hedl_json_error() {
let json_result: Result<serde_json::Value, serde_json::Error> =
serde_json::from_str("invalid json");
let hedl_result = json_result.map_err_to_hedl(|e: serde_json::Error| {
HedlError::conversion(format!("JSON parse error: {e}"))
});
let err = hedl_result.unwrap_err();
assert_eq!(err.kind, HedlErrorKind::Conversion);
assert!(err.message.contains("JSON parse error"));
}
#[test]
fn test_real_world_parse_with_context() {
let invalid_hedl = "this is not valid HEDL";
let err = parse(invalid_hedl)
.context("failed to parse user configuration")
.unwrap_err();
assert!(err.context.is_some());
assert!(err.context.unwrap().contains("user configuration"));
}
#[test]
fn test_nested_function_context() {
fn inner() -> Result<(), HedlError> {
Err(HedlError::reference("undefined @User:1", 5))
}
fn middle() -> Result<(), HedlError> {
inner().context("in middle layer")
}
fn outer() -> Result<(), HedlError> {
middle().context("in outer layer")
}
let err = outer().unwrap_err();
let ctx = err.context.unwrap();
assert!(ctx.contains("outer layer"));
assert!(ctx.contains("middle layer"));
}
#[test]
fn test_io_context() {
use std::fs;
let result = fs::read_to_string("/this/path/does/not/exist")
.context("failed to load configuration file");
let err = result.unwrap_err();
assert_eq!(err.kind, HedlErrorKind::IO);
assert!(err.context.is_some());
}
#[test]
fn test_io_with_context() {
use std::fs;
let path = "/nonexistent/path";
let result = fs::read_to_string(path).with_context(|| format!("while reading {path}"));
let err = result.unwrap_err();
assert_eq!(err.kind, HedlErrorKind::IO);
assert!(err.context.unwrap().contains("nonexistent"));
}
#[test]
fn test_unicode_in_context() {
let result: Result<(), HedlError> = Err(HedlError::syntax("error", 1));
let err = result.context("日本語エラー 🎉").unwrap_err();
assert!(err.context.unwrap().contains("🎉"));
}
#[test]
fn test_very_long_context() {
let long_context = "x".repeat(10000);
let result: Result<(), HedlError> = Err(HedlError::syntax("error", 1));
let err = result.context(long_context.clone()).unwrap_err();
assert_eq!(err.context, Some(long_context));
}
#[test]
fn test_context_with_newlines() {
let result: Result<(), HedlError> = Err(HedlError::syntax("error", 1));
let err = result.context("line 1\nline 2\nline 3").unwrap_err();
let ctx = err.context.unwrap();
assert!(ctx.contains("line 1"));
assert!(ctx.contains("line 2"));
assert!(ctx.contains("line 3"));
}
#[test]
fn test_multiple_empty_contexts() {
let result: Result<(), HedlError> = Err(HedlError::syntax("error", 1));
let err = result
.context("")
.context("")
.context("real context")
.unwrap_err();
assert_eq!(err.context, Some("real context".to_string()));
}
#[test]
fn test_context_is_zero_cost_on_ok() {
let result: Result<i32, HedlError> = Ok(42);
let value = result
.context("ctx1")
.context("ctx2")
.context("ctx3")
.context("ctx4")
.context("ctx5")
.unwrap();
assert_eq!(value, 42);
}
#[test]
fn test_with_context_only_evaluates_once() {
use std::sync::atomic::{AtomicUsize, Ordering};
static EVAL_COUNT: AtomicUsize = AtomicUsize::new(0);
let result: Result<(), HedlError> = Err(HedlError::syntax("error", 1));
let _ = result
.with_context(|| {
EVAL_COUNT.fetch_add(1, Ordering::SeqCst);
"context"
})
.unwrap_err();
assert_eq!(EVAL_COUNT.load(Ordering::SeqCst), 1);
}
}