ADR-0019 implementation: friendly error layer + i18n catalog
All eight implementation steps from ADR-0019's §"Order of
operations":
Step 1 — `src/friendly/` module skeleton; `t!()` macro; YAML
catalog loader (`include_str!` + `serde_yml`); `{name}`
substitution helper that rejects format specifiers per §8.4.
Step 2 — `error.*` catalog populated for UNIQUE / FK /
NOT NULL / CHECK / type-mismatch / not_found / already_exists /
generic / invalid_value, with verbose hints per
pedagogical-voice rule (§5). Anchor phrases (§10) preserved
verbatim.
Step 3 — `FriendlyError { headline, hint, diagnostic_table }`
+ renderer composing the three blocks per §7.
Step 4 — `translate(&DbError, &TranslateContext) → FriendlyError`.
Classifies by `SqliteErrorKind` first, then by message text
for the constraint family. `change column` failures route to
the type-mismatch headline, subsuming the previous
`friendly_change_column_engine_error` helper.
Step 5 — `DbError::friendly_message()` delegates to the
translator with default context. Removed
`friendly_change_column_engine_error` (absorbed) and
`enrich_fk_message` (FK list moves to the deferred re-query
step). One test rewritten to assert on the engine-classified
payload rather than the removed enrichment text.
Step 6 — `messages (short|verbose)` app-level command parallel
to `mode`. `App::messages_verbosity` (default verbose)
threaded into `TranslateContext` via
`App::build_translate_context`. `AppEvent::DslFailed` now
carries the structured `DbError`, plus the App extracts the
user's attempted value from `Command::Insert` / `Update`
to fill the `{value}` placeholder for UNIQUE / NOT NULL.
Step 7 — Catalog validator (§8.6) checks for missing keys,
unused/undeclared placeholders, format specifiers, and
forbidden engine vocabulary. `main.rs` parses the embedded
catalog at startup so a corrupted build artefact fails
loudly there rather than at the first `t!()` call.
Step 8 — Anchor phrases (§10) held: existing tests asserting
on "no such table", "already exists", "cannot be converted",
etc. all pass without rewording.
## Tally
603 tests passing (was 561: +42 net). Clippy clean with
nursery lints. Release binary 7.7 MB.
## Deliberately deferred
- Schema-aware enrichment for FK violations (parent_table /
parent_column / child_table) and the multi-value
natural-order INSERT case for UNIQUE. Both need the
Database handle in scope at translation time, so they
bundle naturally with the row-pinpoint re-query work
(ADR-0019 §6) — that follow-on adds runtime-side
enrichment via a `Database` lookup and a structured
failure-context carried on `DslFailed`. Until then,
unfilled placeholders render as their `{name}` form for
visual consistency with the catalog.
- Migration sweep (§9). Only `error.*` is catalog-driven so
far; `help.*`, `ok.*`, `client_side.*`, `replay.*`,
`parse.*`, modal labels, etc. migrate per-PR.
- Settings persistence for `messages`. In-session state for
now; waits on the future settings ADR.
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
//! Friendly error layer and i18n message catalog (ADR-0019).
|
||||
//!
|
||||
//! Single chokepoint for user-visible message text. Engine
|
||||
//! errors flow through `translate()` into a structured
|
||||
//! [`FriendlyError`] payload that the renderer (in
|
||||
//! `output_render`) composes into final output. Every other
|
||||
//! user-visible string in the codebase migrates to this
|
||||
//! catalog over time via the [`t!`] macro (ADR-0019 §9).
|
||||
//!
|
||||
//! ## Catalog
|
||||
//!
|
||||
//! The catalog lives in `strings/<locale>.yaml`, embedded at
|
||||
//! compile time and parsed once on first access. Today the only
|
||||
//! locale is `en-US`; runtime selection is deferred (ADR-0019
|
||||
//! §8.2). Hierarchical YAML keys flatten to dot-paths
|
||||
//! internally — `error.unique.insert.verbose` is the path the
|
||||
//! `t!()` macro and the translator look up.
|
||||
//!
|
||||
//! ## The `t!()` macro
|
||||
//!
|
||||
//! ```ignore
|
||||
//! use rdbms_playground::t;
|
||||
//! let s = t!("error.unique.insert.verbose",
|
||||
//! table = "Customers", column = "id");
|
||||
//! ```
|
||||
//!
|
||||
//! Placeholder values implement `Display`. Format specifiers
|
||||
//! (`{name:08.2}`, `{name:>10}`, …) are explicitly rejected at
|
||||
//! substitution time — see ADR-0019 §8.4.
|
||||
|
||||
pub mod error;
|
||||
pub mod format;
|
||||
pub mod keys;
|
||||
pub mod translate;
|
||||
|
||||
pub use error::{DiagnosticTable, FriendlyError};
|
||||
pub use format::{catalog, Catalog};
|
||||
pub use translate::{Operation, TranslateContext, Verbosity};
|
||||
|
||||
// `translate::translate` and `format::translate` are different
|
||||
// callables — the former is the structured DbError → FriendlyError
|
||||
// classifier (the H1 entry point); the latter is the lower-level
|
||||
// catalog lookup the `t!()` macro expands to. Re-export both
|
||||
// under non-conflicting names.
|
||||
pub use format::translate;
|
||||
pub use translate::translate as translate_error;
|
||||
|
||||
/// Look up `key` in the catalog and substitute named arguments.
|
||||
///
|
||||
/// Panics if the key is missing, if the template has malformed
|
||||
/// placeholders, or if the args don't supply every name the
|
||||
/// template references. The catalog validator unit test (Step 7,
|
||||
/// ADR-0019 §8.6) catches these at build time so they should
|
||||
/// never fire at runtime.
|
||||
#[macro_export]
|
||||
macro_rules! t {
|
||||
($key:literal $(, $name:ident = $value:expr)* $(,)?) => {{
|
||||
$crate::friendly::translate(
|
||||
$key,
|
||||
&[$( (stringify!($name), &$value as &dyn ::std::fmt::Display) ),*],
|
||||
)
|
||||
}};
|
||||
}
|
||||
Reference in New Issue
Block a user