use super::state::{CommandState, Fact, GuideGroup};
pub const LOCKED_BY_GUIDE: &str = "Locked by the guide";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Check {
Editor,
Design,
TierSelected,
CanUndo,
CanRedo,
Idle,
SolveRunning,
DeepSolveRunning,
OptimizeRunning,
DeepSolveAvailable,
OptimizeAvailable,
HasSnapshot,
HasLastSaved,
LibrarySelected,
PreviewBatchIdle,
TiltBatchIdle,
EditView,
NotDiagramView,
AdvancedInterfaceOn,
SimpleInterfaceOn,
NeedsAdvancedInterface,
Guide(GuideGroup),
}
impl Check {
pub fn verdict(self, state: &CommandState) -> Result<(), &'static str> {
let (holds, reason) = match self {
Self::Editor => (state.has(Fact::Editor), "The editor is not ready yet"),
Self::Design => (state.has(Fact::Design), "Open or create a design first"),
Self::TierSelected => (state.has(Fact::TierSelected), "Select a tier first"),
Self::CanUndo => (state.has(Fact::CanUndo), "Nothing to undo"),
Self::CanRedo => (state.has(Fact::CanRedo), "Nothing to redo"),
Self::Idle => (
!state.has(Fact::Busy),
"Wait for the running solve to finish",
),
Self::SolveRunning => (state.has(Fact::SolveRunning), "No solve is running"),
Self::DeepSolveRunning => (
state.has(Fact::DeepSolveRunning),
"Deep Solve is not running",
),
Self::OptimizeRunning => (state.has(Fact::OptimizeRunning), "Optimize is not running"),
Self::DeepSolveAvailable => (
state.has(Fact::DeepSolveAvailable),
"Needs a library design with printed proportions",
),
Self::OptimizeAvailable => (
state.has(Fact::OptimizeAvailable),
"Needs at least one free tier",
),
Self::HasSnapshot => (state.has(Fact::HasSnapshot), "Take a snapshot first"),
Self::HasLastSaved => (state.has(Fact::HasLastSaved), "Nothing has been saved yet"),
Self::LibrarySelected => (
state.has(Fact::LibrarySelected),
"Select a design in the library first",
),
Self::PreviewBatchIdle => (
!state.has(Fact::PreviewBatchRunning),
"A preview batch is already running",
),
Self::TiltBatchIdle => (
!state.has(Fact::TiltBatchRunning),
"A tilt-curve batch is already running",
),
Self::EditView => (state.has(Fact::EditView), "Open the Edit tab first"),
Self::NotDiagramView => (
!state.has(Fact::DiagramView),
"Not available in the Diagram view",
),
Self::AdvancedInterfaceOn => (
!state.has(Fact::SimpleInterface),
"Already using the Simple interface",
),
Self::SimpleInterfaceOn => (
state.has(Fact::SimpleInterface),
"Already using the Advanced interface",
),
Self::NeedsAdvancedInterface => (
!state.has(Fact::SimpleInterface),
"Switch to the Advanced interface first",
),
Self::Guide(group) => (state.has(Fact::Allows(group)), LOCKED_BY_GUIDE),
};
if holds { Ok(()) } else { Err(reason) }
}
}
pub fn first_failure(checks: &[Check], state: &CommandState) -> Result<(), &'static str> {
checks.iter().try_for_each(|check| check.verdict(state))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_ready_state_passes_every_check_except_the_opposite_interface_ones() {
let state = CommandState::ready();
assert_eq!(
Check::SimpleInterfaceOn.verdict(&state),
Err("Already using the Advanced interface")
);
for check in [
Check::Editor,
Check::Design,
Check::TierSelected,
Check::CanUndo,
Check::CanRedo,
Check::Idle,
Check::SolveRunning,
Check::DeepSolveRunning,
Check::OptimizeRunning,
Check::DeepSolveAvailable,
Check::OptimizeAvailable,
Check::HasSnapshot,
Check::HasLastSaved,
Check::LibrarySelected,
Check::PreviewBatchIdle,
Check::TiltBatchIdle,
Check::EditView,
Check::NotDiagramView,
Check::AdvancedInterfaceOn,
Check::NeedsAdvancedInterface,
] {
assert_eq!(check.verdict(&state), Ok(()), "{check:?}");
}
}
#[test]
fn every_requirement_names_a_reason_when_it_fails() {
let empty = CommandState::default();
for check in [
Check::Editor,
Check::Design,
Check::TierSelected,
Check::CanUndo,
Check::CanRedo,
Check::SolveRunning,
Check::DeepSolveRunning,
Check::OptimizeRunning,
Check::DeepSolveAvailable,
Check::OptimizeAvailable,
Check::HasSnapshot,
Check::HasLastSaved,
Check::LibrarySelected,
Check::EditView,
Check::SimpleInterfaceOn,
Check::Guide(GuideGroup::Advanced),
] {
let reason = check.verdict(&empty).unwrap_err();
assert!(!reason.is_empty(), "{check:?}");
}
}
#[test]
fn the_busy_checks_fail_only_while_something_runs() {
let busy = CommandState::ready().with(Fact::Busy, true);
assert_eq!(
Check::Idle.verdict(&busy),
Err("Wait for the running solve to finish")
);
let batches = CommandState::ready()
.with(Fact::PreviewBatchRunning, true)
.with(Fact::TiltBatchRunning, true);
assert!(Check::PreviewBatchIdle.verdict(&batches).is_err());
assert!(Check::TiltBatchIdle.verdict(&batches).is_err());
}
#[test]
fn a_guide_lock_says_so_and_names_no_other_reason() {
for group in GuideGroup::ALL {
let locked = CommandState::ready().with(Fact::Allows(group), false);
assert_eq!(
Check::Guide(group).verdict(&locked),
Err(LOCKED_BY_GUIDE),
"{group:?}"
);
assert_eq!(Check::Guide(group).verdict(&CommandState::ready()), Ok(()));
}
}
#[test]
fn the_first_failing_check_gives_the_reason() {
let state = CommandState::ready()
.with(Fact::Allows(GuideGroup::Advanced), false)
.with(Fact::Design, false);
let checks = [Check::Guide(GuideGroup::Advanced), Check::Design];
assert_eq!(first_failure(&checks, &state), Err(LOCKED_BY_GUIDE));
let unlocked = state.with(Fact::Allows(GuideGroup::Advanced), true);
assert_eq!(
first_failure(&checks, &unlocked),
Err("Open or create a design first")
);
assert_eq!(first_failure(&[], &CommandState::default()), Ok(()));
}
#[test]
fn the_diagram_view_blocks_the_viewport_only_commands() {
let diagram = CommandState::ready().with(Fact::DiagramView, true);
assert_eq!(
Check::NotDiagramView.verdict(&diagram),
Err("Not available in the Diagram view")
);
}
#[test]
fn the_interface_switch_checks_follow_the_current_mode() {
let simple = CommandState::ready().with(Fact::SimpleInterface, true);
assert_eq!(Check::SimpleInterfaceOn.verdict(&simple), Ok(()));
assert_eq!(
Check::AdvancedInterfaceOn.verdict(&simple),
Err("Already using the Simple interface")
);
}
#[test]
fn a_control_that_only_the_advanced_interface_has_says_to_switch() {
let simple = CommandState::ready().with(Fact::SimpleInterface, true);
assert_eq!(
Check::NeedsAdvancedInterface.verdict(&simple),
Err("Switch to the Advanced interface first")
);
assert_eq!(
Check::NeedsAdvancedInterface.verdict(&CommandState::ready()),
Ok(())
);
}
}