Skip to main content

link_cli/
link_storage.rs

1//! LinkStorage - Persistent storage for links
2//!
3//! This module provides the LinkStorage struct for managing link persistence.
4
5use anyhow::{Context, Result};
6use std::collections::{HashMap, HashSet};
7use std::fs::{File, OpenOptions};
8use std::io::{BufRead, BufReader, BufWriter, Write};
9use std::path::Path;
10
11use crate::error::LinkError;
12use crate::link::Link;
13
14/// LinkStorage provides persistent storage for links
15/// Corresponds to the storage functionality in NamedLinksDecorator in C#
16pub struct LinkStorage {
17    links: HashMap<u32, Link>,
18    names: HashMap<u32, String>,
19    name_to_id: HashMap<String, u32>,
20    next_id: u32,
21    db_path: String,
22    trace: bool,
23}
24
25impl LinkStorage {
26    /// Creates a new LinkStorage instance
27    pub fn new(db_path: &str, trace: bool) -> Result<Self> {
28        let mut storage = Self {
29            links: HashMap::new(),
30            names: HashMap::new(),
31            name_to_id: HashMap::new(),
32            next_id: 1,
33            db_path: db_path.to_string(),
34            trace,
35        };
36
37        // Load existing database if it exists
38        if Path::new(db_path).exists() {
39            storage.load()?;
40        }
41
42        Ok(storage)
43    }
44
45    /// Loads links from the database file
46    fn load(&mut self) -> Result<()> {
47        let file = File::open(&self.db_path)
48            .with_context(|| format!("Failed to open database: {}", self.db_path))?;
49
50        let reader = BufReader::new(file);
51
52        for line in reader.lines() {
53            let line = line?;
54            let line = line.trim();
55
56            if line.is_empty() || line.starts_with('#') {
57                continue;
58            }
59
60            // Parse link format: (index source target) or (index source target "name")
61            if let Some((link, name)) = self.parse_link_line(line) {
62                self.links.insert(link.index, link);
63                if link.index >= self.next_id {
64                    self.next_id = link.index + 1;
65                }
66                if let Some(name) = name {
67                    self.names.insert(link.index, name.clone());
68                    self.name_to_id.insert(name, link.index);
69                }
70            }
71        }
72
73        if self.trace {
74            eprintln!(
75                "[TRACE] Loaded {} links from {}",
76                self.links.len(),
77                self.db_path
78            );
79        }
80
81        Ok(())
82    }
83
84    /// Parses a single link line from the database
85    fn parse_link_line(&self, line: &str) -> Option<(Link, Option<String>)> {
86        // Simple format: (index source target) or (index source target "name")
87        let line = line.trim_matches(|c| c == '(' || c == ')');
88        let parts: Vec<&str> = line.split_whitespace().collect();
89
90        if parts.len() >= 3 {
91            let index = parts[0].parse().ok()?;
92            let source = parts[1].parse().ok()?;
93            let target = parts[2].parse().ok()?;
94            let name = if parts.len() > 3 {
95                Some(parts[3].trim_matches('"').to_string())
96            } else {
97                None
98            };
99            return Some((Link::new(index, source, target), name));
100        }
101
102        None
103    }
104
105    /// Saves all links to the database file
106    pub fn save(&self) -> Result<()> {
107        let file = OpenOptions::new()
108            .write(true)
109            .create(true)
110            .truncate(true)
111            .open(&self.db_path)
112            .with_context(|| format!("Failed to create database: {}", self.db_path))?;
113
114        let mut writer = BufWriter::new(file);
115
116        // Sort by index for consistent output
117        let mut links: Vec<_> = self.links.values().collect();
118        links.sort_by_key(|l| l.index);
119
120        for link in links {
121            if let Some(name) = self.names.get(&link.index) {
122                writeln!(
123                    writer,
124                    "({} {} {} \"{}\")",
125                    link.index, link.source, link.target, name
126                )?;
127            } else {
128                writeln!(writer, "({} {} {})", link.index, link.source, link.target)?;
129            }
130        }
131
132        writer.flush()?;
133
134        if self.trace {
135            eprintln!(
136                "[TRACE] Saved {} links to {}",
137                self.links.len(),
138                self.db_path
139            );
140        }
141
142        Ok(())
143    }
144
145    /// Creates a new link and returns its ID
146    pub fn create(&mut self, source: u32, target: u32) -> u32 {
147        let id = self.next_id;
148        self.next_id += 1;
149
150        let link = Link::new(id, source, target);
151        self.links.insert(id, link);
152
153        if self.trace {
154            eprintln!("[TRACE] Created link: ({} {} {})", id, source, target);
155        }
156
157        id
158    }
159
160    /// Creates a link with a specific ID, ensuring all links up to that ID exist
161    pub fn ensure_created(&mut self, id: u32) -> u32 {
162        if self.links.contains_key(&id) {
163            return id;
164        }
165
166        if self.next_id > id {
167            let link = Link::new(id, 0, 0);
168            self.links.insert(id, link);
169            if self.trace {
170                eprintln!("[TRACE] Ensured link: ({} 0 0)", id);
171            }
172            return id;
173        }
174
175        // Create placeholder links up to the requested ID
176        while self.next_id <= id {
177            let placeholder_id = self.next_id;
178            self.next_id += 1;
179            if placeholder_id == id {
180                let link = Link::new(id, 0, 0);
181                self.links.insert(id, link);
182                if self.trace {
183                    eprintln!("[TRACE] Ensured link: ({} 0 0)", id);
184                }
185                return id;
186            }
187        }
188
189        id
190    }
191
192    /// Gets a link by ID
193    pub fn get(&self, id: u32) -> Option<&Link> {
194        self.links.get(&id)
195    }
196
197    /// Checks if a link exists
198    pub fn exists(&self, id: u32) -> bool {
199        self.links.contains_key(&id)
200    }
201
202    /// Updates a link's source and target
203    pub fn update(&mut self, id: u32, source: u32, target: u32) -> Result<Link> {
204        if let Some(link) = self.links.get_mut(&id) {
205            let before = *link;
206            if self.trace {
207                eprintln!(
208                    "[TRACE] Updating link {} from ({} {}) to ({} {})",
209                    id, link.source, link.target, source, target
210                );
211            }
212            link.source = source;
213            link.target = target;
214            Ok(before)
215        } else {
216            Err(LinkError::NotFound(id).into())
217        }
218    }
219
220    /// Deletes a link by ID
221    pub fn delete(&mut self, id: u32) -> Result<Link> {
222        // Also remove the name mapping
223        if let Some(name) = self.names.remove(&id) {
224            self.name_to_id.remove(&name);
225        }
226
227        if let Some(link) = self.links.remove(&id) {
228            if self.trace {
229                eprintln!(
230                    "[TRACE] Deleted link: ({} {} {})",
231                    link.index, link.source, link.target
232                );
233            }
234            Ok(link)
235        } else {
236            Err(LinkError::NotFound(id).into())
237        }
238    }
239
240    /// Returns all links
241    pub fn all(&self) -> Vec<&Link> {
242        self.links.values().collect()
243    }
244
245    /// Returns all links matching a query pattern
246    pub fn query(
247        &self,
248        index: Option<u32>,
249        source: Option<u32>,
250        target: Option<u32>,
251    ) -> Vec<&Link> {
252        self.links
253            .values()
254            .filter(|link| {
255                (index.is_none() || index == Some(link.index))
256                    && (source.is_none() || source == Some(link.source))
257                    && (target.is_none() || target == Some(link.target))
258            })
259            .collect()
260    }
261
262    /// Searches for a link with the given source and target
263    pub fn search(&self, source: u32, target: u32) -> Option<u32> {
264        for link in self.links.values() {
265            if link.source == source && link.target == target {
266                return Some(link.index);
267            }
268        }
269        None
270    }
271
272    /// Gets or creates a link with the given source and target
273    pub fn get_or_create(&mut self, source: u32, target: u32) -> u32 {
274        if let Some(id) = self.search(source, target) {
275            id
276        } else {
277            self.create(source, target)
278        }
279    }
280
281    /// Formats a link for display
282    pub fn format(&self, link: &Link) -> String {
283        // Use name if available
284        let index_str = self
285            .names
286            .get(&link.index)
287            .cloned()
288            .unwrap_or_else(|| link.index.to_string());
289        let source_str = self
290            .names
291            .get(&link.source)
292            .cloned()
293            .unwrap_or_else(|| link.source.to_string());
294        let target_str = self
295            .names
296            .get(&link.target)
297            .cloned()
298            .unwrap_or_else(|| link.target.to_string());
299        format!("({} {} {})", index_str, source_str, target_str)
300    }
301
302    /// Formats a link as LiNo suitable for database export.
303    pub fn format_lino(&self, link: &Link) -> String {
304        format!(
305            "({}: {} {})",
306            self.format_lino_reference(link.index),
307            self.format_lino_reference(link.source),
308            self.format_lino_reference(link.target)
309        )
310    }
311
312    /// Returns all database links as sorted LiNo lines.
313    pub fn lino_lines(&self) -> Vec<String> {
314        let mut links: Vec<_> = self.all();
315        links.sort_by_key(|l| l.index);
316        links
317            .into_iter()
318            .map(|link| self.format_lino(link))
319            .collect()
320    }
321
322    /// Writes the complete database as LiNo.
323    pub fn write_lino_output<P: AsRef<Path>>(&self, path: P) -> Result<()> {
324        let path = path.as_ref();
325        let file = OpenOptions::new()
326            .write(true)
327            .create(true)
328            .truncate(true)
329            .open(path)
330            .with_context(|| format!("Failed to create LiNo output: {}", path.display()))?;
331
332        let mut writer = BufWriter::new(file);
333        for line in self.lino_lines() {
334            writeln!(writer, "{line}")?;
335        }
336        writer.flush()?;
337        Ok(())
338    }
339
340    /// Formats the structure of a link
341    pub fn format_structure(&self, id: u32) -> Result<String> {
342        let mut visited = HashSet::new();
343        self.format_structure_recursive(id, &mut visited)
344    }
345
346    /// Recursively formats a link structure
347    fn format_structure_recursive(&self, id: u32, visited: &mut HashSet<u32>) -> Result<String> {
348        let link = self.get(id).ok_or(LinkError::NotFound(id))?;
349        if !visited.insert(id) {
350            return Ok(self.format_lino_reference(id));
351        }
352
353        let source = if self.exists(link.source) && !visited.contains(&link.source) {
354            self.format_structure_recursive(link.source, visited)?
355        } else {
356            self.format_lino_reference(link.source)
357        };
358        let target = self.format_lino_reference(link.target);
359        let index = self.format_lino_reference(link.index);
360        visited.remove(&id);
361
362        Ok(format!("({index}: {source} {target})"))
363    }
364
365    /// Prints all links
366    pub fn print_all_links(&self) {
367        let mut links: Vec<_> = self.all();
368        links.sort_by_key(|l| l.index);
369        for link in links {
370            println!("{}", self.format(link));
371        }
372    }
373
374    /// Prints a change (before -> after)
375    pub fn print_change(&self, before: &Option<Link>, after: &Option<Link>) {
376        let before_text = before.map(|l| self.format(&l)).unwrap_or_default();
377        let after_text = after.map(|l| self.format(&l)).unwrap_or_default();
378        println!("({}) ({})", before_text, after_text);
379    }
380
381    // Named links functionality (corresponds to NamedLinks.cs)
382
383    /// Gets or creates a link with a name
384    pub fn get_or_create_named(&mut self, name: &str) -> u32 {
385        if let Some(&id) = self.name_to_id.get(name) {
386            id
387        } else {
388            // Create a self-referential link for the name
389            let id = self.create(0, 0);
390            self.update(id, id, id).ok();
391            self.names.insert(id, name.to_string());
392            self.name_to_id.insert(name.to_string(), id);
393            if self.trace {
394                eprintln!("[TRACE] Created named link: {} => {}", name, id);
395            }
396            id
397        }
398    }
399
400    /// Sets the name for a link
401    pub fn set_name(&mut self, id: u32, name: &str) {
402        // Remove old name mapping if exists
403        if let Some(old_name) = self.names.remove(&id) {
404            self.name_to_id.remove(&old_name);
405        }
406        self.names.insert(id, name.to_string());
407        self.name_to_id.insert(name.to_string(), id);
408        if self.trace {
409            eprintln!("[TRACE] Set name: {} => {}", id, name);
410        }
411    }
412
413    /// Gets the name of a link
414    pub fn get_name(&self, id: u32) -> Option<&String> {
415        self.names.get(&id)
416    }
417
418    /// Gets a link ID by name
419    pub fn get_by_name(&self, name: &str) -> Option<u32> {
420        self.name_to_id.get(name).copied()
421    }
422
423    /// Removes the name for a link
424    pub fn remove_name(&mut self, id: u32) {
425        if let Some(name) = self.names.remove(&id) {
426            self.name_to_id.remove(&name);
427            if self.trace {
428                eprintln!("[TRACE] Removed name: {} => {}", id, name);
429            }
430        }
431    }
432
433    /// Returns true if trace mode is enabled
434    pub fn is_trace_enabled(&self) -> bool {
435        self.trace
436    }
437
438    fn format_lino_reference(&self, id: u32) -> String {
439        self.names
440            .get(&id)
441            .map(|name| escape_lino_reference(name))
442            .unwrap_or_else(|| id.to_string())
443    }
444}
445
446fn escape_lino_reference(reference: &str) -> String {
447    if reference.is_empty() || reference.trim().is_empty() {
448        return String::new();
449    }
450
451    let has_single_quote = reference.contains('\'');
452    let has_double_quote = reference.contains('"');
453    let needs_quoting = reference.contains(':')
454        || reference.contains('(')
455        || reference.contains(')')
456        || reference.contains(' ')
457        || reference.contains('\t')
458        || reference.contains('\n')
459        || reference.contains('\r')
460        || has_single_quote
461        || has_double_quote;
462
463    if has_single_quote && has_double_quote {
464        return format!("'{}'", reference.replace('\'', "\\'"));
465    }
466
467    if has_double_quote {
468        return format!("'{reference}'");
469    }
470
471    if has_single_quote {
472        return format!("\"{reference}\"");
473    }
474
475    if needs_quoting {
476        return format!("'{reference}'");
477    }
478
479    reference.to_string()
480}