use std::collections::BTreeSet;
use toml::{Table, Value};
use super::scenario::{HOUSEHOLD_KEY, ID_KEY, PEOPLE_PATH, SCHEMA_KEY, is_keyed, split_household};
use super::{Plan, PlanError, to_table};
const SECTIONS: [&str; 14] = [
"plan",
"household",
"medicare",
"events",
"accounts",
"income",
"expenses",
"cliffs",
"transfers",
"conversions",
"contributions",
"residency",
"market",
"estimate",
];
#[derive(Debug, Clone, PartialEq)]
pub struct Change {
pub section: String,
pub field: Option<String>,
pub item: Option<Table>,
pub kind: ChangeKind,
}
#[derive(Debug, Clone, PartialEq)]
pub enum ChangeKind {
Added,
Removed,
Changed {
from: Option<Value>,
to: Option<Value>,
},
}
pub fn diff(base: &Plan, other: &Plan) -> Result<Vec<Change>, PlanError> {
let (base, other) = (to_table(base)?, to_table(other)?);
let mut changes = Vec::new();
for section in sections(&base, &other) {
let (from, to) = (base.get(section), other.get(section));
if is_keyed(section) {
diff_items(&mut changes, section, from, to);
} else if section == HOUSEHOLD_KEY {
diff_household(&mut changes, from, to);
} else {
diff_fields(&mut changes, (section, None), from, to);
}
}
Ok(changes)
}
pub(super) fn sections<'a>(base: &'a Table, other: &'a Table) -> Vec<&'a str> {
let mut sections: Vec<&str> = keys(base, other)
.into_iter()
.filter(|§ion| section != SCHEMA_KEY)
.collect();
let place = |section: &str| SECTIONS.iter().position(|&known| known == section);
sections.sort_by_key(|§ion| place(section).unwrap_or(SECTIONS.len()));
sections
}
fn keys<'a>(base: &'a Table, other: &'a Table) -> BTreeSet<&'a str> {
base.keys()
.chain(other.keys())
.map(String::as_str)
.collect()
}
type At<'a> = (&'a str, Option<String>);
fn diff_fields(changes: &mut Vec<Change>, at: At, from: Option<&Value>, to: Option<&Value>) {
if from == to {
return;
}
let empty = Table::new();
let (section, field) = at;
let (Some(from_table), Some(to_table)) = (as_table(from, &empty), as_table(to, &empty)) else {
changes.push(changed(section, None, field, (from, to)));
return;
};
for key in keys(from_table, to_table) {
let within = field
.as_ref()
.map_or_else(|| key.to_owned(), |outer| format!("{outer}.{key}"));
diff_fields(
changes,
(section, Some(within)),
from_table.get(key),
to_table.get(key),
);
}
}
fn as_table<'a>(value: Option<&'a Value>, empty: &'a Table) -> Option<&'a Table> {
value.map_or(Some(empty), Value::as_table)
}
fn diff_household(changes: &mut Vec<Change>, from: Option<&Value>, to: Option<&Value>) {
let ((from_own, from_people), (to_own, to_people)) =
(split_household(from), split_household(to));
diff_fields(
changes,
(HOUSEHOLD_KEY, None),
Some(&from_own),
Some(&to_own),
);
diff_items(
changes,
PEOPLE_PATH,
from_people.as_ref(),
to_people.as_ref(),
);
}
fn items(value: Option<&Value>) -> Vec<&Table> {
let listed = value.and_then(Value::as_array).map(Vec::as_slice);
listed
.unwrap_or_default()
.iter()
.filter_map(Value::as_table)
.collect()
}
fn diff_items(changes: &mut Vec<Change>, section: &str, from: Option<&Value>, to: Option<&Value>) {
let (base, other) = (items(from), items(to));
let position = |among: &[&Table], item: &Table| {
among
.iter()
.position(|candidate| candidate.get(ID_KEY) == item.get(ID_KEY))
};
let mut removed: Vec<Vec<&Table>> = vec![Vec::new(); base.len() + 1];
let mut after = 0;
for (at, &item) in base.iter().enumerate() {
if position(&other, item).is_some() {
after = at + 1;
} else {
removed[after].push(item);
}
}
let mut remove = |after: usize, changes: &mut Vec<Change>| {
for gone in std::mem::take(&mut removed[after]) {
changes.push(whole(section, gone, ChangeKind::Removed));
}
};
remove(0, changes);
for &item in &other {
match position(&base, item) {
Some(at) => {
diff_item(changes, section, base[at], item);
remove(at + 1, changes);
}
None => changes.push(whole(section, item, ChangeKind::Added)),
}
}
}
fn diff_item(changes: &mut Vec<Change>, section: &str, from: &Table, to: &Table) {
for key in keys(from, to) {
let values = (from.get(key), to.get(key));
if values.0 != values.1 {
changes.push(changed(section, Some(to), Some(key.to_owned()), values));
}
}
}
fn whole(section: &str, item: &Table, kind: ChangeKind) -> Change {
Change {
section: section.to_owned(),
field: None,
item: Some(item.clone()),
kind,
}
}
fn changed(
section: &str,
item: Option<&Table>,
field: Option<String>,
(from, to): (Option<&Value>, Option<&Value>),
) -> Change {
Change {
section: section.to_owned(),
field,
item: item.cloned(),
kind: ChangeKind::Changed {
from: from.cloned(),
to: to.cloned(),
},
}
}