1use serde::Serialize;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
10#[serde(rename_all = "lowercase")]
11pub enum Level {
12 Error,
14 Warn,
16}
17
18impl Level {
19 pub fn as_str(self) -> &'static str {
21 match self {
22 Level::Error => "error",
23 Level::Warn => "warn",
24 }
25 }
26}
27
28pub mod code {
30 pub const PARSE: &str = "E_PARSE";
33 pub const META_MISSING: &str = "E_META_MISSING";
35 pub const SPEC_UNSUPPORTED: &str = "E_SPEC_UNSUPPORTED";
37 pub const KIND_INVALID: &str = "E_KIND_INVALID";
39 pub const KIND_DEPTH: &str = "E_KIND_DEPTH";
41 pub const SCHEMA_FIELD: &str = "E_SCHEMA_FIELD";
43 pub const ID_MISMATCH: &str = "E_ID_MISMATCH";
45 pub const ID_NOT_UUID: &str = "E_ID_NOT_UUID";
47 pub const ID_VERSION: &str = "E_ID_VERSION";
49 pub const ID_DUP: &str = "E_ID_DUP";
51 pub const ENTRY_ROLE_DEPTH: &str = "E_ENTRY_ROLE_DEPTH";
53 pub const ENTRY_ID_MISMATCH: &str = "E_ENTRY_ID_MISMATCH";
55 pub const DEPTH_EXCEEDED: &str = "E_DEPTH_EXCEEDED";
57
58 pub const REF_NO_TARGET: &str = "E_REF_NO_TARGET";
61 pub const REF_SELF: &str = "E_REF_SELF";
63 pub const REF_CYCLE: &str = "E_REF_CYCLE";
65
66 pub const MANIFEST_MISSING: &str = "E_MANIFEST_MISSING";
69 pub const MANIFEST_GHOST: &str = "E_MANIFEST_GHOST";
71 pub const MANIFEST_HASH: &str = "E_MANIFEST_HASH";
73 pub const MANIFEST_DIGEST_MISSING: &str = "E_MANIFEST_DIGEST_MISSING";
75 pub const MANIFEST_DUP: &str = "E_MANIFEST_DUP";
77 pub const RESERVED_NAME: &str = "E_RESERVED_NAME";
79 pub const REVISION_STALE: &str = "E_REVISION_STALE";
81 pub const SCHEMA_FAIL: &str = "E_SCHEMA_FAIL";
83
84 pub const BUNDLE_SUFFIX: &str = "W_BUNDLE_SUFFIX";
87 pub const DOTFILE: &str = "W_DOTFILE";
89 pub const ROOT_STRAY: &str = "W_ROOT_STRAY";
91 pub const NO_SUMMARY: &str = "W_NO_SUMMARY";
93 pub const NO_TYPE: &str = "W_NO_TYPE";
95 pub const DEEP_TREE: &str = "W_DEEP_TREE";
97 pub const LARGE_ASSET: &str = "W_LARGE_ASSET";
99 pub const OPTIONAL_MISSING: &str = "W_OPTIONAL_MISSING";
101 pub const MANIFEST_MISSING_W: &str = "W_MANIFEST_MISSING";
103 pub const MANIFEST_GHOST_W: &str = "W_MANIFEST_GHOST";
105 pub const MANIFEST_HASH_W: &str = "W_MANIFEST_HASH";
107
108 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#[derive(Debug, Clone, Serialize)]
150pub struct Issue {
151 pub code: String,
153 pub level: Level,
155 pub path: String,
157 pub message: String,
159}
160
161impl Issue {
162 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 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 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#[derive(Debug, Clone, Default, Serialize)]
190pub struct Stats {
191 pub nodes: usize,
193 pub branches: usize,
195 pub entries: usize,
197 pub depth: usize,
199}
200
201#[derive(Debug, Clone, Default)]
203pub struct Report {
204 pub bundle: String,
206 pub issues: Vec<Issue>,
208 pub stats: Stats,
210}
211
212impl Report {
213 pub fn push(&mut self, issue: Issue) {
215 self.issues.push(issue);
216 }
217
218 pub fn errors(&self) -> impl Iterator<Item = &Issue> {
220 self.issues.iter().filter(|i| i.level == Level::Error)
221 }
222
223 pub fn warnings(&self) -> impl Iterator<Item = &Issue> {
225 self.issues.iter().filter(|i| i.level == Level::Warn)
226 }
227
228 pub fn error_count(&self) -> usize {
230 self.errors().count()
231 }
232
233 pub fn warning_count(&self) -> usize {
235 self.warnings().count()
236 }
237
238 pub fn exit_code(&self) -> i32 {
240 if self.error_count() > 0 { 1 } else { 0 }
241 }
242
243 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 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#[derive(Debug, thiserror::Error)]
295pub enum Error {
296 #[error("路径不存在:{0}")]
298 NotFound(String),
299 #[error("参数错误:{0}")]
301 BadArg(String),
302 #[error("I/O 错误({path}):{source}")]
304 Io {
305 path: String,
307 source: std::io::Error,
309 },
310 #[error("校验未通过:{0} 项错误")]
312 Validation(usize),
313 #[error("{0}")]
315 Other(String),
316}
317
318impl Error {
319 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
328pub type Result<T> = std::result::Result<T, Error>;