error-path 0.1.0

Attach stable, structured error paths to Result-based Rust errors with lightweight adapters.
Documentation
# error-path

[한국어]README.md | **English**

`error-path` is a lightweight **operational error address** layer for existing Rust error systems. It does not replace `anyhow`, application-specific types built with `thiserror`, `eyre`, or `error-stack`. Instead, it gives an error a stable address describing where it belongs in your system.

```text
login.api.request_login
```

Use that address consistently in logs, support workflows, operational documentation, UI messages, and an error catalog. This crate does not reconstruct a call history like a stack trace; it accumulates deliberate, stable operational names.

## Installation

For the attribute macros, enable the `macros` feature:

```toml
[dependencies]
error-path = { version = "0.1", features = ["macros"] }
```

For `ErrorPath` and `WithErrorPath` alone, no feature is required:

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

The minimum supported Rust version is 1.77.

## Quick start

Choose a stable path segment at the innermost boundary, then add address segments at higher boundaries.

```rust
use error_path::{error_path, ErrorPath, ErrorPathExt, WithErrorPath};

#[error_path("login.api.request_login")]
fn request_login() -> Result<(), ServiceError> {
    Err(ServiceError::new())
}

#[derive(Debug)]
struct ServiceError {
    path: ErrorPath,
}

impl ServiceError {
    fn new() -> Self {
        Self { path: ErrorPath::new() }
    }
}

impl WithErrorPath for ServiceError {
    fn with_error_path(mut self, path: &'static str) -> Self {
        self.path.prepend_path(path);
        self
    }
}

impl ErrorPathExt for ServiceError {
    fn error_path(&self) -> Option<ErrorPath> {
        Some(self.path.clone())
    }
}

let error = request_login().unwrap_err();
assert_eq!(error.path.to_string(), "login.api.request_login");
```

The central contract is `WithErrorPath`: keep an `ErrorPath` in your own error type and implement the trait to use the same macros.

```rust
use error_path::{ErrorPath, WithErrorPath};

struct MyError { path: ErrorPath }

impl WithErrorPath for MyError {
    fn with_error_path(mut self, path: &'static str) -> Self {
        self.path.prepend_path(path);
        self
    }
}
```

## Read an address at an error-handling boundary

`WithErrorPath` accumulates an address; `ErrorPathExt` reads only the address from an error that already exists. Import the trait at a logging, UI, or support boundary, then call `error.error_path()`.

```rust
use error_path::ErrorPathExt;

let error = request_login().unwrap_err();
let path = error.error_path().expect("ServiceError stores ErrorPath");

assert_eq!(path.to_string(), "login.api.request_login");
```

The default result is `None` for an error type that does not implement `ErrorPathExt`.

## Addresses and paths

An address is ordered from outermost to innermost. `#[error_path]` and `#[error_path_impl]` prepend stable path segments.

Your error type can expose a structured `ErrorPath`. Use `segments()` directly in logs or JSON, without parsing a rendered string, and use `to_string_with()` to render it with a chosen delimiter.

## Attribute macros

The `macros` feature re-exports three attributes:

```rust
use error_path::{error_path_impl, error_path_skip};

struct LoginApi;

#[error_path_impl("login.api")]
impl LoginApi {
    fn request_login(&self) -> Result<(), ServiceError> {
        Err(ServiceError::new())
    }

    #[error_path_skip]
    fn health_check(&self) -> Result<(), ServiceError> {
        Ok(())
    }
}

```

- `#[error_path]` uses the function name when no argument is given, or uses its string argument.
- `#[error_path_impl]` adds `{base}.{method_name}` to each result-returning method. Without an argument, the base is the trait name for a trait implementation or the type name for an inherent implementation.
- `#[error_path_skip]` excludes one method inside an `#[error_path_impl]` block.

Generated code calls `WithErrorPath::with_error_path(e, path)` on the returned `Err(E)`. The error type `E` must therefore implement `WithErrorPath`.

