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 = def.merge.identity.as_ref()?;
248
249 let identity_key = match identity {
250 IdentityRule::Key { key } => key,
251 IdentityRule::PrimitiveValue => return None, };
253
254 let base_arr = base.as_array()?;
256 let current_arr = current.as_array()?;
257 let proposed_arr = proposed.as_array()?;
258
259 for i in 0..base_arr.len().min(current_arr.len()) {
261 let base_id = base_arr[i].get(identity_key);
262 let cur_id = current_arr[i].get(identity_key);
263 if base_id != cur_id {
264 let base_id_str = base_id.and_then(|v| v.as_str()).unwrap_or("?");
265 let sub_path = format!("root.{path}[{base_id_str}]");
266 let proposed_val = proposed_arr.get(i).cloned().unwrap_or(Value::Null);
267 return Some(Conflict::identity_mutation(
268 sub_path,
269 base_arr[i].clone(),
270 current_arr[i].clone(),
271 proposed_val,
272 ));
273 }
274 }
275
276 for i in 0..base_arr.len().min(proposed_arr.len()) {
278 let base_id = base_arr[i].get(identity_key);
279 let prop_id = proposed_arr[i].get(identity_key);
280 if base_id != prop_id {
281 let base_id_str = base_id.and_then(|v| v.as_str()).unwrap_or("?");
282 let sub_path = format!("root.{path}[{base_id_str}]");
283 let current_val = current_arr.get(i).cloned().unwrap_or(Value::Null);
284 return Some(Conflict::identity_mutation(
285 sub_path,
286 base_arr[i].clone(),
287 current_val,
288 proposed_arr[i].clone(),
289 ));
290 }
291 }
292
293 None
294 }
295
296 fn apply_strategy(
298 &self,
299 path: &str,
300 def: &PropertyDef,
301 base: &Value,
302 current: &Value,
303 proposed: &Value,
304 ) -> MergeResult {
305 let full_path = format!("root.{path}");
306
307 match def.merge.strategy_type {
308 MergeStrategyType::Replace => {
309 strategies::replace::merge_replace(&full_path, base, current, proposed)
310 }
311
312 MergeStrategyType::DeepMerge => {
313 strategies::deep_merge::merge_deep(&full_path, base, current, proposed)
314 }
315
316 MergeStrategyType::Atomic => {
317 strategies::atomic::merge_atomic(&full_path, base, current, proposed)
318 }
319
320 MergeStrategyType::OrderedUnique => {
321 let identity = def
322 .merge
323 .identity
324 .clone()
325 .unwrap_or(IdentityRule::PrimitiveValue);
326 strategies::ordered_unique::merge_ordered_unique(
327 &full_path, base, current, proposed, &identity,
328 )
329 }
330
331 MergeStrategyType::SetUnion => {
332 let identity = def
333 .merge
334 .identity
335 .clone()
336 .unwrap_or(IdentityRule::PrimitiveValue);
337 strategies::set_union::merge_set_union(
338 &full_path, base, current, proposed, &identity,
339 )
340 }
341
342 MergeStrategyType::EdgeList => {
343 let identity = def.merge.identity.clone().unwrap_or(IdentityRule::Key {
344 key: "id".to_string(),
345 });
346 let source_key = def
347 .merge
348 .source_key
349 .clone()
350 .unwrap_or_else(|| "source".to_string());
351 let target_key = def
352 .merge
353 .target_key
354 .clone()
355 .unwrap_or_else(|| "target".to_string());
356 strategies::edge_list::merge_edge_list(
357 &full_path,
358 base,
359 current,
360 proposed,
361 &identity,
362 &source_key,
363 &target_key,
364 )
365 }
366 }
367 }
368}
369
370fn set_value_at_path(root: &mut Value, path: &str, value: Value) -> Result<(), String> {
375 let canonical =
376 CanonicalPath::parse(path).map_err(|e| format!("invalid path '{path}': {e}"))?;
377
378 let segments = canonical.segments().to_vec();
379 if segments.is_empty() {
380 return Err("empty path".to_string());
381 }
382
383 let parent_segments = &segments[..segments.len() - 1];
385 let last_segment = &segments[segments.len() - 1];
386
387 let mut current = root;
388
389 for segment in parent_segments {
391 match segment {
392 crate::merge::path::PathSegment::Key(key) => {
393 if !current.is_object() {
394 return Err(format!("cannot enter non-object at '{key}'"));
395 }
396 current = current
397 .as_object_mut()
398 .unwrap()
399 .entry(key.clone())
400 .or_insert_with(|| Value::Object(serde_json::Map::new()));
401 }
402 crate::merge::path::PathSegment::Identity(id) => {
403 if let Some(item) = find_item_by_identity(current, id) {
406 current = item;
407 } else {
408 return Err(format!("identity '{id}' not found in array"));
409 }
410 }
411 }
412 }
413
414 match last_segment {
416 crate::merge::path::PathSegment::Key(key) => {
417 if let Value::Object(map) = current {
418 map.insert(key.clone(), value);
419 Ok(())
420 } else {
421 Err(format!("cannot set key '{key}' on non-object"))
422 }
423 }
424 crate::merge::path::PathSegment::Identity(id) => {
425 if let Some(item) = find_item_by_identity(current, id) {
427 *item = value;
428 Ok(())
429 } else {
430 Err(format!("identity '{id}' not found in array"))
431 }
432 }
433 }
434}
435
436fn find_item_by_identity<'a>(root: &'a mut Value, identity: &str) -> Option<&'a mut Value> {
438 match root {
439 Value::Array(arr) => {
440 for item in arr.iter_mut() {
441 if has_identity(item, identity) {
442 return Some(item);
443 }
444 }
445 None
446 }
447 _ => None,
448 }
449}
450
451fn has_identity(item: &Value, identity: &str) -> bool {
453 match item {
454 Value::Object(map) => map.values().any(|v| v.as_str() == Some(identity)),
455 Value::String(s) => s == identity,
456 Value::Number(n) => n.to_string() == identity,
457 Value::Bool(b) => b.to_string() == identity,
458 _ => false,
459 }
460}
461
462#[cfg(test)]
463mod tests {
464 use super::*;
465 use serde_json::json;
466
467 fn simple_sdl() -> SdlDocument {
468 SdlDocument::from_yaml(
469 r#"
470schema:
471 version: "1.0"
472 required: []
473 properties:
474 name:
475 type: string
476 merge:
477 type: replace
478 version:
479 type: number
480 merge:
481 type: atomic
482 tags:
483 type: array
484 merge:
485 type: ordered_unique
486 identity:
487 mode: primitive_value
488 characters:
489 type: array
490 merge:
491 type: ordered_unique
492 identity:
493 mode: key
494 key: id
495 edges:
496 type: array
497 merge:
498 type: edge_list
499 source_key: source
500 target_key: target
501 identity:
502 mode: key
503 key: id
504"#,
505 )
506 .unwrap()
507 }
508
509 #[test]
510 fn test_merge_simple_replace() {
511 let engine = MergeEngine::new(simple_sdl());
512
513 let base = json!({"name": "Woody"});
514 let current = json!({"name": "Woody"});
515 let proposed = json!({"name": "Woody"});
516
517 let result = engine.merge(base, current, proposed);
518 assert!(result.is_merged());
519 assert_eq!(result.unwrap_merged().get("name"), Some(&json!("Woody")));
520 }
521
522 #[test]
523 fn test_merge_replace_conflict() {
524 let engine = MergeEngine::new(simple_sdl());
525
526 let base = json!({"name": "Woody"});
527 let current = json!({"name": "Sheriff Woody"});
528 let proposed = json!({"name": "Sid"});
529
530 let result = engine.merge(base, current, proposed);
531 assert!(result.is_conflict());
532 }
533
534 #[test]
535 fn test_merge_missing_field_preserved() {
536 let engine = MergeEngine::new(simple_sdl());
537
538 let base = json!({"name": "Buzz", "version": 1});
539 let current = json!({"name": "Buzz Lightyear"});
540 let proposed = json!({"name": "Buzz"});
541
542 let result = engine.merge(base, current, proposed);
544 assert!(result.is_merged());
545 let merged = result.unwrap_merged();
546 assert_eq!(merged.get("name"), Some(&json!("Buzz Lightyear")));
547 assert_eq!(merged.get("version"), Some(&json!(1)));
548 }
549
550 #[test]
551 fn test_merge_null_is_deletion() {
552 let engine = MergeEngine::new(simple_sdl());
553
554 let base = json!({"name": "Woody", "version": 1});
555 let current = json!({"name": "Woody", "version": 1});
556 let proposed = json!({"name": "Woody", "version": null});
557
558 let result = engine.merge(base, current, proposed);
559 assert!(result.is_merged());
560 let merged = result.unwrap_merged();
561 assert_eq!(merged.get("version"), Some(&Value::Null));
563 }
564
565 #[test]
566 fn test_merge_ordered_unique_objects() {
567 let engine = MergeEngine::new(simple_sdl());
568
569 let base = json!({"characters": [{"id": "A", "name": "Alpha"}]});
570 let current =
571 json!({"characters": [{"id": "A", "name": "Alpha"}, {"id": "B", "name": "Beta"}]});
572 let proposed =
573 json!({"characters": [{"id": "A", "name": "Alpha"}, {"id": "C", "name": "Gamma"}]});
574
575 let result = engine.merge(base, current, proposed);
576 assert!(result.is_merged());
577 let merged = result.unwrap_merged();
578 let chars = merged["characters"].as_array().unwrap();
579 assert_eq!(chars.len(), 3);
580 assert_eq!(chars[0]["id"], json!("A"));
581 assert_eq!(chars[1]["id"], json!("B"));
582 assert_eq!(chars[2]["id"], json!("C"));
583 }
584
585 #[test]
586 fn test_merge_ordered_unique_primitives() {
587 let engine = MergeEngine::new(simple_sdl());
588
589 let base = json!({"tags": ["a", "b"]});
590 let current = json!({"tags": ["a", "b", "c"]});
591 let proposed = json!({"tags": ["a", "b", "d"]});
592
593 let result = engine.merge(base, current, proposed);
594 assert!(result.is_merged());
595 let merged = result.unwrap_merged();
596 assert_eq!(
597 merged["tags"].as_array().unwrap(),
598 &[json!("a"), json!("b"), json!("c"), json!("d")]
599 );
600 }
601
602 #[test]
603 fn test_merge_atomic_conflict() {
604 let engine = MergeEngine::new(simple_sdl());
605
606 let base = json!({"version": 1});
607 let current = json!({"version": 2});
608 let proposed = json!({"version": 3});
609
610 let result = engine.merge(base, current, proposed);
611 assert!(result.is_conflict());
612 }
613
614 #[test]
615 fn test_merge_edge_list() {
616 let engine = MergeEngine::new(simple_sdl());
617
618 let base = json!({"edges": [{"id": "e1", "source": "a", "target": "b"}]});
619 let current = json!({
620 "edges": [
621 {"id": "e1", "source": "a", "target": "b"},
622 {"id": "e2", "source": "b", "target": "c"}
623 ]
624 });
625 let proposed = json!({
626 "edges": [
627 {"id": "e1", "source": "a", "target": "b"},
628 {"id": "e3", "source": "c", "target": "a"}
629 ]
630 });
631
632 let result = engine.merge(base, current, proposed);
633 assert!(result.is_merged());
634 let merged = result.unwrap_merged();
635 assert_eq!(merged["edges"].as_array().unwrap().len(), 3);
636 }
637
638 #[test]
639 fn test_merge_identity_mutation_conflict() {
640 let engine = MergeEngine::new(simple_sdl());
641
642 let base = json!({"characters": [{"id": "buzzlightyear", "name": "Buzz"}]});
643 let current = json!({"characters": [{"id": "buzz", "name": "Buzz"}]}); let proposed = json!({"characters": [{"id": "buzzlightyear", "name": "Buzz"}]});
645
646 let result = engine.merge(base, current, proposed);
647 assert!(result.is_conflict());
648 }
649
650 #[test]
651 fn test_merge_deterministic() {
652 let engine = MergeEngine::new(simple_sdl());
653
654 let base = json!({"name": "Woody", "version": 1, "tags": ["a"]});
655 let current = json!({"name": "Woody", "version": 2, "tags": ["a", "b"]});
656 let proposed = json!({"name": "Woody", "version": 1, "tags": ["a", "c"]});
657
658 let result1 = engine.merge(base.clone(), current.clone(), proposed.clone());
659 let result2 = engine.merge(base, current, proposed);
660
661 match (result1, result2) {
663 (MergeResult::Merged(a), MergeResult::Merged(b)) => assert_eq!(a, b),
664 (MergeResult::Conflicts(a), MergeResult::Conflicts(b)) => {
665 assert_eq!(a.len(), b.len());
666 for (ca, cb) in a.iter().zip(b.iter()) {
667 assert_eq!(ca.path, cb.path);
668 }
669 }
670 _ => panic!("results should be same variant"),
671 }
672 }
673
674 #[test]
675 fn test_merge_empty_documents() {
676 let engine = MergeEngine::new(simple_sdl());
677
678 let base = json!({});
679 let current = json!({});
680 let proposed = json!({});
681
682 let result = engine.merge(base, current, proposed);
683 assert!(result.is_merged());
684 assert_eq!(result.unwrap_merged(), json!({}));
685 }
686
687 #[test]
688 fn test_merge_additions_from_both_sides() {
689 let engine = MergeEngine::new(simple_sdl());
690
691 let base = json!({});
692 let current = json!({"name": "From Current"});
693 let proposed = json!({"version": 42});
694
695 let result = engine.merge(base, current, proposed);
696 assert!(result.is_merged());
697 let merged = result.unwrap_merged();
698 assert_eq!(merged.get("name"), Some(&json!("From Current")));
699 assert_eq!(merged.get("version"), Some(&json!(42)));
700 }
701
702 #[test]
703 fn test_merge_both_add_same_field_conflict() {
704 let engine = MergeEngine::new(simple_sdl());
705
706 let base = json!({});
707 let current = json!({"name": "From Current"});
708 let proposed = json!({"name": "From Proposed"});
709
710 let result = engine.merge(base, current, proposed);
711 assert!(result.is_conflict());
713 }
714
715 #[test]
716 fn test_merge_ordered_unique_modification_accepted() {
717 let engine = MergeEngine::new(simple_sdl());
718
719 let base = json!({"characters": [{"id": "A", "val": 1}]});
720 let current = json!({"characters": [{"id": "A", "val": 2}]}); let proposed = json!({"characters": [{"id": "A", "val": 1}]}); let result = engine.merge(base, current, proposed);
724 assert!(result.is_merged());
725 let merged = result.unwrap_merged();
726 assert_eq!(merged["characters"][0]["val"], json!(2));
727 }
728
729 #[test]
733 fn test_deterministic_random_iterations() {
734 use rand::Rng;
735 let engine = MergeEngine::new(simple_sdl());
736 let mut rng = rand::rng();
737
738 for _ in 0..50 {
739 let mut base = serde_json::Map::new();
741 let mut current = serde_json::Map::new();
742 let mut proposed = serde_json::Map::new();
743
744 let field_count = rng.random_range(0..6);
746 for i in 0..field_count {
747 let key = format!("field_{i}");
748 let base_val = rng.random_range(0..100);
749 let cur_val = if rng.random_bool(0.5) {
750 base_val + rng.random_range(-5..=5)
751 } else {
752 base_val
753 };
754 let prop_val = if rng.random_bool(0.5) {
755 base_val + rng.random_range(-5..=5)
756 } else {
757 base_val
758 };
759 base.insert(key.clone(), json!(base_val));
760 current.insert(key.clone(), json!(cur_val));
761 proposed.insert(key.clone(), json!(prop_val));
762 }
763
764 let tag_count = rng.random_range(0..4);
766 let mut tags: Vec<Value> = (0..tag_count).map(|i| json!(format!("tag_{i}"))).collect();
767 if rng.random_bool(0.3) {
768 tags.push(json!("tag_0")); }
770 if !tags.is_empty() {
771 base.insert("tags".to_string(), Value::Array(tags.clone()));
772 if rng.random_bool(0.5) {
773 tags.push(json!("tag_new"));
774 }
775 current.insert("tags".to_string(), Value::Array(tags.clone()));
776 if rng.random_bool(0.5) {
777 tags.push(json!("tag_extra"));
778 }
779 proposed.insert("tags".to_string(), Value::Array(tags));
780 }
781
782 let base_val = Value::Object(base);
783 let current_val = Value::Object(current);
784 let proposed_val = Value::Object(proposed);
785
786 let result1 = engine.merge(base_val.clone(), current_val.clone(), proposed_val.clone());
788 let result2 = engine.merge(base_val, current_val, proposed_val);
789
790 match (&result1, &result2) {
792 (MergeResult::Merged(a), MergeResult::Merged(b)) => {
793 assert_eq!(a, b, "deterministic merge failed");
794 }
795 (MergeResult::Conflicts(a), MergeResult::Conflicts(b)) => {
796 assert_eq!(a.len(), b.len(), "different conflict counts");
797 for (ca, cb) in a.iter().zip(b.iter()) {
798 assert_eq!(ca.path, cb.path, "different conflict paths");
799 }
800 }
801 _ => panic!("merge results differ in type"),
802 }
803 }
804 }
805}