use serde_yaml_ng::Value;
use std::collections::BTreeSet;
pub fn changed_paths(a: &Value, b: &Value) -> BTreeSet<Vec<String>> {
let mut out = BTreeSet::new();
walk(&mut Vec::new(), a, b, &mut out);
out
}
fn walk(prefix: &mut Vec<String>, a: &Value, b: &Value, out: &mut BTreeSet<Vec<String>>) {
match (a, b) {
(Value::Mapping(x), Value::Mapping(y)) => walk_map(prefix, x, y, out),
(Value::Sequence(x), Value::Sequence(y)) if x.len() == y.len() => {
for (i, (ea, eb)) in x.iter().zip(y.iter()).enumerate() {
prefix.push(i.to_string());
walk(prefix, ea, eb, out);
prefix.pop();
}
}
_ => {
if a != b {
out.insert(prefix.clone());
}
}
}
}
fn walk_map(
prefix: &mut Vec<String>,
x: &serde_yaml_ng::Mapping,
y: &serde_yaml_ng::Mapping,
out: &mut BTreeSet<Vec<String>>,
) {
let mut keys: Vec<&Value> = Vec::new();
for k in x.keys().chain(y.keys()) {
if !keys.contains(&k) {
keys.push(k);
}
}
for k in keys {
prefix.push(key_label(k));
match (x.get(k), y.get(k)) {
(Some(va), Some(vb)) => walk(prefix, va, vb, out),
_ => {
out.insert(prefix.clone());
}
}
prefix.pop();
}
}
fn key_label(key: &Value) -> String {
match key {
Value::String(s) => s.clone(),
other => serde_yaml_ng::to_string(other)
.unwrap_or_default()
.trim_end()
.to_string(),
}
}
pub fn changed_paths_of_text(before: &str, after: &str) -> Result<BTreeSet<Vec<String>>, String> {
let a: Value = serde_yaml_ng::from_str(before)
.map_err(|e| format!("the document did not parse before the edit: {e}"))?;
let b: Value = serde_yaml_ng::from_str(after)
.map_err(|e| format!("the edit produced YAML that does not parse: {e}"))?;
Ok(changed_paths(&a, &b))
}