use crate::{
public_item::{PublicItem, PublicItemPath},
PublicApi,
};
use hashbag::HashBag;
use std::collections::HashMap;
type ItemsWithPath = HashMap<PublicItemPath, Vec<PublicItem>>;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ChangedPublicItem {
pub old: PublicItem,
pub new: PublicItem,
}
impl ChangedPublicItem {
#[must_use]
pub fn grouping_cmp(&self, other: &Self) -> std::cmp::Ordering {
match PublicItem::grouping_cmp(&self.old, &other.old) {
std::cmp::Ordering::Equal => PublicItem::grouping_cmp(&self.new, &other.new),
ordering => ordering,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PublicApiDiff {
pub removed: Vec<PublicItem>,
pub changed: Vec<ChangedPublicItem>,
pub added: Vec<PublicItem>,
}
impl PublicApiDiff {
#[must_use]
pub fn between(old: PublicApi, new: PublicApi) -> Self {
let old = old.into_items().collect::<HashBag<_>>();
let new = new.into_items().collect::<HashBag<_>>();
let all_removed = old.difference(&new);
let all_added = new.difference(&old);
let mut removed_paths: ItemsWithPath = bag_to_path_map(all_removed);
let mut added_paths: ItemsWithPath = bag_to_path_map(all_added);
let mut removed: Vec<PublicItem> = vec![];
let mut changed: Vec<ChangedPublicItem> = vec![];
let mut added: Vec<PublicItem> = vec![];
let mut touched_paths: Vec<PublicItemPath> = vec![];
touched_paths.extend::<Vec<_>>(removed_paths.keys().cloned().collect());
touched_paths.extend::<Vec<_>>(added_paths.keys().cloned().collect());
for path in touched_paths {
let mut removed_items = removed_paths.remove(&path).unwrap_or_default();
let mut added_items = added_paths.remove(&path).unwrap_or_default();
loop {
match (removed_items.pop(), added_items.pop()) {
(Some(old), Some(new)) => changed.push(ChangedPublicItem { old, new }),
(Some(old), None) => removed.push(old),
(None, Some(new)) => added.push(new),
(None, None) => break,
}
}
}
removed.sort_by(PublicItem::grouping_cmp);
changed.sort_by(ChangedPublicItem::grouping_cmp);
added.sort_by(PublicItem::grouping_cmp);
Self {
removed,
changed,
added,
}
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.removed.is_empty() && self.changed.is_empty() && self.added.is_empty()
}
}
fn bag_to_path_map<'a>(difference: impl Iterator<Item = (&'a PublicItem, usize)>) -> ItemsWithPath {
let mut map: ItemsWithPath = HashMap::new();
for (item, occurrences) in difference {
let items = map.entry(item.sortable_path.clone()).or_default();
for _ in 0..occurrences {
items.push(item.clone());
}
}
map
}
#[cfg(test)]
mod tests {
use crate::tokens::Token;
use super::*;
#[test]
fn single_and_only_item_removed() {
let old = api([item_with_path("foo")]);
let new = api([]);
let actual = PublicApiDiff::between(old, new);
let expected = PublicApiDiff {
removed: vec![item_with_path("foo")],
changed: vec![],
added: vec![],
};
assert_eq!(actual, expected);
assert!(!actual.is_empty());
}
#[test]
fn single_and_only_item_added() {
let old = api([]);
let new = api([item_with_path("foo")]);
let actual = PublicApiDiff::between(old, new);
let expected = PublicApiDiff {
removed: vec![],
changed: vec![],
added: vec![item_with_path("foo")],
};
assert_eq!(actual, expected);
assert!(!actual.is_empty());
}
#[test]
fn middle_item_added() {
let old = api([item_with_path("1"), item_with_path("3")]);
let new = api([
item_with_path("1"),
item_with_path("2"),
item_with_path("3"),
]);
let actual = PublicApiDiff::between(old, new);
let expected = PublicApiDiff {
removed: vec![],
changed: vec![],
added: vec![item_with_path("2")],
};
assert_eq!(actual, expected);
assert!(!actual.is_empty());
}
#[test]
fn middle_item_removed() {
let old = api([
item_with_path("1"),
item_with_path("2"),
item_with_path("3"),
]);
let new = api([item_with_path("1"), item_with_path("3")]);
let actual = PublicApiDiff::between(old, new);
let expected = PublicApiDiff {
removed: vec![item_with_path("2")],
changed: vec![],
added: vec![],
};
assert_eq!(actual, expected);
assert!(!actual.is_empty());
}
#[test]
fn many_identical_items() {
let old = api([
item_with_path("1"),
item_with_path("2"),
item_with_path("2"),
item_with_path("3"),
item_with_path("3"),
item_with_path("3"),
fn_with_param_type(&["a", "b"], "i32"),
fn_with_param_type(&["a", "b"], "i32"),
]);
let new = api([
item_with_path("1"),
item_with_path("2"),
item_with_path("3"),
item_with_path("4"),
item_with_path("4"),
fn_with_param_type(&["a", "b"], "i64"),
fn_with_param_type(&["a", "b"], "i64"),
]);
let actual = PublicApiDiff::between(old, new);
let expected = PublicApiDiff {
removed: vec![
item_with_path("2"),
item_with_path("3"),
item_with_path("3"),
],
changed: vec![
ChangedPublicItem {
old: fn_with_param_type(&["a", "b"], "i32"),
new: fn_with_param_type(&["a", "b"], "i64"),
},
ChangedPublicItem {
old: fn_with_param_type(&["a", "b"], "i32"),
new: fn_with_param_type(&["a", "b"], "i64"),
},
],
added: vec![item_with_path("4"), item_with_path("4")],
};
assert_eq!(actual, expected);
assert!(!actual.is_empty());
}
#[test]
fn no_off_by_one_diff_skewing() {
let old = api([
fn_with_param_type(&["a", "b"], "i8"),
fn_with_param_type(&["a", "b"], "i32"),
fn_with_param_type(&["a", "b"], "i64"),
]);
let new = api([
fn_with_param_type(&["a", "b"], "u8"), fn_with_param_type(&["a", "b"], "i8"),
fn_with_param_type(&["a", "b"], "i32"),
fn_with_param_type(&["a", "b"], "i64"),
]);
let expected = PublicApiDiff {
removed: vec![],
changed: vec![],
added: vec![fn_with_param_type(&["a", "b"], "u8")],
};
let actual = PublicApiDiff::between(old, new);
assert_eq!(actual, expected);
assert!(!actual.is_empty());
}
#[test]
fn no_diff_means_empty_diff() {
let old = api([item_with_path("foo")]);
let new = api([item_with_path("foo")]);
let actual = PublicApiDiff::between(old, new);
let expected = PublicApiDiff {
removed: vec![],
changed: vec![],
added: vec![],
};
assert_eq!(actual, expected);
assert!(actual.is_empty());
}
fn item_with_path(path_str: &str) -> PublicItem {
new_public_item(
path_str
.split("::")
.map(std::string::ToString::to_string)
.collect(),
vec![crate::tokens::Token::identifier(path_str)],
)
}
fn api(items: impl IntoIterator<Item = PublicItem>) -> PublicApi {
PublicApi {
items: items.into_iter().collect(),
missing_item_ids: vec![],
}
}
fn fn_with_param_type(path_str: &[&str], type_: &str) -> PublicItem {
let path: Vec<_> = path_str
.iter()
.map(std::string::ToString::to_string)
.collect();
let mut tokens = vec![q("pub"), w(), k("fn"), w()];
tokens.extend(itertools::intersperse(
path.iter().cloned().map(Token::identifier),
Token::symbol("::"),
));
tokens.extend(vec![q("("), i("x"), s(":"), w(), t(type_), q(")")]);
new_public_item(path, tokens)
}
fn new_public_item(path: PublicItemPath, tokens: Vec<Token>) -> PublicItem {
PublicItem {
sortable_path: path,
tokens,
}
}
fn s(s: &str) -> Token {
Token::symbol(s)
}
fn t(s: &str) -> Token {
Token::type_(s)
}
fn q(s: &str) -> Token {
Token::qualifier(s)
}
fn k(s: &str) -> Token {
Token::kind(s)
}
fn i(s: &str) -> Token {
Token::identifier(s)
}
fn w() -> Token {
Token::Whitespace
}
}