Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

使用教程

本文档以当前源码、测试和 examples/ 为准,描述 orion-error 的主路径用法。

安装

[dependencies]
orion-error = "0.8.0"

常见可选 feature:

[dependencies]
orion-error = { version = "0.8.0", features = ["serde"] }
# 或
orion-error = { version = "0.8.0", features = ["tracing"] }
# 或
orion-error = { version = "0.8.0", features = ["serde_json"] }

默认 feature 包含:

  • derive
  • log

导入约定

推荐优先使用下面两种方式:

#![allow(unused)]
fn main() {
use orion_error::prelude::*;
use orion_error::runtime::OperationContext;
}

或:

#![allow(unused)]
fn main() {
use orion_error::{StructError, OrionError};
use orion_error::conversion::{ErrorWith, SourceErr, ConvErr};
use orion_error::protocol::DefaultExposurePolicy;
use orion_error::reason::UnifiedReason;
use orion_error::runtime::OperationContext;
}

其中:

  • prelude::* 只导出主路径:OrionErrorStructErrorSourceErrErrorWithConvErr
  • 新业务代码默认先用 prelude::*;只有在模块要显式表达 runtime / conversion / protocol 等边界时,再补 layered imports
  • DefaultExposurePolicy 只从 protocol::* 导入,因为它只属于 exposure/projection 边界
  • 需要更明确边界时,再按职责补 runtime / conversion / report / bridge / reason / protocol

一分钟上手

#![allow(unused)]
fn main() {
use derive_more::From;
use orion_error::{
    prelude::*,
    runtime::OperationContext,
};

#[derive(Debug, Clone, PartialEq, From, OrionError)]
enum AppReason {
    #[orion_error(identity = "biz.invalid_request")]
    InvalidRequest,
    #[orion_error(transparent)]
    General(UnifiedReason),
}

fn load_config() -> Result<String, StructError<AppReason>> {
    let ctx = OperationContext::doing("load config")
        .with_field("path", "config.toml")
        .with_meta("component.name", "config_loader");

    std::fs::read_to_string("config.toml")
        .source_err(AppReason::system_error(), "read config failed")
        .doing("read config file")
        .with_context(&ctx)
}
}

这个例子覆盖了当前主路径的四个核心点:

  • 领域 reason 用 OrionError 定义
  • 错误进入结构化体系用 source_err(...)(统一入口)
  • 运行时语义上下文用 doing(...)
  • 诊断字段和 metadata 写到 OperationContext

1. 定义 reason

1.1 领域 reason

新代码推荐直接 derive OrionError

#![allow(unused)]
fn main() {
use derive_more::From;
use orion_error::{OrionError, UnifiedReason};

#[derive(Debug, Clone, PartialEq, From, OrionError)]
enum OrderReason {
    #[orion_error(identity = "biz.order_not_found")]
    OrderNotFound,
    #[orion_error(identity = "biz.insufficient_funds")]
    InsufficientFunds,
    #[orion_error(transparent)]
    General(UnifiedReason),
}
}

OrionError 会为该类型生成:

  • Display
  • DomainReason
  • ErrorCode
  • ErrorIdentityProvider

默认规则:

  • identity = "biz.order_not_found" 生成 stable code
  • category 默认由 identity 前缀推导
  • message 未显式指定时,会从 identity 最后一段推导出显示文案
  • code 未显式指定时,兼容数值码默认是 500

1.2 通用 reason

UnifiedReason 是 crate 内置的通用错误分类,已经实现:

  • DomainReason
  • ErrorCode
  • ErrorIdentityProvider

常用构造:

  • UnifiedReason::validation_error()
  • UnifiedReason::business_error()
  • UnifiedReason::system_error()
  • UnifiedReason::network_error()
  • UnifiedReason::timeout_error()
  • UnifiedReason::core_conf()
  • UnifiedReason::logic_error()

2. 构造 StructError

2.1 直接构造

