tawasuyu

tawasuyu/brahman

Rust 326TOML 123JSON 17Markdown 144606 Definiciones

⎇ 72758e75ce515f355f27615671c95ecaf8c2a8bc crates/modules/ui_engine/libs/meta-runtime/src/delta.rs Rust · 134 líneas · 4.7 KB Historia Quién tocó qué crudo

refactor(yahweh): Fase 2 — extraer helpers puros a yahweh-meta-runtime 610448449 · Sergio ·

1//! Cálculo del delta entre el record actual y la propuesta del form.
2//!
3//! Sirve a un runtime de edición para emitir SOLO los Set/Clear que
4//! cambian algo: log + apply minimales, no-op edits = 0 entries.
5
6use serde_json::Value;
7
8/// Calcula el delta entre el record actual y los valores propuestos
9/// del form. Devuelve un Map con sólo los campos cuyo valor difiere.
10///
11/// Comparación: igualdad estructural sobre `serde_json::Value`. Un
12/// `current=Value::Null` (record no encontrado) hace que todos los
13/// campos del `proposed` sean considerados nuevos. Un campo del
14/// proposed que coincide con el del current se omite. Campos que
15/// están en current pero NO en proposed se preservan tal cual (el
16/// edit no los toca; ver [`compute_clear_fields`] para borrar
17/// explícito desde un input vacío).
18pub fn compute_field_delta(
19    current: &Value,
20    proposed: &serde_json::Map<String, Value>,
21) -> serde_json::Map<String, Value> {
22    proposed
23        .iter()
24        .filter(|(field, value)| current.get(field.as_str()) != Some(*value))
25        .map(|(k, v)| (k.clone(), v.clone()))
26        .collect()
27}
28
29/// Decide cuáles fields del `to_clear` candidate list ameritan
30/// realmente un `FieldOp::Clear`: sólo los que existen en el current
31/// con un valor non-null. Para fields ausentes o ya null, Clear es
32/// no-op semántico (el post-state es el mismo) y dropearlos
33/// preserva la propiedad "1 op = 1 cambio efectivo" del log.
34///
35/// Preserva el orden del input para que el log entry sea estable.
36pub fn compute_clear_fields(current: &Value, to_clear: &[String]) -> Vec<String> {
37    to_clear
38        .iter()
39        .filter(|f| match current.get(f.as_str()) {
40            None | Some(Value::Null) => false,
41            Some(_) => true,
42        })
43        .cloned()
44        .collect()
45}
46
47#[cfg(test)]
48mod tests {
49    use super::*;
50    use serde_json::json;
51
52    fn map(items: &[(&str, Value)]) -> serde_json::Map<String, Value> {
53        items.iter().map(|(k, v)| (k.to_string(), v.clone())).collect()
54    }
55
56    #[test]
57    fn delta_empty_when_all_fields_match() {
58        let current = json!({"name": "Acme", "saldo": 100_i64, "currency": "USD"});
59        let proposed = map(&[
60            ("name", json!("Acme")),
61            ("saldo", json!(100_i64)),
62            ("currency", json!("USD")),
63        ]);
64        assert!(compute_field_delta(&current, &proposed).is_empty());
65    }
66
67    #[test]
68    fn delta_includes_only_changed_field() {
69        let current = json!({"name": "Acme", "saldo": 100_i64});
70        let proposed = map(&[("name", json!("Acme")), ("saldo", json!(200_i64))]);
71        let d = compute_field_delta(&current, &proposed);
72        assert_eq!(d.len(), 1);
73        assert_eq!(d.get("saldo"), Some(&json!(200_i64)));
74    }
75
76    #[test]
77    fn delta_treats_missing_record_as_all_new() {
78        let current = Value::Null;
79        let proposed = map(&[("name", json!("Acme")), ("saldo", json!(0_i64))]);
80        assert_eq!(compute_field_delta(&current, &proposed).len(), 2);
81    }
82
83    #[test]
84    fn delta_distinguishes_int_from_string_repr() {
85        let current = json!({"qty": 100_i64});
86        let proposed = map(&[("qty", json!(100_i64))]);
87        assert!(compute_field_delta(&current, &proposed).is_empty());
88
89        let current_str = json!({"qty": "100"});
90        let proposed_int = map(&[("qty", json!(100_i64))]);
91        assert_eq!(compute_field_delta(&current_str, &proposed_int).len(), 1);
92    }
93
94    #[test]
95    fn delta_skips_fields_absent_from_proposed() {
96        let current = json!({"name": "Acme", "saldo": 100_i64, "extra": "x"});
97        let proposed = map(&[("name", json!("Acme")), ("saldo", json!(150_i64))]);
98        let d = compute_field_delta(&current, &proposed);
99        assert_eq!(d.len(), 1);
100        assert!(!d.contains_key("extra"));
101    }
102
103    #[test]
104    fn clear_fields_skips_absent_and_null() {
105        let current = json!({"name": "Acme", "notes": "lorem", "tag": null});
106        let to_clear = vec![
107            "name".into(),
108            "notes".into(),
109            "tag".into(),
110            "missing".into(),
111        ];
112        assert_eq!(
113            compute_clear_fields(&current, &to_clear),
114            vec!["name".to_string(), "notes".to_string()]
115        );
116    }
117
118    #[test]
119    fn clear_fields_preserves_input_order() {
120        let current = json!({"a": 1, "b": 2, "c": 3});
121        let to_clear = vec!["c".into(), "a".into(), "b".into()];
122        assert_eq!(
123            compute_clear_fields(&current, &to_clear),
124            vec!["c", "a", "b"]
125        );
126    }
127
128    #[test]
129    fn clear_fields_empty_when_current_is_null() {
130        let current = Value::Null;
131        let to_clear = vec!["name".into()];
132        assert!(compute_clear_fields(&current, &to_clear).is_empty());
133    }
134}