use crate::error::Error;
use super::{PickItem, PickOutcome, Picker};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ScriptedStep {
Cycle {
row: usize,
},
Apply,
Abort,
}
pub struct ScriptedPicker {
script: Vec<ScriptedStep>,
}
impl ScriptedPicker {
pub fn new(script: Vec<ScriptedStep>) -> Self {
assert!(
!script.is_empty(),
"ScriptedPicker script must not be empty"
);
assert!(
script
.iter()
.any(|s| matches!(s, ScriptedStep::Apply | ScriptedStep::Abort)),
"ScriptedPicker script must contain at least one Apply or Abort terminator",
);
Self { script }
}
}
impl Picker for ScriptedPicker {
fn pick(&mut self, _title: &str, items: &mut [PickItem<'_>]) -> Result<PickOutcome, Error> {
for step in std::mem::take(&mut self.script) {
match step {
ScriptedStep::Cycle { row } => {
let total = items.len();
let item = items.get_mut(row).unwrap_or_else(|| {
panic!(
"ScriptedPicker: Cycle row={row} out of bounds (items.len()={total})",
)
});
if item.choices.len() > 1 {
item.choice = (item.choice + 1) % item.choices.len();
}
}
ScriptedStep::Apply => return Ok(PickOutcome::Apply),
ScriptedStep::Abort => return Ok(PickOutcome::Abort),
}
}
unreachable!("constructor ensures script ends in a terminator")
}
}
#[cfg(test)]
mod tests {
use std::borrow::Cow;
use similar_asserts::assert_eq;
use super::*;
use crate::picker::ChoiceLabel;
fn two_choice() -> [ChoiceLabel; 2] {
[
ChoiceLabel::new("a", "first"),
ChoiceLabel::new("b", "second"),
]
}
fn one_choice() -> [ChoiceLabel; 1] {
[ChoiceLabel::new("only", "only")]
}
#[test]
fn apply_returns_apply_and_advances_addressed_row() {
let choices = two_choice();
let mut items = [PickItem {
label: Cow::Borrowed("row"),
note: None,
choices: &choices,
choice: 0,
}];
let mut picker =
ScriptedPicker::new(vec![ScriptedStep::Cycle { row: 0 }, ScriptedStep::Apply]);
assert_eq!(picker.pick("t", &mut items).unwrap(), PickOutcome::Apply);
assert_eq!(items[0].choice, 1);
}
#[test]
fn cycle_wraps_modulo_choices_len() {
let choices = two_choice();
let mut items = [PickItem {
label: Cow::Borrowed("row"),
note: None,
choices: &choices,
choice: 0,
}];
let mut picker = ScriptedPicker::new(vec![
ScriptedStep::Cycle { row: 0 },
ScriptedStep::Cycle { row: 0 },
ScriptedStep::Apply,
]);
picker.pick("t", &mut items).unwrap();
assert_eq!(items[0].choice, 0);
}
#[test]
fn cycle_on_disabled_row_is_a_noop() {
let only = one_choice();
let mut items = [PickItem {
label: Cow::Borrowed("row"),
note: None,
choices: &only,
choice: 0,
}];
let mut picker =
ScriptedPicker::new(vec![ScriptedStep::Cycle { row: 0 }, ScriptedStep::Apply]);
picker.pick("t", &mut items).unwrap();
assert_eq!(items[0].choice, 0);
}
#[test]
fn abort_returns_abort_and_skips_remaining_steps() {
let choices = two_choice();
let mut items = [PickItem {
label: Cow::Borrowed("row"),
note: None,
choices: &choices,
choice: 0,
}];
let mut picker =
ScriptedPicker::new(vec![ScriptedStep::Abort, ScriptedStep::Cycle { row: 0 }]);
assert_eq!(picker.pick("t", &mut items).unwrap(), PickOutcome::Abort);
assert_eq!(items[0].choice, 0);
}
#[test]
#[should_panic(expected = "must not be empty")]
fn empty_script_panics_on_construction() {
let _ = ScriptedPicker::new(vec![]);
}
#[test]
#[should_panic(expected = "must contain at least one Apply or Abort terminator")]
fn script_without_terminator_panics_on_construction() {
let _ = ScriptedPicker::new(vec![ScriptedStep::Cycle { row: 0 }]);
}
#[test]
#[should_panic(expected = "out of bounds")]
fn cycle_out_of_bounds_panics_on_pick() {
let mut items: [PickItem<'_>; 0] = [];
let mut picker =
ScriptedPicker::new(vec![ScriptedStep::Cycle { row: 5 }, ScriptedStep::Apply]);
let _ = picker.pick("t", &mut items);
}
}