Skip to main content

rustrails_model/
dirty.rs

1use std::collections::HashMap;
2
3use serde_json::Value;
4
5/// Change tracking behavior for mutable model attributes.
6pub trait Dirty {
7    /// Returns `true` when at least one attribute differs from its original value.
8    fn changed(&self) -> bool;
9
10    /// Returns the current unsaved changes as `attribute -> [old, new]`.
11    fn changes(&self) -> HashMap<String, [Value; 2]>;
12
13    /// Returns the names of every currently changed attribute.
14    fn changed_attributes(&self) -> Vec<String>;
15
16    /// Returns the changes that were most recently applied.
17    fn previous_changes(&self) -> &HashMap<String, [Value; 2]>;
18
19    /// Returns `true` when a specific attribute currently differs from its original value.
20    fn attribute_changed(&self, name: &str) -> bool;
21
22    /// Returns the original value for a changed attribute.
23    fn attribute_was(&self, name: &str) -> Option<Value>;
24
25    /// Restores every changed attribute to its original value.
26    fn restore_attributes(&mut self);
27
28    /// Clears all dirty-tracking state.
29    fn clear_changes(&mut self);
30
31    /// Marks the current changes as applied and records them as previous changes.
32    fn changes_applied(&mut self);
33}
34
35/// Concrete helper that stores original, current, and previous attribute values.
36#[derive(Debug, Clone, Default, PartialEq)]
37pub struct ChangeTracker {
38    original_values: HashMap<String, Value>,
39    changes: HashMap<String, [Value; 2]>,
40    previous_changes: HashMap<String, [Value; 2]>,
41}
42
43impl ChangeTracker {
44    /// Creates an empty change tracker.
45    #[must_use]
46    pub fn new() -> Self {
47        Self::default()
48    }
49
50    /// Tracks a change from `old` to `new` for the named attribute.
51    ///
52    /// If the attribute has already changed, the original value is preserved.
53    /// If `new` matches the original value, the attribute is removed from the
54    /// current change set.
55    pub fn track_change(&mut self, name: &str, old: Value, new: Value) {
56        let key = name.to_owned();
57
58        if let Some(original) = self.original_values.get(&key).cloned() {
59            if new == original {
60                self.original_values.remove(&key);
61                self.changes.remove(&key);
62            } else {
63                self.changes.insert(key, [original, new]);
64            }
65            return;
66        }
67
68        if old == new {
69            return;
70        }
71
72        self.original_values.insert(key.clone(), old.clone());
73        self.changes.insert(key, [old, new]);
74    }
75
76    /// Returns `true` when at least one attribute is currently changed.
77    #[must_use]
78    pub fn changed(&self) -> bool {
79        !self.changes.is_empty()
80    }
81
82    /// Returns the current unsaved changes.
83    #[must_use]
84    pub fn changes(&self) -> &HashMap<String, [Value; 2]> {
85        &self.changes
86    }
87
88    /// Returns the previously applied changes.
89    #[must_use]
90    pub fn previous_changes(&self) -> &HashMap<String, [Value; 2]> {
91        &self.previous_changes
92    }
93
94    /// Returns `true` when the named attribute is currently changed.
95    #[must_use]
96    pub fn attribute_changed(&self, name: &str) -> bool {
97        self.changes.contains_key(name)
98    }
99
100    /// Returns the original value for the named changed attribute.
101    #[must_use]
102    pub fn attribute_was(&self, name: &str) -> Option<&Value> {
103        self.original_values.get(name)
104    }
105
106    /// Restores a single attribute by removing it from the tracked changes.
107    ///
108    /// The returned value is the original value that callers should write back
109    /// to the underlying attribute storage.
110    pub fn restore_attribute(&mut self, name: &str) -> Option<Value> {
111        self.changes.remove(name);
112        self.original_values.remove(name)
113    }
114
115    /// Clears both current and previous change history.
116    pub fn clear(&mut self) {
117        self.original_values.clear();
118        self.changes.clear();
119        self.previous_changes.clear();
120    }
121
122    /// Moves current changes into `previous_changes` and resets the tracker.
123    pub fn apply(&mut self) {
124        self.previous_changes = self.changes.clone();
125        self.original_values.clear();
126        self.changes.clear();
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use std::collections::HashMap;
133
134    use serde_json::{Value, json};
135
136    use super::{ChangeTracker, Dirty};
137
138    #[derive(Debug, Clone, PartialEq)]
139    struct TestProfile {
140        name: String,
141        age: i32,
142        tracker: ChangeTracker,
143    }
144
145    impl TestProfile {
146        fn new(name: &str, age: i32) -> Self {
147            Self {
148                name: name.to_owned(),
149                age,
150                tracker: ChangeTracker::new(),
151            }
152        }
153
154        fn set_name(&mut self, new_name: &str) {
155            let old = Value::String(self.name.clone());
156            let new = Value::String(new_name.to_owned());
157            self.tracker.track_change("name", old, new.clone());
158            self.name = new_name.to_owned();
159        }
160
161        fn set_age(&mut self, new_age: i32) {
162            let old = json!(self.age);
163            let new = json!(new_age);
164            self.tracker.track_change("age", old, new.clone());
165            self.age = new_age;
166        }
167
168        fn apply_value(&mut self, name: &str, value: Value) {
169            match name {
170                "name" => {
171                    if let Value::String(text) = value {
172                        self.name = text;
173                    }
174                }
175                "age" => {
176                    if let Some(number) = value.as_i64().and_then(|entry| i32::try_from(entry).ok())
177                    {
178                        self.age = number;
179                    }
180                }
181                _ => {}
182            }
183        }
184    }
185
186    impl Dirty for TestProfile {
187        fn changed(&self) -> bool {
188            self.tracker.changed()
189        }
190
191        fn changes(&self) -> HashMap<String, [Value; 2]> {
192            self.tracker.changes().clone()
193        }
194
195        fn changed_attributes(&self) -> Vec<String> {
196            self.tracker.changes().keys().cloned().collect()
197        }
198
199        fn previous_changes(&self) -> &HashMap<String, [Value; 2]> {
200            self.tracker.previous_changes()
201        }
202
203        fn attribute_changed(&self, name: &str) -> bool {
204            self.tracker.attribute_changed(name)
205        }
206
207        fn attribute_was(&self, name: &str) -> Option<Value> {
208            self.tracker.attribute_was(name).cloned()
209        }
210
211        fn restore_attributes(&mut self) {
212            let names = self.changed_attributes();
213            for name in names {
214                if let Some(original) = self.tracker.restore_attribute(&name) {
215                    self.apply_value(&name, original);
216                }
217            }
218        }
219
220        fn clear_changes(&mut self) {
221            self.tracker.clear();
222        }
223
224        fn changes_applied(&mut self) {
225            self.tracker.apply();
226        }
227    }
228
229    #[test]
230    fn track_single_change_records_original_and_new_values() {
231        let mut tracker = ChangeTracker::new();
232        tracker.track_change("name", json!("Alice"), json!("Bob"));
233
234        assert!(tracker.changed());
235        assert_eq!(
236            tracker.changes().get("name"),
237            Some(&[json!("Alice"), json!("Bob")])
238        );
239        assert_eq!(tracker.attribute_was("name"), Some(&json!("Alice")));
240    }
241
242    #[test]
243    fn track_multiple_changes_preserves_the_first_original_value() {
244        let mut tracker = ChangeTracker::new();
245        tracker.track_change("name", json!("Alice"), json!("Bob"));
246        tracker.track_change("name", json!("Bob"), json!("Carol"));
247
248        assert_eq!(
249            tracker.changes().get("name"),
250            Some(&[json!("Alice"), json!("Carol")])
251        );
252        assert_eq!(tracker.attribute_was("name"), Some(&json!("Alice")));
253    }
254
255    #[test]
256    fn changing_back_to_original_removes_the_change() {
257        let mut tracker = ChangeTracker::new();
258        tracker.track_change("name", json!("Alice"), json!("Bob"));
259        tracker.track_change("name", json!("Bob"), json!("Alice"));
260
261        assert!(!tracker.changed());
262        assert!(!tracker.attribute_changed("name"));
263        assert_eq!(tracker.attribute_was("name"), None);
264        assert!(tracker.changes().is_empty());
265    }
266
267    #[test]
268    fn restore_attribute_returns_original_value_and_clears_one_change() {
269        let mut tracker = ChangeTracker::new();
270        tracker.track_change("name", json!("Alice"), json!("Bob"));
271        tracker.track_change("age", json!(30), json!(31));
272
273        let restored = tracker.restore_attribute("name");
274
275        assert_eq!(restored, Some(json!("Alice")));
276        assert!(!tracker.attribute_changed("name"));
277        assert!(tracker.attribute_changed("age"));
278    }
279
280    #[test]
281    fn apply_moves_current_changes_into_previous_changes() {
282        let mut tracker = ChangeTracker::new();
283        tracker.track_change("name", json!("Alice"), json!("Bob"));
284        tracker.track_change("age", json!(30), json!(31));
285
286        tracker.apply();
287
288        assert!(!tracker.changed());
289        assert!(tracker.changes().is_empty());
290        assert_eq!(
291            tracker.previous_changes().get("name"),
292            Some(&[json!("Alice"), json!("Bob")])
293        );
294        assert_eq!(
295            tracker.previous_changes().get("age"),
296            Some(&[json!(30), json!(31)])
297        );
298    }
299
300    #[test]
301    fn apply_resets_originals_for_future_change_tracking() {
302        let mut tracker = ChangeTracker::new();
303        tracker.track_change("name", json!("Alice"), json!("Bob"));
304        tracker.apply();
305        tracker.track_change("name", json!("Bob"), json!("Carol"));
306
307        assert_eq!(tracker.attribute_was("name"), Some(&json!("Bob")));
308        assert_eq!(
309            tracker.changes().get("name"),
310            Some(&[json!("Bob"), json!("Carol")])
311        );
312    }
313
314    #[test]
315    fn clear_resets_current_and_previous_tracking() {
316        let mut tracker = ChangeTracker::new();
317        tracker.track_change("name", json!("Alice"), json!("Bob"));
318        tracker.apply();
319
320        tracker.clear();
321
322        assert!(!tracker.changed());
323        assert!(tracker.changes().is_empty());
324        assert!(tracker.previous_changes().is_empty());
325    }
326
327    #[test]
328    fn dirty_trait_reports_changes_and_original_values() {
329        let mut profile = TestProfile::new("Alice", 30);
330        profile.set_name("Bob");
331        profile.set_age(31);
332
333        let changes = profile.changes();
334
335        assert!(profile.changed());
336        assert!(profile.attribute_changed("name"));
337        assert_eq!(profile.attribute_was("name"), Some(json!("Alice")));
338        assert_eq!(changes.get("name"), Some(&[json!("Alice"), json!("Bob")]));
339        assert_eq!(changes.get("age"), Some(&[json!(30), json!(31)]));
340    }
341
342    #[test]
343    fn dirty_trait_restore_attributes_reverts_all_values() {
344        let mut profile = TestProfile::new("Alice", 30);
345        profile.set_name("Bob");
346        profile.set_age(31);
347
348        profile.restore_attributes();
349
350        assert_eq!(profile.name, "Alice");
351        assert_eq!(profile.age, 30);
352        assert!(!profile.changed());
353        assert!(profile.changes().is_empty());
354    }
355
356    #[test]
357    fn dirty_trait_changes_applied_exposes_previous_changes() {
358        let mut profile = TestProfile::new("Alice", 30);
359        profile.set_name("Bob");
360
361        profile.changes_applied();
362
363        assert!(!profile.changed());
364        assert_eq!(
365            profile.previous_changes().get("name"),
366            Some(&[json!("Alice"), json!("Bob")])
367        );
368    }
369
370    #[test]
371    fn dirty_trait_clear_changes_drops_history() {
372        let mut profile = TestProfile::new("Alice", 30);
373        profile.set_name("Bob");
374        profile.changes_applied();
375
376        profile.clear_changes();
377
378        assert!(profile.previous_changes().is_empty());
379        assert!(!profile.changed());
380    }
381    #[test]
382    fn track_change_ignores_equal_values() {
383        let mut tracker = ChangeTracker::new();
384        tracker.track_change("name", json!("Alice"), json!("Alice"));
385
386        assert!(!tracker.changed());
387        assert!(tracker.changes().is_empty());
388    }
389
390    #[test]
391    fn restore_attribute_returns_none_for_unknown_name() {
392        let mut tracker = ChangeTracker::new();
393        tracker.track_change("name", json!("Alice"), json!("Bob"));
394
395        assert_eq!(tracker.restore_attribute("email"), None);
396        assert!(tracker.attribute_changed("name"));
397    }
398
399    #[test]
400    fn attribute_was_returns_none_for_unknown_attribute() {
401        let tracker = ChangeTracker::new();
402        assert_eq!(tracker.attribute_was("name"), None);
403    }
404
405    #[test]
406    fn apply_without_changes_keeps_previous_changes_empty() {
407        let mut tracker = ChangeTracker::new();
408        tracker.apply();
409
410        assert!(!tracker.changed());
411        assert!(tracker.previous_changes().is_empty());
412    }
413
414    #[test]
415    fn dirty_trait_changed_attributes_lists_each_change_once() {
416        let mut profile = TestProfile::new("Alice", 30);
417        profile.set_name("Bob");
418        profile.set_age(31);
419
420        let mut changed = profile.changed_attributes();
421        changed.sort();
422
423        assert_eq!(changed, vec!["age".to_owned(), "name".to_owned()]);
424    }
425
426    #[test]
427    fn restore_attributes_after_apply_is_a_noop() {
428        let mut profile = TestProfile::new("Alice", 30);
429        profile.set_name("Bob");
430        profile.changes_applied();
431        profile.restore_attributes();
432
433        assert_eq!(profile.name, "Bob");
434        assert_eq!(profile.age, 30);
435        assert!(profile.previous_changes().contains_key("name"));
436    }
437
438    #[test]
439    fn clear_changes_removes_unsaved_history() {
440        let mut profile = TestProfile::new("Alice", 30);
441        profile.set_name("Bob");
442
443        profile.clear_changes();
444
445        assert!(!profile.changed());
446        assert!(profile.changes().is_empty());
447        assert_eq!(profile.attribute_was("name"), None);
448    }
449
450    #[test]
451    fn tracker_is_unchanged_when_new() {
452        let tracker = ChangeTracker::new();
453
454        assert!(!tracker.changed());
455        assert!(tracker.changes().is_empty());
456    }
457
458    #[test]
459    fn tracker_attribute_changed_is_false_for_unknown_name() {
460        let tracker = ChangeTracker::new();
461
462        assert!(!tracker.attribute_changed("email"));
463    }
464
465    #[test]
466    fn tracker_previous_changes_are_empty_when_new() {
467        let tracker = ChangeTracker::new();
468
469        assert!(tracker.previous_changes().is_empty());
470    }
471
472    #[test]
473    fn tracker_changes_preserve_nested_json_values() {
474        let mut tracker = ChangeTracker::new();
475        tracker.track_change(
476            "settings",
477            json!({ "theme": "dark", "flags": { "beta": false } }),
478            json!({ "theme": "light", "flags": { "beta": true } }),
479        );
480
481        assert_eq!(
482            tracker.changes().get("settings"),
483            Some(&[
484                json!({ "theme": "dark", "flags": { "beta": false } }),
485                json!({ "theme": "light", "flags": { "beta": true } }),
486            ])
487        );
488    }
489
490    #[test]
491    fn tracker_multiple_updates_keep_latest_new_value() {
492        let mut tracker = ChangeTracker::new();
493        tracker.track_change("name", json!("Alice"), json!("Bob"));
494        tracker.track_change("name", json!("Bob"), json!("Carol"));
495        tracker.track_change("name", json!("Carol"), json!("Dora"));
496
497        assert_eq!(
498            tracker.changes().get("name"),
499            Some(&[json!("Alice"), json!("Dora")])
500        );
501    }
502
503    #[test]
504    fn tracker_apply_replaces_previous_changes_instead_of_accumulating() {
505        let mut tracker = ChangeTracker::new();
506        tracker.track_change("name", json!("Alice"), json!("Bob"));
507        tracker.apply();
508        tracker.track_change("age", json!(30), json!(31));
509        tracker.apply();
510
511        assert_eq!(tracker.previous_changes().len(), 1);
512        assert!(!tracker.previous_changes().contains_key("name"));
513        assert_eq!(
514            tracker.previous_changes().get("age"),
515            Some(&[json!(30), json!(31)])
516        );
517    }
518
519    #[test]
520    fn tracker_restore_attribute_allows_retracking_from_original_value() {
521        let mut tracker = ChangeTracker::new();
522        tracker.track_change("name", json!("Alice"), json!("Bob"));
523
524        assert_eq!(tracker.restore_attribute("name"), Some(json!("Alice")));
525
526        tracker.track_change("name", json!("Alice"), json!("Carol"));
527
528        assert_eq!(tracker.attribute_was("name"), Some(&json!("Alice")));
529        assert_eq!(
530            tracker.changes().get("name"),
531            Some(&[json!("Alice"), json!("Carol")])
532        );
533    }
534
535    #[test]
536    fn tracker_restore_attribute_only_clears_requested_entry() {
537        let mut tracker = ChangeTracker::new();
538        tracker.track_change("name", json!("Alice"), json!("Bob"));
539        tracker.track_change("age", json!(30), json!(31));
540
541        let original = tracker.restore_attribute("age");
542
543        assert_eq!(original, Some(json!(30)));
544        assert!(!tracker.attribute_changed("age"));
545        assert!(tracker.attribute_changed("name"));
546        assert_eq!(tracker.attribute_was("name"), Some(&json!("Alice")));
547    }
548
549    #[test]
550    fn tracker_clear_after_unsaved_changes_drops_current_state() {
551        let mut tracker = ChangeTracker::new();
552        tracker.track_change("name", json!("Alice"), json!("Bob"));
553        tracker.track_change("age", json!(30), json!(31));
554
555        tracker.clear();
556
557        assert!(!tracker.changed());
558        assert!(tracker.changes().is_empty());
559        assert!(tracker.previous_changes().is_empty());
560    }
561
562    #[test]
563    fn tracker_apply_after_clear_keeps_previous_changes_empty() {
564        let mut tracker = ChangeTracker::new();
565        tracker.track_change("name", json!("Alice"), json!("Bob"));
566        tracker.clear();
567        tracker.apply();
568
569        assert!(tracker.previous_changes().is_empty());
570    }
571
572    #[test]
573    fn tracker_reverting_one_attribute_keeps_other_changes() {
574        let mut tracker = ChangeTracker::new();
575        tracker.track_change("name", json!("Alice"), json!("Bob"));
576        tracker.track_change("age", json!(30), json!(31));
577        tracker.track_change("name", json!("Bob"), json!("Alice"));
578
579        assert!(!tracker.attribute_changed("name"));
580        assert_eq!(tracker.changes().get("age"), Some(&[json!(30), json!(31)]));
581    }
582
583    #[test]
584    fn tracker_attribute_was_uses_last_applied_value_after_new_change() {
585        let mut tracker = ChangeTracker::new();
586        tracker.track_change("name", json!("Alice"), json!("Bob"));
587        tracker.apply();
588        tracker.track_change("name", json!("Bob"), json!("Carol"));
589
590        assert_eq!(tracker.attribute_was("name"), Some(&json!("Bob")));
591    }
592
593    #[test]
594    fn tracker_nested_values_can_revert_to_original_and_clear_state() {
595        let mut tracker = ChangeTracker::new();
596        tracker.track_change(
597            "settings",
598            json!({ "theme": "dark" }),
599            json!({ "theme": "light" }),
600        );
601        tracker.track_change(
602            "settings",
603            json!({ "theme": "light" }),
604            json!({ "theme": "dark" }),
605        );
606
607        assert!(!tracker.changed());
608        assert!(tracker.changes().is_empty());
609    }
610
611    #[test]
612    fn profile_is_clean_when_new() {
613        let profile = TestProfile::new("Alice", 30);
614
615        assert!(!profile.changed());
616        assert!(profile.changes().is_empty());
617    }
618
619    #[test]
620    fn profile_previous_changes_are_empty_when_new() {
621        let profile = TestProfile::new("Alice", 30);
622
623        assert!(profile.previous_changes().is_empty());
624    }
625
626    #[test]
627    fn profile_attribute_changed_is_false_for_unknown_attribute() {
628        let profile = TestProfile::new("Alice", 30);
629
630        assert!(!profile.attribute_changed("email"));
631    }
632
633    #[test]
634    fn profile_attribute_was_is_none_when_attribute_is_clean() {
635        let profile = TestProfile::new("Alice", 30);
636
637        assert_eq!(profile.attribute_was("name"), None);
638    }
639
640    #[test]
641    fn profile_changes_report_original_and_new_values() {
642        let mut profile = TestProfile::new("Alice", 30);
643        profile.set_name("Bob");
644        profile.set_age(31);
645
646        let changes = profile.changes();
647
648        assert_eq!(changes.get("name"), Some(&[json!("Alice"), json!("Bob")]));
649        assert_eq!(changes.get("age"), Some(&[json!(30), json!(31)]));
650    }
651
652    #[test]
653    fn profile_changed_attributes_include_all_dirty_fields() {
654        let mut profile = TestProfile::new("Alice", 30);
655        profile.set_name("Bob");
656        profile.set_age(31);
657
658        let mut changed = profile.changed_attributes();
659        changed.sort();
660
661        assert_eq!(changed, vec!["age".to_owned(), "name".to_owned()]);
662    }
663
664    #[test]
665    fn profile_restore_attributes_clears_changed_attributes() {
666        let mut profile = TestProfile::new("Alice", 30);
667        profile.set_name("Bob");
668        profile.set_age(31);
669
670        profile.restore_attributes();
671
672        assert!(profile.changed_attributes().is_empty());
673    }
674
675    #[test]
676    fn profile_changes_applied_replaces_previous_changes_on_second_save() {
677        let mut profile = TestProfile::new("Alice", 30);
678        profile.set_name("Bob");
679        profile.changes_applied();
680        profile.set_age(31);
681        profile.changes_applied();
682
683        assert_eq!(profile.previous_changes().len(), 1);
684        assert!(!profile.previous_changes().contains_key("name"));
685        assert_eq!(
686            profile.previous_changes().get("age"),
687            Some(&[json!(30), json!(31)])
688        );
689    }
690
691    #[test]
692    fn profile_restore_attributes_after_multiple_unsaved_changes_restores_saved_values() {
693        let mut profile = TestProfile::new("Alice", 30);
694        profile.set_name("Bob");
695        profile.set_age(31);
696        profile.changes_applied();
697        profile.set_name("Carol");
698        profile.set_name("Dora");
699        profile.set_age(32);
700
701        profile.restore_attributes();
702
703        assert_eq!(profile.name, "Bob");
704        assert_eq!(profile.age, 31);
705        assert!(!profile.changed());
706    }
707
708    #[test]
709    fn profile_setting_name_to_same_value_is_noop() {
710        let mut profile = TestProfile::new("Alice", 30);
711        profile.set_name("Alice");
712
713        assert!(!profile.changed());
714        assert!(profile.changes().is_empty());
715    }
716
717    #[test]
718    fn profile_setting_age_to_same_value_is_noop() {
719        let mut profile = TestProfile::new("Alice", 30);
720        profile.set_age(30);
721
722        assert!(!profile.changed());
723        assert!(profile.changes().is_empty());
724    }
725
726    #[test]
727    fn profile_clear_changes_after_save_clears_previous_changes_too() {
728        let mut profile = TestProfile::new("Alice", 30);
729        profile.set_name("Bob");
730        profile.changes_applied();
731
732        profile.clear_changes();
733
734        assert!(profile.previous_changes().is_empty());
735        assert!(!profile.changed());
736    }
737
738    #[test]
739    fn profile_changes_returns_a_detached_snapshot() {
740        let mut profile = TestProfile::new("Alice", 30);
741        profile.set_name("Bob");
742
743        let mut snapshot = profile.changes();
744        snapshot.insert("nickname".to_owned(), [json!("Ace"), json!("Bobby")]);
745
746        assert!(!profile.changes().contains_key("nickname"));
747        assert_eq!(
748            profile.changes().get("name"),
749            Some(&[json!("Alice"), json!("Bob")])
750        );
751    }
752
753    #[test]
754    fn profile_multiple_unsaved_name_changes_keep_first_original_value() {
755        let mut profile = TestProfile::new("Alice", 30);
756        profile.set_name("Bob");
757        profile.set_name("Carol");
758        profile.set_name("Dora");
759
760        assert_eq!(profile.attribute_was("name"), Some(json!("Alice")));
761        assert_eq!(
762            profile.changes().get("name"),
763            Some(&[json!("Alice"), json!("Dora")])
764        );
765    }
766
767    #[test]
768    fn profile_attribute_was_tracks_last_saved_age_after_save_and_change() {
769        let mut profile = TestProfile::new("Alice", 30);
770        profile.set_age(31);
771        profile.changes_applied();
772        profile.set_age(32);
773
774        assert_eq!(profile.attribute_was("age"), Some(json!(31)));
775        assert_eq!(profile.changes().get("age"), Some(&[json!(31), json!(32)]));
776    }
777
778    #[test]
779    fn profile_changes_applied_without_new_changes_clears_previous_changes() {
780        let mut profile = TestProfile::new("Alice", 30);
781        profile.set_name("Bob");
782        profile.changes_applied();
783
784        profile.changes_applied();
785
786        assert!(profile.previous_changes().is_empty());
787    }
788    #[test]
789    fn apply_after_reverting_all_changes_clears_previous_changes() {
790        let mut tracker = ChangeTracker::new();
791        tracker.track_change("name", json!("Alice"), json!("Bob"));
792        tracker.track_change("name", json!("Bob"), json!("Alice"));
793
794        tracker.apply();
795
796        assert!(!tracker.changed());
797        assert!(tracker.changes().is_empty());
798        assert!(tracker.previous_changes().is_empty());
799    }
800
801    #[test]
802    fn restore_attribute_after_apply_keeps_previous_changes_intact() {
803        let mut tracker = ChangeTracker::new();
804        tracker.track_change("name", json!("Alice"), json!("Bob"));
805        tracker.apply();
806
807        let restored = tracker.restore_attribute("name");
808
809        assert_eq!(restored, None);
810        assert_eq!(
811            tracker.previous_changes().get("name"),
812            Some(&[json!("Alice"), json!("Bob")])
813        );
814    }
815
816    #[test]
817    fn profile_restore_attributes_preserves_previous_changes_from_last_save() {
818        let mut profile = TestProfile::new("Alice", 30);
819        profile.set_name("Bob");
820        profile.changes_applied();
821        profile.set_name("Carol");
822
823        profile.restore_attributes();
824
825        assert_eq!(profile.name, "Bob");
826        assert!(!profile.changed());
827        assert_eq!(
828            profile.previous_changes().get("name"),
829            Some(&[json!("Alice"), json!("Bob")])
830        );
831    }
832
833    #[test]
834    fn profile_clear_changes_rebases_future_tracking_to_current_values() {
835        let mut profile = TestProfile::new("Alice", 30);
836        profile.set_name("Bob");
837
838        profile.clear_changes();
839        profile.set_name("Carol");
840
841        assert_eq!(profile.attribute_was("name"), Some(json!("Bob")));
842        assert_eq!(
843            profile.changes().get("name"),
844            Some(&[json!("Bob"), json!("Carol")])
845        );
846        assert!(profile.previous_changes().is_empty());
847    }
848    #[test]
849    fn rails_setting_new_attributes_should_not_affect_previous_changes() {
850        let mut profile = TestProfile::new("Alice", 30);
851        profile.set_name("Jericho Cane");
852        profile.set_age(31);
853        profile.changes_applied();
854
855        profile.set_name("DudeFella ManGuy");
856        profile.set_age(32);
857
858        assert_eq!(
859            profile.previous_changes().get("name"),
860            Some(&[json!("Alice"), json!("Jericho Cane")])
861        );
862        assert_eq!(
863            profile.previous_changes().get("age"),
864            Some(&[json!(30), json!(31)])
865        );
866    }
867
868    #[test]
869    fn rails_previous_value_is_preserved_when_changed_after_save() {
870        let mut profile = TestProfile::new("Alice", 30);
871        profile.set_name("Paul");
872        profile.set_age(31);
873        profile.changes_applied();
874
875        profile.set_name("John");
876        profile.set_age(32);
877
878        let mut changed = profile.changed_attributes();
879        changed.sort();
880
881        assert_eq!(changed, vec!["age".to_owned(), "name".to_owned()]);
882        assert_eq!(profile.attribute_was("name"), Some(json!("Paul")));
883        assert_eq!(profile.attribute_was("age"), Some(json!(31)));
884        assert_eq!(
885            profile.previous_changes().get("name"),
886            Some(&[json!("Alice"), json!("Paul")])
887        );
888        assert_eq!(
889            profile.previous_changes().get("age"),
890            Some(&[json!(30), json!(31)])
891        );
892    }
893
894    #[test]
895    #[ignore = "Dirty::restore_attributes restores every tracked attribute and exposes no per-attribute variant"]
896    fn rails_restore_attributes_can_restore_only_some_attributes() {}
897}