cs/tree/builder.rs
1//! # Ownership and Borrowing - Rust Book Chapter 4
2//!
3//! This module demonstrates ownership, borrowing, and references from
4//! [The Rust Book Chapter 4](https://doc.rust-lang.org/book/ch04-00-understanding-ownership.html).
5//!
6//! ## Key Concepts Demonstrated
7//!
8//! 1. **Borrowing with `&` References** (Chapter 4.2)
9//! - The `build()` method takes `&SearchResult` instead of `SearchResult`
10//! - Allows reading data without taking ownership
11//! - Multiple borrows can exist simultaneously for reading
12//!
13//! 2. **Ownership Transfer with `.clone()`** (Chapter 4.1)
14//! - `result.query.clone()` creates a new owned `String`
15//! - Necessary when building nodes that need to own their data
16//! - Trade-off: memory cost vs. ownership flexibility
17//!
18//! 3. **Iterators and Borrowing** (Chapter 4.2 + 13.2)
19//! - `.iter()` creates an iterator of references (`&T`)
20//! - Allows processing collections without taking ownership
21//! - The original collection remains usable after iteration
22//!
23//! 4. **References in Collections** (Chapter 4.2)
24//! - `Vec<_>` with `.iter()` creates `Vec<&T>`
25//! - Temporary collections of references avoid cloning
26//! - References must not outlive the data they point to
27//!
28//! ## Learning Notes
29//!
30//! **Why use `&SearchResult` instead of `SearchResult`?**
31//! - Caller retains ownership and can reuse the data
32//! - More efficient - no need to clone the entire structure
33//! - Follows Rust convention: borrow when you don't need ownership
34//!
35//! **When to clone vs. when to borrow?**
36//! - Clone: When you need to store data in a new structure (like TreeNode)
37//! - Borrow: When you only need to read data temporarily
38//!
39//! **The `.iter()` pattern:**
40//! ```rust,ignore
41//! for entry in &result.translation_entries { // Borrows each element
42//! // entry is &TranslationEntry
43//! }
44//! // vs
45//! for entry in result.translation_entries { // Takes ownership
46//! // entry is TranslationEntry (moved)
47//! }
48//! ```
49
50use crate::tree::{Location, NodeType, ReferenceTree, TreeNode};
51use crate::{CodeReference, SearchResult, TranslationEntry};
52
53/// Builder for constructing reference trees from search results.
54///
55/// # Rust Book Reference
56///
57/// **Chapter 4: Understanding Ownership**
58/// https://doc.rust-lang.org/book/ch04-00-understanding-ownership.html
59///
60/// This builder demonstrates how to work with borrowed data to construct
61/// owned data structures efficiently.
62pub struct ReferenceTreeBuilder;
63
64impl ReferenceTreeBuilder {
65 /// Build a reference tree from search results.
66 ///
67 /// Creates a hierarchical tree structure:
68 /// - Root: search query text
69 /// - Translation: translation file entry
70 /// - KeyPath: full translation key
71 /// - CodeRef: code reference using the key
72 ///
73 /// # Rust Book Reference
74 ///
75 /// **Chapter 4.2: References and Borrowing**
76 /// https://doc.rust-lang.org/book/ch04-02-references-and-borrowing.html
77 ///
78 /// # Educational Notes - Borrowing with `&SearchResult`
79 ///
80 /// This method signature demonstrates **immutable borrowing**:
81 ///
82 /// ```rust,ignore
83 /// pub fn build(result: &SearchResult) -> ReferenceTree
84 /// // ^
85 /// // Borrows result, doesn't take ownership
86 /// ```
87 ///
88 /// **Why borrow instead of taking ownership?**
89 ///
90 /// 1. **Caller keeps ownership:**
91 /// ```rust,ignore
92 /// let result = search_translations("add new")?;
93 /// let tree = ReferenceTreeBuilder::build(&result); // Borrow
94 /// // result is still usable here!
95 /// println!("Found {} entries", result.translation_entries.len());
96 /// ```
97 ///
98 /// 2. **No unnecessary cloning:**
99 /// - If we took ownership: `build(result: SearchResult)`
100 /// - Caller would need: `build(result.clone())` - expensive!
101 /// - With borrowing: `build(&result)` - zero cost!
102 ///
103 /// 3. **Rust's borrowing rules ensure safety:**
104 /// - The reference `&result` is valid for the entire function
105 /// - We can read all fields: `result.query`, `result.translation_entries`
106 /// - We cannot modify the data (immutable borrow)
107 /// - The original data cannot be moved while borrowed
108 ///
109 /// **Inside the function:**
110 /// - We borrow fields: `&result.translation_entries`
111 /// - We clone when we need ownership: `result.query.clone()`
112 /// - We iterate with `.iter()` to borrow elements: `for entry in &result.translation_entries`
113 ///
114 /// **Key Insight:** Borrowing is Rust's way of saying "I just need to look at
115 /// this data temporarily, I don't need to own it."
116 pub fn build(result: &SearchResult) -> ReferenceTree {
117 // OWNERSHIP: Clone the query string because TreeNode needs to own its content
118 // Chapter 4.1: The query is a String, which doesn't implement Copy
119 // We must clone to create a new owned value for the TreeNode
120 let mut root = TreeNode::new(NodeType::Root, result.query.clone());
121 let mut used_code_refs = std::collections::HashSet::new();
122
123 // BORROWING: Iterate over references to avoid moving the vector
124 // Chapter 4.2: `&result.translation_entries` borrows the Vec
125 // `for entry in &vec` is shorthand for `for entry in vec.iter()`
126 // Each `entry` is a `&TranslationEntry` (reference)
127 for entry in &result.translation_entries {
128 let mut translation_node = Self::build_translation_node(entry);
129 let mut key_node = Self::build_key_node(entry);
130
131 // ITERATORS AND BORROWING: Build a collection of references
132 // Chapter 13.2: `.iter()` creates an iterator of references
133 // `.enumerate()` adds indices: (usize, &CodeReference)
134 // `.filter()` borrows each element to check the condition
135 // Result: Vec<(usize, &CodeReference)> - no cloning needed!
136 let matching_refs: Vec<_> = result
137 .code_references
138 .iter()
139 .enumerate()
140 .filter(|(_, r)| r.key_path == entry.key)
141 .collect();
142
143 // Add code reference nodes as children of the key node
144 for (idx, code_ref) in matching_refs {
145 let code_node = Self::build_code_node(code_ref);
146 key_node.add_child(code_node);
147 used_code_refs.insert(idx);
148 }
149
150 // Only add the key node if it has code references
151 if key_node.has_children() {
152 translation_node.children.push(key_node);
153 }
154
155 root.add_child(translation_node);
156 }
157
158 // Handle direct matches (code references not associated with any translation entry)
159 let direct_matches: Vec<_> = result
160 .code_references
161 .iter()
162 .enumerate()
163 .filter(|(idx, _)| !used_code_refs.contains(idx))
164 .map(|(_, r)| r)
165 .collect();
166
167 if !direct_matches.is_empty() {
168 // Create a virtual node for direct matches
169 let mut direct_matches_node =
170 TreeNode::new(NodeType::KeyPath, "Direct Matches".to_string());
171
172 for code_ref in direct_matches {
173 let code_node = Self::build_code_node(code_ref);
174 direct_matches_node.add_child(code_node);
175 }
176
177 root.add_child(direct_matches_node);
178 }
179
180 ReferenceTree::new(root)
181 }
182
183 /// Build a translation node from a translation entry.
184 ///
185 /// # Rust Book Reference
186 ///
187 /// **Chapter 4.2: References and Borrowing**
188 /// https://doc.rust-lang.org/book/ch04-02-references-and-borrowing.html
189 ///
190 /// # Educational Notes - Borrowing vs. Cloning
191 ///
192 /// This method takes `&TranslationEntry` (a reference) but returns an owned `TreeNode`.
193 ///
194 /// **The pattern:**
195 /// ```text
196 /// fn build_translation_node(entry: &TranslationEntry) -> TreeNode
197 /// // ^ ^
198 /// // Borrow input Own output
199 /// ```
200 ///
201 /// **Inside the function:**
202 /// - We borrow: `entry.file`, `entry.line` (read-only access)
203 /// - We clone: `entry.key.clone()`, `entry.value.clone()` (need owned data)
204 /// - We move: `location` into the TreeNode (transfer ownership)
205 ///
206 /// **Why this pattern?**
207 /// - Input: Borrow because we don't need to consume the entry
208 /// - Output: Own because TreeNode needs to live independently
209 /// - Clone: Only the strings we need to store, not the entire entry
210 ///
211 /// **Memory perspective:**
212 /// ```text
213 /// entry (borrowed) TreeNode (owned)
214 /// ├─ key: "invoice.add" --clone--> content: "invoice.add"
215 /// ├─ value: "Add New" --clone--> metadata: "Add New"
216 /// ├─ file: "en.yml" --clone--> location.file: "en.yml"
217 /// └─ line: 42 --copy---> location.line: 42
218 /// ```
219 fn build_translation_node(entry: &TranslationEntry) -> TreeNode {
220 // CLONE: PathBuf doesn't implement Copy, so we clone to create owned data
221 // Chapter 4.1: Clone creates a deep copy on the heap
222 let location = Location::new(entry.file.clone(), entry.line);
223
224 // CLONE: String doesn't implement Copy, clone creates new heap allocation
225 let mut node = TreeNode::with_location(NodeType::Translation, entry.key.clone(), location);
226
227 // CLONE: Store owned copy of the translation value
228 node.metadata = Some(entry.value.clone());
229
230 // MOVE: Transfer ownership of node to caller
231 // Chapter 4.1: The node is moved out of this function
232 node
233 }
234
235 /// Build a key path node from a translation entry.
236 ///
237 /// # Educational Notes - Minimal Borrowing
238 ///
239 /// This is the simplest borrowing pattern:
240 /// - Borrow the entry: `&TranslationEntry`
241 /// - Clone only what we need: `entry.key.clone()`
242 /// - Return owned result: `TreeNode`
243 ///
244 /// We could have taken ownership: `fn build_key_node(entry: TranslationEntry)`
245 /// But then the caller would lose access to `entry` after calling this function.
246 fn build_key_node(entry: &TranslationEntry) -> TreeNode {
247 TreeNode::new(NodeType::KeyPath, entry.key.clone())
248 }
249
250 /// Build a code reference node.
251 ///
252 /// # Educational Notes - Borrowing Composite Types
253 ///
254 /// `CodeReference` contains multiple fields:
255 /// - `file: PathBuf` - heap-allocated path
256 /// - `line: usize` - stack-allocated number (Copy type)
257 /// - `context: String` - heap-allocated string
258 /// - `key_path: String` - heap-allocated string
259 ///
260 /// By borrowing `&CodeReference`, we can access all fields without cloning
261 /// the entire struct. We only clone the specific fields we need to store.
262 fn build_code_node(code_ref: &CodeReference) -> TreeNode {
263 let location = Location::new(code_ref.file.clone(), code_ref.line);
264 let mut node =
265 TreeNode::with_location(NodeType::CodeRef, code_ref.context.clone(), location);
266 // Store the key path (or search pattern) in metadata for highlighting
267 node.metadata = Some(code_ref.key_path.clone());
268 node
269 }
270}
271
272#[cfg(test)]
273mod tests {
274 use super::*;
275 use std::path::PathBuf;
276
277 fn create_test_translation_entry() -> TranslationEntry {
278 TranslationEntry {
279 key: "invoice.labels.add_new".to_string(),
280 value: "add new".to_string(),
281 line: 4,
282 file: PathBuf::from("en.yml"),
283 }
284 }
285
286 fn create_test_code_reference() -> CodeReference {
287 CodeReference {
288 file: PathBuf::from("invoices.ts"),
289 line: 14,
290 pattern: r#"I18n\.t\(['"]([^'"]+)['"]\)"#.to_string(),
291 context: "I18n.t('invoice.labels.add_new')".to_string(),
292 key_path: "invoice.labels.add_new".to_string(),
293 context_before: vec![],
294 context_after: vec![],
295 }
296 }
297
298 #[test]
299 fn test_build_translation_node() {
300 let entry = create_test_translation_entry();
301 let node = ReferenceTreeBuilder::build_translation_node(&entry);
302
303 assert_eq!(node.node_type, NodeType::Translation);
304 assert_eq!(node.content, "invoice.labels.add_new");
305 assert_eq!(node.metadata.as_deref(), Some("add new"));
306 assert!(node.location.is_some());
307 assert_eq!(node.location.unwrap().line, 4);
308 }
309
310 #[test]
311 fn test_build_key_node() {
312 let entry = create_test_translation_entry();
313 let node = ReferenceTreeBuilder::build_key_node(&entry);
314
315 assert_eq!(node.node_type, NodeType::KeyPath);
316 assert_eq!(node.content, "invoice.labels.add_new");
317 assert!(node.location.is_none());
318 }
319
320 #[test]
321 fn test_build_code_node() {
322 let code_ref = create_test_code_reference();
323 let node = ReferenceTreeBuilder::build_code_node(&code_ref);
324
325 assert_eq!(node.node_type, NodeType::CodeRef);
326 assert_eq!(node.content, "I18n.t('invoice.labels.add_new')");
327 assert!(node.location.is_some());
328 assert_eq!(node.location.as_ref().unwrap().line, 14);
329 }
330
331 #[test]
332 fn test_build_tree_single_match() {
333 let result = SearchResult {
334 query: "add new".to_string(),
335 translation_entries: vec![create_test_translation_entry()],
336 code_references: vec![create_test_code_reference()],
337 };
338
339 let tree = ReferenceTreeBuilder::build(&result);
340
341 assert_eq!(tree.root.content, "add new");
342 assert_eq!(tree.root.node_type, NodeType::Root);
343 assert_eq!(tree.root.children.len(), 1);
344
345 // Check translation node
346 let translation = &tree.root.children[0];
347 assert_eq!(translation.node_type, NodeType::Translation);
348 assert_eq!(translation.content, "invoice.labels.add_new");
349 assert_eq!(translation.metadata.as_deref(), Some("add new"));
350 assert_eq!(translation.children.len(), 1);
351
352 // Check key path node
353 let key_path = &translation.children[0];
354 assert_eq!(key_path.node_type, NodeType::KeyPath);
355 assert_eq!(key_path.content, "invoice.labels.add_new");
356 assert_eq!(key_path.children.len(), 1);
357
358 // Check code reference node
359 let code_ref = &key_path.children[0];
360 assert_eq!(code_ref.node_type, NodeType::CodeRef);
361 assert!(code_ref.content.contains("I18n.t"));
362 }
363
364 #[test]
365 fn test_build_tree_multiple_code_refs() {
366 let entry = create_test_translation_entry();
367 let code_ref1 = create_test_code_reference();
368 let mut code_ref2 = create_test_code_reference();
369 code_ref2.line = 20;
370 code_ref2.context = "I18n.t('invoice.labels.add_new') // another usage".to_string();
371
372 let result = SearchResult {
373 query: "add new".to_string(),
374 translation_entries: vec![entry],
375 code_references: vec![code_ref1, code_ref2],
376 };
377
378 let tree = ReferenceTreeBuilder::build(&result);
379
380 // Should have one translation node
381 assert_eq!(tree.root.children.len(), 1);
382
383 // Translation should have one key path node
384 let translation = &tree.root.children[0];
385 assert_eq!(translation.children.len(), 1);
386
387 // Key path should have two code reference nodes
388 let key_path = &translation.children[0];
389 assert_eq!(key_path.children.len(), 2);
390 }
391
392 #[test]
393 fn test_build_tree_multiple_translations() {
394 let entry1 = create_test_translation_entry();
395 let mut entry2 = create_test_translation_entry();
396 entry2.key = "invoice.labels.edit".to_string();
397 entry2.value = "edit invoice".to_string();
398
399 let code_ref1 = create_test_code_reference();
400 let mut code_ref2 = create_test_code_reference();
401 code_ref2.key_path = "invoice.labels.edit".to_string();
402 code_ref2.context = "I18n.t('invoice.labels.edit')".to_string();
403
404 let result = SearchResult {
405 query: "invoice".to_string(),
406 translation_entries: vec![entry1, entry2],
407 code_references: vec![code_ref1, code_ref2],
408 };
409
410 let tree = ReferenceTreeBuilder::build(&result);
411
412 // Should have two translation nodes
413 assert_eq!(tree.root.children.len(), 2);
414
415 // Each translation should have one key path with one code ref
416 for translation in &tree.root.children {
417 assert_eq!(translation.children.len(), 1);
418 assert_eq!(translation.children[0].children.len(), 1);
419 }
420 }
421
422 #[test]
423 fn test_build_tree_no_code_refs() {
424 let result = SearchResult {
425 query: "add new".to_string(),
426 translation_entries: vec![create_test_translation_entry()],
427 code_references: vec![],
428 };
429
430 let tree = ReferenceTreeBuilder::build(&result);
431
432 // Should have one translation node
433 assert_eq!(tree.root.children.len(), 1);
434
435 // Translation should have no children (no key path without code refs)
436 let translation = &tree.root.children[0];
437 assert_eq!(translation.children.len(), 0);
438 }
439
440 #[test]
441 fn test_build_tree_empty_result() {
442 let result = SearchResult {
443 query: "nonexistent".to_string(),
444 translation_entries: vec![],
445 code_references: vec![],
446 };
447
448 let tree = ReferenceTreeBuilder::build(&result);
449
450 assert_eq!(tree.root.content, "nonexistent");
451 assert_eq!(tree.root.children.len(), 0);
452 assert!(!tree.has_results());
453 }
454
455 #[test]
456 fn test_build_tree_structure() {
457 let result = SearchResult {
458 query: "add new".to_string(),
459 translation_entries: vec![create_test_translation_entry()],
460 code_references: vec![create_test_code_reference()],
461 };
462
463 let tree = ReferenceTreeBuilder::build(&result);
464
465 // Verify tree structure
466 assert_eq!(tree.node_count(), 4); // root + translation + key + code_ref
467 assert_eq!(tree.max_depth(), 4);
468 assert!(tree.has_results());
469 }
470
471 #[test]
472 fn test_build_tree_filters_unmatched_code_refs() {
473 let entry = create_test_translation_entry();
474 let code_ref1 = create_test_code_reference();
475 let mut code_ref2 = create_test_code_reference();
476 code_ref2.key_path = "different.key".to_string();
477
478 let result = SearchResult {
479 query: "add new".to_string(),
480 translation_entries: vec![entry],
481 code_references: vec![code_ref1, code_ref2],
482 };
483
484 let tree = ReferenceTreeBuilder::build(&result);
485
486 // Should only include the matching code ref
487 let key_path = &tree.root.children[0].children[0];
488 assert_eq!(key_path.children.len(), 1);
489 assert!(key_path.children[0]
490 .content
491 .contains("invoice.labels.add_new"));
492 }
493}