use super::ShadowBaselines;
use crate::editor::form;
use serde_json::{Map, Value};
pub(crate) struct ArgsChange {
pub(crate) name: String,
pub(crate) ty: String,
pub(crate) before: Map<String, Value>,
pub(crate) args: Map<String, Value>,
pub(crate) keys: Vec<String>,
}
fn field<'a>(entry: &'a Value, key: &str) -> Option<&'a str> {
entry.get(key).and_then(|v| v.as_str())
}
fn args_of(entry: &Value) -> Map<String, Value> {
entry
.get("args")
.and_then(|v| v.as_object())
.cloned()
.unwrap_or_default()
}
pub(crate) fn same_assets(before: &[Value], after: &[Value]) -> bool {
before.len() == after.len()
&& before.iter().zip(after).all(|(old, new)| {
field(old, "name") == field(new, "name")
&& field(old, "type") == field(new, "type")
&& field(new, "name").is_some()
&& field(new, "type").is_some()
})
}
pub(crate) fn args_changes(
before: &[Value],
after: &[Value],
shadows: &ShadowBaselines,
) -> Option<Vec<ArgsChange>> {
if !same_assets(before, after) {
return None;
}
let mut out = Vec::new();
for (old, new) in before.iter().zip(after) {
let (name, ty) = (field(new, "name")?, field(new, "type")?);
let old_args = effective(ty, name, &args_of(old), shadows);
let new_args = effective(ty, name, &args_of(new), shadows);
let keys: Vec<String> = new_args
.keys()
.chain(old_args.keys())
.filter(|k| old_args.get(*k) != new_args.get(*k))
.cloned()
.collect::<std::collections::BTreeSet<String>>()
.into_iter()
.collect();
if !keys.is_empty() {
out.push(ArgsChange {
name: name.to_string(),
ty: ty.to_string(),
before: old_args,
args: new_args,
keys,
});
}
}
Some(out)
}
fn effective(
ty: &str,
name: &str,
args: &Map<String, Value>,
shadows: &ShadowBaselines,
) -> Map<String, Value> {
let authored = match shadows.get(name) {
Some(baseline) => {
concinnity_cook::build_only::merge_args(baseline, &Value::Object(args.clone()))
.as_object()
.cloned()
.unwrap_or_else(|| args.clone())
}
None => args.clone(),
};
form::working_args(ty, Some(&authored))
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn entry(name: &str, ty: &str, args: Value) -> Value {
json!({ "name": name, "type": ty, "args": args })
}
fn changes(before: &[Value], after: &[Value]) -> Option<Vec<ArgsChange>> {
args_changes(before, after, &ShadowBaselines::new())
}
#[test]
fn an_unchanged_list_yields_no_changes() {
let list = vec![entry("a", "Sprite", json!({ "width": 4.0 }))];
assert!(changes(&list, &list).unwrap().is_empty());
}
#[test]
fn a_changed_value_reports_only_its_key() {
let before = vec![entry("a", "Sprite", json!({ "width": 4.0, "height": 4.0 }))];
let after = vec![entry("a", "Sprite", json!({ "width": 8.0, "height": 4.0 }))];
let changes = changes(&before, &after).unwrap();
assert_eq!(changes.len(), 1);
assert_eq!(changes[0].name, "a");
assert_eq!(changes[0].keys, ["width"]);
}
#[test]
fn a_key_written_out_at_its_default_is_not_a_change() {
let before = vec![entry("a", "Sprite", json!({ "width": 4.0 }))];
let defaults = form::base_args("Sprite");
let mut spelled_out = defaults.clone();
spelled_out.insert("width".to_string(), json!(8.0));
let after = vec![entry("a", "Sprite", Value::Object(spelled_out))];
assert_eq!(changes(&before, &after).unwrap()[0].keys, ["width"]);
}
#[test]
fn a_dropped_key_reports_the_value_it_uncovers() {
let before = vec![entry("a", "Sprite", json!({ "width": 8.0 }))];
let after = vec![entry("a", "Sprite", json!({}))];
let changes = changes(&before, &after).unwrap();
assert_eq!(changes[0].keys, ["width"]);
assert_eq!(
changes[0].args.get("width"),
form::base_args("Sprite").get("width"),
"the type default is what it falls back to"
);
}
#[test]
fn a_patch_line_compares_against_its_baseline() {
let mut shadows = ShadowBaselines::new();
shadows.insert("a".to_string(), json!({ "width": 4.0, "height": 12.0 }));
let before = vec![entry("a", "Sprite", json!({}))];
let after = vec![entry("a", "Sprite", json!({ "width": 9.0 }))];
let changed = args_changes(&before, &after, &shadows).unwrap();
assert_eq!(changed[0].keys, ["width"]);
assert_eq!(
changed[0].args.get("height"),
Some(&json!(12.0)),
"the baseline fills what the patch leaves out"
);
}
#[test]
fn structural_edits_report_none() {
let one = vec![entry("a", "Sprite", json!({}))];
let two = vec![
entry("a", "Sprite", json!({})),
entry("b", "Sprite", json!({})),
];
assert!(changes(&one, &two).is_none(), "a line was added");
assert!(
changes(&one, &[entry("z", "Sprite", json!({}))]).is_none(),
"the line was renamed"
);
assert!(
changes(&one, &[entry("a", "TextLabel", json!({}))]).is_none(),
"the line changed type"
);
}
#[test]
fn a_reorder_reports_none() {
let before = vec![
entry("a", "Sprite", json!({})),
entry("b", "Sprite", json!({})),
];
let after = vec![
entry("b", "Sprite", json!({})),
entry("a", "Sprite", json!({})),
];
assert!(changes(&before, &after).is_none());
}
#[test]
fn the_pre_check_matches_on_identity_alone() {
let before = vec![entry("a", "Sprite", json!({ "width": 1.0 }))];
let after = vec![entry("a", "Sprite", json!({ "width": 2.0 }))];
assert!(same_assets(&before, &after));
assert!(!same_assets(&before, &[entry("b", "Sprite", json!({}))]));
assert!(!same_assets(&before, &[json!({ "type": "Sprite" })]));
}
}