use super::defaults;
pub struct Rename {
pub table: &'static str,
pub from: &'static str,
pub to: &'static str,
pub since: &'static str,
}
pub const RENAMED: &[Rename] = &[];
pub fn renamed<'a>(table: &str, key: &str, rows: &'a [Rename]) -> Option<&'a Rename> {
rows.iter().find(|r| r.table == table && r.from == key)
}
pub fn plan(text: &str, rows: &[Rename]) -> (String, Vec<String>) {
let mut out = String::with_capacity(text.len());
let mut changes = vec![];
let mut table = String::new();
for (i, line) in text.split_inclusive('\n').enumerate() {
let t = line.trim();
if t.starts_with('[') && !t.starts_with("[[") {
table = t.trim_matches(|c| c == '[' || c == ']').trim().to_string();
} else if t.starts_with("[[") {
table = t.trim_matches(|c| c == '[' || c == ']').trim().to_string();
}
let key = t.split_once('=').map(|(k, _)| k.trim()).unwrap_or("");
if table == "spec" && key == "schema" {
let current = format!("schema = {}", defaults::schema());
let old: Option<u32> = t.split_once('=').and_then(|(_, v)| v.split('#').next()?.trim().parse().ok());
if t != current && old.is_some_and(|n| n < defaults::schema()) {
changes.push(format!("line {}: {t} -> {current}", i + 1));
out.push_str(&line.replacen(t, ¤t, 1));
continue;
}
}
if !key.is_empty()
&& let Some(r) = renamed(&table, key, rows)
{
changes.push(format!("line {}: [{}] {} -> {} (renamed in {})", i + 1, r.table, r.from, r.to, r.since));
out.push_str(&line.replacen(r.from, r.to, 1));
continue;
}
out.push_str(line);
}
(out, changes)
}
#[cfg(test)]
mod tests {
use super::*;
const ROWS: &[Rename] = &[Rename { table: "aliases", from: "path", to: "file", since: "0.9" }];
#[test]
fn renames_keys_under_their_table_and_keeps_comments() {
let text = "[spec]\nschema = 1\n# keep me\n[aliases]\npath = \"a.tsv\" # note\n[other]\npath = 1\n";
let (out, changes) = plan(text, ROWS);
assert_eq!(out, "[spec]\nschema = 1\n# keep me\n[aliases]\nfile = \"a.tsv\" # note\n[other]\npath = 1\n");
assert_eq!(changes.len(), 1, "{changes:?}");
}
#[test]
fn raises_an_old_schema_but_never_lowers_a_newer_one() {
assert_eq!(plan("[spec]\nschema = 0\n", RENAMED).0, "[spec]\nschema = 1\n");
assert!(plan("[spec]\nschema = 7\n", RENAMED).1.is_empty());
}
#[test]
fn a_current_file_has_no_changes() {
let text = "[spec]\nschema = 1\n";
assert_eq!(plan(text, RENAMED), (text.to_string(), vec![]));
}
}