use serde::Serialize;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum Level {
Error,
Warn,
}
impl Level {
pub fn as_str(self) -> &'static str {
match self {
Level::Error => "error",
Level::Warn => "warn",
}
}
}
pub mod code {
pub const PARSE: &str = "E_PARSE";
pub const META_MISSING: &str = "E_META_MISSING";
pub const SPEC_UNSUPPORTED: &str = "E_SPEC_UNSUPPORTED";
pub const KIND_INVALID: &str = "E_KIND_INVALID";
pub const KIND_DEPTH: &str = "E_KIND_DEPTH";
pub const SCHEMA_FIELD: &str = "E_SCHEMA_FIELD";
pub const ID_MISMATCH: &str = "E_ID_MISMATCH";
pub const ID_NOT_UUID: &str = "E_ID_NOT_UUID";
pub const ID_VERSION: &str = "E_ID_VERSION";
pub const ID_DUP: &str = "E_ID_DUP";
pub const ENTRY_ROLE_DEPTH: &str = "E_ENTRY_ROLE_DEPTH";
pub const ENTRY_ID_MISMATCH: &str = "E_ENTRY_ID_MISMATCH";
pub const DEPTH_EXCEEDED: &str = "E_DEPTH_EXCEEDED";
pub const REF_NO_TARGET: &str = "E_REF_NO_TARGET";
pub const REF_SELF: &str = "E_REF_SELF";
pub const REF_CYCLE: &str = "E_REF_CYCLE";
pub const MANIFEST_MISSING: &str = "E_MANIFEST_MISSING";
pub const MANIFEST_GHOST: &str = "E_MANIFEST_GHOST";
pub const MANIFEST_HASH: &str = "E_MANIFEST_HASH";
pub const MANIFEST_DIGEST_MISSING: &str = "E_MANIFEST_DIGEST_MISSING";
pub const MANIFEST_DUP: &str = "E_MANIFEST_DUP";
pub const RESERVED_NAME: &str = "E_RESERVED_NAME";
pub const REVISION_STALE: &str = "E_REVISION_STALE";
pub const SCHEMA_FAIL: &str = "E_SCHEMA_FAIL";
pub const BUNDLE_SUFFIX: &str = "W_BUNDLE_SUFFIX";
pub const DOTFILE: &str = "W_DOTFILE";
pub const ROOT_STRAY: &str = "W_ROOT_STRAY";
pub const NO_SUMMARY: &str = "W_NO_SUMMARY";
pub const NO_TYPE: &str = "W_NO_TYPE";
pub const DEEP_TREE: &str = "W_DEEP_TREE";
pub const LARGE_ASSET: &str = "W_LARGE_ASSET";
pub const OPTIONAL_MISSING: &str = "W_OPTIONAL_MISSING";
pub const MANIFEST_MISSING_W: &str = "W_MANIFEST_MISSING";
pub const MANIFEST_GHOST_W: &str = "W_MANIFEST_GHOST";
pub const MANIFEST_HASH_W: &str = "W_MANIFEST_HASH";
pub const ALL: &[&str] = &[
PARSE,
META_MISSING,
SPEC_UNSUPPORTED,
KIND_INVALID,
KIND_DEPTH,
SCHEMA_FIELD,
ID_MISMATCH,
ID_NOT_UUID,
ID_VERSION,
ID_DUP,
ENTRY_ROLE_DEPTH,
ENTRY_ID_MISMATCH,
DEPTH_EXCEEDED,
REF_NO_TARGET,
REF_SELF,
REF_CYCLE,
MANIFEST_MISSING,
MANIFEST_GHOST,
MANIFEST_HASH,
MANIFEST_DIGEST_MISSING,
MANIFEST_DUP,
RESERVED_NAME,
REVISION_STALE,
SCHEMA_FAIL,
BUNDLE_SUFFIX,
DOTFILE,
ROOT_STRAY,
NO_SUMMARY,
NO_TYPE,
DEEP_TREE,
LARGE_ASSET,
OPTIONAL_MISSING,
MANIFEST_MISSING_W,
MANIFEST_GHOST_W,
MANIFEST_HASH_W,
];
}
#[derive(Debug, Clone, Serialize)]
pub struct Issue {
pub code: String,
pub level: Level,
pub path: String,
pub message: String,
}
impl Issue {
pub fn new(
code: &'static str,
level: Level,
path: impl Into<String>,
message: impl Into<String>,
) -> Self {
Self {
code: code.to_string(),
level,
path: path.into(),
message: message.into(),
}
}
pub fn error(code: &'static str, path: impl Into<String>, message: impl Into<String>) -> Self {
Self::new(code, Level::Error, path, message)
}
pub fn warn(code: &'static str, path: impl Into<String>, message: impl Into<String>) -> Self {
Self::new(code, Level::Warn, path, message)
}
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct Stats {
pub nodes: usize,
pub branches: usize,
pub entries: usize,
pub depth: usize,
}
#[derive(Debug, Clone, Default)]
pub struct Report {
pub bundle: String,
pub issues: Vec<Issue>,
pub stats: Stats,
}
impl Report {
pub fn push(&mut self, issue: Issue) {
self.issues.push(issue);
}
pub fn errors(&self) -> impl Iterator<Item = &Issue> {
self.issues.iter().filter(|i| i.level == Level::Error)
}
pub fn warnings(&self) -> impl Iterator<Item = &Issue> {
self.issues.iter().filter(|i| i.level == Level::Warn)
}
pub fn error_count(&self) -> usize {
self.errors().count()
}
pub fn warning_count(&self) -> usize {
self.warnings().count()
}
pub fn exit_code(&self) -> i32 {
if self.error_count() > 0 { 1 } else { 0 }
}
pub fn to_json(&self) -> serde_json::Value {
let conv = |it: &Issue| {
serde_json::json!({
"code": it.code,
"level": it.level.as_str(),
"path": it.path,
"message": it.message,
})
};
serde_json::json!({
"bundle": self.bundle,
"errors": self.errors().map(conv).collect::<Vec<_>>(),
"warnings": self.warnings().map(conv).collect::<Vec<_>>(),
"stats": {
"nodes": self.stats.nodes,
"branches": self.stats.branches,
"entries": self.stats.entries,
"depth": self.stats.depth,
},
})
}
pub fn to_text(&self) -> String {
let mut out = String::new();
out.push_str(&format!(
"{} {} nodes / {} branches / {} entries depth={}\n",
self.bundle, self.stats.nodes, self.stats.branches, self.stats.entries, self.stats.depth
));
for it in &self.issues {
let mark = match it.level {
Level::Error => "✗",
Level::Warn => "⚠",
};
out.push_str(&format!(
" {} {:<28} {} {}\n",
mark, it.code, it.path, it.message
));
}
out.push_str(&format!(
"{} errors, {} warnings exit={}",
self.error_count(),
self.warning_count(),
self.exit_code()
));
out
}
}
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("路径不存在:{0}")]
NotFound(String),
#[error("参数错误:{0}")]
BadArg(String),
#[error("I/O 错误({path}):{source}")]
Io {
path: String,
source: std::io::Error,
},
#[error("校验未通过:{0} 项错误")]
Validation(usize),
#[error("{0}")]
Other(String),
}
impl Error {
pub fn io(path: impl AsRef<std::path::Path>, source: std::io::Error) -> Self {
Error::Io {
path: path.as_ref().display().to_string(),
source,
}
}
}
pub type Result<T> = std::result::Result<T, Error>;