Skip to main content

link_cli/
lino_database_input.rs

1//! LiNo database import helpers.
2
3use anyhow::{bail, Context, Result};
4use std::fs;
5use std::path::Path;
6
7use crate::lino_link::LinoLink;
8use crate::named_type_links::NamedTypeLinks;
9use crate::parser::Parser;
10
11pub fn import_lino_file<T, P>(storage: &mut T, path: P) -> Result<()>
12where
13    T: NamedTypeLinks,
14    P: AsRef<Path>,
15{
16    let path = path.as_ref();
17    let text = fs::read_to_string(path)
18        .with_context(|| format!("Failed to read LiNo input: {}", path.display()))?;
19    import_lino_text(storage, &text)?;
20    storage.save()?;
21    Ok(())
22}
23
24pub fn import_lino_text<T>(storage: &mut T, links_notation: &str) -> Result<()>
25where
26    T: NamedTypeLinks,
27{
28    let parser = Parser::new();
29    let normalized_links_notation = normalize_links_notation(links_notation);
30    let links = parser.parse(&normalized_links_notation)?;
31
32    for link in links.iter().filter(|link| !link.is_empty()) {
33        import_link(storage, link, false)?;
34    }
35
36    Ok(())
37}
38
39fn import_link<T>(storage: &mut T, link: &LinoLink, allow_anonymous_index: bool) -> Result<u32>
40where
41    T: NamedTypeLinks,
42{
43    let values = link
44        .values
45        .as_ref()
46        .filter(|values| values.len() == 2)
47        .ok_or_else(|| anyhow::anyhow!("LiNo import supports links with exactly two values"))?;
48
49    let source = resolve_reference(storage, &values[0])?;
50    let target = resolve_reference(storage, &values[1])?;
51    let identifier = normalize_identifier(link.id.as_deref());
52
53    if identifier.is_empty() {
54        if !allow_anonymous_index {
55            bail!("Top-level LiNo import links must have an index or name");
56        }
57
58        return Ok(storage.get_or_create(source, target));
59    }
60
61    let index = resolve_index(storage, &identifier)?;
62    update_link(storage, index, source, target)?;
63    Ok(index)
64}
65
66fn resolve_reference<T>(storage: &mut T, reference: &LinoLink) -> Result<u32>
67where
68    T: NamedTypeLinks,
69{
70    if reference.values_count() == 2 {
71        return import_link(storage, reference, true);
72    }
73
74    if reference.values_count() > 0 {
75        bail!(
76            "LiNo import references must be scalar values or nested links with exactly two values"
77        );
78    }
79
80    let identifier = normalize_identifier(reference.id.as_deref());
81    if identifier.is_empty() {
82        bail!("LiNo import references must have a value");
83    }
84
85    if let Some(id) = parse_reference_number(&identifier, true) {
86        return Ok(id);
87    }
88
89    storage.get_or_create_named(&identifier)
90}
91
92fn resolve_index<T>(storage: &mut T, identifier: &str) -> Result<u32>
93where
94    T: NamedTypeLinks,
95{
96    if let Some(id) = parse_reference_number(identifier, false) {
97        if !storage.exists(id) {
98            storage.ensure_created(id);
99        }
100
101        return Ok(id);
102    }
103
104    storage.get_or_create_named(identifier)
105}
106
107fn update_link<T>(storage: &mut T, index: u32, source: u32, target: u32) -> Result<()>
108where
109    T: NamedTypeLinks,
110{
111    if !storage.exists(index) {
112        storage.ensure_created(index);
113    }
114
115    if let Some(current) = storage.get_link(index) {
116        if current.source == source && current.target == target {
117            return Ok(());
118        }
119    }
120
121    storage.update(index, source, target)?;
122    Ok(())
123}
124
125fn parse_reference_number(identifier: &str, allow_zero: bool) -> Option<u32> {
126    let id = identifier.parse::<u32>().ok()?;
127    if id == 0 && !allow_zero {
128        return None;
129    }
130
131    Some(id)
132}
133
134fn normalize_identifier(identifier: Option<&str>) -> String {
135    let normalized = identifier
136        .unwrap_or_default()
137        .trim()
138        .trim_end_matches(':')
139        .to_string();
140
141    if normalized.len() >= 2 && normalized.starts_with('\'') && normalized.ends_with('\'') {
142        return normalized[1..normalized.len() - 1].replace("\\'", "'");
143    }
144
145    if normalized.len() >= 2 && normalized.starts_with('"') && normalized.ends_with('"') {
146        return normalized[1..normalized.len() - 1].replace("\\\"", "\"");
147    }
148
149    normalized
150}
151
152fn normalize_links_notation(links_notation: &str) -> String {
153    links_notation
154        .lines()
155        .map(str::trim)
156        .filter(|line| !line.is_empty())
157        .collect::<Vec<_>>()
158        .join("\n")
159}