round-5 follow-up r2: migrate all thiserror Display attributes to catalog
Completes the i18n sweep started in the previous commit. All
remaining hand-rolled user-facing English strings inside
thiserror #[error(...)] attributes have been moved into the
catalog. Drops the thiserror dependency entirely.
Twelve error types migrated:
- dsl::action::UnknownAction → parse.custom.unknown_action
- dsl::parser::ParseError → parse.error_wrapper + parse.empty
- dsl::value::ValueError → value.{type_mismatch,format}
- persistence::csv_io::CsvError → persistence.csv.*
- persistence::mod::PersistenceError → persistence.{io,encode}
- persistence::yaml::YamlError → persistence.yaml.*
- persistence::migrations::MigrateError → persistence.migrate.*
- project::lock::LockError → project.lock.*
- project::naming::NamingError → project.naming.*
- project::naming::UserNameError → project.user_name.*
- project::mod::ProjectError → project.{path_not_found,...}
- project::mod::SafeDeleteError → project.safe_delete.*
- archive::ArchiveError → archive.*
- cli::ArgsError → cli.*
- db::DbError → db.error.*
Pattern per type: drop thiserror::Error derive, write manual
Display calling crate::t!(), keep #[from] semantics via
explicit From impls, override Error::source() where applicable
so #[source]-style chaining is preserved.
Why this matters (user rationale): "fine to have fallbacks for
errors that are purely technical, but lift the output to a
place where it can be localized later and where an adjustment
with friendly text is easily possible if any of them become
part of the happy path." All surface strings now live in
en-US.yaml and can be reworded or localized without touching
Rust source.
Tests: 769 passing, 0 failed, 1 ignored. Clippy clean with
-D warnings. Cargo.toml: drop thiserror = "2.0.18".
This commit is contained in:
+69
-23
@@ -57,45 +57,91 @@ const IMPORT_SUFFIX_LIMIT: u32 = 99;
|
||||
/// explicit filename.
|
||||
const EXPORT_SEQUENCE_LIMIT: u32 = 99;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[derive(Debug)]
|
||||
pub enum ArchiveError {
|
||||
#[error("io error on `{}`: {source}", path.display())]
|
||||
Io {
|
||||
path: PathBuf,
|
||||
#[source]
|
||||
source: io::Error,
|
||||
},
|
||||
#[error("zip error on `{}`: {message}", path.display())]
|
||||
Zip { path: PathBuf, message: String },
|
||||
#[error(
|
||||
"could not pick an export filename for `{project}` in `{}`: \
|
||||
all sequence numbers up to {limit} are taken",
|
||||
target_dir.display(),
|
||||
)]
|
||||
Zip {
|
||||
path: PathBuf,
|
||||
message: String,
|
||||
},
|
||||
ExportSequenceExhausted {
|
||||
project: String,
|
||||
target_dir: PathBuf,
|
||||
limit: u32,
|
||||
},
|
||||
#[error(
|
||||
"destination `{}` already exists and the auto-suffix retries \
|
||||
(-02 through -{limit:02}) are also taken; use \
|
||||
`import <zip> as <target>` to choose a different name",
|
||||
path.display(),
|
||||
)]
|
||||
ImportCollisionExhausted { path: PathBuf, limit: u32 },
|
||||
#[error("zip is malformed: {0}")]
|
||||
ImportCollisionExhausted {
|
||||
path: PathBuf,
|
||||
limit: u32,
|
||||
},
|
||||
InvalidZip(String),
|
||||
#[error("zip does not contain a project (no `project.yaml` under a single top-level folder)")]
|
||||
NotAProjectArchive,
|
||||
#[error("zip contains more than one top-level folder; refusing to extract")]
|
||||
MultipleTopFolders,
|
||||
#[error(
|
||||
"zip entry `{0}` would escape the target directory; refusing to extract"
|
||||
)]
|
||||
UnsafeEntry(String),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ArchiveError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Io { path, source } => f.write_str(&crate::t!(
|
||||
"archive.io",
|
||||
path = path.display(),
|
||||
source = source,
|
||||
)),
|
||||
Self::Zip { path, message } => f.write_str(&crate::t!(
|
||||
"archive.zip",
|
||||
path = path.display(),
|
||||
message = message,
|
||||
)),
|
||||
Self::ExportSequenceExhausted {
|
||||
project,
|
||||
target_dir,
|
||||
limit,
|
||||
} => f.write_str(&crate::t!(
|
||||
"archive.export_sequence_exhausted",
|
||||
project = project,
|
||||
target_dir = target_dir.display(),
|
||||
limit = limit,
|
||||
)),
|
||||
Self::ImportCollisionExhausted { path, limit } => {
|
||||
// {limit:02} is a format specifier — the catalog
|
||||
// helper rejects those (ADR-0019 §8.4). Pre-format
|
||||
// the limit before substitution.
|
||||
f.write_str(&crate::t!(
|
||||
"archive.import_collision_exhausted",
|
||||
path = path.display(),
|
||||
limit = format_args!("{limit:02}"),
|
||||
))
|
||||
}
|
||||
Self::InvalidZip(detail) => f.write_str(&crate::t!(
|
||||
"archive.invalid_zip",
|
||||
detail = detail,
|
||||
)),
|
||||
Self::NotAProjectArchive => {
|
||||
f.write_str(&crate::t!("archive.not_a_project_archive"))
|
||||
}
|
||||
Self::MultipleTopFolders => {
|
||||
f.write_str(&crate::t!("archive.multiple_top_folders"))
|
||||
}
|
||||
Self::UnsafeEntry(entry) => f.write_str(&crate::t!(
|
||||
"archive.unsafe_entry",
|
||||
entry = entry,
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for ArchiveError {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match self {
|
||||
Self::Io { source, .. } => Some(source),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Default export-zip filename for `<date>-<project>-export-NN.zip`.
|
||||
///
|
||||
/// `date_yyyymmdd` is the today-local prefix (8 digits);
|
||||
|
||||
Reference in New Issue
Block a user