注册表错误带上底层错误并实现 source
export/registry.rs:新增 RegistryErrorCause,io/serde 失败保留原始错误类型(拿不到类型时用 Message 兜底),RegistryError 与 Cause 都实现 std::error::Error::source,Display 文案不变。 export/draft/xhs_minitool/error.rs:只在命令边界把 cause 转成 String,配合新增测试锁定 io::ErrorKind 可透出。
This commit is contained in:
@@ -192,7 +192,9 @@ macro_rules! impl_from_registry_error {
|
||||
fn from(error: crate::export::RegistryError) -> Self {
|
||||
match error {
|
||||
crate::export::RegistryError::Malformed { cause, .. } => {
|
||||
Self::RegistryMalformed(RegistryMalformed { cause })
|
||||
Self::RegistryMalformed(RegistryMalformed {
|
||||
cause: cause.to_string(),
|
||||
})
|
||||
}
|
||||
other => Self::ExportUnavailable(ExportUnavailable::new(other.to_string())),
|
||||
}
|
||||
|
||||
@@ -24,15 +24,103 @@ pub(crate) struct Loaded<T> {
|
||||
pub(crate) created: bool,
|
||||
}
|
||||
|
||||
/// 注册表失败的底层现场。
|
||||
///
|
||||
/// 能拿到底层错误类型时(`io` / `serde`)就把类型带出来,这样 Rust 侧调用方可以按
|
||||
/// `std::io::ErrorKind` 或 serde 错误分流,日志也能沿 `source()` 追到根因;只有本模块自己拼
|
||||
/// 出来的一句话(例如「注册表路径缺少父目录」)才退化成 [`RegistryErrorCause::Message`]。
|
||||
/// `context` 是给原始错误补的中文说明,只影响展示,不影响类型。
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum RegistryErrorCause {
|
||||
Io {
|
||||
context: Option<&'static str>,
|
||||
error: std::io::Error,
|
||||
},
|
||||
Serde {
|
||||
context: Option<&'static str>,
|
||||
error: serde_json::Error,
|
||||
},
|
||||
Message(String),
|
||||
}
|
||||
|
||||
impl RegistryErrorCause {
|
||||
fn message(message: impl Into<String>) -> Self {
|
||||
Self::Message(message.into())
|
||||
}
|
||||
|
||||
fn io(error: std::io::Error) -> Self {
|
||||
Self::Io {
|
||||
context: None,
|
||||
error,
|
||||
}
|
||||
}
|
||||
|
||||
fn io_with_context(context: &'static str, error: std::io::Error) -> Self {
|
||||
Self::Io {
|
||||
context: Some(context),
|
||||
error,
|
||||
}
|
||||
}
|
||||
|
||||
fn serde(error: serde_json::Error) -> Self {
|
||||
Self::Serde {
|
||||
context: None,
|
||||
error,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for RegistryErrorCause {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Io { context, error } => match context {
|
||||
Some(context) => write!(formatter, "{context}:{error}"),
|
||||
None => write!(formatter, "{error}"),
|
||||
},
|
||||
Self::Serde { context, error } => match context {
|
||||
Some(context) => write!(formatter, "{context}:{error}"),
|
||||
None => write!(formatter, "{error}"),
|
||||
},
|
||||
Self::Message(message) => formatter.write_str(message),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for RegistryErrorCause {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match self {
|
||||
Self::Io { error, .. } => Some(error),
|
||||
Self::Serde { error, .. } => Some(error),
|
||||
Self::Message(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 注册表文件级失败。目标模块负责把它映射成自己的 typed error。
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum RegistryError {
|
||||
/// 路径不可读写(含父目录创建失败)。
|
||||
Io { path: PathBuf, cause: String },
|
||||
Io {
|
||||
path: PathBuf,
|
||||
cause: RegistryErrorCause,
|
||||
},
|
||||
/// 文件内容不是本目标期望的形状。fail closed:不猜测、不修补。
|
||||
Malformed { path: PathBuf, cause: String },
|
||||
Malformed {
|
||||
path: PathBuf,
|
||||
cause: RegistryErrorCause,
|
||||
},
|
||||
/// 宿主自己的 DTO 序列化失败,属于编程错误。
|
||||
Encode { cause: String },
|
||||
Encode { cause: RegistryErrorCause },
|
||||
}
|
||||
|
||||
impl std::error::Error for RegistryError {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match self {
|
||||
Self::Io { cause, .. } | Self::Malformed { cause, .. } | Self::Encode { cause } => {
|
||||
Some(cause)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for RegistryError {
|
||||
@@ -83,13 +171,13 @@ where
|
||||
// 被报成「路径 / 权限故障」,把用户和 agent 往修权限的方向引。
|
||||
return Err(RegistryError::Malformed {
|
||||
path: path.to_path_buf(),
|
||||
cause: format!("注册表不是有效 UTF-8:{error}"),
|
||||
cause: RegistryErrorCause::io_with_context("注册表不是有效 UTF-8", error),
|
||||
});
|
||||
}
|
||||
Err(error) => {
|
||||
return Err(RegistryError::Io {
|
||||
path: path.to_path_buf(),
|
||||
cause: error.to_string(),
|
||||
cause: RegistryErrorCause::io(error),
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -111,7 +199,7 @@ pub(crate) fn save<T: Serialize>(path: &Path, value: &T) -> Result<String, Regis
|
||||
/// 规范形式:两空格缩进 + 结尾换行。agent 会手改这个文件,人类可读比紧凑更重要。
|
||||
fn encode<T: Serialize>(value: &T) -> Result<String, RegistryError> {
|
||||
let mut text = serde_json::to_string_pretty(value).map_err(|error| RegistryError::Encode {
|
||||
cause: error.to_string(),
|
||||
cause: RegistryErrorCause::serde(error),
|
||||
})?;
|
||||
text.push('\n');
|
||||
Ok(text)
|
||||
@@ -120,7 +208,7 @@ fn encode<T: Serialize>(value: &T) -> Result<String, RegistryError> {
|
||||
fn decode<T: DeserializeOwned>(path: &Path, text: &str) -> Result<T, RegistryError> {
|
||||
serde_json::from_str(text).map_err(|error| RegistryError::Malformed {
|
||||
path: path.to_path_buf(),
|
||||
cause: error.to_string(),
|
||||
cause: RegistryErrorCause::serde(error),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -131,11 +219,11 @@ fn decode<T: DeserializeOwned>(path: &Path, text: &str) -> Result<T, RegistryErr
|
||||
fn write_atomically(path: &Path, text: &str) -> Result<(), RegistryError> {
|
||||
let parent = path.parent().ok_or_else(|| RegistryError::Io {
|
||||
path: path.to_path_buf(),
|
||||
cause: "注册表路径缺少父目录".to_string(),
|
||||
cause: RegistryErrorCause::message("注册表路径缺少父目录"),
|
||||
})?;
|
||||
fs::create_dir_all(parent).map_err(|error| RegistryError::Io {
|
||||
path: path.to_path_buf(),
|
||||
cause: format!("创建导出目录失败:{error}"),
|
||||
cause: RegistryErrorCause::io_with_context("创建导出目录失败", error),
|
||||
})?;
|
||||
let temp = temp_path(path);
|
||||
let write = || -> std::io::Result<()> {
|
||||
@@ -153,14 +241,14 @@ fn write_atomically(path: &Path, text: &str) -> Result<(), RegistryError> {
|
||||
let _ = fs::remove_file(&temp);
|
||||
return Err(RegistryError::Io {
|
||||
path: path.to_path_buf(),
|
||||
cause: error.to_string(),
|
||||
cause: RegistryErrorCause::io(error),
|
||||
});
|
||||
}
|
||||
fs::rename(&temp, path).map_err(|error| {
|
||||
let _ = fs::remove_file(&temp);
|
||||
RegistryError::Io {
|
||||
path: path.to_path_buf(),
|
||||
cause: error.to_string(),
|
||||
cause: RegistryErrorCause::io(error),
|
||||
}
|
||||
})?;
|
||||
// rename 只改目录条目:不 fsync 父目录的话,崩溃后「哪个文件才是新的」这件事可能没落盘,
|
||||
@@ -292,4 +380,24 @@ mod tests {
|
||||
assert_eq!(loaded.value, value);
|
||||
assert_eq!(loaded.content_hash, hash);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_keeps_the_underlying_io_kind_for_rust_callers() {
|
||||
let error = RegistryError::Io {
|
||||
path: PathBuf::from("/tmp/registry.json"),
|
||||
cause: RegistryErrorCause::io_with_context(
|
||||
"创建导出目录失败",
|
||||
std::io::Error::from(std::io::ErrorKind::PermissionDenied),
|
||||
),
|
||||
};
|
||||
|
||||
// 展示文案保持原样,Rust 侧则能沿 source() 一路拿回 io::Error 并按 ErrorKind 分流。
|
||||
assert!(error.to_string().contains("创建导出目录失败"));
|
||||
let cause = std::error::Error::source(&error).expect("cause");
|
||||
let io = cause
|
||||
.source()
|
||||
.and_then(|source| source.downcast_ref::<std::io::Error>())
|
||||
.expect("io source");
|
||||
assert_eq!(io.kind(), std::io::ErrorKind::PermissionDenied);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user