1use std::fmt;
2use std::collections::HashMap;
3use archidoc_types::ModuleDoc;
4
5#[derive(Debug)]
7pub struct MergeError {
8 pub module_path: String,
9 pub message: String,
10}
11
12impl fmt::Display for MergeError {
13 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
14 write!(f, "merge conflict at '{}': {}", self.module_path, self.message)
15 }
16}
17
18pub fn merge_ir(sources: Vec<Vec<ModuleDoc>>) -> Result<Vec<ModuleDoc>, MergeError> {
26 let mut merged: HashMap<String, ModuleDoc> = HashMap::new();
27
28 for source_set in sources {
29 for doc in source_set {
30 let module_path = doc.module_path.clone();
31
32 if let Some(existing) = merged.get(&module_path) {
33 if existing.c4_level != doc.c4_level {
34 return Err(MergeError {
35 module_path: module_path.clone(),
36 message: format!(
37 "conflicting C4 levels: existing '{}' vs new '{}'",
38 existing.c4_level,
39 doc.c4_level
40 ),
41 });
42 }
43
44 eprintln!(
45 "warning: duplicate module '{}' at C4 level '{}', overwriting with later source",
46 module_path,
47 doc.c4_level
48 );
49 }
50
51 merged.insert(module_path, doc);
52 }
53 }
54
55 let mut result: Vec<ModuleDoc> = merged.into_values().collect();
56 result.sort_by(|a, b| a.module_path.cmp(&b.module_path));
57
58 Ok(result)
59}
60
61#[cfg(test)]
62mod tests {
63 use super::*;
64 use archidoc_types::{C4Level, PatternStatus, Relationship};
65
66 fn make_doc(path: &str, level: C4Level) -> ModuleDoc {
67 ModuleDoc {
68 module_path: path.to_string(),
69 content: String::new(),
70 source_file: format!("src/{}/mod.rs", path),
71 c4_level: level,
72 pattern: "--".to_string(),
73 pattern_status: PatternStatus::Planned,
74 description: format!("Module {}", path),
75 parent_container: None,
76 relationships: vec![],
77 files: vec![],
78 }
79 }
80
81 #[test]
82 fn merge_combines_disjoint_sets() {
83 let set1 = vec![
84 make_doc("api", C4Level::Container),
85 make_doc("core", C4Level::Container),
86 ];
87 let set2 = vec![
88 make_doc("database", C4Level::Component),
89 make_doc("ui", C4Level::Component),
90 ];
91
92 let result = merge_ir(vec![set1, set2]).unwrap();
93
94 assert_eq!(result.len(), 4);
95 assert_eq!(result[0].module_path, "api");
96 assert_eq!(result[1].module_path, "core");
97 assert_eq!(result[2].module_path, "database");
98 assert_eq!(result[3].module_path, "ui");
99 }
100
101 #[test]
102 fn merge_deduplicates_same_level() {
103 let set1 = vec![
104 make_doc("api", C4Level::Container),
105 ];
106 let mut set2 = vec![
107 make_doc("api", C4Level::Container),
108 ];
109 set2[0].description = "Updated API module".to_string();
110
111 let result = merge_ir(vec![set1, set2]).unwrap();
112
113 assert_eq!(result.len(), 1);
114 assert_eq!(result[0].module_path, "api");
115 assert_eq!(result[0].description, "Updated API module");
116 }
117
118 #[test]
119 fn merge_rejects_conflicting_c4_levels() {
120 let set1 = vec![
121 make_doc("api", C4Level::Container),
122 ];
123 let set2 = vec![
124 make_doc("api", C4Level::Component),
125 ];
126
127 let result = merge_ir(vec![set1, set2]);
128
129 assert!(result.is_err());
130 let err = result.unwrap_err();
131 assert_eq!(err.module_path, "api");
132 assert!(err.message.contains("conflicting C4 levels"));
133 assert!(err.message.contains("container"));
134 assert!(err.message.contains("component"));
135 }
136
137 #[test]
138 fn merge_sorts_by_module_path() {
139 let set1 = vec![
140 make_doc("zebra", C4Level::Container),
141 make_doc("alpha", C4Level::Container),
142 ];
143 let set2 = vec![
144 make_doc("middle", C4Level::Component),
145 ];
146
147 let result = merge_ir(vec![set1, set2]).unwrap();
148
149 assert_eq!(result.len(), 3);
150 assert_eq!(result[0].module_path, "alpha");
151 assert_eq!(result[1].module_path, "middle");
152 assert_eq!(result[2].module_path, "zebra");
153 }
154
155 #[test]
156 fn merge_empty_inputs_returns_empty() {
157 let result1 = merge_ir(vec![]).unwrap();
158 assert_eq!(result1.len(), 0);
159
160 let result2 = merge_ir(vec![vec![], vec![]]).unwrap();
161 assert_eq!(result2.len(), 0);
162 }
163
164 #[test]
165 fn merge_preserves_relationships() {
166 let mut doc1 = make_doc("api", C4Level::Container);
167 doc1.relationships = vec![
168 Relationship {
169 target: "database".to_string(),
170 label: "Persists data".to_string(),
171 protocol: "sqlx".to_string(),
172 },
173 ];
174
175 let mut doc2 = make_doc("database", C4Level::Component);
176 doc2.relationships = vec![
177 Relationship {
178 target: "storage".to_string(),
179 label: "Writes files".to_string(),
180 protocol: "fs".to_string(),
181 },
182 ];
183
184 let result = merge_ir(vec![vec![doc1], vec![doc2]]).unwrap();
185
186 assert_eq!(result.len(), 2);
187
188 let api_doc = result.iter().find(|d| d.module_path == "api").unwrap();
189 assert_eq!(api_doc.relationships.len(), 1);
190 assert_eq!(api_doc.relationships[0].target, "database");
191
192 let db_doc = result.iter().find(|d| d.module_path == "database").unwrap();
193 assert_eq!(db_doc.relationships.len(), 1);
194 assert_eq!(db_doc.relationships[0].target, "storage");
195 }
196}