1use crate::tree::{Location, NodeType, ReferenceTree, TreeNode};
2use crate::{CodeReference, SearchResult, TranslationEntry};
3
4pub struct ReferenceTreeBuilder;
6
7impl ReferenceTreeBuilder {
8 pub fn build(result: &SearchResult) -> ReferenceTree {
16 let mut root = TreeNode::new(NodeType::Root, result.query.clone());
17
18 for entry in &result.translation_entries {
20 let mut translation_node = Self::build_translation_node(entry);
21 let mut key_node = Self::build_key_node(entry);
22
23 let matching_refs: Vec<_> = result
25 .code_references
26 .iter()
27 .filter(|r| r.key_path == entry.key)
28 .collect();
29
30 for code_ref in matching_refs {
32 let code_node = Self::build_code_node(code_ref);
33 key_node.add_child(code_node);
34 }
35
36 if key_node.has_children() {
38 translation_node.children.push(key_node);
39 }
40
41 root.add_child(translation_node);
42 }
43
44 ReferenceTree::new(root)
45 }
46
47 fn build_translation_node(entry: &TranslationEntry) -> TreeNode {
49 let content = format!("{}: '{}'", entry.key, entry.value);
50 let location = Location::new(entry.file.clone(), entry.line);
51
52 TreeNode::with_location(NodeType::Translation, content, location)
53 }
54
55 fn build_key_node(entry: &TranslationEntry) -> TreeNode {
57 TreeNode::new(NodeType::KeyPath, entry.key.clone())
58 }
59
60 fn build_code_node(code_ref: &CodeReference) -> TreeNode {
62 let location = Location::new(code_ref.file.clone(), code_ref.line);
63 TreeNode::with_location(NodeType::CodeRef, code_ref.context.clone(), location)
64 }
65}
66
67#[cfg(test)]
68mod tests {
69 use super::*;
70 use std::path::PathBuf;
71
72 fn create_test_translation_entry() -> TranslationEntry {
73 TranslationEntry {
74 key: "invoice.labels.add_new".to_string(),
75 value: "add new".to_string(),
76 line: 4,
77 file: PathBuf::from("en.yml"),
78 }
79 }
80
81 fn create_test_code_reference() -> CodeReference {
82 CodeReference {
83 file: PathBuf::from("invoices.ts"),
84 line: 14,
85 pattern: r#"I18n\.t\(['"]([^'"]+)['"]\)"#.to_string(),
86 context: "I18n.t('invoice.labels.add_new')".to_string(),
87 key_path: "invoice.labels.add_new".to_string(),
88 }
89 }
90
91 #[test]
92 fn test_build_translation_node() {
93 let entry = create_test_translation_entry();
94 let node = ReferenceTreeBuilder::build_translation_node(&entry);
95
96 assert_eq!(node.node_type, NodeType::Translation);
97 assert_eq!(node.content, "invoice.labels.add_new: 'add new'");
98 assert!(node.location.is_some());
99 assert_eq!(node.location.unwrap().line, 4);
100 }
101
102 #[test]
103 fn test_build_key_node() {
104 let entry = create_test_translation_entry();
105 let node = ReferenceTreeBuilder::build_key_node(&entry);
106
107 assert_eq!(node.node_type, NodeType::KeyPath);
108 assert_eq!(node.content, "invoice.labels.add_new");
109 assert!(node.location.is_none());
110 }
111
112 #[test]
113 fn test_build_code_node() {
114 let code_ref = create_test_code_reference();
115 let node = ReferenceTreeBuilder::build_code_node(&code_ref);
116
117 assert_eq!(node.node_type, NodeType::CodeRef);
118 assert_eq!(node.content, "I18n.t('invoice.labels.add_new')");
119 assert!(node.location.is_some());
120 assert_eq!(node.location.as_ref().unwrap().line, 14);
121 }
122
123 #[test]
124 fn test_build_tree_single_match() {
125 let result = SearchResult {
126 query: "add new".to_string(),
127 translation_entries: vec![create_test_translation_entry()],
128 code_references: vec![create_test_code_reference()],
129 };
130
131 let tree = ReferenceTreeBuilder::build(&result);
132
133 assert_eq!(tree.root.content, "add new");
134 assert_eq!(tree.root.node_type, NodeType::Root);
135 assert_eq!(tree.root.children.len(), 1);
136
137 let translation = &tree.root.children[0];
139 assert_eq!(translation.node_type, NodeType::Translation);
140 assert!(translation.content.contains("invoice.labels.add_new"));
141 assert_eq!(translation.children.len(), 1);
142
143 let key_path = &translation.children[0];
145 assert_eq!(key_path.node_type, NodeType::KeyPath);
146 assert_eq!(key_path.content, "invoice.labels.add_new");
147 assert_eq!(key_path.children.len(), 1);
148
149 let code_ref = &key_path.children[0];
151 assert_eq!(code_ref.node_type, NodeType::CodeRef);
152 assert!(code_ref.content.contains("I18n.t"));
153 }
154
155 #[test]
156 fn test_build_tree_multiple_code_refs() {
157 let entry = create_test_translation_entry();
158 let code_ref1 = create_test_code_reference();
159 let mut code_ref2 = create_test_code_reference();
160 code_ref2.line = 20;
161 code_ref2.context = "I18n.t('invoice.labels.add_new') // another usage".to_string();
162
163 let result = SearchResult {
164 query: "add new".to_string(),
165 translation_entries: vec![entry],
166 code_references: vec![code_ref1, code_ref2],
167 };
168
169 let tree = ReferenceTreeBuilder::build(&result);
170
171 assert_eq!(tree.root.children.len(), 1);
173
174 let translation = &tree.root.children[0];
176 assert_eq!(translation.children.len(), 1);
177
178 let key_path = &translation.children[0];
180 assert_eq!(key_path.children.len(), 2);
181 }
182
183 #[test]
184 fn test_build_tree_multiple_translations() {
185 let entry1 = create_test_translation_entry();
186 let mut entry2 = create_test_translation_entry();
187 entry2.key = "invoice.labels.edit".to_string();
188 entry2.value = "edit invoice".to_string();
189
190 let code_ref1 = create_test_code_reference();
191 let mut code_ref2 = create_test_code_reference();
192 code_ref2.key_path = "invoice.labels.edit".to_string();
193 code_ref2.context = "I18n.t('invoice.labels.edit')".to_string();
194
195 let result = SearchResult {
196 query: "invoice".to_string(),
197 translation_entries: vec![entry1, entry2],
198 code_references: vec![code_ref1, code_ref2],
199 };
200
201 let tree = ReferenceTreeBuilder::build(&result);
202
203 assert_eq!(tree.root.children.len(), 2);
205
206 for translation in &tree.root.children {
208 assert_eq!(translation.children.len(), 1);
209 assert_eq!(translation.children[0].children.len(), 1);
210 }
211 }
212
213 #[test]
214 fn test_build_tree_no_code_refs() {
215 let result = SearchResult {
216 query: "add new".to_string(),
217 translation_entries: vec![create_test_translation_entry()],
218 code_references: vec![],
219 };
220
221 let tree = ReferenceTreeBuilder::build(&result);
222
223 assert_eq!(tree.root.children.len(), 1);
225
226 let translation = &tree.root.children[0];
228 assert_eq!(translation.children.len(), 0);
229 }
230
231 #[test]
232 fn test_build_tree_empty_result() {
233 let result = SearchResult {
234 query: "nonexistent".to_string(),
235 translation_entries: vec![],
236 code_references: vec![],
237 };
238
239 let tree = ReferenceTreeBuilder::build(&result);
240
241 assert_eq!(tree.root.content, "nonexistent");
242 assert_eq!(tree.root.children.len(), 0);
243 assert!(!tree.has_results());
244 }
245
246 #[test]
247 fn test_build_tree_structure() {
248 let result = SearchResult {
249 query: "add new".to_string(),
250 translation_entries: vec![create_test_translation_entry()],
251 code_references: vec![create_test_code_reference()],
252 };
253
254 let tree = ReferenceTreeBuilder::build(&result);
255
256 assert_eq!(tree.node_count(), 4); assert_eq!(tree.max_depth(), 4);
259 assert!(tree.has_results());
260 }
261
262 #[test]
263 fn test_build_tree_filters_unmatched_code_refs() {
264 let entry = create_test_translation_entry();
265 let code_ref1 = create_test_code_reference();
266 let mut code_ref2 = create_test_code_reference();
267 code_ref2.key_path = "different.key".to_string();
268
269 let result = SearchResult {
270 query: "add new".to_string(),
271 translation_entries: vec![entry],
272 code_references: vec![code_ref1, code_ref2],
273 };
274
275 let tree = ReferenceTreeBuilder::build(&result);
276
277 let key_path = &tree.root.children[0].children[0];
279 assert_eq!(key_path.children.len(), 1);
280 assert!(key_path.children[0]
281 .content
282 .contains("invoice.labels.add_new"));
283 }
284}