visual_rubric/batch/
snapshot.rs1use std::collections::{BTreeMap, BTreeSet};
2use std::fs;
3use std::path::{Path, PathBuf};
4
5use serde::{Deserialize, Serialize};
6
7#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
9pub struct AssetSnapshot {
10 hashes: BTreeMap<PathBuf, String>,
11}
12
13impl AssetSnapshot {
14 pub fn capture<I, P, F>(paths: I, hasher: F) -> std::io::Result<Self>
20 where
21 I: IntoIterator<Item = P>,
22 P: AsRef<Path>,
23 F: Fn(&[u8]) -> String,
24 {
25 let mut hashes = BTreeMap::new();
26 for path in paths {
27 let path = path.as_ref();
28 let bytes = fs::read(path)?;
29 hashes.insert(path.to_path_buf(), hasher(&bytes));
30 }
31 Ok(Self { hashes })
32 }
33
34 #[must_use]
36 pub fn from_hashes<I, P, S>(entries: I) -> Self
37 where
38 I: IntoIterator<Item = (P, S)>,
39 P: Into<PathBuf>,
40 S: Into<String>,
41 {
42 Self {
43 hashes: entries
44 .into_iter()
45 .map(|(path, hash)| (path.into(), hash.into()))
46 .collect(),
47 }
48 }
49}
50
51#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
53#[serde(tag = "change", content = "path", rename_all = "snake_case")]
54pub enum AssetChange {
55 Added(PathBuf),
57 Changed(PathBuf),
59 Deleted(PathBuf),
61 Unchanged(PathBuf),
63}
64
65impl AssetChange {
66 #[must_use]
68 pub fn path(&self) -> &Path {
69 match self {
70 Self::Added(path)
71 | Self::Changed(path)
72 | Self::Deleted(path)
73 | Self::Unchanged(path) => path,
74 }
75 }
76
77 pub(super) fn status(&self) -> &'static str {
78 match self {
79 Self::Added(_) => "added",
80 Self::Changed(_) => "changed",
81 Self::Deleted(_) => "deleted",
82 Self::Unchanged(_) => "unchanged",
83 }
84 }
85}
86
87#[must_use]
89pub fn diff_snapshots(before: &AssetSnapshot, after: &AssetSnapshot) -> Vec<AssetChange> {
90 let keys: BTreeSet<_> = before.hashes.keys().chain(after.hashes.keys()).collect();
91 keys.into_iter()
92 .map(
93 |path| match (before.hashes.get(path), after.hashes.get(path)) {
94 (None, Some(_)) => AssetChange::Added(path.clone()),
95 (Some(_), None) => AssetChange::Deleted(path.clone()),
96 (Some(old), Some(new)) if old == new => AssetChange::Unchanged(path.clone()),
97 (Some(_), Some(_)) => AssetChange::Changed(path.clone()),
98 (None, None) => unreachable!("path came from snapshot keys"),
99 },
100 )
101 .collect()
102}
103
104#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
106#[serde(rename_all = "snake_case")]
107pub enum SelectionMode {
108 #[default]
110 ChangedOnly,
111 IncludeUnchanged,
113}
114
115#[must_use]
117pub fn select_changed(changes: &[AssetChange], mode: SelectionMode) -> Vec<PathBuf> {
118 changes
119 .iter()
120 .filter_map(|change| match (change, mode) {
121 (AssetChange::Added(path) | AssetChange::Changed(path), _) => Some(path.clone()),
122 (AssetChange::Unchanged(path), SelectionMode::IncludeUnchanged) => Some(path.clone()),
123 (AssetChange::Deleted(_) | AssetChange::Unchanged(_), SelectionMode::ChangedOnly) => {
124 None
125 }
126 (AssetChange::Deleted(_), SelectionMode::IncludeUnchanged) => None,
127 })
128 .collect()
129}