#![allow(unused)]
fn main() {
use orion_error::{StructError, UnifiedReason};

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

2.2 Builder 构造

#![allow(unused)]
fn main() {
use orion_error::{
    runtime::OperationContext,
    StructError,
    UnifiedReason,
};

let ctx = OperationContext::doing("validate request");

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

2.3 挂载 source

已有 StructError 时:

#![allow(unused)]
fn main() {
use orion_error::{StructError, UnifiedReason};

let err = StructError::from(UnifiedReason::system_error())
    .with_detail("read config failed")
    .with_source(std::io::Error::other("disk offline"));

assert_eq!(err.source_ref().unwrap().to_string(), "disk offline");
}

Builder 时:

#![allow(unused)]
fn main() {
use orion_error::{StructError, UnifiedReason};

let err = StructError::builder(UnifiedReason::system_error())
    .detail("read config failed")
    .source(std::io::Error::other("disk offline"))
    .finish();

assert_eq!(err.source_ref().unwrap().to_string(), "disk offline");
}

主路径建议优先使用:

  • with_source(...)
  • source(...)

它们会自动处理:

  • 普通 StdError
  • 已结构化的 StructError<_>

下面这些显式 API 属于底层/诊断/测试入口,不作为新业务代码主路径:

  • with_std_source(...)
  • with_struct_source(...)
  • source_std(...)
  • source_struct(...)

3. 使用上下文

OperationContext 是运行时上下文载体。

#![allow(unused)]
fn main() {
use orion_error::OperationContext;

let ctx = OperationContext::doing("place_order")
    .with_field("order_id", "A-1001")
    .with_field("user_id", "42")
    .with_meta("component.name", "order_service")
    .with_meta("tenant.id", "demo");
}

推荐区分两类写法:

  • with_field(...):给人看的诊断字段(chain 模式)
  • with_meta(...):机器消费的结构化 metadata(chain 模式)
  • record_field(...) / record_meta(...):当已有可变引用时使用

3.1 错误侧挂载上下文

#![allow(unused)]
fn main() {
use orion_error::prelude::*;
use orion_error::{OperationContext, StructError, UnifiedReason};

fn check_inventory() -> Result<(), StructError<UnifiedReason>> {
    Err(StructError::from(UnifiedReason::business_error()).with_detail("inventory unavailable"))
}

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

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

assert!(result.is_err());
}

上下文语义:

  • OperationContext::doing(...)action
  • OperationContext::at(...)locator
  • StructError::doing(...) / at(...) 是对应的 error-side 语义糖衣
  • 兼容投影仍然保留 target / path

常用读取方法:

  • action_main()
  • locator_main()
  • target_path()

4. 错误进入和跨层转换

4.1 source_err(reason, detail)

source_err(reason, detail) 是统一入口,同时支持原始 std::error::Error 和已结构化的 StructError<_> 源。

#![allow(unused)]
fn main() {
use orion_error::prelude::*;

let err = std::fs::read_to_string("config.toml")
    .source_err(UnifiedReason::system_error(), "read config failed")
    .unwrap_err();
}

source_err 支持常见的标准错误类型和已结构化的 StructError 源。

当前支持的是一组受控入口:

  • std::io::Error
  • anyhow::Error(启用 anyhow feature)
  • serde_json::Error(启用 serde_json feature)
  • toml::de::Error / toml::ser::Error(启用 toml feature)
  • raw_source(...) 包装后的下游自定义 RawStdError

如果你有第三方错误类型,需要显式 opt-in:

#![allow(unused)]
fn main() {
use std::fmt;
use orion_error::prelude::*;
use orion_error::interop::{raw_source, RawStdError};

#[derive(Debug)]
struct ThirdPartyError;

impl fmt::Display for ThirdPartyError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "third-party failure")
    }
}

impl std::error::Error for ThirdPartyError {}
impl RawStdError for ThirdPartyError {}

let result: Result<(), ThirdPartyError> = Err(ThirdPartyError);
let err = result
    .map_err(raw_source)
    .source_err(UnifiedReason::system_error(), "load failed")
    .unwrap_err();
}

4.2 conv_err()

当只是把下层 reason 收敛到上层 reason,而不想新增一层 detail/source 语义时,使用 conv_err()

#![allow(unused)]
fn main() {
use derive_more::From;
use orion_error::{OrionError, StructError, UnifiedReason};
use orion_error::conversion::ConvErr;
use orion_error::conversion::ToStructError;

#[derive(Debug, Clone, PartialEq, From, OrionError)]
enum RepoReason {
    #[orion_error(transparent)]
    General(UnifiedReason),
}

#[derive(Debug, Clone, PartialEq, From, OrionError)]
enum ServiceReason {
    #[orion_error(transparent)]
    Repo(RepoReason),
}

fn lower_layer_call() -> Result<(), StructError<RepoReason>> {
    Err(RepoReason::system_error().to_err()
        .with_detail("read config failed"))
}

fn upper_layer_call() -> Result<(), StructError<ServiceReason>> {
    lower_layer_call().conv_err()?;
    Ok(())
}

let err = upper_layer_call().unwrap_err();
assert_eq!(err.detail().as_deref(), Some("read config failed"));
}

典型前提是:

  • R2: From<R1>

5. source、report、bridge 的边界

5.1 运行时对象

运行时传播使用:

  • StructError<R>

5.2 人类诊断对象

人类诊断使用:

  • DiagnosticReport

常用入口:

#![allow(unused)]
fn main() {
use orion_error::{StructError, UnifiedReason};

let err = StructError::from(UnifiedReason::system_error())
    .with_detail("read config failed");

let report = err.report();

assert_eq!(report.reason(), "system error");
}

5.4 标准错误生态 bridge

