error-engine 0.2.0

A message catalog + presentation layer on top of thiserror and tracing for consistent error/warning/info rendering.
Documentation
# error-engine

A shared Rust crate to centralize error, warning, and info presentation
across personal projects. It does not replace `thiserror` or `tracing` — it
sits on top of them as a **message catalog + presentation** layer.

Each project defines its errors as an `enum` with `thiserror` (re-exported
by this crate) and implements the `EngineDiagnostic` trait. A TOML catalog
file maps each diagnostic's stable code to a message template and an
optional hint, so wording can be edited without touching Rust code.

## Install

```toml
[dependencies]
error-engine = "0.1"
```

Enable the `cli` feature if you want colored terminal output (`Engine::print`):

```toml
[dependencies]
error-engine = { version = "0.1", features = ["cli"] }
```

## Usage

1. Define your errors with `thiserror`, using `error_engine::thiserror::Error`
   so you don't need `thiserror` as a separate dependency:

   ```rust
   use error_engine::thiserror::Error;

   #[derive(Debug, Error)]
   enum AppError {
       #[error("config not found")]
       ConfigNotFound { path: String },
   }
   ```

2. Implement `EngineDiagnostic` — a stable `code()`, a `severity()`, and
   optional `context()` key-value pairs to interpolate into the template:

   ```rust
   use error_engine::{EngineDiagnostic, Severity};

   impl EngineDiagnostic for AppError {
       fn code(&self) -> &'static str {
           match self {
               AppError::ConfigNotFound { .. } => "CONFIG_NOT_FOUND",
           }
       }
       fn severity(&self) -> Severity {
           Severity::Error
       }
       fn context(&self) -> Vec<(&'static str, String)> {
           match self {
               AppError::ConfigNotFound { path } => vec![("path", path.clone())],
           }
       }
   }
   ```

3. Write a catalog TOML file at your project's root, e.g. `errors.toml`:

   ```toml
   [CONFIG_NOT_FOUND]
   template = "Could not find the configuration file at {path}"
   hint = "Check that the path is correct, or create one with `myapp init`"

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

   - Each table key (`[CODE]`) must match exactly the `code()` returned by
     the corresponding `EngineDiagnostic`.
   - `template` is required, `hint` is optional.
   - `{key}` placeholders are substituted with values from `context()`. A
     placeholder with no matching value is left literal in the text — it
     never panics.
   - `UNK-000` is reserved by the crate (see [Failure behavior]#failure-behavior-harmless-by-design)
     and should not be reused as a project-defined code.

4. Load the catalog and build an `Engine`:

   ```rust
   use error_engine::{Catalog, Engine};

   fn main() -> anyhow::Result<()> {
       tracing_subscriber::fmt::init();

       // Infallible: a missing/broken errors.toml never stops the app —
       // diagnostics will just render as UNK-000 until it's fixed.
       let catalog = Catalog::load_or_fallback("errors.toml");
       let engine = Engine::new(catalog);

       let err = AppError::ConfigNotFound { path: "/etc/app.toml".into() };
       engine.print(&err); // requires the "cli" feature
       engine.log(&err);
       Ok(())
   }
   ```

Run the bundled examples for a working end-to-end reference:

```sh
cargo run --example basic
cargo run --example cli --features cli
```

## Output modes

| Method | Returns | Use case |
|---|---|---|
| `message()` | `String` | GUI apps: plain text only, no logging, no color |
| `log()` | nothing, goes to `tracing` | Structured logging (files, services) |
| `print()` | nothing, goes to stdout/stderr | CLIs: colored output with hint (`cli` feature) |

`print()` lives behind the `cli` feature flag so GUI/service projects don't
pull in terminal-only dependencies (`owo-colors`).

## Failure behavior (harmless by design)

Nothing about diagnostic rendering should ever panic or crash the
application — a broken catalog or a typo'd code is a documentation problem,
not a runtime crash. Two failure modes are unified under a single reserved
code, `UNK-000`:

1. **The catalog itself failed to load** (file missing, unreadable, or
   invalid TOML).
2. **The catalog loaded fine, but the requested code has no entry in it.**

In both cases, rendering returns a message built around `UNK-000` stating
which of the two happened, instead of failing.

- `Catalog::load(path)` — strict loader, returns `Result<Catalog, EngineError>`.
  Use at startup if you want the app to refuse to run without a valid
  catalog.
- `Catalog::load_or_fallback(path)` — infallible loader, and the
  recommended entry point. A broken or missing catalog never panics; every
  diagnostic that would have failed instead renders as `UNK-000` with an
  explanatory message.

## Catalog composition (for library authors)

If your project depends on a library that also uses `error-engine`, the
library can ship its own catalog embedded in the compiled crate — no
runtime file path required — and your app's catalog can layer on top of it.

In the library:

```rust
// lib_a's own crate
pub fn catalog() -> error_engine::Catalog {
    error_engine::Catalog::from_str(include_str!("../errors.toml"))
        .expect("lib_a's own catalog is valid TOML — covered by lib_a's own tests")
}
```

In the consuming app:

```rust
use error_engine::{Catalog, Engine};

let catalog = Catalog::load_or_fallback("errors.toml") // app's own, wins on conflicts
    .merged_with(lib_a::catalog());
let engine = Engine::new(catalog);
```

`merged_with` is app-wins on code collisions: entries in `self` (the
catalog you call it on) take priority over entries in `other`. Codes the
app doesn't define fall through to the library's defaults instead of
rendering `UNK-000`. A `load_error` on the app's own catalog (e.g. a
missing/broken `errors.toml`) is preserved as-is after merging — it still
renders `UNK-000` for everything, but that never poisons the library's
catalog, which keeps working independently. There's no automatic discovery
and no collision warnings (see `docs/error-engine-design.md` section 10 for
the full design rationale and open questions).

## Project structure

```
error-engine/
├── Cargo.toml
├── .cargo/
│   └── config.toml      # rustflags = ["-D", "warnings"]
├── src/
│   ├── lib.rs            # public re-exports (includes `pub use thiserror`)
│   ├── diagnostic.rs      # trait EngineDiagnostic + enum Severity
│   ├── catalog.rs         # TOML catalog loading/rendering + UNK-000 fallback
│   ├── engine.rs          # struct Engine: message() / log() / print()
│   └── error.rs           # crate's own internal errors (catalog loading)
├── examples/
│   ├── basic.rs           # minimal usage: message() + log()
│   ├── cli.rs              # usage with the `cli` feature: print()
│   └── errors.toml         # sample catalog used by both examples
└── tests/
    └── catalog.rs          # catalog loading/rendering tests
```

## Testing

```sh
cargo test --all-features
```

## Non-goals

- No compile-time validation that codes used in the project exist in the catalog.
- No `#[derive(EngineDiagnostic)]` macro.
- No automatic discovery of a dependency's catalog — merging is explicit
  (`.merged_with(...)`), not reflected over `Cargo.toml` dependencies.
- No collision detection/warnings when merged catalogs define the same code.
- No i18n (multiple languages per catalog).

See `docs/error-engine-design.md` for the full design rationale.