use std::fs;
use std::path::{Path, PathBuf};
use nichlink::lexicon;
use crate::{GraftPlanDocument, Registry, StaticGraftCut};
pub use crate::registry_core::tree::graft_ops::{RecordReport, RecordedGraft, ResolvedRecord};
pub fn graft_record_root(package_root: &Path) -> PathBuf {
package_root
.join(lexicon::NICHLINK_DIR)
.join(lexicon::EXTERNAL_GRAFT_DIR)
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum LoadedGraft {
Record(RecordedGraft),
Unreadable {
selector: String,
reason: String,
},
}
impl LoadedGraft {
pub fn selector(&self) -> &str {
match self {
Self::Record(record) => &record.selector,
Self::Unreadable { selector, .. } => selector,
}
}
}
pub fn load_graft_records(package_root: &Path) -> Result<Vec<LoadedGraft>, String> {
let root = graft_record_root(package_root);
let entries = match fs::read_dir(&root) {
Ok(entries) => entries,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
Err(error) => return Err(format!("cannot scan {}: {error}", root.display())),
};
let mut loaded = Vec::new();
for entry in entries {
let entry = entry.map_err(|error| format!("cannot scan {}: {error}", root.display()))?;
if !entry.path().is_dir() {
continue;
}
let selector = entry.file_name().to_string_lossy().into_owned();
let path = entry.path().join(lexicon::GRAFT_PLAN_FILE);
let document = fs::read_to_string(&path)
.map_err(|error| format!("cannot read {}: {error}", path.display()))
.and_then(|text| GraftPlanDocument::parse(&text).map_err(|error| format!("{error}")));
match document {
Ok(document) => {
loaded.push(LoadedGraft::Record(RecordedGraft::new(selector, document)))
}
Err(reason) => loaded.push(LoadedGraft::Unreadable { selector, reason }),
}
}
loaded.sort_by(|left, right| left.selector().cmp(right.selector()));
Ok(loaded)
}
pub fn load_graft_record(package_root: &Path, selector: &str) -> Result<GraftPlanDocument, String> {
let selector = selector.trim();
crate::registry_core::validate_graft_selector(selector)?;
let path = graft_record_root(package_root)
.join(selector)
.join(lexicon::GRAFT_PLAN_FILE);
let text = fs::read_to_string(&path)
.map_err(|error| format!("cannot read {}: {error}", path.display()))?;
GraftPlanDocument::parse(&text).map_err(|error| format!("{}: {error}", path.display()))
}
#[derive(Debug)]
pub struct GraftOverlay {
pub effective: Registry,
pub reports: Vec<RecordReport>,
}
fn report_is_problem(report: &RecordReport) -> bool {
matches!(
report,
RecordReport::UnkeptSlot { .. } | RecordReport::RecordSelectorUnresolved { .. }
)
}
pub(crate) fn graft_report_lines(reports: &[RecordReport]) -> Vec<String> {
reports
.iter()
.map(|report| {
let level = if report_is_problem(report) {
"warning"
} else {
"note"
};
format!("{level}: {report}")
})
.collect()
}
pub fn apply_recorded_grafts(
base: &Registry,
external: &Registry,
declared: &[StaticGraftCut],
package_root: &Path,
) -> Result<GraftOverlay, String> {
let loaded = load_graft_records(package_root)?;
let mut records = Vec::new();
let mut unreadable = Vec::new();
for entry in loaded {
match entry {
LoadedGraft::Record(record) => records.push(record),
LoadedGraft::Unreadable { selector, reason } => unreadable.push((selector, reason)),
}
}
if !unreadable.is_empty() {
let details = unreadable
.iter()
.map(|(selector, reason)| format!("`{selector}`: {reason}"))
.collect::<Vec<_>>()
.join("; ");
return Err(format!(
"{} external graft plan(s) could not be read, so no record was applied: {details}",
unreadable.len()
));
}
let outcome = base
.overlay_recorded(&records, declared, external)
.map_err(|error| error.to_string())?;
for line in graft_report_lines(&outcome.reports) {
eprintln!("{line}");
}
Ok(GraftOverlay {
effective: outcome.effective,
reports: outcome.reports,
})
}
#[cfg(test)]
mod tests {
use super::{RecordReport, graft_report_lines};
use crate::registry_core::root_node_id;
#[test]
fn graft_report_lines_mark_problems_as_warnings() {
let reports = vec![
RecordReport::UnkeptSlot {
selector: "canvas_graft".to_owned(),
target_path: "root/canvas".to_owned(),
},
RecordReport::DeclarationOverridden {
slot: root_node_id("report-lines"),
declared: "canvas_fast".to_owned(),
recorded: "canvas_graft".to_owned(),
},
];
let lines = graft_report_lines(&reports);
assert_eq!(lines.len(), 2, "{lines:?}");
assert!(lines[0].starts_with("warning: "), "{}", lines[0]);
assert!(lines[0].contains("canvas_graft"), "{}", lines[0]);
assert!(lines[0].contains("record skipped"), "{}", lines[0]);
assert!(lines[1].starts_with("note: "), "{}", lines[1]);
}
}