StructError<R> 本身不再直接实现 std::error::Error

需要进入标准错误生态时,使用显式 bridge:

  • as_std()
  • into_std()
  • into_boxed_std()
  • into_dyn_std()

6. 稳定身份和协议投影

6.1 稳定身份

每个错误变体都有一个永久的机器可读名称,不随文案或重构改变:

#![allow(unused)]
fn main() {
use orion_error::{OrionError, StructError};
use orion_error::reason::ErrorIdentityProvider;

#[derive(Debug, PartialEq, OrionError)]
enum ApiReason {
    #[orion_error(identity = "biz.invalid_input")]
    InvalidInput,
}

// 这个字符串是契约——监控、客户端、网关都依赖它:
assert_eq!(ApiReason::InvalidInput.stable_code(), "biz.invalid_input");
assert_eq!(ApiReason::InvalidInput.error_category().as_str(), "biz");
}

对比不稳定 vs 稳定:

不稳定稳定
"invalid input"(显示文案可能改)"biz.invalid_input"(永久)
100(数值码可能冲突)"biz.invalid_input"(带命名空间)
ApiReason::InvalidInput(Rust 路径可能重构)"biz.invalid_input"(独立于源代码)
    biz    .    invalid_input
   ────         ────────────
   category      stable code
   (conf/biz     不变的业务语义
    /logic/sys)

6.2 协议投影

同一个错误,对不同的协议边界输出不同的 JSON 形状,不需要手写映射:

use orion_error::{OrionError, StructError};
use orion_error::protocol::DefaultExposurePolicy;
use orion_error::UnifiedReason;

#[derive(Debug, PartialEq, OrionError)]
enum ApiReason {
    #[orion_error(identity = "biz.invalid_input")]
    InvalidInput,
    #[orion_error(transparent)]
    General(UnifiedReason),
}

let err = StructError::from(ApiReason::system_error())
    .with_detail("disk offline at /dev/sda");

let proto = err.exposure(&DefaultExposurePolicy);

// HTTP 响应——最小字段,对外安全
let http = proto.to_http_error_json().unwrap();
assert_eq!(http["status"], 500);                // 内部错误
assert_eq!(http["message"], "system error");    // 用 reason,不用 detail

// 日志输出——完整上下文,方便排查
let log = proto.to_log_error_json().unwrap();
assert_eq!(log["detail"], "disk offline at /dev/sda");  // 完整 detail
assert!(log["source_frames"].is_array());                  // source 链

// RPC 响应——隐藏内部细节
let rpc = proto.to_rpc_error_json().unwrap();
assert!(rpc["detail"].is_null()); // internal → 隐藏 detail

// CLI 输出——人类可读摘要
let cli = proto.to_cli_error_json().unwrap();
assert_eq!(cli["summary"], "system error: disk offline at /dev/sda");

核心概念:错误是一个三维物体,每个协议边界看到的是它投下的不同形状的影子。ExposurePolicy 决定哪一面对外可见。

      错误本身(StructError<R>)
              │
    ┌─────────┼──────────┐
    │         │          │
    ▼         ▼          ▼
  HTTP      RPC        Log
  {status,  {code,     {code, detail,
   message}  detail}    source_frames}

6.3 入口选择

  • identity_snapshot():查看稳定身份
  • exposure(...):完整协议输入(identity + decision + report)
  • to_*_error_json():协议边界出口 JSON

7. 测试建议

当前测试 helper:

  • assert_err_code(...)
  • assert_err_category(...)
  • assert_err_identity(...)
  • assert_err_operation(...)
  • assert_err_path(...)

这里的 assert_err_code(...) 断言的是 stable code 字符串,不是数值 error_code()

示例:

#![allow(unused)]
fn main() {
use orion_error::prelude::*;
use orion_error::reason::ErrorCategory;
use orion_error::dev::testing::assert_err_identity;

let err = std::fs::read_to_string("config.toml")
    .source_err(UnifiedReason::system_error(), "read config failed")
    .unwrap_err();

assert_err_identity(&err, "sys.io_error", ErrorCategory::Sys);
}

如果你要断言数值码,请直接调用:

#![allow(unused)]
fn main() {
use orion_error::reason::ErrorCode;
use orion_error::{StructError, UnifiedReason};

let err = StructError::from(UnifiedReason::system_error());
assert_eq!(err.reason().error_code(), 201);
}

8. 推荐实践

  • 领域 reason 默认 derive OrionError
  • 对外稳定协议依赖 stable code,不依赖人类文案
  • 所有错误统一使用 source_err(...) 进入结构化体系
  • 只做 reason 收敛优先 conv_err()
  • 需要协议暴露时使用 exposure(&policy)
  • 需要对外协议时使用 exposure(...) 或 projection API
  • 需要进入标准错误生态时使用显式 interop API