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}