Files
rdbms-playground/src/event.rs
T
claude@clouddev1 eac7e5b81d 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.
2026-05-09 12:43:37 +00:00

143 lines
4.8 KiB
Rust

//! Events fed into the application's update function.
//!
//! `AppEvent` is the single input type the runtime delivers to
//! `App::update`. Synthetic instances drive Tier 3 integration
//! tests (see ADR-0008), so the type is plain data with no
//! runtime dependency.
use crossterm::event::KeyEvent;
use crate::db::{
AddColumnResult, ChangeColumnTypeResult, DataResult, DbError, DeleteResult,
InsertResult, TableDescription, UpdateResult,
};
use crate::dsl::Command;
#[derive(Debug, Clone)]
pub enum AppEvent {
Key(KeyEvent),
Resize {
cols: u16,
rows: u16,
},
Tick,
/// A DSL command finished successfully. `description` is
/// `Some` for commands that produce a table view (create,
/// add column) and `None` for commands that don't (drop).
DslSucceeded {
command: Command,
description: Option<TableDescription>,
},
/// A `show data` query succeeded.
DslDataSucceeded { command: Command, data: DataResult },
DslInsertSucceeded {
command: Command,
result: InsertResult,
},
DslUpdateSucceeded {
command: Command,
result: UpdateResult,
},
DslDeleteSucceeded {
command: Command,
result: DeleteResult,
},
/// A `change column …` succeeded. `result` carries both the
/// post-rebuild description (for the auto-show) and the
/// optional `[client-side]` note (ADR-0017 §6).
DslChangeColumnSucceeded {
command: Command,
result: ChangeColumnTypeResult,
},
/// An `add column …` succeeded. `result` carries the
/// post-add description plus any `[client-side]` notes
/// from the auto-fill paths (ADR-0018 §9).
DslAddColumnSucceeded {
command: Command,
result: AddColumnResult,
},
/// A DSL command failed. `error` is the structured
/// payload — App applies its current verbosity setting
/// (`messages_verbosity`) when rendering through
/// `friendly::translate_error` (ADR-0019 §5).
DslFailed {
command: Command,
error: DbError,
},
/// Refreshed list of tables in the database.
TablesRefreshed(Vec<String>),
/// A persistence failure occurred (ADR-0015 §8). The
/// application surfaces a fatal banner and exits cleanly so
/// the message remains above the shell prompt.
PersistenceFatal {
operation: String,
path: std::path::PathBuf,
message: String,
},
/// Runtime has computed the rebuild summary from
/// `project.yaml` + `data/` and is ready for the user to
/// confirm. App opens the confirmation modal.
RebuildPrepared {
summary: String,
},
/// Rebuild completed successfully. App closes the modal,
/// surfaces a friendly outcome message, and refreshes the
/// table list.
RebuildSucceeded {
summary: String,
},
/// Rebuild failed in a non-fatal way (e.g., user-visible
/// constraint problem) — surfaced like other DSL failures.
RebuildFailed {
error: String,
},
/// Runtime has gathered the list of available projects
/// for the load picker. App opens the picker modal.
LoadPickerReady {
entries: Vec<crate::app::LoadPickerEntry>,
},
/// A project switch (load / new / save-as / import)
/// succeeded. Carries the new display name, the temp
/// flag (drives the `[TEMP]` status-bar prefix), and the
/// seed entries for input-history hydration off the new
/// project's `history.log` (I2-persist, ADR-0015 §12).
ProjectSwitched {
display_name: String,
is_temp: bool,
history_entries: Vec<String>,
},
/// A project switch failed in a non-fatal way (target
/// already exists, path unreadable, …). Surfaced as an
/// error in the output panel.
ProjectSwitchFailed {
error: String,
},
/// Export wrote a zip successfully. Carries the resolved
/// final path so the user gets a "wrote to: …" note.
ExportSucceeded {
path: std::path::PathBuf,
},
/// Export failed in a non-fatal way (target exists, IO
/// error, sequence range exhausted, …).
ExportFailed {
error: String,
},
/// A `replay <path>` finished without error, after running
/// `count` non-blank, non-comment commands from the file.
/// Surfaced as `[ok] replay — N command(s)` in the output.
ReplayCompleted {
path: String,
count: usize,
},
/// A `replay <path>` aborted at line `line_number`. `command`
/// is the line text as it appeared in the file (for the
/// user's eyeline so they can locate the failing entry);
/// `error` is the rendered parse or runtime error.
ReplayFailed {
path: String,
line_number: usize,
command: String,
error: String,
},
}