//! `mlf diff [NSID]` — print a unified diff between the local Lexicon //! JSON and the currently-published record for one NSID (or every //! changed / new / removed NSID in the package). use crate::remote_state::{Classification, RemoteState, RemoteStateError, classify}; use similar::{ChangeTag, TextDiff}; pub async fn run_diff(nsid: Option) -> Result<(), RemoteStateError> { let state = RemoteState::load().await?; let classifications = classify(&state); let targets: Vec = match nsid { Some(ref n) => vec![n.clone()], None => classifications .iter() .filter(|(_, c)| !matches!(c, Classification::Unchanged)) .map(|(n, _)| n.clone()) .collect(), }; if targets.is_empty() { println!("No differences."); return Ok(()); } for (i, nsid) in targets.iter().enumerate() { if i > 0 { println!(); } let cls = classifications.get(nsid); print_diff(&state, nsid, cls); } Ok(()) } fn print_diff(state: &RemoteState, nsid: &str, cls: Option<&Classification>) { let local_json = state .local .get(nsid) .map(|l| pretty_json(&l.record_json)) .unwrap_or_default(); let remote_json = state .remote .get(nsid) .map(|r| pretty_json(&r.record_json)) .unwrap_or_default(); let header = match cls { Some(Classification::Unchanged) => format!("= {nsid} (unchanged)"), Some(Classification::New) => format!("+ {nsid} (new, not yet on PDS)"), Some(Classification::Removed) => format!("- {nsid} (removed locally, still on PDS)"), Some(Classification::Changed { local_cid, remote_cid, }) => format!("~ {nsid} {remote_cid} → {local_cid}"), None => format!("? {nsid} (not found locally or remotely)"), }; println!("{header}"); println!("{}", "-".repeat(header.chars().count())); let diff = TextDiff::from_lines(&remote_json, &local_json); for change in diff.iter_all_changes() { let (sign, text) = match change.tag() { ChangeTag::Delete => ("-", change.value()), ChangeTag::Insert => ("+", change.value()), ChangeTag::Equal => (" ", change.value()), }; // Trim trailing newline — `value()` returns `"line\n"` from the // source; we supply our own newline via println. let text = text.strip_suffix('\n').unwrap_or(text); println!("{sign} {text}"); } } fn pretty_json(value: &serde_json::Value) -> String { // `to_string_pretty` preserves insertion order; serde_json uses // `preserve_order` via the `IndexMap` feature by default in this // workspace? If not, key order is lexicographic. Either is fine for // a human-facing diff; the CID comparison happens elsewhere. let mut s = serde_json::to_string_pretty(value).unwrap_or_default(); if !s.ends_with('\n') { s.push('\n'); } s }