Skip to main content

rust_store_core/
error.rs

1//! core 错误类型:FFI 边界的结构化错误(评测报告 rust m-3)。
2//!
3//! core 内部约定错误为 `String`(各层 `?` 直通,消息即契约——绑定层错误文案
4//! 与 JS 参考实现逐字对齐)。本模块把「哨兵识别」从 Host 侧收口到 core:
5//!
6//! - **core 内唯一的哨兵前缀匹配点**是 [`CoreError::classify`]:内层 `String`
7//!   错误在 FFI 边界经 `From<String>` 归类为 [`CoreError`],绑定层 `match`
8//!   枚举构造原生异常(Node `Error` / Python `RuntimeError`),不再各自做
9//!   字符串匹配(对比 nodejs-store m-1 / py-store crud/exec.py 的历史方案)。
10//! - [`Display`] 输出**保留完整原始文案(含哨兵前缀)**:既有的「按前缀识别」
11//!   Host 逻辑(含 `ERR_PERMISSION:` 剥离)不受影响,宿主可渐进迁移。
12//!
13//! 分类依据只引用 [`crate::command`] 的哨兵常量本体,靠 [`tests`] 模块锁定
14//! 「常量前缀 ↔ 变体」关系,文案可自由调整而不破坏分类。
15
16use crate::command::{ERR_NO_CONTEXT, ERR_PERM_PREFIX};
17
18/// 策略视图只读守卫的稳定错误前缀:非 base 视图禁止注册 / 清空 schema(目录唯一真源在 base)。
19/// 归入 [`CoreError::Other`](非权限族),Host 按前缀识别即可(对齐 `no-error-masking`)。
20pub const ERR_POLICY_VIEW_READONLY: &str = "ERR_POLICY_VIEW_READONLY:";
21
22/// core 错误(FFI 边界可程序化穷举)。
23#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
24pub enum CoreError {
25    /// 权限拒绝:内层消息携带 `ERR_PERMISSION:` 稳定前缀
26    /// ([`ERR_PERMISSION`] / [`ERR_NO_WRITE`] / [`ERR_NO_DELETE`] /
27    /// [`ERR_NO_BATCH_WRITE`] 等,Host 据此映射 403 类错误)。
28    #[error("{0}")]
29    Permission(String),
30    /// `require_context` 开启且 ctx 缺失:调用方合约违反(`ERR_NO_CONTEXT:`
31    /// 前缀),Host 一般映射 500 配置/契约错误而非 403。
32    #[error("{0}")]
33    NoContext(String),
34    /// 其余语义错误(schema 校验 / 翻译层 / 参数校验 / 计算列……)
35    #[error("{0}")]
36    Other(String),
37}
38
39impl CoreError {
40    /// 稳定错误码(FFI 边界 `match` 枚举之外的第二选择:日志/指标用)。
41    pub fn code(&self) -> &'static str {
42        match self {
43            CoreError::Permission(_) => "permission",
44            CoreError::NoContext(_) => "no_context",
45            CoreError::Other(_) => "other",
46        }
47    }
48
49    /// 把内层 `String` 错误按哨兵前缀归类(core 内唯一的前缀匹配收口点)。
50    pub fn classify(msg: String) -> Self {
51        if msg.starts_with(ERR_PERM_PREFIX) {
52            CoreError::Permission(msg)
53        } else if msg.starts_with(ERR_NO_CONTEXT) {
54            CoreError::NoContext(msg)
55        } else {
56            CoreError::Other(msg)
57        }
58    }
59
60    /// 完整原始消息(含哨兵前缀;与 `Display` 一致,供宿主按前缀渐进迁移)
61    pub fn message(&self) -> &str {
62        match self {
63            CoreError::Permission(m) | CoreError::NoContext(m) | CoreError::Other(m) => m,
64        }
65    }
66}
67
68impl From<String> for CoreError {
69    fn from(msg: String) -> Self {
70        Self::classify(msg)
71    }
72}
73
74impl From<&str> for CoreError {
75    fn from(msg: &str) -> Self {
76        Self::classify(msg.to_string())
77    }
78}
79
80/// core 公开入口统一错误别名
81pub type CoreResult<T> = Result<T, CoreError>;
82
83#[cfg(test)]
84mod tests {
85    use super::CoreError;
86    use crate::command::{
87        ERR_NO_BATCH_WRITE, ERR_NO_CONTEXT, ERR_NO_DELETE, ERR_NO_WRITE, ERR_PERMISSION,
88    };
89
90    #[test]
91    fn classify_maps_sentinels_to_variants() {
92        // 权限族(四个哨兵全量穷举)→ Permission
93        for m in [
94            ERR_PERMISSION,
95            ERR_NO_WRITE,
96            ERR_NO_DELETE,
97            ERR_NO_BATCH_WRITE,
98        ] {
99            let e = CoreError::classify(m.to_string());
100            assert_eq!(e, CoreError::Permission(m.to_string()));
101            assert_eq!(e.code(), "permission");
102        }
103        // ctx 缺失 → NoContext
104        let e = CoreError::classify(ERR_NO_CONTEXT.to_string());
105        assert_eq!(e, CoreError::NoContext(ERR_NO_CONTEXT.to_string()));
106        assert_eq!(e.code(), "no_context");
107        // 其余 → Other
108        let e = CoreError::classify("translate: 未知命令".to_string());
109        assert_eq!(e, CoreError::Other("translate: 未知命令".to_string()));
110        assert_eq!(e.code(), "other");
111    }
112
113    #[test]
114    fn display_preserves_message_verbatim() {
115        // Display / message() 保留完整原文(含前缀)——宿主按前缀渐进迁移的兼容面
116        let e = CoreError::from(ERR_NO_WRITE);
117        assert_eq!(e.to_string(), ERR_NO_WRITE);
118        assert_eq!(e.message(), ERR_NO_WRITE);
119    }
120
121    #[test]
122    fn from_str_and_string_classify_identically() {
123        assert_eq!(
124            CoreError::from(ERR_PERMISSION),
125            CoreError::classify(ERR_PERMISSION.to_string())
126        );
127    }
128}