Skip to main content

max / quasi

Make a mistyped column name loud instead of an empty cell Naming a cell's column is safer than counting to it in every way but one: a name no column has is dropped, so a typo renders an empty column rather than failing to compile. The row and the column list are usually written in different functions, so nothing above Table::row has both in hand to check. Two debug assertions, at the one place that does. A named cell matching no column is a bug. Two columns sharing a name is also a bug, and a quieter one: resolution takes the first match, so both columns would draw the same value. Debug only. In release the cell is still dropped, because a panic in a description is worse than a blank cell in front of a reader.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session
https://claude.ai/code/session_01MptwXZ8k65v19rFmdGAyki
Author: Max Johnson <me@maxj.phd> · 2026-09-03 03:25 UTC
Signed with PGP, not checked
Commit: d41d00a3715bda4a13ca4730e8fa99e99da16f8d
Parent: 1670206
1 file changed, +58 insertions, -8 deletions
@@ -7324,6 +7324,37 @@
7324 7324 pub fn row(mut self, mut row: Cells) -> Self {
7325 7325 if !row.placed.is_empty() {
7326 7326 let placed = std::mem::take(&mut row.placed);
7327 + debug_assert!(
7328 + placed
7329 + .iter()
7330 + .all(|(name, _)| self.columns.iter().any(|column| column.name == *name)),
7331 + "a cell named a column this table does not have; the name is dropped and the \
7332 + cell is silently lost. Named: {:?}. Columns: {:?}",
7333 + placed.iter().map(|(name, _)| name).collect::<Vec<_>>(),
7334 + self.columns
7335 + .iter()
7336 + .map(|column| &column.name)
7337 + .collect::<Vec<_>>(),
7338 + );
7339 + debug_assert!(
7340 + {
7341 + let mut names = self
7342 + .columns
7343 + .iter()
7344 + .map(|column| column.name.as_str())
7345 + .collect::<Vec<_>>();
7346 + names.sort_unstable();
7347 + let before = names.len();
7348 + names.dedup();
7349 + names.len() == before
7350 + },
7351 + "two columns share a name, so a named cell cannot say which it meant and both \
7352 + take the first one's value. Columns: {:?}",
7353 + self.columns
7354 + .iter()
7355 + .map(|column| &column.name)
7356 + .collect::<Vec<_>>(),
7357 + );
7327 7358 row.values = self
7328 7359 .columns
7329 7360 .iter()
@@ -9539,17 +9570,36 @@
9539 9570 assert_eq!(read(&stranger), ["quasi", "", "the app stack"]);
9540 9571 }
9541 9572
9542 - /// A column nothing named is empty, and a name no column has is dropped.
9573 + /// A column nothing named is empty rather than absent.
9543 9574 #[test]
9544 - fn the_columns_decide_what_a_row_may_say() {
9545 - let table = Table::new([Column::new("Name")]).row(
9546 - Cells::default()
9547 - .at("Name", Cell::new("kick.wav"))
9548 - .at("Nonexistent", Cell::new("ignored")),
9549 - );
9575 + fn a_column_nothing_named_is_empty() {
9576 + let table = Table::new([Column::new("Name"), Column::new("Size")])
9577 + .row(Cells::default().at("Name", Cell::new("kick.wav")));
9550 9578
9551 - assert_eq!(table.rows[0].values.len(), 1);
9579 + assert_eq!(table.rows[0].values.len(), 2);
9552 9580 assert_eq!(table.rows[0].values[0].text(), "kick.wav");
9581 + assert_eq!(table.rows[0].values[1].text(), "");
9582 + }
9583 +
9584 + /// Naming a column the table does not have is a bug, and it is loud.
9585 + ///
9586 + /// The one failure mode naming introduces: the cell is dropped, so a typo
9587 + /// is an empty column rather than a compile error. Nothing above this can
9588 + /// catch it, because the row and the column list are written apart, so the
9589 + /// check lives where both are in hand.
9590 + #[test]
9591 + #[should_panic(expected = "a cell named a column this table does not have")]
9592 + fn naming_a_column_the_table_does_not_have_is_caught() {
9593 + let _ = Table::new([Column::new("Name")])
9594 + .row(Cells::default().at("Nmae", Cell::new("kick.wav")));
9595 + }
9596 +
9597 + /// Two columns with one name cannot be addressed apart.
9598 + #[test]
9599 + #[should_panic(expected = "two columns share a name")]
9600 + fn two_columns_sharing_a_name_is_caught() {
9601 + let _ = Table::new([Column::new("Actions"), Column::new("Actions")])
9602 + .row(Cells::default().at("Actions", Cell::new("edit")));
9553 9603 }
9554 9604
9555 9605 /// A row built by position still works and is untouched by resolution.