orion-error 0.6.3

Struct Error for Large Project
Documentation
# 使用教程

本教程面向 `orion-error 0.6.x` 的 `V1 API`,以当前源码与测试为准。

在调整本教程中的主路径、边界约束和评审标准前,先以 [V1 修复与评审基线](./v1-fix-and-review-plan.md) 为准。

## 安装

```toml
[dependencies]
orion-error = "0.6.1"
```

可选特性:

```toml
[dependencies]
orion-error = { version = "0.6.1", features = ["serde"] }
# 
orion-error = { version = "0.6.1", features = ["tracing"] }
```

默认启用 `log`。

## 导入约定

- `orion_error::prelude::*`:V1 主路径通配导入,包含 `IntoAs``ErrorWrapAs``ErrorConv``ErrorWith` 等推荐 API
- `orion_error::traits_ext::*`:如果你只想按 trait 分组导入 V1 主路径扩展 trait,可以用这一层
- `orion_error::compat_prelude::*` / `orion_error::compat_traits::*`:只用于兼容旧的 `owe_*()` / `err_wrap(...)` 调用路径

如果是新代码,默认不要把 compat 导入和 `prelude::*` 混成一个体系。

## 一分钟上手

```rust
use derive_more::From;
use orion_error::{
    ContextRecord, ErrorCode, ErrorWith, IntoAs, OperationContext, StructError, UvsReason,
};
use thiserror::Error;

#[derive(Debug, Error, Clone, PartialEq, From)]
enum UserError {
    #[error("user not found")]
    UserNotFound,
    #[error("{0}")]
    Uvs(UvsReason),
}

impl ErrorCode for UserError {
    fn error_code(&self) -> i32 {
        match self {
            Self::UserNotFound => 1001,
            Self::Uvs(reason) => reason.error_code(),
        }
    }
}

fn load_user(user_id: u64) -> Result<String, StructError<UserError>> {
    let mut ctx = OperationContext::doing("load_user");
    ctx.record("user_id", user_id.to_string());

    std::fs::read_to_string("user.json")
        .into_as(UserError::from(UvsReason::system_error()), "read user profile failed")
        .doing("read user profile")
        .with(&ctx)
}
```

说明:

- 领域错误一般不必手写 `impl DomainReason`;满足 `From<UvsReason> + Display + PartialEq` 即自动实现。
- `record(...)` 是当前推荐的上下文写法。
- `into_as(...)` 是普通错误进入结构化体系的主路径。
- `doing(...)` 在 V1 中只是 `want(...)` 的命名糖衣,不改变 `OperationContext` 底层语义。
- 如果上游已经是 `StructError<_>`,优先用 `err_conv()``wrap_as(...)`,不要再回退到 `.owe_*()`
## 1. 定义领域错误

推荐模式:

```rust
use derive_more::From;
use orion_error::{ErrorCode, UvsReason};
use thiserror::Error;

#[derive(Debug, Error, Clone, PartialEq, From)]
enum OrderError {
    #[error("insufficient funds")]
    InsufficientFunds,
    #[error("order not found")]
    OrderNotFound,
    #[error("{0}")]
    Uvs(UvsReason),
}

impl ErrorCode for OrderError {
    fn error_code(&self) -> i32 {
        match self {
            Self::InsufficientFunds => 2001,
            Self::OrderNotFound => 2002,
            Self::Uvs(reason) => reason.error_code(),
        }
    }
}
```

## 2. 创建结构化错误

### 直接构造

```rust
use orion_error::{StructError, UvsReason};

let err = StructError::from(UvsReason::validation_error())
    .with_detail("field `email` is required");
```

### Builder 构造

```rust
use orion_error::{OperationContext, StructError, UvsReason};

let ctx = OperationContext::doing("validate_request");

let err = StructError::builder(UvsReason::validation_error())
    .detail("field `email` is required")
    .context_ref(&ctx)
    .finish();
```

### 保留真实 source

