1use serde_json::Value;
18
19use crate::merge::conflict::{Conflict, MergeResult};
20use crate::merge::normalization::normalize;
21use crate::merge::path::{CanonicalPath, build_path_map, path_union};
22use crate::merge::sdl::{IdentityRule, MergeStrategyType, PropertyDef, SdlDocument};
23use crate::merge::strategies;
24
25#[derive(Debug, Clone)]
30pub struct MergeEngine {
31 schema: SdlDocument,
32}
33
34impl MergeEngine {
35 pub fn new(schema: SdlDocument) -> Self {
40 MergeEngine { schema }
41 }
42
43 pub fn schema(&self) -> &SdlDocument {
45 &self.schema
46 }
47
48 pub fn merge(&self, base: Value, current: Value, proposed: Value) -> MergeResult {
70 let normalized_current = normalize(&base, ¤t);
72 let normalized_proposed = normalize(&base, &proposed);
73
74 let base_paths = build_path_map(&base, &self.schema);
76 let current_paths = build_path_map(&normalized_current, &self.schema);
77 let proposed_paths = build_path_map(&normalized_proposed, &self.schema);
78
79 let all_paths = path_union(&[&base_paths, ¤t_paths, &proposed_paths]);
81
82 let mut merged = base.clone();
84 let mut all_conflicts = Vec::new();
85
86 for path in &all_paths {
87 if self.is_identity_array_subpath(path) {
90 continue;
91 }
92
93 let base_val = base_paths.get(path).cloned().unwrap_or(Value::Null);
94 let current_val = current_paths.get(path).cloned().unwrap_or(Value::Null);
95 let proposed_val = proposed_paths.get(path).cloned().unwrap_or(Value::Null);
96
97 if base_val == current_val && current_val == proposed_val {
99 continue;
100 }
101
102 let sdl_path = path.strip_prefix("root.").unwrap_or(path);
104
105 let result = match self.schema.property_def(sdl_path) {
107 Some(def) => {
108 if let Some(mutation_conflict) = self.check_identity_mutation(
110 sdl_path,
111 def,
112 &base_val,
113 ¤t_val,
114 &proposed_val,
115 ) {
116 MergeResult::Conflicts(vec![mutation_conflict])
117 } else {
118 self.apply_strategy(sdl_path, def, &base_val, ¤t_val, &proposed_val)
120 }
121 }
122 None => {
123 strategies::replace::merge_replace(
127 &format!("root.{sdl_path}"),
128 &base_val,
129 ¤t_val,
130 &proposed_val,
131 )
132 }
133 };
134
135 match result {
137 MergeResult::Merged(val) => {
138 if let Err(e) = set_value_at_path(&mut merged, path, val) {
140 all_conflicts.push(Conflict::structural(
142 format!("root.{sdl_path}"),
143 base_val,
144 current_val,
145 proposed_val,
146 ));
147 tracing::debug!(
149 "merge_engine: failed to set value at path '{}': {}",
150 path,
151 e
152 );
153 }
154 }
155 MergeResult::Conflicts(mut cs) => {
156 all_conflicts.append(&mut cs);
157 }
158 }
159 }
160
161 if all_conflicts.is_empty() {
166 MergeResult::Merged(merged)
167 } else {
168 MergeResult::Conflicts(all_conflicts)
169 }
170 }
171
172 fn is_identity_array_subpath(&self, path: &str) -> bool {
179 let parsed = match CanonicalPath::parse(path) {
180 Ok(p) => p,
181 Err(_) => return false,
182 };
183
184 let segments = parsed.segments();
185 if segments.len() <= 1 {
186 return false;
187 }
188
189 for (i, seg) in segments.iter().enumerate() {
192 if matches!(seg, crate::merge::path::PathSegment::Identity(_)) {
193 let parent_path = segments[..i]
196 .iter()
197 .map(|s| match s {
198 crate::merge::path::PathSegment::Key(k) => k.clone(),
199 crate::merge::path::PathSegment::Identity(id) => format!("[{id}]"),
200 })
201 .collect::<Vec<_>>()
202 .join(".");
203
204 if let Some(def) = self.schema.property_def(&parent_path)
207 && matches!(
208 def.merge.strategy_type,
209 MergeStrategyType::OrderedUnique
210 | MergeStrategyType::SetUnion
211 | MergeStrategyType::EdgeList
212 )
213 {
214 return true;
215 }
216
217 if i == segments.len() - 1 {
220 return true; }
222 }
223 }
224
225 false
226 }
227
228 fn check_identity_mutation(
239 &self,
240 path: &str,
241 def: &PropertyDef,
242 base: &Value,
243 current: &Value,
244 proposed: &Value,
245 ) -> Option<Conflict> {
246 let identity = match &def.merge.identity {
248 Some(id) => id,
249 None => return None,
250 };
251
252 let identity_key = match identity {
253 IdentityRule::Key { key } => key,
254 IdentityRule::PrimitiveValue => return None, };
256
257 let base_arr = base.as_array()?;
259 let current_arr = current.as_array()?;
260 let proposed_arr = proposed.as_array()?;
261
262 for i in 0..base_arr.len().min(current_arr.len()) {
264 let base_id = base_arr[i].get(identity_key);
265 let cur_id = current_arr[i].get(identity_key);
266 if base_id != cur_id {
267 let base_id_str = base_id.and_then(|v| v.as_str()).unwrap_or("?");
268 let sub_path = format!("root.{path}[{base_id_str}]");
269 let proposed_val = proposed_arr.get(i).cloned().unwrap_or(Value::Null);
270 return Some(Conflict::identity_mutation(
271 sub_path,
272 base_arr[i].clone(),
273 current_arr[i].clone(),
274 proposed_val,
275 ));
276 }
277 }
278
279 for i in 0..base_arr.len().min(proposed_arr.len()) {
281 let base_id = base_arr[i].get(identity_key);
282 let prop_id = proposed_arr[i].get(identity_key);
283 if base_id != prop_id {
284 let base_id_str = base_id.and_then(|v| v.as_str()).unwrap_or("?");
285 let sub_path = format!("root.{path}[{base_id_str}]");
286 let current_val = current_arr.get(i).cloned().unwrap_or(Value::Null);
287 return Some(Conflict::identity_mutation(
288 sub_path,
289 base_arr[i].clone(),
290 current_val,
291 proposed_arr[i].clone(),
292 ));
293 }
294 }
295
296 None
297 }
298
299 fn apply_strategy(
301 &self,
302 path: &str,
303 def: &PropertyDef,
304 base: &Value,
305 current: &Value,
306 proposed: &Value,
307 ) -> MergeResult {
308 let full_path = format!("root.{path}");
309
310 match def.merge.strategy_type {
311 MergeStrategyType::Replace => {
312 strategies::replace::merge_replace(&full_path, base, current, proposed)
313 }
314
315 MergeStrategyType::DeepMerge => {
316 strategies::deep_merge::merge_deep(&full_path, base, current, proposed)
317 }
318
319 MergeStrategyType::Atomic => {
320 strategies::atomic::merge_atomic(&full_path, base, current, proposed)
321 }
322
323 MergeStrategyType::OrderedUnique => {
324 let identity = def
325 .merge
326 .identity
327 .clone()
328 .unwrap_or(IdentityRule::PrimitiveValue);
329 strategies::ordered_unique::merge_ordered_unique(
330 &full_path, base, current, proposed, &identity,
331 )
332 }
333
334 MergeStrategyType::SetUnion => {
335 let identity = def
336 .merge
337 .identity
338 .clone()
339 .unwrap_or(IdentityRule::PrimitiveValue);
340 strategies::set_union::merge_set_union(
341 &full_path, base, current, proposed, &identity,
342 )
343 }
344
345 MergeStrategyType::EdgeList => {
346 let identity = def.merge.identity.clone().unwrap_or(IdentityRule::Key {
347 key: "id".to_string(),
348 });
349 let source_key = def
350 .merge
351 .source_key
352 .clone()
353 .unwrap_or_else(|| "source".to_string());
354 let target_key = def
355 .merge
356 .target_key
357 .clone()
358 .unwrap_or_else(|| "target".to_string());
359 strategies::edge_list::merge_edge_list(
360 &full_path,
361 base,
362 current,
363 proposed,
364 &identity,
365 &source_key,
366 &target_key,
367 )
368 }
369 }
370 }
371}
372
373fn set_value_at_path(root: &mut Value, path: &str, value: Value) -> Result<(), String> {
378 let canonical =
379 CanonicalPath::parse(path).map_err(|e| format!("invalid path '{path}': {e}"))?;
380
381 let segments = canonical.segments().to_vec();
382 if segments.is_empty() {
383 return Err("empty path".to_string());
384 }
385
386 let parent_segments = &segments[..segments.len() - 1];
388 let last_segment = &segments[segments.len() - 1];
389
390 let mut current = root;
391
392 for segment in parent_segments {
394 match segment {
395 crate::merge::path::PathSegment::Key(key) => {
396 if !current.is_object() {
397 return Err(format!("cannot enter non-object at '{key}'"));
398 }
399 current = current
400 .as_object_mut()
401 .unwrap()
402 .entry(key.clone())
403 .or_insert_with(|| Value::Object(serde_json::Map::new()));
404 }
405 crate::merge::path::PathSegment::Identity(id) => {
406 if let Some(item) = find_item_by_identity(current, id) {
409 current = item;
410 } else {
411 return Err(format!("identity '{id}' not found in array"));
412 }
413 }
414 }
415 }
416
417 match last_segment {
419 crate::merge::path::PathSegment::Key(key) => {
420 if let Value::Object(map) = current {
421 map.insert(key.clone(), value);
422 Ok(())
423 } else {
424 Err(format!("cannot set key '{key}' on non-object"))
425 }
426 }
427 crate::merge::path::PathSegment::Identity(id) => {
428 if let Some(item) = find_item_by_identity(current, id) {
430 *item = value;
431 Ok(())
432 } else {
433 Err(format!("identity '{id}' not found in array"))
434 }
435 }
436 }
437}
438
439fn find_item_by_identity<'a>(root: &'a mut Value, identity: &str) -> Option<&'a mut Value> {
441 match root {
442 Value::Array(arr) => {
443 for item in arr.iter_mut() {
444 if has_identity(item, identity) {
445 return Some(item);
446 }
447 }
448 None
449 }
450 _ => None,
451 }
452}
453
454fn has_identity(item: &Value, identity: &str) -> bool {
456 match item {
457 Value::Object(map) => map.values().any(|v| v.as_str() == Some(identity)),
458 Value::String(s) => s == identity,
459 Value::Number(n) => n.to_string() == identity,
460 Value::Bool(b) => b.to_string() == identity,
461 _ => false,
462 }
463}
464
465#[cfg(test)]
466mod tests {
467 use super::*;
468 use serde_json::json;
469
470 fn simple_sdl() -> SdlDocument {
471 SdlDocument::from_yaml(
472 r#"
473schema:
474 version: "1.0"
475 required: []
476 properties:
477 name:
478 type: string
479 merge:
480 type: replace
481 version:
482 type: number
483 merge:
484 type: atomic
485 tags:
486 type: array
487 merge:
488 type: ordered_unique
489 identity:
490 mode: primitive_value
491 characters:
492 type: array
493 merge:
494 type: ordered_unique
495 identity:
496 mode: key
497 key: id
498 edges:
499 type: array
500 merge:
501 type: edge_list
502 source_key: source
503 target_key: target
504 identity:
505 mode: key
506 key: id
507"#,
508 )
509 .unwrap()
510 }
511
512 #[test]
513 fn test_merge_simple_replace() {
514 let engine = MergeEngine::new(simple_sdl());
515
516 let base = json!({"name": "Luke"});
517 let current = json!({"name": "Luke Skywalker"});
518 let proposed = json!({"name": "Luke"});
519
520 let result = engine.merge(base, current, proposed);
521 assert!(result.is_merged());
522 assert_eq!(
523 result.unwrap_merged().get("name"),
524 Some(&json!("Luke Skywalker"))
525 );
526 }
527
528 #[test]
529 fn test_merge_replace_conflict() {
530 let engine = MergeEngine::new(simple_sdl());
531
532 let base = json!({"name": "Luke"});
533 let current = json!({"name": "Luke Skywalker"});
534 let proposed = json!({"name": "Anakin"});
535
536 let result = engine.merge(base, current, proposed);
537 assert!(result.is_conflict());
538 }
539
540 #[test]
541 fn test_merge_missing_field_preserved() {
542 let engine = MergeEngine::new(simple_sdl());
543
544 let base = json!({"name": "Obi Wan", "version": 1});
545 let current = json!({"name": "Obi Wan Kenobi"});
546 let proposed = json!({"name": "Obi Wan"});
547
548 let result = engine.merge(base, current, proposed);
550 assert!(result.is_merged());
551 let merged = result.unwrap_merged();
552 assert_eq!(merged.get("name"), Some(&json!("Obi Wan Kenobi")));
553 assert_eq!(merged.get("version"), Some(&json!(1)));
554 }
555
556 #[test]
557 fn test_merge_null_is_deletion() {
558 let engine = MergeEngine::new(simple_sdl());
559
560 let base = json!({"name": "Luke", "version": 1});
561 let current = json!({"name": "Luke", "version": 1});
562 let proposed = json!({"name": "Luke", "version": null});
563
564 let result = engine.merge(base, current, proposed);
565 assert!(result.is_merged());
566 let merged = result.unwrap_merged();
567 assert_eq!(merged.get("version"), Some(&Value::Null));
569 }
570
571 #[test]
572 fn test_merge_ordered_unique_objects() {
573 let engine = MergeEngine::new(simple_sdl());
574
575 let base = json!({"characters": [{"id": "A", "name": "Alpha"}]});
576 let current =
577 json!({"characters": [{"id": "A", "name": "Alpha"}, {"id": "B", "name": "Beta"}]});
578 let proposed =
579 json!({"characters": [{"id": "A", "name": "Alpha"}, {"id": "C", "name": "Gamma"}]});
580
581 let result = engine.merge(base, current, proposed);
582 assert!(result.is_merged());
583 let merged = result.unwrap_merged();
584 let chars = merged["characters"].as_array().unwrap();
585 assert_eq!(chars.len(), 3);
586 assert_eq!(chars[0]["id"], json!("A"));
587 assert_eq!(chars[1]["id"], json!("B"));
588 assert_eq!(chars[2]["id"], json!("C"));
589 }
590
591 #[test]
592 fn test_merge_ordered_unique_primitives() {
593 let engine = MergeEngine::new(simple_sdl());
594
595 let base = json!({"tags": ["a", "b"]});
596 let current = json!({"tags": ["a", "b", "c"]});
597 let proposed = json!({"tags": ["a", "b", "d"]});
598
599 let result = engine.merge(base, current, proposed);
600 assert!(result.is_merged());
601 let merged = result.unwrap_merged();
602 assert_eq!(
603 merged["tags"].as_array().unwrap(),
604 &[json!("a"), json!("b"), json!("c"), json!("d")]
605 );
606 }
607
608 #[test]
609 fn test_merge_atomic_conflict() {
610 let engine = MergeEngine::new(simple_sdl());
611
612 let base = json!({"version": 1});
613 let current = json!({"version": 2});
614 let proposed = json!({"version": 3});
615
616 let result = engine.merge(base, current, proposed);
617 assert!(result.is_conflict());
618 }
619
620 #[test]
621 fn test_merge_edge_list() {
622 let engine = MergeEngine::new(simple_sdl());
623
624 let base = json!({"edges": [{"id": "e1", "source": "a", "target": "b"}]});
625 let current = json!({
626 "edges": [
627 {"id": "e1", "source": "a", "target": "b"},
628 {"id": "e2", "source": "b", "target": "c"}
629 ]
630 });
631 let proposed = json!({
632 "edges": [
633 {"id": "e1", "source": "a", "target": "b"},
634 {"id": "e3", "source": "c", "target": "a"}
635 ]
636 });
637
638 let result = engine.merge(base, current, proposed);
639 assert!(result.is_merged());
640 let merged = result.unwrap_merged();
641 assert_eq!(merged["edges"].as_array().unwrap().len(), 3);
642 }
643
644 #[test]
645 fn test_merge_identity_mutation_conflict() {
646 let engine = MergeEngine::new(simple_sdl());
647
648 let base = json!({"characters": [{"id": "obiwan", "name": "Obi-Wan"}]});
649 let current = json!({"characters": [{"id": "ben_kenobi", "name": "Obi-Wan"}]}); let proposed = json!({"characters": [{"id": "obiwan", "name": "Obi-Wan"}]});
651
652 let result = engine.merge(base, current, proposed);
653 assert!(result.is_conflict());
654 }
655
656 #[test]
657 fn test_merge_deterministic() {
658 let engine = MergeEngine::new(simple_sdl());
659
660 let base = json!({"name": "Luke", "version": 1, "tags": ["a"]});
661 let current = json!({"name": "Luke Skywalker", "version": 2, "tags": ["a", "b"]});
662 let proposed = json!({"name": "Luke", "version": 1, "tags": ["a", "c"]});
663
664 let result1 = engine.merge(base.clone(), current.clone(), proposed.clone());
665 let result2 = engine.merge(base, current, proposed);
666
667 match (result1, result2) {
669 (MergeResult::Merged(a), MergeResult::Merged(b)) => assert_eq!(a, b),
670 (MergeResult::Conflicts(a), MergeResult::Conflicts(b)) => {
671 assert_eq!(a.len(), b.len());
672 for (ca, cb) in a.iter().zip(b.iter()) {
673 assert_eq!(ca.path, cb.path);
674 }
675 }
676 _ => panic!("results should be same variant"),
677 }
678 }
679
680 #[test]
681 fn test_merge_empty_documents() {
682 let engine = MergeEngine::new(simple_sdl());
683
684 let base = json!({});
685 let current = json!({});
686 let proposed = json!({});
687
688 let result = engine.merge(base, current, proposed);
689 assert!(result.is_merged());
690 assert_eq!(result.unwrap_merged(), json!({}));
691 }
692
693 #[test]
694 fn test_merge_additions_from_both_sides() {
695 let engine = MergeEngine::new(simple_sdl());
696
697 let base = json!({});
698 let current = json!({"name": "From Current"});
699 let proposed = json!({"version": 42});
700
701 let result = engine.merge(base, current, proposed);
702 assert!(result.is_merged());
703 let merged = result.unwrap_merged();
704 assert_eq!(merged.get("name"), Some(&json!("From Current")));
705 assert_eq!(merged.get("version"), Some(&json!(42)));
706 }
707
708 #[test]
709 fn test_merge_both_add_same_field_conflict() {
710 let engine = MergeEngine::new(simple_sdl());
711
712 let base = json!({});
713 let current = json!({"name": "From Current"});
714 let proposed = json!({"name": "From Proposed"});
715
716 let result = engine.merge(base, current, proposed);
717 assert!(result.is_conflict());
719 }
720
721 #[test]
722 fn test_merge_ordered_unique_modification_accepted() {
723 let engine = MergeEngine::new(simple_sdl());
724
725 let base = json!({"characters": [{"id": "A", "val": 1}]});
726 let current = json!({"characters": [{"id": "A", "val": 2}]}); let proposed = json!({"characters": [{"id": "A", "val": 1}]}); let result = engine.merge(base, current, proposed);
730 assert!(result.is_merged());
731 let merged = result.unwrap_merged();
732 assert_eq!(merged["characters"][0]["val"], json!(2));
733 }
734
735 #[test]
739 fn test_deterministic_random_iterations() {
740 use rand::Rng;
741 let engine = MergeEngine::new(simple_sdl());
742 let mut rng = rand::rng();
743
744 for _ in 0..50 {
745 let mut base = serde_json::Map::new();
747 let mut current = serde_json::Map::new();
748 let mut proposed = serde_json::Map::new();
749
750 let field_count = rng.random_range(0..6);
752 for i in 0..field_count {
753 let key = format!("field_{i}");
754 let base_val = rng.random_range(0..100);
755 let cur_val = if rng.random_bool(0.5) {
756 base_val + rng.random_range(-5..=5)
757 } else {
758 base_val
759 };
760 let prop_val = if rng.random_bool(0.5) {
761 base_val + rng.random_range(-5..=5)
762 } else {
763 base_val
764 };
765 base.insert(key.clone(), json!(base_val));
766 current.insert(key.clone(), json!(cur_val));
767 proposed.insert(key.clone(), json!(prop_val));
768 }
769
770 let tag_count = rng.random_range(0..4);
772 let mut tags: Vec<Value> = (0..tag_count).map(|i| json!(format!("tag_{i}"))).collect();
773 if rng.random_bool(0.3) {
774 tags.push(json!("tag_0")); }
776 if !tags.is_empty() {
777 base.insert("tags".to_string(), Value::Array(tags.clone()));
778 if rng.random_bool(0.5) {
779 tags.push(json!("tag_new"));
780 }
781 current.insert("tags".to_string(), Value::Array(tags.clone()));
782 if rng.random_bool(0.5) {
783 tags.push(json!("tag_extra"));
784 }
785 proposed.insert("tags".to_string(), Value::Array(tags));
786 }
787
788 let base_val = Value::Object(base);
789 let current_val = Value::Object(current);
790 let proposed_val = Value::Object(proposed);
791
792 let result1 = engine.merge(base_val.clone(), current_val.clone(), proposed_val.clone());
794 let result2 = engine.merge(base_val, current_val, proposed_val);
795
796 match (&result1, &result2) {
798 (MergeResult::Merged(a), MergeResult::Merged(b)) => {
799 assert_eq!(a, b, "deterministic merge failed");
800 }
801 (MergeResult::Conflicts(a), MergeResult::Conflicts(b)) => {
802 assert_eq!(a.len(), b.len(), "different conflict counts");
803 for (ca, cb) in a.iter().zip(b.iter()) {
804 assert_eq!(ca.path, cb.path, "different conflict paths");
805 }
806 }
807 _ => panic!("merge results differ in type"),
808 }
809 }
810 }
811}