use super::funejson::Value; use std::collections::HashMap; /// Largest integer JavaScript can hold exactly; beyond it the three languages disagree. const SAFE_INTEGER: i64 = 9007199254740991; /// A record's key as text, rendered the way collections.group-by-key renders a /// group name so the two agree on what "the same key" means. None when absent. /// /// # Panics /// Panics on a float, a list or a map, or an integer outside the safe range. fn key_of(value: &Value, key: &str) -> Option { match value { Value::Null => None, Value::Str(s) => Some(s.clone()), Value::Bool(b) => Some((if *b { "true" } else { "false" }).to_string()), Value::Int(i) => { if i.abs() > SAFE_INTEGER { panic!("cannot diff by the out-of-range number {} at \"{}\"", i, key); } Some(i.to_string()) } Value::Float(f) => panic!("cannot diff by the fractional number {} at \"{}\"", f, key), _ => panic!("cannot diff by the list or map at \"{}\"", key), } } /// Deep JSON equality, spelled out so all three languages agree: numbers by /// value (1 equals 1.0), no coercion between types (true is not 1, "1" is not /// 1), lists in order, and a missing map field equal to a null one. fn same(a: &Value, b: &Value) -> bool { match (a, b) { (Value::Null, Value::Null) => true, (Value::Bool(x), Value::Bool(y)) => x == y, (Value::Int(x), Value::Int(y)) => x == y, (Value::Int(_), Value::Float(_)) | (Value::Float(_), Value::Int(_)) | (Value::Float(_), Value::Float(_)) => { a.as_f64() == b.as_f64() } (Value::Str(x), Value::Str(y)) => x == y, (Value::Arr(x), Value::Arr(y)) => x.len() == y.len() && x.iter().zip(y).all(|(p, q)| same(p, q)), (Value::Obj(x), Value::Obj(y)) => { // `get` answers Null for a missing field, which is the rule. x.iter().all(|(k, v)| same(v, b.get(k))) && y.iter().all(|(k, v)| same(a.get(k), v)) } _ => false, } } fn field_names(record: &Value) -> Vec { match record { Value::Obj(pairs) => pairs.iter().map(|(k, _)| k.clone()).collect(), _ => Vec::new(), } } /// Key every record of one list, refusing missing and duplicate keys. /// /// # Panics /// Panics on a record with no key and on a duplicate key. fn index(records: &[Value], key: &str, side: &str) -> (Vec, HashMap) { let mut order: Vec = Vec::new(); let mut by_key: HashMap = HashMap::new(); for (i, record) in records.iter().enumerate() { let k = match key_of(record.get(key), key) { Some(k) => k, None => panic!("record {} in {} has no value at \"{}\"", i, side, key), }; // Picking one of two duplicates would report changes that never happened. if by_key.contains_key(&k) { panic!("duplicate key \"{}\" in {}", k, side); } by_key.insert(k.clone(), i); order.push(k); } (order, by_key) } /// Compare two lists of records matched by `key`: what was added, what was /// removed, and what changed and in which fields. /// /// # Panics /// Panics on an empty key, a record without a key, a duplicate key, or a key /// value that cannot be rendered. pub fn diff_by_key(before: &[Value], after: &[Value], key: &str) -> RecordDiff { if key.is_empty() { panic!("diff_by_key needs a non-empty key name"); } let (before_order, before_keys) = index(before, key, "before"); let (after_order, after_keys) = index(after, key, "after"); let mut added: Vec = Vec::new(); let mut changed: Vec> = Vec::new(); let mut unchanged: i64 = 0; for (i, k) in after_order.iter().enumerate() { let next = &after[i]; let prev = match before_keys.get(k) { Some(j) => &before[*j], None => { added.push(next.clone()); continue; } }; let mut names = field_names(prev); for name in field_names(next) { if !names.contains(&name) { names.push(name); } } let mut fields: Vec = names .into_iter() .filter(|name| !same(prev.get(name), next.get(name))) .collect(); // String ordering in Rust is byte order, which for UTF-8 is code point order. fields.sort(); if fields.is_empty() { unchanged += 1; } else { changed.push(RecordChange { key: k.clone(), before: prev.clone(), after: next.clone(), fields, }); } } let removed: Vec = before_order .iter() .enumerate() .filter(|(_, k)| !after_keys.contains_key(*k)) .map(|(j, _)| before[j].clone()) .collect(); RecordDiff { added, removed, changed, unchanged, } } pub fn record_change_to_value(change: &RecordChange) -> Value { Value::obj(vec![ ("key", Value::str(&change.key)), ("before", change.before.clone()), ("after", change.after.clone()), ("fields", Value::Arr(change.fields.iter().map(|f| Value::str(f)).collect())), ]) } pub fn record_diff_to_value(diff: &RecordDiff) -> Value { Value::obj(vec![ ("added", Value::Arr(diff.added.clone())), ("removed", Value::Arr(diff.removed.clone())), ("changed", Value::Arr(diff.changed.iter().map(record_change_to_value).collect())), ("unchanged", Value::Int(diff.unchanged)), ]) } pub fn fune_vector(args: &[Value]) -> Value { record_diff_to_value(&diff_by_key(args[0].as_arr(), args[1].as_arr(), args[2].as_str())) }