use std::fs;
use std::path::{Path, PathBuf};
use super::render_record_assignment;
pub(super) const TOML_MERGE_PROVENANCE_MANIFEST: &str = ".alef-toml-merge-provenance.toml";
pub(super) const TOML_MERGE_PROVENANCE_HEADER: &str = "\
# alef toml-merge provenance record -- COMMIT THIS FILE, do not add it to .gitignore.
#
# Records, per merge target and key path, the array values alef itself generated on
# the most recent `alef generate` run -- the baseline the poly.toml merge's prune step
# diffs against to tell \"alef proposed this and later stopped\" from \"the consumer
# wrote this by hand.\" Without this file committed, a fresh clone has no baseline, so
# a value alef stops generating can never be pruned there -- it accumulates forever.
#
# Nothing here is inferred by comparing bytes -- an entry is only ever a copy of
# alef's own past `generated` output for the given key path, captured before merging
# with consumer content. Do not hand-edit; it is rewritten on every `alef generate`.
";
#[derive(serde::Deserialize)]
pub(super) struct TomlMergeProvenanceEntry {
pub(super) relative_path: String,
pub(super) key_path: String,
pub(super) values: Vec<String>,
}
#[derive(Default, serde::Deserialize)]
pub(super) struct TomlMergeProvenanceFile {
#[serde(default)]
pub(super) entries: Vec<TomlMergeProvenanceEntry>,
}
type TomlMergeProvenance = std::collections::BTreeMap<String, std::collections::BTreeMap<String, Vec<String>>>;
fn toml_merge_provenance_path(base_dir: &Path) -> PathBuf {
base_dir.join(TOML_MERGE_PROVENANCE_MANIFEST)
}
fn read_toml_merge_provenance_file(base_dir: &Path) -> TomlMergeProvenance {
let Ok(content) = fs::read_to_string(toml_merge_provenance_path(base_dir)) else {
return TomlMergeProvenance::new();
};
let Ok(parsed) = toml::from_str::<TomlMergeProvenanceFile>(&content) else {
return TomlMergeProvenance::new();
};
let mut all = TomlMergeProvenance::new();
for entry in parsed.entries {
all.entry(entry.relative_path)
.or_default()
.insert(entry.key_path, entry.values);
}
all
}
pub fn read_toml_merge_provenance(
base_dir: &Path,
relative_path: &Path,
) -> std::collections::BTreeMap<String, Vec<String>> {
read_toml_merge_provenance_file(base_dir)
.remove(&relative_path.to_string_lossy().into_owned())
.unwrap_or_default()
}
fn render_toml_merge_provenance(entries: &[TomlMergeProvenanceEntry]) -> String {
let mut body = String::new();
for entry in entries {
if !body.is_empty() {
body.push('\n');
}
body.push_str("[[entries]]\n");
for (key, value) in [("relative_path", &entry.relative_path), ("key_path", &entry.key_path)] {
body.push_str(key);
body.push_str(" = ");
body.push_str(&toml_edit::Value::from(value.as_str()).to_string());
body.push('\n');
}
body.push_str(&render_record_assignment("values", &entry.values));
body.push('\n');
}
body
}
pub fn write_toml_merge_provenance(
base_dir: &Path,
relative_path: &Path,
arrays_by_key_path: &std::collections::BTreeMap<String, Vec<String>>,
) -> anyhow::Result<()> {
let manifest_path = toml_merge_provenance_path(base_dir);
let is_new_manifest = !manifest_path.exists();
let mut all = read_toml_merge_provenance_file(base_dir);
all.insert(relative_path.to_string_lossy().into_owned(), arrays_by_key_path.clone());
let entries: Vec<TomlMergeProvenanceEntry> = all
.into_iter()
.flat_map(|(relative_path, by_key_path)| {
by_key_path
.into_iter()
.map(move |(key_path, values)| TomlMergeProvenanceEntry {
relative_path: relative_path.clone(),
key_path,
values,
})
})
.collect();
fs::create_dir_all(base_dir)?;
let body = render_toml_merge_provenance(&entries);
fs::write(&manifest_path, format!("{TOML_MERGE_PROVENANCE_HEADER}\n{body}"))?;
if is_new_manifest {
tracing::info!(
manifest = %TOML_MERGE_PROVENANCE_MANIFEST,
"created the alef toml-merge provenance record: commit it, or a fresh clone can never \
prune a value alef stops generating"
);
}
Ok(())
}