1use crate::cascade::{Cascade, FieldRef, FieldWrites};
27
28#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct CommitOutcome {
31 pub committed: bool,
34 pub reverted: bool,
36 pub stored: Option<String>,
38 pub display: Option<String>,
48 pub writes: Vec<(u32, String)>,
50}
51
52impl CommitOutcome {
53 #[must_use]
55 pub fn unchanged() -> CommitOutcome {
56 CommitOutcome {
57 committed: true,
58 reverted: false,
59 stored: None,
60 display: None,
61 writes: Vec::new(),
62 }
63 }
64
65 #[must_use]
69 pub fn keeps_focus(&self) -> bool {
70 self.reverted
71 }
72
73 #[must_use]
82 pub fn formats(&self) -> bool {
83 self.committed && !self.reverted && self.stored.is_some()
84 }
85}
86
87pub fn run(
93 field: &FieldRef,
94 stored: &str,
95 edited: &str,
96 cascade: &mut dyn Cascade,
97 max_calculate_depth: u32,
98) -> CommitOutcome {
99 if stored == edited {
100 return CommitOutcome::unchanged();
101 }
102
103 if !cascade.keystroke_commit(field, edited) || !cascade.validate(field, edited) {
114 return CommitOutcome {
115 committed: true,
116 reverted: true,
117 stored: None,
118 display: None,
119 writes: Vec::new(),
120 };
121 }
122
123 let stored_value = edited.to_string();
124
125 let mut writes = FieldWrites::with_max_depth(max_calculate_depth);
126 cascade.calculate(&mut writes, field);
127 let writes: Vec<(u32, String)> = writes.writes().map(|(f, v)| (f, v.to_string())).collect();
128
129 let display = cascade.format(field, &stored_value);
131
132 CommitOutcome {
133 committed: true,
134 reverted: false,
135 stored: Some(stored_value),
136 display,
137 writes,
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144 use crate::cascade::{Keystroke, KeystrokeOutcome, NoScripts};
145
146 fn field() -> FieldRef {
147 FieldRef {
148 name: "Text Box".to_string(),
149 index: Some(0),
150 }
151 }
152
153 #[test]
155 fn an_unchanged_field_runs_no_hook() {
156 struct Counting {
157 calls: u32,
158 }
159 impl Cascade for Counting {
160 fn validate(&mut self, _f: &FieldRef, _v: &str) -> bool {
161 self.calls += 1;
162 true
163 }
164 }
165
166 let mut cascade = Counting { calls: 0 };
167 let outcome = run(&field(), "same", "same", &mut cascade, 8);
168
169 assert_eq!(cascade.calls, 0);
170 assert!(outcome.committed);
171 assert!(!outcome.reverted);
172 assert_eq!(outcome.stored, None);
173 }
174
175 #[test]
177 fn a_changed_field_stores_its_new_value() {
178 let outcome = run(&field(), "old", "new", &mut NoScripts, 8);
179 assert!(outcome.committed);
180 assert!(!outcome.reverted);
181 assert_eq!(outcome.stored.as_deref(), Some("new"));
182 assert_eq!(outcome.display, None, "no formatting without scripts");
183 assert!(outcome.writes.is_empty(), "no calculation without scripts");
184 }
185
186 #[test]
190 fn a_refused_commit_reports_success_and_keeps_the_field() {
191 struct Refusing;
192 impl Cascade for Refusing {
193 fn validate(&mut self, _f: &FieldRef, _v: &str) -> bool {
194 false
195 }
196 }
197
198 let outcome = run(&field(), "old", "new", &mut Refusing, 8);
199 assert!(
200 outcome.committed,
201 "a refusal must not report the commit as failed"
202 );
203 assert!(outcome.reverted, "…but it must say the value was put back");
204 assert_eq!(outcome.stored, None, "and nothing was stored");
205 assert!(
206 outcome.keeps_focus(),
207 "[oracle-bug] the user must be able to correct what was rejected"
208 );
209 }
210
211 #[test]
213 fn the_keystroke_gate_refuses_the_same_way() {
214 struct Refusing;
215 impl Cascade for Refusing {
216 fn keystroke_commit(&mut self, _f: &FieldRef, _v: &str) -> bool {
217 false
218 }
219 }
220
221 let outcome = run(&field(), "old", "new", &mut Refusing, 8);
222 assert!(outcome.committed);
223 assert!(outcome.reverted);
224 assert_eq!(outcome.stored, None);
225 assert!(outcome.keeps_focus());
226 }
227
228 #[test]
231 fn an_accepted_commit_lets_focus_move_on() {
232 assert!(!run(&field(), "old", "new", &mut NoScripts, 8).keeps_focus());
233 assert!(!CommitOutcome::unchanged().keeps_focus());
234 }
235
236 #[test]
238 fn the_hooks_run_in_the_specified_order() {
239 #[derive(Default)]
240 struct Recording {
241 seen: Vec<&'static str>,
242 }
243 impl Cascade for Recording {
244 fn keystroke(&mut self, _f: &FieldRef, c: Keystroke) -> KeystrokeOutcome {
245 self.seen.push("keystroke");
246 KeystrokeOutcome::Accept(c)
247 }
248 fn keystroke_commit(&mut self, _f: &FieldRef, _v: &str) -> bool {
249 self.seen.push("keystroke_commit");
250 true
251 }
252 fn validate(&mut self, _f: &FieldRef, _v: &str) -> bool {
253 self.seen.push("validate");
254 true
255 }
256 fn calculate(&mut self, _w: &mut FieldWrites, _t: &FieldRef) {
257 self.seen.push("calculate");
258 }
259 fn format(&mut self, _f: &FieldRef, _v: &str) -> Option<String> {
260 self.seen.push("format");
261 None
262 }
263 }
264
265 let mut cascade = Recording::default();
266 run(&field(), "old", "new", &mut cascade, 8);
267
268 assert_eq!(
269 cascade.seen,
270 vec!["keystroke_commit", "validate", "calculate", "format"]
271 );
272 }
273
274 #[test]
277 fn a_refusal_stops_the_cascade() {
278 #[derive(Default)]
279 struct Recording {
280 seen: Vec<&'static str>,
281 }
282 impl Cascade for Recording {
283 fn validate(&mut self, _f: &FieldRef, _v: &str) -> bool {
284 self.seen.push("validate");
285 false
286 }
287 fn calculate(&mut self, _w: &mut FieldWrites, _t: &FieldRef) {
288 self.seen.push("calculate");
289 }
290 fn format(&mut self, _f: &FieldRef, _v: &str) -> Option<String> {
291 self.seen.push("format");
292 None
293 }
294 }
295
296 let mut cascade = Recording::default();
297 run(&field(), "old", "new", &mut cascade, 8);
298 assert_eq!(cascade.seen, vec!["validate"]);
299 }
300
301 #[test]
304 fn a_display_string_does_not_change_the_stored_value() {
305 struct Formatting;
306 impl Cascade for Formatting {
307 fn format(&mut self, _f: &FieldRef, value: &str) -> Option<String> {
308 Some(format!("${value}.00"))
309 }
310 }
311
312 let outcome = run(&field(), "0", "1234", &mut Formatting, 8);
313 assert_eq!(outcome.stored.as_deref(), Some("1234"));
314 assert_eq!(outcome.display.as_deref(), Some("$1234.00"));
315 }
316
317 #[test]
318 fn a_calculation_reaches_the_outcome() {
319 struct Calculating;
320 impl Cascade for Calculating {
321 fn calculate(&mut self, writes: &mut FieldWrites, _t: &FieldRef) {
322 writes.set(7, "total");
323 }
324 }
325
326 let outcome = run(&field(), "old", "new", &mut Calculating, 8);
327 assert_eq!(outcome.writes, vec![(7, "total".to_string())]);
328 }
329
330 #[test]
333 fn the_cascade_is_taken_by_reference() {
334 let mut owned = NoScripts;
335 let cascade: &mut dyn Cascade = &mut owned;
336 let outcome = run(&field(), "a", "b", cascade, 8);
337 assert_eq!(outcome.stored.as_deref(), Some("b"));
338 }
339}