Skip to main content

rust_store_core/
error.rs

1//! core 错误类型:FFI 边界的结构化错误(评测报告 rust m-3;框架缺陷修复 R3)。
2//!
3//! core 内部约定错误为 `String`(各层 `?` 直通,消息即契约——绑定层错误文案
4//! 与 JS 参考实现逐字对齐)。本模块把「哨兵识别」从 Host 侧收口到 core:
5//!
6//! - **core 内唯一的哨兵前缀匹配点**是 [`CoreError::classify`]:内层 `String`
7//!   错误在 FFI 边界经 `From<String>` 归类为 [`CoreError`],绑定层按 [`CoreError::code`]
8//!   透出稳定 machine code 并 `match` 枚举构造原生异常(Node `Error` / Python `RuntimeError`),
9//!   不再各自做字符串匹配(对比 nodejs-store m-1 / py-store crud/exec.py 的历史方案)。
10//! - **machine code 稳定且可穷举**([`CoreError::code`]):`permission_denied` /
11//!   `no_context` / `profile_blocked` / `other`——四端(宿主 + 四皮)据此映射对外语义,
12//!   与字符串前缀解耦。`Parse` / `NotFound` / `Validation` 三档按 R3 分步计划后续细化
13//!   (本步先落上列四档,见 `doc/execution/2026/10/...-02-...` §4.3)。
14//! - [`Display`] 输出**保留完整原始文案(含哨兵前缀)**:既有的「按前缀识别」
15//!   Host 逻辑(含 `ERR_PERMISSION:` 剥离)不受影响,宿主可渐进迁移。
16//!
17//! 分类依据只引用 [`crate::command`] 的哨兵常量本体,靠 [`tests`] 模块锁定
18//! 「常量前缀 ↔ 变体 ↔ machine code」关系,文案可自由调整而不破坏分类。
19
20use crate::command::{ERR_NO_CONTEXT, ERR_PERM_PREFIX, ERR_TEXT2QUERY};
21
22/// 策略视图只读守卫的稳定错误前缀:非 base 视图禁止注册 / 清空 schema(目录唯一真源在 base)。
23/// 归入 [`CoreError::Other`](非权限族),Host 按前缀识别即可(对齐 `no-error-masking`)。
24pub const ERR_POLICY_VIEW_READONLY: &str = "ERR_POLICY_VIEW_READONLY:";
25
26/// core 错误(FFI 边界可程序化穷举)。
27///
28/// 每个变体携带稳定 machine code([`CoreError::code`]),Host 与四皮据此映射对外语义:
29///
30/// | 变体 | machine code | 触发前缀 | 对外语义 |
31/// |---|---|---|---|
32/// | [`Permission`](CoreError::Permission) | `permission_denied` | `ERR_PERMISSION:` | 403 / `PERMISSION_DENIED` |
33/// | [`NoContext`](CoreError::NoContext) | `no_context` | `ERR_NO_CONTEXT:` | 403 / `PERMISSION_DENIED` |
34/// | [`ProfileBlocked`](CoreError::ProfileBlocked) | `profile_blocked` | `ERR_TEXT2QUERY:` | 档位拦截(emit 反馈) |
35/// | [`Other`](CoreError::Other) | `other` | —(兜底) | 500 透传原文 |
36#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
37pub enum CoreError {
38    /// 权限拒绝:内层消息携带 `ERR_PERMISSION:` 稳定前缀
39    /// ([`ERR_PERMISSION`](crate::command::ERR_PERMISSION) /
40    /// [`ERR_NO_WRITE`](crate::command::ERR_NO_WRITE) /
41    /// [`ERR_NO_DELETE`](crate::command::ERR_NO_DELETE) /
42    /// [`ERR_NO_BATCH_WRITE`](crate::command::ERR_NO_BATCH_WRITE) 等,Host 据此映射 403 类错误)。
43    #[error("{0}")]
44    Permission(String),
45    /// `require_context` 开启且 ctx 缺失(`ERR_NO_CONTEXT:` 前缀):调用方合约违反。
46    ///
47    /// 对外语义为 **403 / `PERMISSION_DENIED`**——依据 `store-api/spec/04-context.md`
48    /// (`requireContext` 开启且未注入上下文的请求按权限类映射 403,适配层不加第二层判断),
49    /// 与 [`Permission`](CoreError::Permission) 同档对外呈现,但保留独立 machine code
50    /// (`no_context`)以便宿主/皮区分语义与告警。
51    #[error("{0}")]
52    NoContext(String),
53    /// 档位拦截:内层消息携带 `ERR_TEXT2QUERY:` 稳定前缀(text2query 档命中硬限制/收缩项)。
54    ///
55    /// Host 据此 emit `profile_blocked` 反馈并构造各自的 ProfileViolation(构造后剥离前缀),
56    /// 不对中文文案做脆弱匹配——与 `ERR_PERMISSION:` 同构。
57    #[error("{0}")]
58    ProfileBlocked(String),
59    /// 其余语义错误(schema 校验 / 翻译层 / 参数校验 / 计算列……):无专属前缀,落兜底档,
60    /// 服务端按 500 透传原文(`no-error-masking`)。
61    #[error("{0}")]
62    Other(String),
63}
64
65impl CoreError {
66    /// 稳定 machine code(FFI 边界构造原生异常与四端映射的唯一依据;非字符串前缀)。
67    ///
68    /// 取值穷举见枚举文档表;新增档须单列,禁复用既有前缀语义。
69    pub fn code(&self) -> &'static str {
70        match self {
71            CoreError::Permission(_) => "permission_denied",
72            CoreError::NoContext(_) => "no_context",
73            CoreError::ProfileBlocked(_) => "profile_blocked",
74            CoreError::Other(_) => "other",
75        }
76    }
77
78    /// 把内层 `String` 错误按哨兵前缀归类(core 内唯一的前缀匹配收口点)。
79    pub fn classify(msg: String) -> Self {
80        if msg.starts_with(ERR_PERM_PREFIX) {
81            CoreError::Permission(msg)
82        } else if msg.starts_with(ERR_NO_CONTEXT) {
83            CoreError::NoContext(msg)
84        } else if msg.starts_with(ERR_TEXT2QUERY) {
85            CoreError::ProfileBlocked(msg)
86        } else {
87            CoreError::Other(msg)
88        }
89    }
90
91    /// 完整原始消息(含哨兵前缀;与 `Display` 一致,供宿主按前缀渐进迁移)
92    pub fn message(&self) -> &str {
93        match self {
94            CoreError::Permission(m)
95            | CoreError::NoContext(m)
96            | CoreError::ProfileBlocked(m)
97            | CoreError::Other(m) => m,
98        }
99    }
100}
101
102impl From<String> for CoreError {
103    fn from(msg: String) -> Self {
104        Self::classify(msg)
105    }
106}
107
108impl From<&str> for CoreError {
109    fn from(msg: &str) -> Self {
110        Self::classify(msg.to_string())
111    }
112}
113
114/// core 公开入口统一错误别名
115pub type CoreResult<T> = Result<T, CoreError>;
116
117#[cfg(test)]
118mod tests {
119    use super::CoreError;
120    use crate::command::{
121        ERR_NO_BATCH_WRITE, ERR_NO_CONTEXT, ERR_NO_DELETE, ERR_NO_WRITE, ERR_PERMISSION,
122        ERR_TEXT2QUERY,
123    };
124
125    #[test]
126    fn classify_maps_sentinels_to_variants() {
127        // 权限族(四个哨兵全量穷举)→ Permission
128        for m in [
129            ERR_PERMISSION,
130            ERR_NO_WRITE,
131            ERR_NO_DELETE,
132            ERR_NO_BATCH_WRITE,
133        ] {
134            let e = CoreError::classify(m.to_string());
135            assert_eq!(e, CoreError::Permission(m.to_string()));
136            assert_eq!(e.code(), "permission_denied");
137        }
138        // ctx 缺失 → NoContext(machine code `no_context`)
139        let e = CoreError::classify(ERR_NO_CONTEXT.to_string());
140        assert_eq!(e, CoreError::NoContext(ERR_NO_CONTEXT.to_string()));
141        assert_eq!(e.code(), "no_context");
142        // 档位拦截 → ProfileBlocked(machine code `profile_blocked`)
143        let e = CoreError::classify(format!("{ERR_TEXT2QUERY}单次取数行数超限"));
144        assert_eq!(
145            e,
146            CoreError::ProfileBlocked(format!("{ERR_TEXT2QUERY}单次取数行数超限"))
147        );
148        assert_eq!(e.code(), "profile_blocked");
149        // 其余 → Other
150        let e = CoreError::classify("translate: 未知命令".to_string());
151        assert_eq!(e, CoreError::Other("translate: 未知命令".to_string()));
152        assert_eq!(e.code(), "other");
153    }
154
155    #[test]
156    fn display_preserves_message_verbatim() {
157        // Display / message() 保留完整原文(含前缀)——宿主按前缀渐进迁移的兼容面
158        let e = CoreError::from(ERR_NO_WRITE);
159        assert_eq!(e.to_string(), ERR_NO_WRITE);
160        assert_eq!(e.message(), ERR_NO_WRITE);
161    }
162
163    #[test]
164    fn from_str_and_string_classify_identically() {
165        assert_eq!(
166            CoreError::from(ERR_PERMISSION),
167            CoreError::classify(ERR_PERMISSION.to_string())
168        );
169    }
170}