Skip to main content

robinpath_modules/modules/
diff_mod.rs

1use robinpath::{RobinPath, Value};
2
3pub fn register(rp: &mut RobinPath) {
4    rp.register_builtin("diff.lines", |args, _| {
5        let a = args.first().map(|v| v.to_display_string()).unwrap_or_default();
6        let b = args.get(1).map(|v| v.to_display_string()).unwrap_or_default();
7        let a_tokens: Vec<&str> = a.lines().collect();
8        let b_tokens: Vec<&str> = b.lines().collect();
9        Ok(Value::Array(diff_tokens(&a_tokens, &b_tokens)))
10    });
11
12    rp.register_builtin("diff.chars", |args, _| {
13        let a = args.first().map(|v| v.to_display_string()).unwrap_or_default();
14        let b = args.get(1).map(|v| v.to_display_string()).unwrap_or_default();
15        let a_chars: Vec<String> = a.chars().map(|c| c.to_string()).collect();
16        let b_chars: Vec<String> = b.chars().map(|c| c.to_string()).collect();
17        let a_refs: Vec<&str> = a_chars.iter().map(|s| s.as_str()).collect();
18        let b_refs: Vec<&str> = b_chars.iter().map(|s| s.as_str()).collect();
19        Ok(Value::Array(diff_tokens(&a_refs, &b_refs)))
20    });
21
22    rp.register_builtin("diff.words", |args, _| {
23        let a = args.first().map(|v| v.to_display_string()).unwrap_or_default();
24        let b = args.get(1).map(|v| v.to_display_string()).unwrap_or_default();
25        let a_words: Vec<&str> = a.split_whitespace().collect();
26        let b_words: Vec<&str> = b.split_whitespace().collect();
27        Ok(Value::Array(diff_tokens(&a_words, &b_words)))
28    });
29
30    rp.register_builtin("diff.objects", |args, _| {
31        let a = args.first().cloned().unwrap_or(Value::Null);
32        let b = args.get(1).cloned().unwrap_or(Value::Null);
33        Ok(Value::Array(diff_objects(&a, &b, String::new())))
34    });
35
36    rp.register_builtin("diff.arrays", |args, _| {
37        let a = args.first().cloned().unwrap_or(Value::Null);
38        let b = args.get(1).cloned().unwrap_or(Value::Null);
39        if let (Value::Array(arr_a), Value::Array(arr_b)) = (&a, &b) {
40            let a_keys: Vec<String> = arr_a.iter().map(|v| v.to_json_string()).collect();
41            let b_keys: Vec<String> = arr_b.iter().map(|v| v.to_json_string()).collect();
42
43            let added: Vec<Value> = arr_b
44                .iter()
45                .filter(|v| !a_keys.contains(&v.to_json_string()))
46                .cloned()
47                .collect();
48            let removed: Vec<Value> = arr_a
49                .iter()
50                .filter(|v| !b_keys.contains(&v.to_json_string()))
51                .cloned()
52                .collect();
53            let common: Vec<Value> = arr_a
54                .iter()
55                .filter(|v| b_keys.contains(&v.to_json_string()))
56                .cloned()
57                .collect();
58
59            let mut obj = indexmap::IndexMap::new();
60            obj.insert("added".to_string(), Value::Array(added));
61            obj.insert("removed".to_string(), Value::Array(removed));
62            obj.insert("common".to_string(), Value::Array(common));
63            Ok(Value::Object(obj))
64        } else {
65            let mut obj = indexmap::IndexMap::new();
66            obj.insert("added".to_string(), Value::Array(vec![]));
67            obj.insert("removed".to_string(), Value::Array(vec![]));
68            obj.insert("common".to_string(), Value::Array(vec![]));
69            Ok(Value::Object(obj))
70        }
71    });
72
73    rp.register_builtin("diff.patch", |args, _| {
74        let diff_arr = args.first().cloned().unwrap_or(Value::Null);
75        if let Value::Array(items) = &diff_arr {
76            let mut result = String::new();
77            for item in items {
78                if let Value::Object(obj) = item {
79                    let typ = obj
80                        .get("type")
81                        .map(|v| v.to_display_string())
82                        .unwrap_or_default();
83                    let val = obj
84                        .get("value")
85                        .map(|v| v.to_display_string())
86                        .unwrap_or_default();
87                    if typ != "removed" {
88                        result.push_str(&val);
89                    }
90                }
91            }
92            Ok(Value::String(result))
93        } else {
94            Ok(Value::String(String::new()))
95        }
96    });
97
98    rp.register_builtin("diff.unified", |args, _| {
99        let a = args.first().map(|v| v.to_display_string()).unwrap_or_default();
100        let b = args.get(1).map(|v| v.to_display_string()).unwrap_or_default();
101        let a_lines: Vec<&str> = a.lines().collect();
102        let b_lines: Vec<&str> = b.lines().collect();
103        let tokens = diff_tokens(&a_lines, &b_lines);
104
105        let mut output = String::new();
106        output.push_str("--- a\n");
107        output.push_str("+++ b\n");
108
109        for token in &tokens {
110            if let Value::Object(obj) = token {
111                let typ = obj
112                    .get("type")
113                    .map(|v| v.to_display_string())
114                    .unwrap_or_default();
115                let val = obj
116                    .get("value")
117                    .map(|v| v.to_display_string())
118                    .unwrap_or_default();
119                match typ.as_str() {
120                    "added" => output.push_str(&format!("+{}\n", val)),
121                    "removed" => output.push_str(&format!("-{}\n", val)),
122                    _ => output.push_str(&format!(" {}\n", val)),
123                }
124            }
125        }
126        Ok(Value::String(output.trim_end().to_string()))
127    });
128
129    rp.register_builtin("diff.isEqual", |args, _| {
130        let a = args.first().cloned().unwrap_or(Value::Null);
131        let b = args.get(1).cloned().unwrap_or(Value::Null);
132        Ok(Value::Bool(a.deep_eq(&b)))
133    });
134
135    rp.register_builtin("diff.stats", |args, _| {
136        let diff_arr = args.first().cloned().unwrap_or(Value::Null);
137        let mut additions = 0u64;
138        let mut deletions = 0u64;
139        let mut unchanged = 0u64;
140        if let Value::Array(items) = &diff_arr {
141            for item in items {
142                if let Value::Object(obj) = item {
143                    let typ = obj
144                        .get("type")
145                        .map(|v| v.to_display_string())
146                        .unwrap_or_default();
147                    match typ.as_str() {
148                        "added" => additions += 1,
149                        "removed" => deletions += 1,
150                        _ => unchanged += 1,
151                    }
152                }
153            }
154        }
155        let mut obj = indexmap::IndexMap::new();
156        obj.insert("additions".to_string(), Value::Number(additions as f64));
157        obj.insert("deletions".to_string(), Value::Number(deletions as f64));
158        obj.insert("unchanged".to_string(), Value::Number(unchanged as f64));
159        Ok(Value::Object(obj))
160    });
161}
162
163/// LCS-based token diff
164fn diff_tokens(a: &[&str], b: &[&str]) -> Vec<Value> {
165    let m = a.len();
166    let n = b.len();
167
168    // Build LCS table
169    let mut dp = vec![vec![0usize; n + 1]; m + 1];
170    for i in 1..=m {
171        for j in 1..=n {
172            if a[i - 1] == b[j - 1] {
173                dp[i][j] = dp[i - 1][j - 1] + 1;
174            } else {
175                dp[i][j] = dp[i - 1][j].max(dp[i][j - 1]);
176            }
177        }
178    }
179
180    // Backtrack to produce diff
181    let mut result = Vec::new();
182    let mut i = m;
183    let mut j = n;
184    while i > 0 || j > 0 {
185        if i > 0 && j > 0 && a[i - 1] == b[j - 1] {
186            result.push(make_diff_entry("unchanged", a[i - 1]));
187            i -= 1;
188            j -= 1;
189        } else if j > 0 && (i == 0 || dp[i][j - 1] >= dp[i - 1][j]) {
190            result.push(make_diff_entry("added", b[j - 1]));
191            j -= 1;
192        } else {
193            result.push(make_diff_entry("removed", a[i - 1]));
194            i -= 1;
195        }
196    }
197    result.reverse();
198    result
199}
200
201fn make_diff_entry(typ: &str, value: &str) -> Value {
202    let mut obj = indexmap::IndexMap::new();
203    obj.insert("type".to_string(), Value::String(typ.to_string()));
204    obj.insert("value".to_string(), Value::String(value.to_string()));
205    Value::Object(obj)
206}
207
208fn diff_objects(a: &Value, b: &Value, prefix: String) -> Vec<Value> {
209    let mut changes = Vec::new();
210    if let (Value::Object(obj_a), Value::Object(obj_b)) = (a, b) {
211        for (k, v_a) in obj_a {
212            let path = if prefix.is_empty() {
213                k.clone()
214            } else {
215                format!("{}.{}", prefix, k)
216            };
217            match obj_b.get(k) {
218                Some(v_b) => {
219                    if let (Value::Object(_), Value::Object(_)) = (v_a, v_b) {
220                        changes.extend(diff_objects(v_a, v_b, path));
221                    } else if !v_a.deep_eq(v_b) {
222                        let mut obj = indexmap::IndexMap::new();
223                        obj.insert("path".to_string(), Value::String(path));
224                        obj.insert("type".to_string(), Value::String("changed".to_string()));
225                        obj.insert("oldValue".to_string(), v_a.clone());
226                        obj.insert("newValue".to_string(), v_b.clone());
227                        changes.push(Value::Object(obj));
228                    }
229                }
230                None => {
231                    let mut obj = indexmap::IndexMap::new();
232                    obj.insert("path".to_string(), Value::String(path));
233                    obj.insert("type".to_string(), Value::String("removed".to_string()));
234                    obj.insert("oldValue".to_string(), v_a.clone());
235                    changes.push(Value::Object(obj));
236                }
237            }
238        }
239        for (k, v_b) in obj_b {
240            if !obj_a.contains_key(k) {
241                let path = if prefix.is_empty() {
242                    k.clone()
243                } else {
244                    format!("{}.{}", prefix, k)
245                };
246                let mut obj = indexmap::IndexMap::new();
247                obj.insert("path".to_string(), Value::String(path));
248                obj.insert("type".to_string(), Value::String("added".to_string()));
249                obj.insert("newValue".to_string(), v_b.clone());
250                changes.push(Value::Object(obj));
251            }
252        }
253    }
254    changes
255}