An address is prepended only when an annotated function returns `Err`. In a nested `A -> B -> C` call, a failure from `C` crosses return boundaries during unwind and becomes `A.B.C`. Function entry does not automatically receive a parent address; task-local, thread-local, and function-argument propagation are out of scope.

For `::`, configure the delimiter before the first `ErrorPath` creation or enrichment, then use the same delimiter in macro input.

```rust
use error_path::{error_path_impl, set_path_delimiter};

set_path_delimiter("::")?;

#[error_path_impl("login::api")]
impl LoginApi for LoginApiImpl {
    fn request_login(&self) -> Result<(), ServiceError> { todo!() }
}

// login::api::request_login
```

## Optional features and adapters

Enable only the adapter you use:

```toml
[dependencies]
error-path = { version = "0.1", features = ["macros", "anyhow"] }
anyhow = "1"
```

- `macros`: re-export `#[error_path]`, `#[error_path_impl]`, and `#[error_path_skip]`
- `anyhow`: add and query macro paths as typed context
- `eyre`: add a path as `eyre::Report` context
- `error-stack`: attach `ErrorPathSegment` to `error_stack::Report<C>`
- `stacked-errors`: add a locationless kind to `stacked_errors::Error`
- `full`: enable every feature above

External adapters use each library's own context or attachment mechanism. `anyhow::Error`, `eyre::Report`, `error_stack::Report<C>`, and `stacked_errors::Error` all implement `ErrorPathExt`, so after macro boundaries `error.error_path()` returns the address segments added by macros. It never interprets or includes an original error message.

```rust
use error_path::{ErrorPathExt, WithErrorPath};

let error = anyhow::anyhow!("HTTP communication failed");
let error = error.with_error_path("request_login");
let error = error.with_error_path("login.api");

assert_eq!(error.error_path().unwrap().to_string(), "login.api.request_login");
```

An application-owned error can store an `ErrorPath` and implement `ErrorPathExt` for additional structured data. `thiserror` is a derive macro that creates application-specific error types, so it needs no adapter. Implement `WithErrorPath` on the generated type. A macro such as `anyhow::bail!` works when its output error type implements `WithErrorPath` or its adapter feature is enabled.

## Delimiter configuration

The default delimiter is `.`. At application startup, you may set one process-wide delimiter for parsing and display.

```rust
use error_path::set_path_delimiter;

set_path_delimiter("::").expect("configure it once before the first ErrorPath");
```

After configuration, `ErrorPath` parsing and formatting through `Display` and `to_string()` use `::`. Write macro arguments and direct prefixes with the same delimiter, for example `login::api::request_login`. If configuration has not happened before the first `ErrorPath` creation, enrichment, or formatting, the default `.` is fixed. An empty delimiter and a second configuration attempt return an error; no environment variable is read.

For a one-off format independent of global configuration, use `ErrorPath::to_string_with("/")`.

## Scope and limitations

- The crate works at `Result<T, E>` boundaries. It does not automatically capture or transform panics, threads, or errors inside spawned tasks.
- Paths are never inferred from runtime stack traces or source file paths. Choose stable operational names explicitly, or use macro defaults.
- Apply `#[error_path]` only to functions returning `Result`. `#[error_path_impl]` automatically wraps only methods whose return-type name ends in `Result`.
- Generated macros refer to `::error_path`; if you rename the dependency, re-export it under the `error_path` name.
- The global delimiter affects `ErrorPath` rendering. It does not rewrite context or attachments already stored by external adapters.

## Learn more

- [Project documentation and examples]https://github.com/yongaru/error-path-kit
- [API documentation]https://docs.rs/error-path
- [Changelog]https://github.com/yongaru/error-path-kit/blob/main/CHANGELOG.md

## License

Licensed under either Apache-2.0 or MIT, at your option.

## Author

Created and maintained by 용사장. [akira76@gmail.com](mailto:akira76@gmail.com)