1use crate::{LatticeRegistry, Op, Patch, Path};
7use std::collections::BTreeSet;
8
9#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct Conflict {
12 pub path: Path,
14 pub kind: ConflictKind,
16}
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum ConflictKind {
21 ExactMatch,
23 PrefixConflict,
26}
27
28pub fn compute_touched(patch: &Patch, include_parents: bool) -> BTreeSet<Path> {
57 let mut touched = BTreeSet::new();
58
59 for op in patch.ops() {
60 let path = op.path();
61
62 if include_parents {
63 let mut current = Path::root();
65 for seg in path.segments() {
66 current = current.with_segment(seg.clone());
67 touched.insert(current.clone());
68 }
69 } else {
70 touched.insert(path.clone());
71 }
72 }
73
74 touched
75}
76
77pub fn detect_conflicts(a: &BTreeSet<Path>, b: &BTreeSet<Path>) -> Vec<Conflict> {
103 let mut conflicts = Vec::new();
104
105 for path_a in a {
106 for path_b in b {
107 if path_a == path_b {
108 conflicts.push(Conflict {
109 path: path_a.clone(),
110 kind: ConflictKind::ExactMatch,
111 });
112 } else if path_a.is_prefix_of(path_b) || path_b.is_prefix_of(path_a) {
113 let conflict_path = if path_a.len() < path_b.len() {
115 path_a.clone()
116 } else {
117 path_b.clone()
118 };
119 conflicts.push(Conflict {
120 path: conflict_path,
121 kind: ConflictKind::PrefixConflict,
122 });
123 }
124 }
125 }
126
127 conflicts
128}
129
130fn is_lattice_only_at(patch: &Patch, path: &Path) -> bool {
132 patch
133 .ops()
134 .iter()
135 .filter(|op| op.path() == path)
136 .all(|op| matches!(op, Op::LatticeMerge { .. }))
137}
138
139pub fn conflicts_with_registry(a: &Patch, b: &Patch, registry: &LatticeRegistry) -> Vec<Conflict> {
145 let touched_a = compute_touched(a, false);
146 let touched_b = compute_touched(b, false);
147 detect_conflicts(&touched_a, &touched_b)
148 .into_iter()
149 .filter(|conflict| {
150 !(registry.get(&conflict.path).is_some()
151 && is_lattice_only_at(a, &conflict.path)
152 && is_lattice_only_at(b, &conflict.path))
153 })
154 .collect()
155}
156
157pub trait PatchExt {
159 fn touched(&self, include_parents: bool) -> BTreeSet<Path>;
161
162 fn conflicts_with(&self, other: &Patch) -> Vec<Conflict>;
164}
165
166impl PatchExt for Patch {
167 fn touched(&self, include_parents: bool) -> BTreeSet<Path> {
168 compute_touched(self, include_parents)
169 }
170
171 fn conflicts_with(&self, other: &Patch) -> Vec<Conflict> {
172 let touched_self = compute_touched(self, false);
173 let touched_other = compute_touched(other, false);
174 detect_conflicts(&touched_self, &touched_other)
175 }
176}
177
178#[cfg(test)]
179mod tests {
180 use super::*;
181 use crate::{path, GSet, Op};
182 use serde_json::json;
183
184 #[test]
185 fn test_compute_touched_without_parents() {
186 let patch = Patch::new()
187 .with_op(Op::set(path!("a", "b"), json!(1)))
188 .with_op(Op::set(path!("x", "y", "z"), json!(2)));
189
190 let touched = compute_touched(&patch, false);
191
192 assert_eq!(touched.len(), 2);
193 assert!(touched.contains(&path!("a", "b")));
194 assert!(touched.contains(&path!("x", "y", "z")));
195 assert!(!touched.contains(&path!("a")));
196 }
197
198 #[test]
199 fn test_compute_touched_with_parents() {
200 let patch = Patch::new().with_op(Op::set(path!("a", "b", "c"), json!(1)));
201
202 let touched = compute_touched(&patch, true);
203
204 assert_eq!(touched.len(), 3);
205 assert!(touched.contains(&path!("a")));
206 assert!(touched.contains(&path!("a", "b")));
207 assert!(touched.contains(&path!("a", "b", "c")));
208 }
209
210 #[test]
211 fn test_detect_exact_conflict() {
212 let mut a = BTreeSet::new();
213 a.insert(path!("user", "name"));
214
215 let mut b = BTreeSet::new();
216 b.insert(path!("user", "name"));
217
218 let conflicts = detect_conflicts(&a, &b);
219
220 assert_eq!(conflicts.len(), 1);
221 assert_eq!(conflicts[0].kind, ConflictKind::ExactMatch);
222 assert_eq!(conflicts[0].path, path!("user", "name"));
223 }
224
225 #[test]
226 fn test_detect_prefix_conflict() {
227 let mut a = BTreeSet::new();
228 a.insert(path!("user"));
229
230 let mut b = BTreeSet::new();
231 b.insert(path!("user", "name"));
232
233 let conflicts = detect_conflicts(&a, &b);
234
235 assert_eq!(conflicts.len(), 1);
236 assert_eq!(conflicts[0].kind, ConflictKind::PrefixConflict);
237 assert_eq!(conflicts[0].path, path!("user"));
238 }
239
240 #[test]
241 fn test_no_conflict() {
242 let mut a = BTreeSet::new();
243 a.insert(path!("user", "name"));
244
245 let mut b = BTreeSet::new();
246 b.insert(path!("user", "age"));
247
248 let conflicts = detect_conflicts(&a, &b);
249
250 assert!(conflicts.is_empty());
251 }
252
253 #[test]
254 fn test_patch_ext_conflicts_with() {
255 let patch_a = Patch::new().with_op(Op::set(path!("user"), json!({})));
256 let patch_b = Patch::new().with_op(Op::set(path!("user", "name"), json!("Alice")));
257
258 let conflicts = patch_a.conflicts_with(&patch_b);
259
260 assert!(!conflicts.is_empty());
261 }
262
263 #[test]
264 fn test_conflicts_with_registry_suppresses_lattice_only() {
265 let mut registry = LatticeRegistry::new();
266 registry.register::<GSet<String>>(path!("tags"));
267
268 let patch_a = Patch::new().with_op(Op::lattice_merge(path!("tags"), json!(["a"])));
269 let patch_b = Patch::new().with_op(Op::lattice_merge(path!("tags"), json!(["b"])));
270
271 let conflicts = patch_a.conflicts_with(&patch_b);
273 assert!(!conflicts.is_empty());
274
275 let conflicts = conflicts_with_registry(&patch_a, &patch_b, ®istry);
277 assert!(conflicts.is_empty());
278 }
279
280 #[test]
281 fn test_conflicts_with_registry_mixed_ops_still_conflict() {
282 let mut registry = LatticeRegistry::new();
283 registry.register::<GSet<String>>(path!("tags"));
284
285 let patch_a = Patch::new()
286 .with_op(Op::lattice_merge(path!("tags"), json!(["a"])))
287 .with_op(Op::set(path!("tags"), json!(["override"])));
288 let patch_b = Patch::new().with_op(Op::lattice_merge(path!("tags"), json!(["b"])));
289
290 let conflicts = conflicts_with_registry(&patch_a, &patch_b, ®istry);
291 assert!(!conflicts.is_empty(), "mixed ops should still conflict");
292 }
293
294 #[test]
295 fn test_conflicts_with_registry_unregistered_path_still_conflicts() {
296 let registry = LatticeRegistry::new(); let patch_a = Patch::new().with_op(Op::lattice_merge(path!("tags"), json!(["a"])));
299 let patch_b = Patch::new().with_op(Op::lattice_merge(path!("tags"), json!(["b"])));
300
301 let conflicts = conflicts_with_registry(&patch_a, &patch_b, ®istry);
302 assert!(
303 !conflicts.is_empty(),
304 "unregistered path should still conflict"
305 );
306 }
307
308 #[test]
309 fn test_conflicts_with_registry_disjoint_paths_no_conflict() {
310 let registry = LatticeRegistry::new();
311
312 let patch_a = Patch::new().with_op(Op::set(path!("alpha"), json!(1)));
313 let patch_b = Patch::new().with_op(Op::set(path!("beta"), json!(2)));
314
315 let conflicts = conflicts_with_registry(&patch_a, &patch_b, ®istry);
316 assert!(conflicts.is_empty());
317 }
318}