error-engine 0.1.0

A message catalog + presentation layer on top of thiserror and tracing for consistent error/warning/info rendering.
Documentation
use error_engine::{Catalog, EngineError, UNKNOWN_CODE};

fn write_temp_toml(contents: &str) -> tempfile_path::TempPath {
    tempfile_path::TempPath::new(contents)
}

// Minimal self-contained temp-file helper (no extra dependency needed).
mod tempfile_path {
    use std::path::{Path, PathBuf};

    pub struct TempPath {
        path: PathBuf,
    }

    impl TempPath {
        pub fn new(contents: &str) -> Self {
            let mut path = std::env::temp_dir();
            let unique = format!(
                "error-engine-test-{}-{}.toml",
                std::process::id(),
                std::time::SystemTime::now()
                    .duration_since(std::time::UNIX_EPOCH)
                    .unwrap()
                    .as_nanos()
            );
            path.push(unique);
            std::fs::write(&path, contents).unwrap();
            Self { path }
        }

        pub fn as_str(&self) -> &str {
            self.path.to_str().unwrap()
        }
    }

    impl Drop for TempPath {
        fn drop(&mut self) {
            let _ = std::fs::remove_file(&self.path);
        }
    }

    impl AsRef<Path> for TempPath {
        fn as_ref(&self) -> &Path {
            &self.path
        }
    }
}

const VALID_CATALOG: &str = r#"
[CONFIG_NOT_FOUND]
template = "Could not find the configuration file at {path}"
hint = "Check that the path is correct"

[DB_CONN_FAILED]
template = "Could not connect to the database: {reason}"
"#;

#[test]
fn load_valid_toml_and_render_existing_code() {
    let file = write_temp_toml(VALID_CATALOG);
    let catalog = Catalog::load(file.as_str()).expect("valid catalog should load");

    let rendered = catalog.render("CONFIG_NOT_FOUND", &[("path", "/etc/app.toml".to_string())]);
    assert_eq!(rendered, "Could not find the configuration file at /etc/app.toml");
}

#[test]
fn load_missing_path_returns_catalog_read_error() {
    let result = Catalog::load("/definitely/does/not/exist/errors.toml");
    assert!(matches!(result, Err(EngineError::CatalogRead { .. })));
}

#[test]
fn load_malformed_toml_returns_catalog_parse_error() {
    let file = write_temp_toml("this is not valid = = toml [[[");
    let result = Catalog::load(file.as_str());
    assert!(matches!(result, Err(EngineError::CatalogParse { .. })));
}

#[test]
fn load_or_fallback_never_panics_on_missing_file() {
    let catalog = Catalog::load_or_fallback("/definitely/does/not/exist/errors.toml");
    let rendered = catalog.render("ANY_CODE", &[]);
    assert!(rendered.contains(UNKNOWN_CODE));
}

#[test]
fn render_with_load_error_returns_unk_000_for_any_code() {
    let catalog = Catalog::load_or_fallback("/definitely/does/not/exist/errors.toml");

    let rendered = catalog.render("CONFIG_NOT_FOUND", &[]);
    assert!(rendered.contains(UNKNOWN_CODE));
    assert!(rendered.contains("catalog failed to load"));
}

#[test]
fn render_unknown_code_on_valid_catalog_returns_unk_000() {
    let file = write_temp_toml(VALID_CATALOG);
    let catalog = Catalog::load(file.as_str()).expect("valid catalog should load");

    let rendered = catalog.render("NOT_A_REAL_CODE", &[]);
    assert!(rendered.contains(UNKNOWN_CODE));
    assert!(rendered.contains("NOT_A_REAL_CODE"));
}

#[test]
fn placeholder_with_no_context_value_stays_literal() {
    let file = write_temp_toml(VALID_CATALOG);
    let catalog = Catalog::load(file.as_str()).expect("valid catalog should load");

    let rendered = catalog.render("DB_CONN_FAILED", &[]);
    assert_eq!(rendered, "Could not connect to the database: {reason}");
}

#[test]
fn hint_returns_none_on_load_failure() {
    let catalog = Catalog::load_or_fallback("/definitely/does/not/exist/errors.toml");
    assert_eq!(catalog.hint("CONFIG_NOT_FOUND"), None);
}

#[test]
fn hint_returns_none_on_missing_code() {
    let file = write_temp_toml(VALID_CATALOG);
    let catalog = Catalog::load(file.as_str()).expect("valid catalog should load");
    assert_eq!(catalog.hint("NOT_A_REAL_CODE"), None);
}

#[test]
fn hint_returns_value_when_present() {
    let file = write_temp_toml(VALID_CATALOG);
    let catalog = Catalog::load(file.as_str()).expect("valid catalog should load");
    assert_eq!(catalog.hint("CONFIG_NOT_FOUND"), Some("Check that the path is correct"));
}