#[derive(Debug, Clone, Default, PartialEq)]
pub struct ListPatch {
pub replace_all: Option<Vec<String>>,
pub clear: bool,
pub remove: Vec<String>,
pub replace: Vec<(String, String)>,
pub after: Vec<(String, Vec<String>)>,
pub before: Vec<(String, Vec<String>)>,
pub prepend: Vec<String>,
pub add: Vec<String>,
}
impl ListPatch {
pub fn apply(&self, base: &[String]) -> Vec<String> {
if let Some(all) = &self.replace_all {
return dedup(all.clone());
}
let mut out: Vec<String> = if self.clear {
Vec::new()
} else {
base.to_vec()
};
if !self.remove.is_empty() {
out.retain(|x| !self.remove.iter().any(|r| r == x));
}
for (old, new) in &self.replace {
if let Some(pos) = out.iter().position(|x| x == old) {
out[pos] = new.clone();
}
}
for (anchor, items) in &self.after {
if let Some(pos) = out.iter().position(|x| x == anchor) {
out.splice(pos + 1..pos + 1, items.iter().cloned());
}
}
for (anchor, items) in &self.before {
if let Some(pos) = out.iter().position(|x| x == anchor) {
out.splice(pos..pos, items.iter().cloned());
}
}
if !self.prepend.is_empty() {
let mut front = self.prepend.clone();
front.extend(out);
out = front;
}
out.extend(self.add.iter().cloned());
dedup(out)
}
pub fn is_noop(&self) -> bool {
self.replace_all.is_none()
&& !self.clear
&& self.remove.is_empty()
&& self.replace.is_empty()
&& self.after.is_empty()
&& self.before.is_empty()
&& self.prepend.is_empty()
&& self.add.is_empty()
}
}
fn dedup(list: Vec<String>) -> Vec<String> {
let mut seen = std::collections::HashSet::new();
list.into_iter()
.filter(|x| seen.insert(x.clone()))
.collect()
}
#[derive(Debug, Clone, Default)]
pub struct ReportOverride {
pub columns: ListPatch,
pub card: ListPatch,
pub stats: ListPatch,
pub size: ListPatch,
pub filter: ListPatch,
}
#[cfg(test)]
mod tests {
use super::*;
fn v(xs: &[&str]) -> Vec<String> {
xs.iter().map(|s| s.to_string()).collect()
}
#[test]
fn apply_covers_every_op() {
let base = v(&["kind", "sloc", "hk", "volume", "effort"]);
let p = ListPatch {
remove: v(&["volume", "effort"]),
add: v(&["unsafe", "hk"]), ..Default::default()
};
assert_eq!(p.apply(&base), v(&["kind", "sloc", "hk", "unsafe"]));
let p = ListPatch {
replace: vec![("sloc".into(), "lloc".into())],
..Default::default()
};
assert_eq!(
p.apply(&base),
v(&["kind", "lloc", "hk", "volume", "effort"])
);
let p = ListPatch {
clear: true,
add: v(&["kind", "hk"]),
..Default::default()
};
assert_eq!(p.apply(&base), v(&["kind", "hk"]));
let p = ListPatch {
after: vec![("hk".into(), v(&["tsr"]))],
..Default::default()
};
assert_eq!(
p.apply(&base),
v(&["kind", "sloc", "hk", "tsr", "volume", "effort"])
);
let p = ListPatch {
before: vec![("hk".into(), v(&["tsr"]))],
..Default::default()
};
assert_eq!(
p.apply(&base),
v(&["kind", "sloc", "tsr", "hk", "volume", "effort"])
);
let p = ListPatch {
prepend: v(&["unsafe"]),
..Default::default()
};
assert_eq!(
p.apply(&base),
v(&["unsafe", "kind", "sloc", "hk", "volume", "effort"])
);
let p = ListPatch {
replace_all: Some(v(&["a", "b", "a"])),
..Default::default()
};
assert_eq!(p.apply(&base), v(&["a", "b"]));
assert!(ListPatch::default().is_noop());
assert_eq!(ListPatch::default().apply(&base), base);
}
}