use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum DiffKind {
Added,
Modified,
Deleted,
#[default]
Unchanged,
}
impl DiffKind {
pub fn is_change(&self) -> bool {
!matches!(self, DiffKind::Unchanged)
}
}
impl fmt::Display for DiffKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DiffKind::Added => write!(f, "added"),
DiffKind::Modified => write!(f, "modified"),
DiffKind::Deleted => write!(f, "deleted"),
DiffKind::Unchanged => write!(f, "unchanged"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FileChange {
pub path: String,
pub kind: DiffKind,
}
impl FileChange {
pub fn new(path: impl Into<String>, kind: DiffKind) -> Self {
Self {
path: path.into(),
kind,
}
}
pub fn added(path: impl Into<String>) -> Self {
Self::new(path, DiffKind::Added)
}
pub fn modified(path: impl Into<String>) -> Self {
Self::new(path, DiffKind::Modified)
}
pub fn deleted(path: impl Into<String>) -> Self {
Self::new(path, DiffKind::Deleted)
}
pub fn into_tuple(self) -> (String, DiffKind) {
(self.path, self.kind)
}
pub fn from_tuple((path, kind): (String, DiffKind)) -> Self {
Self { path, kind }
}
}
impl From<(String, DiffKind)> for FileChange {
fn from(tuple: (String, DiffKind)) -> Self {
Self::from_tuple(tuple)
}
}
impl From<FileChange> for (String, DiffKind) {
fn from(change: FileChange) -> Self {
change.into_tuple()
}
}
#[derive(Debug, Clone, Default)]
pub struct FileChangeSet {
changes: Vec<FileChange>,
}
impl FileChangeSet {
pub fn new() -> Self {
Self::default()
}
pub fn with_capacity(capacity: usize) -> Self {
Self {
changes: Vec::with_capacity(capacity),
}
}
pub fn push(&mut self, change: FileChange) {
self.changes.push(change);
}
pub fn push_added(&mut self, path: impl Into<String>) {
self.changes.push(FileChange::added(path));
}
pub fn push_modified(&mut self, path: impl Into<String>) {
self.changes.push(FileChange::modified(path));
}
pub fn push_deleted(&mut self, path: impl Into<String>) {
self.changes.push(FileChange::deleted(path));
}
pub fn is_empty(&self) -> bool {
self.changes.is_empty()
}
pub fn len(&self) -> usize {
self.changes.len()
}
pub fn iter(&self) -> impl Iterator<Item = &FileChange> {
self.changes.iter()
}
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut FileChange> {
self.changes.iter_mut()
}
pub fn into_vec(self) -> Vec<FileChange> {
self.changes
}
pub fn added(&self) -> impl Iterator<Item = &FileChange> {
self.changes.iter().filter(|c| c.kind == DiffKind::Added)
}
pub fn modified(&self) -> impl Iterator<Item = &FileChange> {
self.changes.iter().filter(|c| c.kind == DiffKind::Modified)
}
pub fn deleted(&self) -> impl Iterator<Item = &FileChange> {
self.changes.iter().filter(|c| c.kind == DiffKind::Deleted)
}
pub fn is_clean(&self) -> bool {
self.changes.is_empty()
}
pub fn added_count(&self) -> usize {
self.added().count()
}
pub fn modified_count(&self) -> usize {
self.modified().count()
}
pub fn deleted_count(&self) -> usize {
self.deleted().count()
}
}
impl Extend<FileChange> for FileChangeSet {
fn extend<T: IntoIterator<Item = FileChange>>(&mut self, iter: T) {
self.changes.extend(iter);
}
}
impl From<Vec<FileChange>> for FileChangeSet {
fn from(changes: Vec<FileChange>) -> Self {
Self { changes }
}
}
impl From<Vec<(String, DiffKind)>> for FileChangeSet {
fn from(changes: Vec<(String, DiffKind)>) -> Self {
Self {
changes: changes.into_iter().map(FileChange::from_tuple).collect(),
}
}
}
impl IntoIterator for FileChangeSet {
type Item = FileChange;
type IntoIter = std::vec::IntoIter<FileChange>;
fn into_iter(self) -> Self::IntoIter {
self.changes.into_iter()
}
}
impl<'a> IntoIterator for &'a FileChangeSet {
type Item = &'a FileChange;
type IntoIter = std::slice::Iter<'a, FileChange>;
fn into_iter(self) -> Self::IntoIter {
self.changes.iter()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_file_change_creation() {
let change = FileChange::added("src/main.rs");
assert_eq!(change.path, "src/main.rs");
assert_eq!(change.kind, DiffKind::Added);
let change = FileChange::modified("src/lib.rs");
assert_eq!(change.kind, DiffKind::Modified);
let change = FileChange::deleted("old.txt");
assert_eq!(change.kind, DiffKind::Deleted);
}
#[test]
fn test_file_change_tuple_conversion() {
let change = FileChange::added("foo.txt");
let tuple: (String, DiffKind) = change.into();
assert_eq!(tuple, (String::from("foo.txt"), DiffKind::Added));
let change: FileChange = (String::from("bar.txt"), DiffKind::Modified).into();
assert_eq!(change.path, "bar.txt");
assert_eq!(change.kind, DiffKind::Modified);
}
#[test]
fn test_file_change_set_basic() {
let mut set = FileChangeSet::new();
assert!(set.is_empty());
assert_eq!(set.len(), 0);
set.push_added("a.txt");
set.push_modified("b.txt");
set.push_deleted("c.txt");
assert!(!set.is_empty());
assert_eq!(set.len(), 3);
assert_eq!(set.added_count(), 1);
assert_eq!(set.modified_count(), 1);
assert_eq!(set.deleted_count(), 1);
}
#[test]
fn test_file_change_set_iterators() {
let mut set = FileChangeSet::new();
set.push_added("a.txt");
set.push_modified("b.txt");
set.push_deleted("c.txt");
let added: Vec<_> = set.added().map(|c| &c.path).collect();
assert_eq!(added, vec!["a.txt"]);
let modified: Vec<_> = set.modified().map(|c| &c.path).collect();
assert_eq!(modified, vec!["b.txt"]);
let deleted: Vec<_> = set.deleted().map(|c| &c.path).collect();
assert_eq!(deleted, vec!["c.txt"]);
}
#[test]
fn test_file_change_set_conversion() {
let tuples = vec![
(String::from("a.txt"), DiffKind::Added),
(String::from("b.txt"), DiffKind::Modified),
];
let set = FileChangeSet::from(tuples);
assert_eq!(set.len(), 2);
assert_eq!(set.added_count(), 1);
assert_eq!(set.modified_count(), 1);
}
#[test]
fn test_diff_kind_display() {
assert_eq!(DiffKind::Added.to_string(), "added");
assert_eq!(DiffKind::Modified.to_string(), "modified");
assert_eq!(DiffKind::Deleted.to_string(), "deleted");
assert_eq!(DiffKind::Unchanged.to_string(), "unchanged");
}
#[test]
fn test_diff_kind_is_change() {
assert!(!DiffKind::Unchanged.is_change());
assert!(DiffKind::Added.is_change());
assert!(DiffKind::Modified.is_change());
assert!(DiffKind::Deleted.is_change());
}
}