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": "Luke"});
514 let current = json!({"name": "Luke Skywalker"});
515 let proposed = json!({"name": "Luke"});
516
517 let result = engine.merge(base, current, proposed);
518 assert!(result.is_merged());
519 assert_eq!(
520 result.unwrap_merged().get("name"),
521 Some(&json!("Luke Skywalker"))
522 );
523 }
524
525 #[test]
526 fn test_merge_replace_conflict() {
527 let engine = MergeEngine::new(simple_sdl());
528
529 let base = json!({"name": "Luke"});
530 let current = json!({"name": "Luke Skywalker"});
531 let proposed = json!({"name": "Anakin"});
532
533 let result = engine.merge(base, current, proposed);
534 assert!(result.is_conflict());
535 }
536
537 #[test]
538 fn test_merge_missing_field_preserved() {
539 let engine = MergeEngine::new(simple_sdl());
540
541 let base = json!({"name": "Obi Wan", "version": 1});
542 let current = json!({"name": "Obi Wan Kenobi"});
543 let proposed = json!({"name": "Obi Wan"});
544
545 let result = engine.merge(base, current, proposed);
547 assert!(result.is_merged());
548 let merged = result.unwrap_merged();
549 assert_eq!(merged.get("name"), Some(&json!("Obi Wan Kenobi")));
550 assert_eq!(merged.get("version"), Some(&json!(1)));
551 }
552
553 #[test]
554 fn test_merge_null_is_deletion() {
555 let engine = MergeEngine::new(simple_sdl());
556
557 let base = json!({"name": "Luke", "version": 1});
558 let current = json!({"name": "Luke", "version": 1});
559 let proposed = json!({"name": "Luke", "version": null});
560
561 let result = engine.merge(base, current, proposed);
562 assert!(result.is_merged());
563 let merged = result.unwrap_merged();
564 assert_eq!(merged.get("version"), Some(&Value::Null));
566 }
567
568 #[test]
569 fn test_merge_ordered_unique_objects() {
570 let engine = MergeEngine::new(simple_sdl());
571
572 let base = json!({"characters": [{"id": "A", "name": "Alpha"}]});
573 let current =
574 json!({"characters": [{"id": "A", "name": "Alpha"}, {"id": "B", "name": "Beta"}]});
575 let proposed =
576 json!({"characters": [{"id": "A", "name": "Alpha"}, {"id": "C", "name": "Gamma"}]});
577
578 let result = engine.merge(base, current, proposed);
579 assert!(result.is_merged());
580 let merged = result.unwrap_merged();
581 let chars = merged["characters"].as_array().unwrap();
582 assert_eq!(chars.len(), 3);
583 assert_eq!(chars[0]["id"], json!("A"));
584 assert_eq!(chars[1]["id"], json!("B"));
585 assert_eq!(chars[2]["id"], json!("C"));
586 }
587
588 #[test]
589 fn test_merge_ordered_unique_primitives() {
590 let engine = MergeEngine::new(simple_sdl());
591
592 let base = json!({"tags": ["a", "b"]});
593 let current = json!({"tags": ["a", "b", "c"]});
594 let proposed = json!({"tags": ["a", "b", "d"]});
595
596 let result = engine.merge(base, current, proposed);
597 assert!(result.is_merged());
598 let merged = result.unwrap_merged();
599 assert_eq!(
600 merged["tags"].as_array().unwrap(),
601 &[json!("a"), json!("b"), json!("c"), json!("d")]
602 );
603 }
604
605 #[test]
606 fn test_merge_atomic_conflict() {
607 let engine = MergeEngine::new(simple_sdl());
608
609 let base = json!({"version": 1});
610 let current = json!({"version": 2});
611 let proposed = json!({"version": 3});
612
613 let result = engine.merge(base, current, proposed);
614 assert!(result.is_conflict());
615 }
616
617 #[test]
618 fn test_merge_edge_list() {
619 let engine = MergeEngine::new(simple_sdl());
620
621 let base = json!({"edges": [{"id": "e1", "source": "a", "target": "b"}]});
622 let current = json!({
623 "edges": [
624 {"id": "e1", "source": "a", "target": "b"},
625 {"id": "e2", "source": "b", "target": "c"}
626 ]
627 });
628 let proposed = json!({
629 "edges": [
630 {"id": "e1", "source": "a", "target": "b"},
631 {"id": "e3", "source": "c", "target": "a"}
632 ]
633 });
634
635 let result = engine.merge(base, current, proposed);
636 assert!(result.is_merged());
637 let merged = result.unwrap_merged();
638 assert_eq!(merged["edges"].as_array().unwrap().len(), 3);
639 }
640
641 #[test]
642 fn test_merge_identity_mutation_conflict() {
643 let engine = MergeEngine::new(simple_sdl());
644
645 let base = json!({"characters": [{"id": "obiwan", "name": "Obi-Wan"}]});
646 let current = json!({"characters": [{"id": "ben_kenobi", "name": "Obi-Wan"}]}); let proposed = json!({"characters": [{"id": "obiwan", "name": "Obi-Wan"}]});
648
649 let result = engine.merge(base, current, proposed);
650 assert!(result.is_conflict());
651 }
652
653 #[test]
654 fn test_merge_deterministic() {
655 let engine = MergeEngine::new(simple_sdl());
656
657 let base = json!({"name": "Luke", "version": 1, "tags": ["a"]});
658 let current = json!({"name": "Luke Skywalker", "version": 2, "tags": ["a", "b"]});
659 let proposed = json!({"name": "Luke", "version": 1, "tags": ["a", "c"]});
660
661 let result1 = engine.merge(base.clone(), current.clone(), proposed.clone());
662 let result2 = engine.merge(base, current, proposed);
663
664 match (result1, result2) {
666 (MergeResult::Merged(a), MergeResult::Merged(b)) => assert_eq!(a, b),
667 (MergeResult::Conflicts(a), MergeResult::Conflicts(b)) => {
668 assert_eq!(a.len(), b.len());
669 for (ca, cb) in a.iter().zip(b.iter()) {
670 assert_eq!(ca.path, cb.path);
671 }
672 }
673 _ => panic!("results should be same variant"),
674 }
675 }
676
677 #[test]
678 fn test_merge_empty_documents() {
679 let engine = MergeEngine::new(simple_sdl());
680
681 let base = json!({});
682 let current = json!({});
683 let proposed = json!({});
684
685 let result = engine.merge(base, current, proposed);
686 assert!(result.is_merged());
687 assert_eq!(result.unwrap_merged(), json!({}));
688 }
689
690 #[test]
691 fn test_merge_additions_from_both_sides() {
692 let engine = MergeEngine::new(simple_sdl());
693
694 let base = json!({});
695 let current = json!({"name": "From Current"});
696 let proposed = json!({"version": 42});
697
698 let result = engine.merge(base, current, proposed);
699 assert!(result.is_merged());
700 let merged = result.unwrap_merged();
701 assert_eq!(merged.get("name"), Some(&json!("From Current")));
702 assert_eq!(merged.get("version"), Some(&json!(42)));
703 }
704
705 #[test]
706 fn test_merge_both_add_same_field_conflict() {
707 let engine = MergeEngine::new(simple_sdl());
708
709 let base = json!({});
710 let current = json!({"name": "From Current"});
711 let proposed = json!({"name": "From Proposed"});
712
713 let result = engine.merge(base, current, proposed);
714 assert!(result.is_conflict());
716 }
717
718 #[test]
719 fn test_merge_ordered_unique_modification_accepted() {
720 let engine = MergeEngine::new(simple_sdl());
721
722 let base = json!({"characters": [{"id": "A", "val": 1}]});
723 let current = json!({"characters": [{"id": "A", "val": 2}]}); let proposed = json!({"characters": [{"id": "A", "val": 1}]}); let result = engine.merge(base, current, proposed);
727 assert!(result.is_merged());
728 let merged = result.unwrap_merged();
729 assert_eq!(merged["characters"][0]["val"], json!(2));
730 }
731
732 #[test]
736 fn test_deterministic_random_iterations() {
737 use rand::Rng;
738 let engine = MergeEngine::new(simple_sdl());
739 let mut rng = rand::rng();
740
741 for _ in 0..50 {
742 let mut base = serde_json::Map::new();
744 let mut current = serde_json::Map::new();
745 let mut proposed = serde_json::Map::new();
746
747 let field_count = rng.random_range(0..6);
749 for i in 0..field_count {
750 let key = format!("field_{i}");
751 let base_val = rng.random_range(0..100);
752 let cur_val = if rng.random_bool(0.5) {
753 base_val + rng.random_range(-5..=5)
754 } else {
755 base_val
756 };
757 let prop_val = if rng.random_bool(0.5) {
758 base_val + rng.random_range(-5..=5)
759 } else {
760 base_val
761 };
762 base.insert(key.clone(), json!(base_val));
763 current.insert(key.clone(), json!(cur_val));
764 proposed.insert(key.clone(), json!(prop_val));
765 }
766
767 let tag_count = rng.random_range(0..4);
769 let mut tags: Vec<Value> = (0..tag_count).map(|i| json!(format!("tag_{i}"))).collect();
770 if rng.random_bool(0.3) {
771 tags.push(json!("tag_0")); }
773 if !tags.is_empty() {
774 base.insert("tags".to_string(), Value::Array(tags.clone()));
775 if rng.random_bool(0.5) {
776 tags.push(json!("tag_new"));
777 }
778 current.insert("tags".to_string(), Value::Array(tags.clone()));
779 if rng.random_bool(0.5) {
780 tags.push(json!("tag_extra"));
781 }
782 proposed.insert("tags".to_string(), Value::Array(tags));
783 }
784
785 let base_val = Value::Object(base);
786 let current_val = Value::Object(current);
787 let proposed_val = Value::Object(proposed);
788
789 let result1 = engine.merge(base_val.clone(), current_val.clone(), proposed_val.clone());
791 let result2 = engine.merge(base_val, current_val, proposed_val);
792
793 match (&result1, &result2) {
795 (MergeResult::Merged(a), MergeResult::Merged(b)) => {
796 assert_eq!(a, b, "deterministic merge failed");
797 }
798 (MergeResult::Conflicts(a), MergeResult::Conflicts(b)) => {
799 assert_eq!(a.len(), b.len(), "different conflict counts");
800 for (ca, cb) in a.iter().zip(b.iter()) {
801 assert_eq!(ca.path, cb.path, "different conflict paths");
802 }
803 }
804 _ => panic!("merge results differ in type"),
805 }
806 }
807 }
808}