```rust
use orion_error::{StructError, UvsReason};

let err = StructError::builder(UvsReason::system_error())
    .detail("failed to read config")
    .source_std(std::io::Error::other("disk offline"))
    .finish();
```

## 3. 使用上下文

```rust
use orion_error::{ContextRecord, ErrorWith, OperationContext};

let mut ctx = OperationContext::doing("place_order");
ctx.record("order_id", "A-1001");
ctx.record("user_id", "42");

let result = check_inventory()
    .doing("check inventory")
    .with(&ctx);
```

推荐约定:

- `OperationContext::doing(...)` 是 V1 推荐主命名,但在 V1 中仍只是 `want(...)` 的命名糖衣
- 错误链上的 `.doing(...)` 只追加内部步骤,形成完整 `Path`
- `at(...)` 在 V1 中只是 `with(...)` 的命名糖衣,不承诺写入结构化 target/path
- `record(...)` 写关键诊断键值
- `detail(...)` / `with_detail(...)` 写补充调试说明

上面的例子里,最终错误会体现为:

- `Want`: `place_order`
- `Path`: `place_order / check inventory`

如果你要读取这个语义:

- `target_main()` 返回最外层目标
- `target_path()` 返回完整调用路径
- `target()` 保留为 `target_main()` 的兼容别名

## 4. 错误转换策略

### 默认推荐:普通错误进入结构化体系

当上游错误实现 `std::error::Error`,并且这是第一次进入结构化错误体系时,优先使用 `into_as(...)`:

```rust
use orion_error::IntoAs;

std::fs::read_to_string("config.toml")
    .into_as(UvsReason::system_error(), "read config failed")?;

call_http_service()
    .into_as(UvsReason::network_error(), "call http service failed")?;
```

这是 V1 推荐主路径。

### 兼容路径:只保留 detail

当上游值只实现 `Display`,或者它本身不是一个真正的 error type 时,再使用 `owe_*()`:

```rust
parse_input().owe_validation()?;
run_business_rule().owe_biz()?;
```

### 自定义 reason

```rust
use orion_error::IntoAs;

some_io_result.into_as(UvsReason::system_error(), "load file failed")?;
```

### `StructError<R1>``StructError<R2>`

```rust
repo_call().err_conv()?;
```

`err_conv()` 会保留:

- `detail`
- `position`
- context stack
- source

### 跨层包装

如果你不是做 reason 类型转换,而是要在上层重新定义一个新 reason,同时把下层 `StructError` 整个作为 source 保留下来:

```rust
use orion_error::{ErrorWrapAs, StructError, UvsReason};

fn service_call() -> Result<(), StructError<UvsReason>> {
    repo_call()
        .wrap_as(UvsReason::system_error(), "service call failed")
        .map(|_| ())
}
```

这种方式更适合作为 V1 的公开主路径,用于 service/repository/infrastructure 分层包装。

兼容说明:

- `err_wrap(...)` 仍然保留
- 但它属于兼容入口,不属于 V1 推荐主路径

### 推荐决策顺序

- 上游是普通 `Error` 类型:优先 `into_as(...)`
- 需要显式声明“这是显式实现了 `RawStdError` 的 raw StdError 类型”时:`raw_source(...)` 后再 `into_as(...)`
- 上游只实现 `Display`:仍走兼容路径 `owe_*()`
- 上游已经是 `StructError<_>` 且只做 reason 映射:优先 `err_conv()`
- 上游已经是 `StructError<_>` 且要新建上层语义边界:优先 `wrap_as(...)`

## 5. `UvsReason` 选择建议

- `validation_error()`:输入、格式、约束校验失败
- `business_error()`:业务规则冲突
- `not_found_error()`:资源不存在
- `permission_error()`:认证或授权失败
- `logic_error()`:内部逻辑错误或不变量被破坏
- `data_error()`:数据处理或序列化问题
- `system_error()`:文件系统、OS、锁、进程环境问题
- `network_error()`:网络连接、DNS、HTTP 传输失败
- `resource_error()`:资源耗尽
- `timeout_error()`:超时
- `core_conf()` / `feature_conf()` / `dynamic_conf()`:配置错误
- `external_error()`:第三方系统失败

