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:
claude@clouddev1
2026-05-13 21:24:51 +00:00
parent 1e06490572
commit 6ca297579e
17 changed files with 680 additions and 117 deletions
+14 -4
View File
@@ -197,16 +197,26 @@ pub(crate) struct RawCell {
pub was_quoted: bool,
}
#[derive(Debug, thiserror::Error)]
#[derive(Debug)]
pub(crate) enum CsvError {
#[error("CSV is empty")]
Empty,
#[error("invalid UTF-8 in CSV body")]
InvalidUtf8,
#[error("unterminated quoted field")]
UnterminatedQuote,
}
impl std::fmt::Display for CsvError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let key = match self {
Self::Empty => "persistence.csv.empty",
Self::InvalidUtf8 => "persistence.csv.invalid_utf8",
Self::UnterminatedQuote => "persistence.csv.unterminated_quote",
};
f.write_str(&crate::friendly::translate(key, &[]))
}
}
impl std::error::Error for CsvError {}
/// Tokenize a CSV body. Returns the header (column names from
/// the first record) and the data rows. Each cell preserves a
/// `was_quoted` flag so the caller can distinguish an empty
+46 -15
View File
@@ -79,37 +79,68 @@ impl Default for MigratorRegistry {
}
}
#[derive(Debug, thiserror::Error)]
#[derive(Debug)]
pub enum MigrateError {
#[error("could not read version field from project.yaml: {0}")]
VersionParse(String),
#[error(
"project.yaml is at version {file} but this build only understands \
up to version {latest}; upgrade the application or restore an \
older project.yaml"
)]
NewerThanSupported { file: u32, latest: u32 },
#[error(
"no migrator registered for version {0} (programmer error: \
registry latest_version disagrees with migrators length)"
)]
NoMigratorForVersion(u32),
#[error("migrator from v{from} to v{to} failed: {source}")]
StepFailed {
from: u32,
to: u32,
source: Box<Self>,
},
#[error("migrator produced an unparseable result: {0}")]
BadOutput(String),
#[error("io error during migration on `{}`: {source}", path.display())]
Io {
path: std::path::PathBuf,
#[source]
source: std::io::Error,
},
}
impl std::fmt::Display for MigrateError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::VersionParse(detail) => f.write_str(&crate::t!(
"persistence.migrate.version_parse",
detail = detail,
)),
Self::NewerThanSupported { file, latest } => f.write_str(&crate::t!(
"persistence.migrate.newer_than_supported",
file = file,
latest = latest,
)),
Self::NoMigratorForVersion(v) => f.write_str(&crate::t!(
"persistence.migrate.no_migrator",
version = v,
)),
Self::StepFailed { from, to, source } => f.write_str(&crate::t!(
"persistence.migrate.step_failed",
from = from,
to = to,
source = source,
)),
Self::BadOutput(detail) => f.write_str(&crate::t!(
"persistence.migrate.bad_output",
detail = detail,
)),
Self::Io { path, source } => f.write_str(&crate::t!(
"persistence.migrate.io",
path = path.display(),
source = source,
)),
}
}
}
impl std::error::Error for MigrateError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::StepFailed { source, .. } => Some(source.as_ref()),
Self::Io { source, .. } => Some(source),
_ => None,
}
}
}
/// Result of running [`migrate_to_latest`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MigrationOutcome {
+37 -4
View File
@@ -39,16 +39,13 @@ pub struct Persistence {
project_path: PathBuf,
}
#[derive(Debug, thiserror::Error)]
#[derive(Debug)]
pub enum PersistenceError {
#[error("could not {operation} `{path}`: {source}")]
Io {
operation: &'static str,
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("could not encode {kind} for `{path}`: {message}")]
Encode {
kind: &'static str,
path: PathBuf,
@@ -56,6 +53,42 @@ pub enum PersistenceError {
},
}
impl std::fmt::Display for PersistenceError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Io {
operation,
path,
source,
} => f.write_str(&crate::t!(
"persistence.io",
operation = operation,
path = path.display(),
source = source,
)),
Self::Encode {
kind,
path,
message,
} => f.write_str(&crate::t!(
"persistence.encode",
kind = kind,
path = path.display(),
message = message,
)),
}
}
}
impl std::error::Error for PersistenceError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Io { source, .. } => Some(source),
Self::Encode { .. } => None,
}
}
}
impl PersistenceError {
/// Path the failure was associated with.
#[must_use]
+28 -5
View File
@@ -222,22 +222,45 @@ pub(crate) fn parse_schema(body: &str) -> Result<SchemaSnapshot, YamlError> {
})
}
#[derive(Debug, thiserror::Error)]
#[derive(Debug)]
pub(crate) enum YamlError {
#[error("project.yaml syntax error: {0}")]
Syntax(String),
#[error("unsupported project.yaml version: {0} (expected 1)")]
UnsupportedVersion(u32),
#[error("unknown user-facing column type `{raw}` for `{table}.{column}`")]
UnknownType {
table: String,
column: String,
raw: String,
},
#[error("unknown referential action `{0}`")]
UnknownAction(String),
}
impl std::fmt::Display for YamlError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Syntax(msg) => f.write_str(&crate::t!(
"persistence.yaml.syntax",
detail = msg,
)),
Self::UnsupportedVersion(v) => f.write_str(&crate::t!(
"persistence.yaml.unsupported_version",
version = v,
)),
Self::UnknownType { table, column, raw } => f.write_str(&crate::t!(
"persistence.yaml.unknown_type",
table = table,
column = column,
raw = raw,
)),
Self::UnknownAction(raw) => f.write_str(&crate::t!(
"persistence.yaml.unknown_action",
raw = raw,
)),
}
}
}
impl std::error::Error for YamlError {}
fn parse_action(s: &str) -> Option<ReferentialAction> {
match s {
"no_action" => Some(ReferentialAction::NoAction),