use std::io::Write;
use std::path::Path;
use nichlink::lexicon;
use nichlink::plugin::graft_document::GraftPlanDocument;
use nichlink_build_method::{DeclaredGrafts, FaceView, declared_grafts, face_views};
use serde_json::{Value, json};
use crate::explain::json::{render_json, write_error};
use super::resolve_package;
pub(crate) fn grafts(
args: &mut impl Iterator<Item = String>,
out: &mut dyn Write,
) -> Result<(), String> {
let mut json_output = false;
let mut directory: Option<String> = None;
for arg in args.by_ref() {
match arg.as_str() {
"--json" => json_output = true,
_ if arg.starts_with('-') => return Err(format!("unexpected argument '{arg}'")),
_ if directory.is_none() => directory = Some(arg),
_ => return Err("grafts accepts at most one path".to_owned()),
}
}
let directory = directory.unwrap_or_else(|| ".".to_owned());
let (manifest, package) = resolve_package(&directory)?;
let mut problems = Vec::new();
let faces = match face_views(&manifest, &package) {
Ok(faces) => faces,
Err(error) => {
problems.push(format!("host source tree: {error}"));
Vec::new()
}
};
let declared = declared_grafts(&manifest);
let rows = match plan_rows(&manifest, &faces, declared.as_ref().ok()) {
Ok(rows) => rows,
Err(error) => {
problems.push(error);
Vec::new()
}
};
let entry = declared
.as_ref()
.map(|declared| declared.entry.display().to_string())
.ok();
let entry_error = declared.as_ref().err().cloned();
if let Some(error) = &entry_error {
problems.push(format!("host entry: {error}"));
}
if json_output {
let report = json!({
"schema": "nichlink.grafts/1",
"entry": entry,
"entry_error": entry_error,
"plans": rows,
});
writeln!(out, "{}", render_json(&report)).map_err(write_error)?;
return report_problems(problems);
}
match (&entry, &entry_error) {
(Some(entry), _) => writeln!(out, "host entry: {entry}").map_err(write_error)?,
(None, Some(error)) => {
writeln!(out, "host entry: unreadable ({error})").map_err(write_error)?
}
(None, None) => {}
}
if rows.is_empty() {
writeln!(
out,
"no external graft plans under .nichlink/external-grafts/"
)
.map_err(write_error)?;
return report_problems(problems);
}
for row in &rows {
match row.get("error").and_then(Value::as_str) {
Some(error) => writeln!(
out,
"{}: unreadable ({error})",
row["selector"].as_str().unwrap_or("<unknown>")
)
.map_err(write_error)?,
None => {
let declared = match row["declared"].as_bool() {
Some(true) => "declared",
Some(false) => "NOT declared by the host entry",
None => "declaration unknown",
};
writeln!(
out,
"{}: target={} graft={} full={} [{declared}]",
row["selector"].as_str().unwrap_or("<unknown>"),
row["target_path"].as_str().unwrap_or("<unknown>"),
row["graft"].as_str().unwrap_or("<unknown>"),
row["full"]
)
.map_err(write_error)?;
if let Some(cut) = row["declared_by"].as_object() {
writeln!(
out,
" declared at line {} as cut `{}` graft `{}`",
cut["line"],
cut["cut"].as_str().unwrap_or("<unknown>"),
cut["graft"].as_str().unwrap_or("<unknown>")
)
.map_err(write_error)?;
}
}
}
}
report_problems(problems)
}
fn report_problems(problems: Vec<String>) -> Result<(), String> {
if problems.is_empty() {
return Ok(());
}
Err(format!(
"incomplete: could not read {}",
problems.join("; ")
))
}
pub(crate) fn plan_rows(
manifest: &Path,
faces: &[FaceView],
declared: Option<&DeclaredGrafts>,
) -> Result<Vec<Value>, String> {
let directory = manifest
.join(lexicon::NICHLINK_DIR)
.join(lexicon::EXTERNAL_GRAFT_DIR);
let entries = match std::fs::read_dir(&directory) {
Ok(entries) => entries,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
Err(error) => {
return Err(format!("cannot read {}: {error}", directory.display()));
}
};
let mut rows = Vec::new();
for entry in entries.flatten() {
if !entry.path().is_dir() {
continue;
}
let selector = entry.file_name().to_string_lossy().into_owned();
let plan = entry.path().join(lexicon::GRAFT_PLAN_FILE);
let text = match std::fs::read_to_string(&plan) {
Ok(text) => text,
Err(error) => {
rows.push(json!({
"selector": selector,
"error": format!("cannot read {}: {error}", plan.display()),
"target": Value::Null,
"target_path": Value::Null,
"graft": Value::Null,
"full": Value::Null,
"declared": Value::Null,
"declared_by": Value::Null,
}));
continue;
}
};
let document = match GraftPlanDocument::parse(&text) {
Ok(document) => document,
Err(error) => {
rows.push(json!({
"selector": selector,
"error": error.to_string(),
"target": Value::Null,
"target_path": Value::Null,
"graft": Value::Null,
"full": Value::Null,
"declared": Value::Null,
"declared_by": Value::Null,
}));
continue;
}
};
let module = faces
.iter()
.find(|face| face.id == document.target)
.map(|face| face.module.as_str());
let matched = declared.and_then(|declared| {
declared
.cuts
.iter()
.find(|cut| cut.names_face(&document.target_path, module))
});
let declared_state = match (declared, matched) {
(None, _) => Value::Null,
(Some(_), Some(_)) => Value::Bool(true),
(Some(_), None) => Value::Bool(false),
};
let declared_by = matched.map(|cut| {
json!({
"cut": match &cut.cut_end {
Some(end) => format!("{} to {end}", cut.cut),
None => cut.cut.clone(),
},
"graft": cut.graft,
"full": cut.full,
"line": cut.line,
})
});
rows.push(json!({
"selector": selector,
"error": Value::Null,
"target": document.target.to_string(),
"target_path": document.target_path,
"graft": document.graft,
"full": document.full,
"declared": declared_state,
"declared_by": declared_by,
}));
}
rows.sort_by(|left, right| {
left["selector"]
.as_str()
.unwrap_or_default()
.cmp(right["selector"].as_str().unwrap_or_default())
});
Ok(rows)
}