## 6. 日志与作用域

```rust
use orion_error::{op_context, ContextRecord};

fn process_order(order_id: &str) -> Result<(), MyError> {
    let mut ctx = op_context!("process_order").with_auto_log();
    ctx.record("order_id", order_id);
    ctx.info("starting");

    {
        let mut scope = ctx.scoped_success();
        scope.record("phase", "validation");
        validate(order_id)?;
    }

    Ok(())
}
```

注意:

- `OperationContext` 默认结果是失败
- 成功路径要么显式 `mark_suc()`,要么使用 `scoped_success()`
- `with_exit_log()` 已废弃,改用 `with_auto_log()`

## 7. 与 `thiserror` 的配合

推荐:

1. `thiserror` 定义领域错误枚举
2.`derive_more::From` 接入 `UvsReason`
3. 实现 `ErrorCode`
4. 在业务里优先使用 `IntoAs` / `wrap_as(...)` / `err_conv()`

导入建议:

- 新代码优先 `use orion_error::prelude::*;`
- 如果必须维护旧的 `owe_*()` / `err_wrap(...)`,再显式补 `use orion_error::compat_prelude::*;`

详见 [thiserror-comparison.md](./thiserror-comparison.md)。

## 8. source-chain 使用建议

如果你需要:

- `source()`
- `root_cause()`
- 更真实的底层错误链
- 监控里区分包装错误和根因

优先使用:

- `with_std_source(...)`
- `builder.source_std(...)`
- `into_as(...)`
- `with_struct_source(...)`
- `wrap_as(...)`

常用链路查看方法:

- `source_ref()`
- `root_cause()`
- `root_cause_frame()`
- `source_chain()`
- `source_frames()`
- `display_chain()`

启用 `serde` 后,序列化输出会包含 `want`、`path`、`source_frames`、`source_message` 和 `source_chain`。

其中 `source_frames` 是结构化链路,每一帧至少包含:

- `index`
- `message`
- 可选 `display`
- 可选 `type_name`
- 可选 `error_code`
- 可选 `reason`
- 可选 `want`
- 可选 `path`
- 可选 `detail`
- `is_root_cause`

当 source frame 来自下层 `StructError` 时,`message` 是稳定的 reason 文本,`display` 才是完整格式化错误。`debug` 在运行时仍可通过 `SourceFrame` 读取,但默认不会进入 serde 输出,因为 `Debug` 文本可能包含内部字段或敏感信息。

治理侧建议优先消费 `source_frames`;`source_chain` / `source_message` 主要作为兼容摘要保留。`type_name` 是 best-effort 信息,不应作为完整且稳定的分类键。

底层 trait object 本体仍然不会直接序列化。

旧的 `owe_*()` / `owe_*_source()` / `want(...)` / `with_source(...)` 仍可用,但都已进入 deprecated path;V1 不建议新增使用。

## 9. 当前版本的兼容提示

以下旧写法不要再新增:

- `OperationContext::with(...)`
- `OperationContext::with_path(...)`
- `with_exit_log()`
- `with_source(...)`
- `want(...)`
- `owe_*()` / `owe_*_source()`
- `impl DomainReason for MyError {}` 这种空实现
- `UvsReason::validation_error("msg")` 这种带参数构造

当前正确写法:

```rust
let mut ctx = OperationContext::doing("op");
ctx.record("key", "value");

let err = StructError::from(UvsReason::validation_error())
    .with_detail("message");
```

## 10. 验证命令

在 `orion-error/` 目录执行:

```bash
cargo fmt --all -- --check
cargo clippy --all-targets --all-features -- -D warnings
cargo test --all-features -- --test-threads=1
cargo run --example order_case
cargo run --example logging_example --features log
```