refactor: remove Axis::Unassigned; axis_of returns Option<Axis>
Axis::Unassigned served two purposes that Option already covers:
1. "this category has no assignment yet" → None
2. "this category doesn't exist" → None
By removing the variant and changing axis_of to return Option<Axis>,
callers are forced by the compiler to handle the absent-category case
explicitly (via match or unwrap_or), rather than silently treating it
like a real axis value.
SetAxis { axis: String } also upgraded to SetAxis { axis: Axis }.
Previously, constructing SetAxis with an invalid string (e.g. "diagonal")
would compile and then silently fail at dispatch. Now the type only admits
valid axis values; the dispatch string-parser is gone.
Axis gains #[serde(rename_all = "lowercase")] so existing JSON command
files (smoke.jsonl, etc.) using "row"/"column"/"page" continue to work.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@ -1,21 +1,19 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum Axis {
|
||||
Row,
|
||||
Column,
|
||||
Page,
|
||||
/// Not yet assigned
|
||||
Unassigned,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Axis {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Axis::Row => write!(f, "Row ↕"),
|
||||
Axis::Row => write!(f, "Row ↕"),
|
||||
Axis::Column => write!(f, "Col ↔"),
|
||||
Axis::Page => write!(f, "Page ☰"),
|
||||
Axis::Unassigned => write!(f, "─"),
|
||||
Axis::Page => write!(f, "Page ☰"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -61,8 +61,8 @@ impl View {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn axis_of(&self, cat_name: &str) -> Axis {
|
||||
self.category_axes.get(cat_name).copied().unwrap_or(Axis::Unassigned)
|
||||
pub fn axis_of(&self, cat_name: &str) -> Option<Axis> {
|
||||
self.category_axes.get(cat_name).copied()
|
||||
}
|
||||
|
||||
pub fn categories_on(&self, axis: Axis) -> Vec<&str> {
|
||||
@ -72,6 +72,7 @@ impl View {
|
||||
.collect()
|
||||
}
|
||||
|
||||
|
||||
pub fn set_page_selection(&mut self, cat_name: &str, item: &str) {
|
||||
self.page_selections.insert(cat_name.to_string(), item.to_string());
|
||||
}
|
||||
@ -112,12 +113,10 @@ impl View {
|
||||
|
||||
/// Cycle axis for a category: Row → Column → Page → Row
|
||||
pub fn cycle_axis(&mut self, cat_name: &str) {
|
||||
let current = self.axis_of(cat_name);
|
||||
let next = match current {
|
||||
Axis::Row => Axis::Column,
|
||||
Axis::Column => Axis::Page,
|
||||
Axis::Page => Axis::Row,
|
||||
Axis::Unassigned => Axis::Row,
|
||||
let next = match self.axis_of(cat_name) {
|
||||
Some(Axis::Row) | None => Axis::Column,
|
||||
Some(Axis::Column) => Axis::Page,
|
||||
Some(Axis::Page) => Axis::Row,
|
||||
};
|
||||
self.set_axis(cat_name, next);
|
||||
self.selected = (0, 0);
|
||||
@ -140,27 +139,27 @@ mod tests {
|
||||
#[test]
|
||||
fn first_category_assigned_to_row() {
|
||||
let v = view_with_cats(&["Region"]);
|
||||
assert_eq!(v.axis_of("Region"), Axis::Row);
|
||||
assert_eq!(v.axis_of("Region"), Some(Axis::Row));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn second_category_assigned_to_column() {
|
||||
let v = view_with_cats(&["Region", "Product"]);
|
||||
assert_eq!(v.axis_of("Product"), Axis::Column);
|
||||
assert_eq!(v.axis_of("Product"), Some(Axis::Column));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn third_and_later_categories_assigned_to_page() {
|
||||
let v = view_with_cats(&["Region", "Product", "Time", "Scenario"]);
|
||||
assert_eq!(v.axis_of("Time"), Axis::Page);
|
||||
assert_eq!(v.axis_of("Scenario"), Axis::Page);
|
||||
assert_eq!(v.axis_of("Time"), Some(Axis::Page));
|
||||
assert_eq!(v.axis_of("Scenario"), Some(Axis::Page));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_axis_changes_assignment() {
|
||||
let mut v = view_with_cats(&["Region", "Product"]);
|
||||
v.set_axis("Region", Axis::Column);
|
||||
assert_eq!(v.axis_of("Region"), Axis::Column);
|
||||
assert_eq!(v.axis_of("Region"), Some(Axis::Column));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -172,9 +171,9 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn axis_of_unknown_category_returns_unassigned() {
|
||||
fn axis_of_unknown_category_returns_none() {
|
||||
let v = View::new("Test");
|
||||
assert_eq!(v.axis_of("Ghost"), Axis::Unassigned);
|
||||
assert_eq!(v.axis_of("Ghost"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -209,7 +208,7 @@ mod tests {
|
||||
fn cycle_axis_row_to_column() {
|
||||
let mut v = view_with_cats(&["Region", "Product"]);
|
||||
v.cycle_axis("Region");
|
||||
assert_eq!(v.axis_of("Region"), Axis::Column);
|
||||
assert_eq!(v.axis_of("Region"), Some(Axis::Column));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -217,14 +216,14 @@ mod tests {
|
||||
let mut v = view_with_cats(&["Region", "Product"]);
|
||||
v.set_axis("Product", Axis::Column);
|
||||
v.cycle_axis("Product");
|
||||
assert_eq!(v.axis_of("Product"), Axis::Page);
|
||||
assert_eq!(v.axis_of("Product"), Some(Axis::Page));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cycle_axis_page_to_row() {
|
||||
let mut v = view_with_cats(&["Region", "Product", "Time"]);
|
||||
v.cycle_axis("Time");
|
||||
assert_eq!(v.axis_of("Time"), Axis::Row);
|
||||
assert_eq!(v.axis_of("Time"), Some(Axis::Row));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -260,9 +259,9 @@ mod prop_tests {
|
||||
for c in &cats { v.on_category_added(c); }
|
||||
for c in &cats {
|
||||
let axis = v.axis_of(c);
|
||||
prop_assert_ne!(axis, Axis::Unassigned,
|
||||
prop_assert!(axis.is_some(),
|
||||
"category '{}' should be assigned after on_category_added", c);
|
||||
let on_axis = v.categories_on(axis);
|
||||
let on_axis = v.categories_on(axis.unwrap());
|
||||
prop_assert!(on_axis.contains(&c.as_str()),
|
||||
"categories_on({:?}) should contain '{}'", axis, c);
|
||||
}
|
||||
@ -288,9 +287,9 @@ mod prop_tests {
|
||||
fn on_category_added_idempotent(cats in unique_cat_names()) {
|
||||
let mut v = View::new("T");
|
||||
for c in &cats { v.on_category_added(c); }
|
||||
let axes_before: Vec<_> = cats.iter().map(|c| v.axis_of(c)).collect();
|
||||
let axes_before: Vec<Option<Axis>> = cats.iter().map(|c| v.axis_of(c)).collect();
|
||||
for c in &cats { v.on_category_added(c); }
|
||||
let axes_after: Vec<_> = cats.iter().map(|c| v.axis_of(c)).collect();
|
||||
let axes_after: Vec<Option<Axis>> = cats.iter().map(|c| v.axis_of(c)).collect();
|
||||
prop_assert_eq!(axes_before, axes_after);
|
||||
}
|
||||
|
||||
@ -306,7 +305,7 @@ mod prop_tests {
|
||||
let idx = target_idx % cats.len();
|
||||
let cat = &cats[idx];
|
||||
v.set_axis(cat, axis);
|
||||
prop_assert_eq!(v.axis_of(cat), axis);
|
||||
prop_assert_eq!(v.axis_of(cat), Some(axis));
|
||||
}
|
||||
|
||||
/// After set_axis(cat, X), cat is NOT in categories_on(Y) for Y ≠ X
|
||||
|
||||
Reference in New Issue
Block a user