use std::io::Write;
use nichlink::identity::NodeId;
use nichlink_build_method::{FaceView, face_views};
use serde_json::{Value, json};
use super::{build_out_dir, resolve_package};
#[path = "explain_json.rs"]
pub(crate) mod json;
#[path = "explain_overlay.rs"]
mod overlay;
#[path = "explain_report.rs"]
mod report;
pub(crate) fn explain(
args: &mut impl Iterator<Item = String>,
out: &mut dyn Write,
) -> Result<(), String> {
let mut json_output = false;
let mut overlay = false;
let mut directory: Option<String> = None;
let mut target: Option<String> = None;
while let Some(arg) = args.next() {
match arg.as_str() {
"--json" => json_output = true,
"--overlay" => overlay = true,
"--path" => {
directory = Some(args.next().ok_or("--path requires a directory")?);
}
_ if arg.starts_with('-') => return Err(format!("unexpected argument '{arg}'")),
_ if target.is_none() => target = Some(arg),
_ => return Err("explain accepts at most one node id or logical path".to_owned()),
}
}
let directory = directory.unwrap_or_else(|| ".".to_owned());
let (manifest, package) = match resolve_package(&directory) {
Ok(resolved) => resolved,
Err(error) => {
if json_output {
write_unresolved(&target, overlay, &error, out)?;
}
return Err(error);
}
};
let faces = face_views(&manifest, &package)?;
if overlay {
if target.is_some() {
return Err(
"--overlay renders the whole effective tree; drop the node id or path".to_owned(),
);
}
return overlay::overlay_report(&manifest, &faces, json_output, out);
}
let query = target.ok_or("explain requires a node id or a logical path")?;
let face = match resolve_face(&faces, &query) {
Ok(face) => face,
Err(reason) => {
if json_output {
let report = json!({
"schema": "nichlink.explain/1",
"query": query,
"resolved": false,
"reason": reason,
"candidate_count": faces.len(),
});
writeln!(out, "{}", json::render_json(&report)).map_err(json::write_error)?;
} else {
writeln!(out, "unresolved: {reason}").map_err(json::write_error)?;
}
return Err(reason);
}
};
let out_dir = build_out_dir(&manifest);
let current = nichlink_build_method::build_output_is_current(&manifest, &out_dir);
let (scope_json, scope_note) = report::scope_report(&out_dir, face, current);
let (pruning_json, pruning_note) = report::pruning_report(&out_dir, face, current);
let (grafts_json, graft_note) = report::declared_report(&manifest, face);
let kept = scope_json["kept"].as_bool();
if json_output {
let report = json!({
"schema": "nichlink.explain/1",
"query": query,
"resolved": true,
"kept": kept,
"node": {
"id": face.id.to_string(),
"path": face.path,
"kind": face.kind,
"registry_name": face.registry_name,
"module": face.module,
"source": face.source,
"owns_registry": face.owns_registry,
"parent": {
"id": face.parent.to_string(),
"path": report::parent_path(&faces, face.parent),
"resolved": face.parent_resolved,
},
"identity_inputs": {
"namespace": package,
"relative_source": face.source,
"kind": face.kind,
},
},
"scope": scope_json,
"pruning": pruning_json,
"declared_grafts": grafts_json,
});
writeln!(out, "{}", json::render_json(&report)).map_err(json::write_error)?;
} else {
writeln!(out, "node {}", face.id).map_err(json::write_error)?;
writeln!(out, " path: {}", face.path).map_err(json::write_error)?;
writeln!(out, " kind: {}", face.kind).map_err(json::write_error)?;
writeln!(out, " source: {}", face.source).map_err(json::write_error)?;
writeln!(out, " module: {}", face.module).map_err(json::write_error)?;
writeln!(
out,
" identity: namespace={package} relative-source={} kind={}",
face.source, face.kind
)
.map_err(json::write_error)?;
writeln!(
out,
" parent: {} ({}){}",
report::parent_path(&faces, face.parent),
face.parent,
if face.parent_resolved {
""
} else {
" [unresolved: the declaration names a parent this tree does not have]"
}
)
.map_err(json::write_error)?;
writeln!(
out,
" registry: owns_registry={} slot={}",
face.owns_registry, face.registry_name
)
.map_err(json::write_error)?;
for line in scope_note
.iter()
.chain(pruning_note.iter())
.chain(graft_note.iter())
{
writeln!(out, "{line}").map_err(json::write_error)?;
}
}
Ok(())
}
fn write_unresolved(
target: &Option<String>,
overlay: bool,
error: &str,
out: &mut dyn Write,
) -> Result<(), String> {
let report = if overlay {
json!({
"schema": "nichlink.explain-overlay/1",
"kind": "static-projection",
"entry": Value::Null,
"entry_error": Value::Null,
"scope": {"known": false},
"slots": [],
"pruned": [],
"plans": [],
"note": overlay::OVERLAY_NOTE,
"error": error,
})
} else {
json!({
"schema": "nichlink.explain/1",
"query": target,
"resolved": false,
"reason": error,
"candidate_count": 0,
})
};
writeln!(out, "{}", json::render_json(&report)).map_err(json::write_error)
}
fn resolve_face<'a>(faces: &'a [FaceView], query: &str) -> Result<&'a FaceView, String> {
if let Ok(id) = query.parse::<NodeId>() {
return faces
.iter()
.find(|face| face.id == id)
.ok_or_else(|| format!("no registration face has node id `{query}`"));
}
if let Some(face) = faces.iter().find(|face| face.path == query) {
return Ok(face);
}
let mut paths = faces
.iter()
.map(|face| face.path.as_str())
.collect::<Vec<_>>();
paths.sort_unstable();
let shown = paths.iter().take(8).copied().collect::<Vec<_>>().join(", ");
Err(format!(
"no registration face has logical path `{query}`; the tree has {} face(s): {shown}",
paths.len()
))
}