Skip to main content

str_format/
error.rs

1//! 错误码与校验报告。
2//!
3//! 错误码与规范第 6.1 章一一对应。清单类问题(`E_MANIFEST_*`)在
4//! `policies.manifest = "advisory"` 时降级为对应的 `W_MANIFEST_*`。
5
6use serde::Serialize;
7
8/// 问题级别。
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
10#[serde(rename_all = "lowercase")]
11pub enum Level {
12    /// 阻断性错误。
13    Error,
14    /// 非阻断性告警。
15    Warn,
16}
17
18impl Level {
19    /// 输出用字符串。
20    pub fn as_str(self) -> &'static str {
21        match self {
22            Level::Error => "error",
23            Level::Warn => "warn",
24        }
25    }
26}
27
28/// 规范 6.1 章的错误码常量。
29pub mod code {
30    // ── 结构 / 身份 ──────────────────────────────────────────
31    /// `._meta` 非法 TOML / 编码非 UTF-8 / 含 BOM / 时间未带时区偏移。
32    pub const PARSE: &str = "E_PARSE";
33    /// 分支目录缺失 `._meta`。
34    pub const META_MISSING: &str = "E_META_MISSING";
35    /// `str` 主版本不受支持。
36    pub const SPEC_UNSUPPORTED: &str = "E_SPEC_UNSUPPORTED";
37    /// `kind` 非法。
38    pub const KIND_INVALID: &str = "E_KIND_INVALID";
39    /// `kind` 与所在深度不符。
40    pub const KIND_DEPTH: &str = "E_KIND_DEPTH";
41    /// 必填字段缺失 / 类型不符 / 未知字段。
42    pub const SCHEMA_FIELD: &str = "E_SCHEMA_FIELD";
43    /// `id` 与所在目录名不一致。
44    pub const ID_MISMATCH: &str = "E_ID_MISMATCH";
45    /// 分支目录名不是合法 UUID。
46    pub const ID_NOT_UUID: &str = "E_ID_NOT_UUID";
47    /// UUID 版本与 `policies.id_version` 不符。
48    pub const ID_VERSION: &str = "E_ID_VERSION";
49    /// 同一 bundle 内出现重复分支 `id`。
50    pub const ID_DUP: &str = "E_ID_DUP";
51    /// 父级 `entries[].role` 与子目录实际内容或深度不符。
52    pub const ENTRY_ROLE_DEPTH: &str = "E_ENTRY_ROLE_DEPTH";
53    /// `entries[].id` 与子目录名不一致。
54    pub const ENTRY_ID_MISMATCH: &str = "E_ENTRY_ID_MISMATCH";
55    /// 分支树深度超过 `max_depth`。
56    pub const DEPTH_EXCEEDED: &str = "E_DEPTH_EXCEEDED";
57
58    // ── 引用 ────────────────────────────────────────────────
59    /// `refs[].target` 无法解析。
60    pub const REF_NO_TARGET: &str = "E_REF_NO_TARGET";
61    /// `refs[].target` 指向自身。
62    pub const REF_SELF: &str = "E_REF_SELF";
63    /// `refs` 关联图成环。
64    pub const REF_CYCLE: &str = "E_REF_CYCLE";
65
66    // ── 清单 ────────────────────────────────────────────────
67    /// 磁盘存在但 `entries` 未登记。
68    pub const MANIFEST_MISSING: &str = "E_MANIFEST_MISSING";
69    /// `entries` 登记但磁盘不存在。
70    pub const MANIFEST_GHOST: &str = "E_MANIFEST_GHOST";
71    /// `size` / `sha256` 与实际不符。
72    pub const MANIFEST_HASH: &str = "E_MANIFEST_HASH";
73    /// 文件类条目缺少 `size` / `sha256`。
74    pub const MANIFEST_DIGEST_MISSING: &str = "E_MANIFEST_DIGEST_MISSING";
75    /// `entries[].path` 重复。
76    pub const MANIFEST_DUP: &str = "E_MANIFEST_DUP";
77    /// 业务条目以 `._` 开头。
78    pub const RESERVED_NAME: &str = "E_RESERVED_NAME";
79    /// `revision` 非递增整数,或 `updated_at` 早于 `created_at`。
80    pub const REVISION_STALE: &str = "E_REVISION_STALE";
81    /// payload 不满足其声明的 JSON Schema。
82    pub const SCHEMA_FAIL: &str = "E_SCHEMA_FAIL";
83
84    // ── 告警 ────────────────────────────────────────────────
85    /// 根目录名未以 `.str` 结尾。
86    pub const BUNDLE_SUFFIX: &str = "W_BUNDLE_SUFFIX";
87    /// 出现非 `._meta` / `.lock` 的点文件。
88    pub const DOTFILE: &str = "W_DOTFILE";
89    /// ROOT 下出现既非分支目录也非保留名的条目。
90    pub const ROOT_STRAY: &str = "W_ROOT_STRAY";
91    /// 缺 `summary`。
92    pub const NO_SUMMARY: &str = "W_NO_SUMMARY";
93    /// 缺 `type`。
94    pub const NO_TYPE: &str = "W_NO_TYPE";
95    /// 分支树深度超过 `deep_tree_warn`。
96    pub const DEEP_TREE: &str = "W_DEEP_TREE";
97    /// 单文件超过 `large_asset_bytes`。
98    pub const LARGE_ASSET: &str = "W_LARGE_ASSET";
99    /// `optional: true` 的条目实际缺失。
100    pub const OPTIONAL_MISSING: &str = "W_OPTIONAL_MISSING";
101    /// `manifest = advisory` 时的未登记条目。
102    pub const MANIFEST_MISSING_W: &str = "W_MANIFEST_MISSING";
103    /// `manifest = advisory` 时的幽灵条目。
104    pub const MANIFEST_GHOST_W: &str = "W_MANIFEST_GHOST";
105    /// `manifest = advisory` 时的指纹不符。
106    pub const MANIFEST_HASH_W: &str = "W_MANIFEST_HASH";
107
108    /// 全部错误码(测试矩阵与 `--list-codes` 使用)。
109    pub const ALL: &[&str] = &[
110        PARSE,
111        META_MISSING,
112        SPEC_UNSUPPORTED,
113        KIND_INVALID,
114        KIND_DEPTH,
115        SCHEMA_FIELD,
116        ID_MISMATCH,
117        ID_NOT_UUID,
118        ID_VERSION,
119        ID_DUP,
120        ENTRY_ROLE_DEPTH,
121        ENTRY_ID_MISMATCH,
122        DEPTH_EXCEEDED,
123        REF_NO_TARGET,
124        REF_SELF,
125        REF_CYCLE,
126        MANIFEST_MISSING,
127        MANIFEST_GHOST,
128        MANIFEST_HASH,
129        MANIFEST_DIGEST_MISSING,
130        MANIFEST_DUP,
131        RESERVED_NAME,
132        REVISION_STALE,
133        SCHEMA_FAIL,
134        BUNDLE_SUFFIX,
135        DOTFILE,
136        ROOT_STRAY,
137        NO_SUMMARY,
138        NO_TYPE,
139        DEEP_TREE,
140        LARGE_ASSET,
141        OPTIONAL_MISSING,
142        MANIFEST_MISSING_W,
143        MANIFEST_GHOST_W,
144        MANIFEST_HASH_W,
145    ];
146}
147
148/// 单条校验问题。
149#[derive(Debug, Clone, Serialize)]
150pub struct Issue {
151    /// 错误码。
152    pub code: String,
153    /// 级别。
154    pub level: Level,
155    /// 定位路径(bundle 内相对路径)。
156    pub path: String,
157    /// 人类可读说明。
158    pub message: String,
159}
160
161impl Issue {
162    /// 构造一条问题。
163    pub fn new(
164        code: &'static str,
165        level: Level,
166        path: impl Into<String>,
167        message: impl Into<String>,
168    ) -> Self {
169        Self {
170            code: code.to_string(),
171            level,
172            path: path.into(),
173            message: message.into(),
174        }
175    }
176
177    /// 构造一条 error。
178    pub fn error(code: &'static str, path: impl Into<String>, message: impl Into<String>) -> Self {
179        Self::new(code, Level::Error, path, message)
180    }
181
182    /// 构造一条 warning。
183    pub fn warn(code: &'static str, path: impl Into<String>, message: impl Into<String>) -> Self {
184        Self::new(code, Level::Warn, path, message)
185    }
186}
187
188/// 统计信息。
189#[derive(Debug, Clone, Default, Serialize)]
190pub struct Stats {
191    /// 独立节点数(深度 1 的分支)。
192    pub nodes: usize,
193    /// 关联分支数(深度 ≥2 的分支)。
194    pub branches: usize,
195    /// 已登记条目总数。
196    pub entries: usize,
197    /// 实测分支树最大深度。
198    pub depth: usize,
199}
200
201/// 一次校验 / 遍历的完整报告。
202#[derive(Debug, Clone, Default)]
203pub struct Report {
204    /// bundle 名称。
205    pub bundle: String,
206    /// 全部问题,按产生顺序排列。
207    pub issues: Vec<Issue>,
208    /// 统计信息。
209    pub stats: Stats,
210}
211
212impl Report {
213    /// 追加一条问题。
214    pub fn push(&mut self, issue: Issue) {
215        self.issues.push(issue);
216    }
217
218    /// 全部错误。
219    pub fn errors(&self) -> impl Iterator<Item = &Issue> {
220        self.issues.iter().filter(|i| i.level == Level::Error)
221    }
222
223    /// 全部告警。
224    pub fn warnings(&self) -> impl Iterator<Item = &Issue> {
225        self.issues.iter().filter(|i| i.level == Level::Warn)
226    }
227
228    /// 错误数。
229    pub fn error_count(&self) -> usize {
230        self.errors().count()
231    }
232
233    /// 告警数。
234    pub fn warning_count(&self) -> usize {
235        self.warnings().count()
236    }
237
238    /// 退出码:有 error 为 1,否则 0。
239    pub fn exit_code(&self) -> i32 {
240        if self.error_count() > 0 { 1 } else { 0 }
241    }
242
243    /// 规范 6.2 规定的 `--json` 结构。
244    pub fn to_json(&self) -> serde_json::Value {
245        let conv = |it: &Issue| {
246            serde_json::json!({
247                "code": it.code,
248                "level": it.level.as_str(),
249                "path": it.path,
250                "message": it.message,
251            })
252        };
253        serde_json::json!({
254            "bundle": self.bundle,
255            "errors": self.errors().map(conv).collect::<Vec<_>>(),
256            "warnings": self.warnings().map(conv).collect::<Vec<_>>(),
257            "stats": {
258                "nodes": self.stats.nodes,
259                "branches": self.stats.branches,
260                "entries": self.stats.entries,
261                "depth": self.stats.depth,
262            },
263        })
264    }
265
266    /// 人类可读输出(规范 6.2)。
267    pub fn to_text(&self) -> String {
268        let mut out = String::new();
269        out.push_str(&format!(
270            "{}  {} nodes / {} branches / {} entries  depth={}\n",
271            self.bundle, self.stats.nodes, self.stats.branches, self.stats.entries, self.stats.depth
272        ));
273        for it in &self.issues {
274            let mark = match it.level {
275                Level::Error => "✗",
276                Level::Warn => "⚠",
277            };
278            out.push_str(&format!(
279                "  {} {:<28} {}  {}\n",
280                mark, it.code, it.path, it.message
281            ));
282        }
283        out.push_str(&format!(
284            "{} errors, {} warnings   exit={}",
285            self.error_count(),
286            self.warning_count(),
287            self.exit_code()
288        ));
289        out
290    }
291}
292
293/// 库级错误。
294#[derive(Debug, thiserror::Error)]
295pub enum Error {
296    /// 路径不存在。
297    #[error("路径不存在:{0}")]
298    NotFound(String),
299    /// 参数非法。
300    #[error("参数错误:{0}")]
301    BadArg(String),
302    /// I/O 失败。
303    #[error("I/O 错误({path}):{source}")]
304    Io {
305        /// 出错路径。
306        path: String,
307        /// 底层错误。
308        source: std::io::Error,
309    },
310    /// 校验未通过。
311    #[error("校验未通过:{0} 项错误")]
312    Validation(usize),
313    /// 其它。
314    #[error("{0}")]
315    Other(String),
316}
317
318impl Error {
319    /// 便捷构造 I/O 错误。
320    pub fn io(path: impl AsRef<std::path::Path>, source: std::io::Error) -> Self {
321        Error::Io {
322            path: path.as_ref().display().to_string(),
323            source,
324        }
325    }
326}
327
328/// 库级结果类型。
329pub type Result<T> = std::result::Result<T, Error>;