use serde_json::{Map, Value};
pub const MINIFIED_MARKER: &str = "composer/2.0";
pub const UNSET: &str = "__unset";
#[must_use]
pub fn expand_versions(versions: Vec<Map<String, Value>>) -> Vec<Map<String, Value>> {
let mut acc: Map<String, Value> = Map::new();
let mut out = Vec::with_capacity(versions.len());
for diff in versions {
apply_diff(&mut acc, diff);
out.push(acc.clone());
}
out
}
#[must_use]
pub fn minify_versions(versions: &[Map<String, Value>]) -> Vec<Map<String, Value>> {
let mut out: Vec<Map<String, Value>> = Vec::with_capacity(versions.len());
for (i, cur) in versions.iter().enumerate() {
if i == 0 {
out.push(cur.clone());
continue;
}
let prev = &versions[i - 1];
let mut diff = Map::new();
for (k, v) in cur {
if prev.get(k) != Some(v) {
diff.insert(k.clone(), v.clone());
}
}
for k in prev.keys() {
if !cur.contains_key(k) {
diff.insert(k.clone(), Value::String(UNSET.to_owned()));
}
}
out.push(diff);
}
out
}
fn apply_diff(acc: &mut Map<String, Value>, diff: Map<String, Value>) {
for (k, v) in diff {
if v.as_str() == Some(UNSET) {
acc.remove(&k);
} else {
acc.insert(k, v);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn obj(v: Value) -> Map<String, Value> {
match v {
Value::Object(m) => m,
_ => panic!("expected a JSON object"),
}
}
#[test]
fn expand_inherits_and_overrides() {
let versions = vec![
obj(json!({"name":"a/b","version":"2.0.0","require":{"php":">=8"}})),
obj(json!({"version":"1.0.0"})),
];
let expanded = expand_versions(versions);
assert_eq!(expanded[1]["name"], json!("a/b"));
assert_eq!(expanded[1]["require"]["php"], json!(">=8"));
assert_eq!(expanded[1]["version"], json!("1.0.0"));
}
#[test]
fn minify_is_inverse_of_expand() {
let full = vec![
obj(json!({"name":"a/b","version":"3.0.0","type":"library","require":{"php":">=8.1"},"dist":{"type":"zip","url":"u3"}})),
obj(json!({"name":"a/b","version":"2.0.0","type":"library","require":{"php":">=8.1"},"dist":{"type":"zip","url":"u2"}})),
obj(json!({"name":"a/b","version":"1.0.0","type":"library","require":{"php":">=7.4"},"dist":{"type":"zip","url":"u1"}})),
];
let mini = minify_versions(&full);
assert!(!mini[1].contains_key("name"));
assert!(!mini[1].contains_key("type"));
assert!(!mini[1].contains_key("require"));
assert!(mini[1].contains_key("version"));
assert!(mini[2].contains_key("require"));
assert_eq!(expand_versions(mini), full);
}
#[test]
fn minify_emits_unset_for_dropped_keys() {
let full = vec![
obj(json!({"name":"a/b","version":"2.0.0","require":{"php":">=8"}})),
obj(json!({"name":"a/b","version":"1.0.0"})), ];
let mini = minify_versions(&full);
assert_eq!(mini[1]["require"], json!("__unset"));
assert_eq!(expand_versions(mini), full);
}
#[test]
fn unset_in_input_removes_key() {
let versions = vec![
obj(json!({"name":"a/b","version":"2.0.0","require":{"php":">=8"}})),
obj(json!({"version":"1.0.0","require":"__unset"})),
];
let expanded = expand_versions(versions);
assert!(expanded[1].get("require").is_none());
assert_eq!(expanded[1]["name"], json!("a/b"));
}
}