use crate::cascade::{Cascade, FieldRef, FieldWrites};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CommitOutcome {
pub committed: bool,
pub reverted: bool,
pub stored: Option<String>,
pub display: Option<String>,
pub writes: Vec<(u32, String)>,
}
impl CommitOutcome {
#[must_use]
pub fn unchanged() -> CommitOutcome {
CommitOutcome {
committed: true,
reverted: false,
stored: None,
display: None,
writes: Vec::new(),
}
}
#[must_use]
pub fn keeps_focus(&self) -> bool {
self.reverted
}
#[must_use]
pub fn formats(&self) -> bool {
self.committed && !self.reverted && self.stored.is_some()
}
}
pub fn run(
field: &FieldRef,
stored: &str,
edited: &str,
cascade: &mut dyn Cascade,
max_calculate_depth: u32,
) -> CommitOutcome {
if stored == edited {
return CommitOutcome::unchanged();
}
if !cascade.keystroke_commit(field, edited) || !cascade.validate(field, edited) {
return CommitOutcome {
committed: true,
reverted: true,
stored: None,
display: None,
writes: Vec::new(),
};
}
let stored_value = edited.to_string();
let mut writes = FieldWrites::with_max_depth(max_calculate_depth);
cascade.calculate(&mut writes, field);
let writes: Vec<(u32, String)> = writes.writes().map(|(f, v)| (f, v.to_string())).collect();
let display = cascade.format(field, &stored_value);
CommitOutcome {
committed: true,
reverted: false,
stored: Some(stored_value),
display,
writes,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cascade::{Keystroke, KeystrokeOutcome, NoScripts};
fn field() -> FieldRef {
FieldRef {
name: "Text Box".to_string(),
index: Some(0),
}
}
#[test]
fn an_unchanged_field_runs_no_hook() {
struct Counting {
calls: u32,
}
impl Cascade for Counting {
fn validate(&mut self, _f: &FieldRef, _v: &str) -> bool {
self.calls += 1;
true
}
}
let mut cascade = Counting { calls: 0 };
let outcome = run(&field(), "same", "same", &mut cascade, 8);
assert_eq!(cascade.calls, 0);
assert!(outcome.committed);
assert!(!outcome.reverted);
assert_eq!(outcome.stored, None);
}
#[test]
fn a_changed_field_stores_its_new_value() {
let outcome = run(&field(), "old", "new", &mut NoScripts, 8);
assert!(outcome.committed);
assert!(!outcome.reverted);
assert_eq!(outcome.stored.as_deref(), Some("new"));
assert_eq!(outcome.display, None, "no formatting without scripts");
assert!(outcome.writes.is_empty(), "no calculation without scripts");
}
#[test]
fn a_refused_commit_reports_success_and_keeps_the_field() {
struct Refusing;
impl Cascade for Refusing {
fn validate(&mut self, _f: &FieldRef, _v: &str) -> bool {
false
}
}
let outcome = run(&field(), "old", "new", &mut Refusing, 8);
assert!(
outcome.committed,
"a refusal must not report the commit as failed"
);
assert!(outcome.reverted, "…but it must say the value was put back");
assert_eq!(outcome.stored, None, "and nothing was stored");
assert!(
outcome.keeps_focus(),
"[oracle-bug] the user must be able to correct what was rejected"
);
}
#[test]
fn the_keystroke_gate_refuses_the_same_way() {
struct Refusing;
impl Cascade for Refusing {
fn keystroke_commit(&mut self, _f: &FieldRef, _v: &str) -> bool {
false
}
}
let outcome = run(&field(), "old", "new", &mut Refusing, 8);
assert!(outcome.committed);
assert!(outcome.reverted);
assert_eq!(outcome.stored, None);
assert!(outcome.keeps_focus());
}
#[test]
fn an_accepted_commit_lets_focus_move_on() {
assert!(!run(&field(), "old", "new", &mut NoScripts, 8).keeps_focus());
assert!(!CommitOutcome::unchanged().keeps_focus());
}
#[test]
fn the_hooks_run_in_the_specified_order() {
#[derive(Default)]
struct Recording {
seen: Vec<&'static str>,
}
impl Cascade for Recording {
fn keystroke(&mut self, _f: &FieldRef, c: Keystroke) -> KeystrokeOutcome {
self.seen.push("keystroke");
KeystrokeOutcome::Accept(c)
}
fn keystroke_commit(&mut self, _f: &FieldRef, _v: &str) -> bool {
self.seen.push("keystroke_commit");
true
}
fn validate(&mut self, _f: &FieldRef, _v: &str) -> bool {
self.seen.push("validate");
true
}
fn calculate(&mut self, _w: &mut FieldWrites, _t: &FieldRef) {
self.seen.push("calculate");
}
fn format(&mut self, _f: &FieldRef, _v: &str) -> Option<String> {
self.seen.push("format");
None
}
}
let mut cascade = Recording::default();
run(&field(), "old", "new", &mut cascade, 8);
assert_eq!(
cascade.seen,
vec!["keystroke_commit", "validate", "calculate", "format"]
);
}
#[test]
fn a_refusal_stops_the_cascade() {
#[derive(Default)]
struct Recording {
seen: Vec<&'static str>,
}
impl Cascade for Recording {
fn validate(&mut self, _f: &FieldRef, _v: &str) -> bool {
self.seen.push("validate");
false
}
fn calculate(&mut self, _w: &mut FieldWrites, _t: &FieldRef) {
self.seen.push("calculate");
}
fn format(&mut self, _f: &FieldRef, _v: &str) -> Option<String> {
self.seen.push("format");
None
}
}
let mut cascade = Recording::default();
run(&field(), "old", "new", &mut cascade, 8);
assert_eq!(cascade.seen, vec!["validate"]);
}
#[test]
fn a_display_string_does_not_change_the_stored_value() {
struct Formatting;
impl Cascade for Formatting {
fn format(&mut self, _f: &FieldRef, value: &str) -> Option<String> {
Some(format!("${value}.00"))
}
}
let outcome = run(&field(), "0", "1234", &mut Formatting, 8);
assert_eq!(outcome.stored.as_deref(), Some("1234"));
assert_eq!(outcome.display.as_deref(), Some("$1234.00"));
}
#[test]
fn a_calculation_reaches_the_outcome() {
struct Calculating;
impl Cascade for Calculating {
fn calculate(&mut self, writes: &mut FieldWrites, _t: &FieldRef) {
writes.set(7, "total");
}
}
let outcome = run(&field(), "old", "new", &mut Calculating, 8);
assert_eq!(outcome.writes, vec![(7, "total".to_string())]);
}
#[test]
fn the_cascade_is_taken_by_reference() {
let mut owned = NoScripts;
let cascade: &mut dyn Cascade = &mut owned;
let outcome = run(&field(), "a", "b", cascade, 8);
assert_eq!(outcome.stored.as_deref(), Some("b"));
}
}