Skip to main content

autoschematic_core/
template.rs

1use lazy_static;
2use regex::{Captures, Regex};
3use serde::{Deserialize, Serialize};
4use std::{
5    collections::{HashMap, HashSet},
6    path::{Path, PathBuf},
7};
8
9use crate::{
10    connector::OutputMapFile,
11    ron::path_at::{Component, find_strings},
12};
13
14#[derive(Debug, Serialize, Deserialize, Hash, Clone, Eq, PartialEq)]
15pub struct ReadOutput {
16    pub addr: PathBuf,
17    pub key: String,
18}
19
20impl ReadOutput {
21    pub fn into_string(&self) -> String {
22        format!("out://{}[{}]", self.addr.to_string_lossy(), self.key)
23    }
24}
25
26lazy_static::lazy_static! {
27   pub static ref OUTREF_REGEX: Regex = Regex::new(r#"out://([^\[]+)\[([^\]]+)\]"#).unwrap();
28}
29
30/// For a given resource config definition,
31/// pull out all of the uses of "out://some_file.ron[key]".
32pub fn get_read_outputs(config: &str) -> Vec<ReadOutput> {
33    // This regex captures:
34    // - Group 1: everything after "out://" until the first '[' (the filename)
35    // - Group 2: the content between '[' and ']' (the key)
36    let re = Regex::new(r#"out://([^\[]+)\[([^\]]+)\]"#).unwrap();
37    let mut outputs = Vec::new();
38    for cap in re.captures_iter(config) {
39        let filename = cap.get(1).map(|m| m.as_str()).unwrap_or("");
40        let key = cap.get(2).map(|m| m.as_str()).unwrap_or("");
41        outputs.push(ReadOutput {
42            addr: PathBuf::from(filename),
43            key: key.to_string(),
44        });
45    }
46    outputs
47}
48
49type RonPath = Vec<Component>;
50
51#[derive(Default)]
52pub struct RonOutputReport {
53    pub root: Option<String>,
54    pub reads_outputs: Vec<(RonPath, ReadOutput)>,
55}
56
57/// Like get_read_outputs, but uses the pest parser to produce the path as well.
58pub fn descend_get_read_outputs(config: &str) -> anyhow::Result<RonOutputReport> {
59    let mut res = RonOutputReport::default();
60
61    let reads_strings = find_strings(config)?;
62
63    res.root = reads_strings.root;
64
65    for (path, s) in reads_strings.strings {
66        if let Some(cap) = OUTREF_REGEX.captures(&s) {
67            let filename = cap.get(1).map(|m| m.as_str()).unwrap_or("");
68            let key = cap.get(2).map(|m| m.as_str()).unwrap_or("");
69            res.reads_outputs.push((
70                path,
71                ReadOutput {
72                    addr: PathBuf::from(filename),
73                    key: key.to_string(),
74                },
75            ));
76        }
77    }
78
79    Ok(res)
80}
81
82#[derive(Debug)]
83pub struct TemplateResourceResult {
84    pub body: String,
85    pub found: HashMap<ReadOutput, String>,
86    pub missing: HashSet<ReadOutput>,
87}
88
89/// Template a resource body, filling in items of the form
90///  out://file_path/etc/etc/other.ron[key]
91///  with their actual output values.
92pub fn template_config(prefix: &Path, config: &str) -> anyhow::Result<TemplateResourceResult> {
93    // Regex to capture the file path and key.
94    let re = Regex::new(r#"out://(?<addr>[^\[]+)\[(?<key>[^\]]+)\]"#)?;
95
96    let mut found = HashMap::new();
97    let mut missing = HashSet::<ReadOutput>::new();
98
99    // Replace each occurrence with its fetched value, if available.
100    let output = re.replace_all(config, |caps: &Captures| {
101        let addr = &caps["addr"];
102        let key = &caps["key"];
103        let phy_addr = PathBuf::from(addr);
104        let addr = PathBuf::from(addr);
105
106        let prefix = prefix.to_path_buf();
107
108        // Attempt to read the output using the provided function.
109        // If successful and a value is returned, replace the match.
110        match OutputMapFile::get(&prefix, &phy_addr, key) {
111            Ok(Some(val)) => {
112                found.insert(
113                    ReadOutput {
114                        addr,
115                        key: key.to_string(),
116                    },
117                    val.clone(),
118                );
119                val
120            }
121            _ => {
122                let val = caps.get(0).unwrap().as_str().to_string();
123                missing.insert(ReadOutput {
124                    addr,
125                    key: key.to_string(),
126                });
127                val
128            }
129        }
130    });
131
132    Ok(TemplateResourceResult {
133        body: output.into_owned(),
134        found,
135        missing,
136    })
137}
138
139/// Given a templated config with "out://resource/address\[key\]" lookups, and a raw config with those raw values,
140/// reverse the template operation to produce a version of the raw config with those raw values
141/// substituted by equivalent template lookups.
142/// Only considers templated values of length > `min_length`.
143pub fn reverse_template_config(
144    prefix: &Path,
145    templated_config: &str,
146    raw_config: &str,
147    min_length: usize,
148) -> anyhow::Result<String> {
149    let mut result = raw_config.to_string();
150    let template_result = template_config(prefix, templated_config)?;
151
152    for (key, value) in template_result.found {
153        if value.len() < min_length {
154            continue;
155        }
156
157        result = result.replace(&value, &key.into_string());
158    }
159
160    Ok(result)
161}
162
163const MIN_ANCHOR_LEN: usize = 3;
164
165#[derive(Debug)]
166pub struct Comment {
167    text: String,
168    after: Option<String>,
169    before: Option<String>,
170}
171
172fn code_lines(target: &str) -> Vec<(usize, String)> {
173    let mut out = Vec::new();
174    let mut idx = 0;
175    for line in target.lines() {
176        out.push((idx, line.to_string()));
177        idx += line.len() + 1; // +1 for '\n'
178    }
179    out
180}
181
182pub fn extract_comments(src: &str) -> Vec<Comment> {
183    let re_line = Regex::new(r"(?m)^\s*//.*").unwrap();
184
185    let mut comments = Vec::new();
186
187    let lines: Vec<&str> = src.lines().collect();
188
189    if let Some(first_line) = lines.first()
190        && re_line.is_match(first_line)
191    {
192        comments.push(Comment {
193            text: first_line.to_string(),
194            after: lines.get(1).map(|s| s.to_string()),
195            before: None,
196        });
197    }
198
199    for window in lines.windows(3) {
200        let prev_line = window
201            .first()
202            .filter(|s| s.trim().len() >= MIN_ANCHOR_LEN)
203            .map(|s| s.to_string());
204
205        let line = window.get(1).unwrap();
206
207        let next_line = window
208            .get(2)
209            .filter(|s| s.trim().len() >= MIN_ANCHOR_LEN)
210            .map(|s| s.to_string());
211
212        if re_line.is_match(line) {
213            comments.push(Comment {
214                text: line.to_string(),
215                after: next_line,
216                before: prev_line,
217            });
218        }
219    }
220
221    if let Some(last_line) = lines.last()
222        && re_line.is_match(last_line)
223    {
224        comments.push(Comment {
225            text: last_line.to_string(),
226            after: None,
227            before: lines.get(lines.len() - 2).map(|s| s.to_string()),
228        });
229    }
230    comments
231}
232
233use strsim::levenshtein;
234
235pub fn apply_comments(mut target: String, comments: Vec<Comment>) -> String {
236    let mut leftover_comments = Vec::new();
237
238    for c in comments {
239        // Exact before match
240        if let Some(ref anchor) = c.before
241            && let Some(pos) = target.find(anchor)
242        {
243            insert_after_line(&mut target, pos, &c.text);
244            continue;
245        }
246
247        // Exact after match
248        if let Some(ref anchor) = c.after
249            && let Some(pos) = target.find(anchor)
250        {
251            insert_before(&mut target, pos, &c.text);
252            continue;
253        }
254
255        // Fuzzy before/after match
256        if let Some(pos) = fuzzy_find(&code_lines(&target), &c.after, &c.before) {
257            insert_before(&mut target, pos, &c.text);
258            continue;
259        }
260
261        leftover_comments.push(c);
262
263        // // ---------- 4) orphan bucket ----------
264        // target.push_str("\n");
265        // target.push_str(&c.text);
266    }
267
268    for c in leftover_comments {
269        // Exact before match
270        if let Some(ref anchor) = c.before
271            && let Some(pos) = target.find(anchor)
272        {
273            insert_after_line(&mut target, pos, &c.text);
274            continue;
275        }
276
277        // Exact after match
278        if let Some(ref anchor) = c.after
279            && let Some(pos) = target.find(anchor)
280        {
281            insert_before(&mut target, pos, &c.text);
282            continue;
283        }
284
285        // Fuzzy before/after match
286        if let Some(pos) = fuzzy_find(&code_lines(&target), &c.after, &c.before) {
287            insert_before(&mut target, pos, &c.text);
288            continue;
289        }
290
291        // leftover_comments.push(c);
292
293        // // // ---------- 4) orphan bucket ----------
294        // // target.push_str("\n");
295        // // target.push_str(&c.text);
296    }
297    target
298}
299
300const MAX_FUZZY_DIST: usize = 6;
301fn fuzzy_find(lines: &[(usize, String)], after: &Option<String>, before: &Option<String>) -> Option<usize> {
302    // let candidates = before.iter().chain(after.iter());
303
304    for dist in 0..=MAX_FUZZY_DIST {
305        // for anchor in candidates.clone() {
306        for window in lines.windows(3) {
307            match (before, after) {
308                (None, None) => {
309                    return None;
310                }
311                (None, Some(after)) => {
312                    let (start, _) = window.get(1).unwrap();
313                    let (_, next_line) = window.get(2).unwrap();
314
315                    if levenshtein(after.trim(), next_line.trim()) <= dist {
316                        return Some(*start);
317                    }
318                }
319                (Some(before), None) => {
320                    let (start, _) = window.get(1).unwrap();
321                    let (_, prev_line) = window.first().unwrap();
322
323                    if levenshtein(before.trim(), prev_line.trim()) <= dist {
324                        return Some(*start);
325                    }
326                }
327                (Some(before), Some(after)) => {
328                    let (_, next_line) = window.get(2).unwrap();
329                    let (start, _) = window.get(1).unwrap();
330                    let (_, prev_line) = window.first().unwrap();
331
332                    if levenshtein(after.trim(), next_line.trim()) <= dist
333                        && levenshtein(before.trim(), prev_line.trim()) <= dist
334                    {
335                        return Some(*start);
336                    }
337                }
338            }
339        }
340        // }
341    }
342    None
343}
344
345/// Insert comment *before* the anchor.
346fn insert_before(buf: &mut String, pos: usize, comment: &str) {
347    buf.insert_str(pos, &format!("{comment}\n"));
348}
349
350/// Insert comment *after* the entire anchor line.
351fn insert_after_line(buf: &mut String, pos: usize, comment: &str) {
352    if let Some(newline_off) = buf[pos..].find('\n') {
353        let insert_at = pos + newline_off + 1;
354        buf.insert_str(insert_at, &format!("{comment}\n"));
355    } else {
356        // anchor was last line – fallback to before
357        insert_before(buf, pos, comment);
358    }
359}