1use super::*;
4
5#[derive(Clone, Debug)]
11pub struct FileState {
12 pub filename: String,
13 pub sha256: String,
14 pub version: i32,
15 pub size_bytes: i64,
16}
17
18#[derive(Clone, Debug)]
20pub struct LineState {
21 pub child: String,
22 pub name: String,
23 pub find_number: i32,
24 pub quantity: f64,
25}
26
27#[derive(Clone, Debug)]
29pub struct Snapshot {
30 pub label: String,
31 pub attributes: Vec<(&'static str, Value)>,
33 pub files: Vec<FileState>,
34 pub bom: Vec<LineState>,
35}
36
37impl Snapshot {
38 pub fn of(rev: &Revision, file_list: &[FileRecord], lines: &[BomLine]) -> Snapshot {
39 Snapshot {
40 label: revisions::label(rev),
41 attributes: vec![
42 ("name", json!(rev.name)),
43 ("description", json!(rev.description)),
44 ("material", json!(rev.material)),
45 ("mass_kg", json!(rev.mass_kg)),
46 ("unit_cost", json!(rev.unit_cost)),
47 ],
48 files: file_list
49 .iter()
50 .map(|f| FileState {
51 filename: f.filename.clone(),
52 sha256: f.sha256.clone(),
53 version: f.version,
54 size_bytes: f.size_bytes,
55 })
56 .collect(),
57 bom: lines
58 .iter()
59 .map(|l| LineState {
60 child: l.child_number.clone(),
61 name: l.child_name.clone(),
62 find_number: l.find_number,
63 quantity: l.quantity,
64 })
65 .collect(),
66 }
67 }
68}
69
70struct ChildUse<'a> {
73 name: &'a str,
74 quantity: f64,
75 find_numbers: BTreeSet<i32>,
76}
77
78fn child_uses(lines: &[LineState]) -> BTreeMap<&str, ChildUse<'_>> {
79 let mut out: BTreeMap<&str, ChildUse> = BTreeMap::new();
80 for l in lines {
81 let e = out.entry(l.child.as_str()).or_insert(ChildUse {
82 name: &l.name,
83 quantity: 0.0,
84 find_numbers: BTreeSet::new(),
85 });
86 e.quantity += l.quantity;
87 e.find_numbers.insert(l.find_number);
88 }
89 out
90}
91
92pub fn compare(old: &Snapshot, new: &Snapshot) -> Value {
95 let attributes: Vec<Value> = old
96 .attributes
97 .iter()
98 .zip(&new.attributes)
99 .filter(|((_, a), (_, b))| a != b)
100 .map(|((field, a), (_, b))| json!({ "field": field, "from": a, "to": b }))
101 .collect();
102
103 let key = |f: &FileState| f.filename.to_lowercase();
104 let old_files: BTreeMap<String, &FileState> = old.files.iter().map(|f| (key(f), f)).collect();
105 let new_files: BTreeMap<String, &FileState> = new.files.iter().map(|f| (key(f), f)).collect();
106 let mut files_added = Vec::new();
107 let mut files_changed = Vec::new();
108 let mut files_unchanged = 0usize;
109 for (k, f) in &new_files {
110 match old_files.get(k) {
111 None => files_added.push(json!({
112 "filename": f.filename, "version": f.version, "size_bytes": f.size_bytes,
113 })),
114 Some(o) if o.sha256 != f.sha256 => files_changed.push(json!({
115 "filename": f.filename,
116 "from_version": o.version,
117 "to_version": f.version,
118 "from_size_bytes": o.size_bytes,
119 "to_size_bytes": f.size_bytes,
120 })),
121 Some(_) => files_unchanged += 1,
122 }
123 }
124 let files_removed: Vec<Value> = old_files
125 .iter()
126 .filter(|(k, _)| !new_files.contains_key(*k))
127 .map(|(_, f)| json!({ "filename": f.filename, "version": f.version }))
128 .collect();
129
130 let old_bom = child_uses(&old.bom);
131 let new_bom = child_uses(&new.bom);
132 let mut bom_added = Vec::new();
133 let mut bom_changed = Vec::new();
134 for (child, n) in &new_bom {
135 match old_bom.get(child) {
136 None => bom_added.push(json!({
137 "item": child, "name": n.name, "quantity": n.quantity, "find_numbers": n.find_numbers,
138 })),
139 Some(o) => {
140 let quantity = (o.quantity - n.quantity).abs() > 1e-9;
141 let find = o.find_numbers != n.find_numbers;
142 if quantity || find {
143 bom_changed.push(json!({
144 "item": child,
145 "name": n.name,
146 "from_quantity": o.quantity,
147 "to_quantity": n.quantity,
148 "from_find_numbers": o.find_numbers,
149 "to_find_numbers": n.find_numbers,
150 }));
151 }
152 }
153 }
154 }
155 let bom_removed: Vec<Value> = old_bom
156 .iter()
157 .filter(|(c, _)| !new_bom.contains_key(*c))
158 .map(|(c, o)| json!({ "item": c, "name": o.name, "quantity": o.quantity }))
159 .collect();
160
161 let identical = attributes.is_empty()
162 && files_added.is_empty()
163 && files_changed.is_empty()
164 && files_removed.is_empty()
165 && bom_added.is_empty()
166 && bom_changed.is_empty()
167 && bom_removed.is_empty();
168 json!({
169 "from": old.label,
170 "to": new.label,
171 "identical": identical,
172 "attributes": attributes,
173 "files": {
174 "added": files_added,
175 "removed": files_removed,
176 "new_content": files_changed,
177 "unchanged": files_unchanged,
178 },
179 "bom": {
180 "added": bom_added,
181 "removed": bom_removed,
182 "changed": bom_changed,
183 },
184 })
185}
186
187async fn snapshot(st: &AppState, me: &Actor, rev: &Revision) -> AppResult<Snapshot> {
188 let file_list = files::list(&st.db, me, rev.id).await?;
189 let lines = bom::lines(&st.db, me, rev.id).await?;
190 Ok(Snapshot::of(rev, &file_list, &lines))
191}
192
193pub(super) async fn revision_pair(
196 st: &AppState,
197 me: &Actor,
198 a: &Args<'_>,
199) -> AppResult<(Revision, Revision)> {
200 let item = items::get_by_number(&st.db, me, &a.req_str("item")?).await?;
201 let revs = items::revisions(&st.db, me, item.id).await?;
203 let find = |code: &str| {
204 revs.iter()
205 .find(|r| r.code.eq_ignore_ascii_case(code))
206 .ok_or_else(|| AppError::NotFound(format!("{} has no revision {code}.", item.number)))
207 };
208 let to = match a.opt_str("to")? {
209 Some(c) if !c.eq_ignore_ascii_case("latest") => find(&c)?,
210 _ => revs
211 .first()
212 .ok_or_else(|| AppError::NotFound(format!("{} has no revisions.", item.number)))?,
213 };
214 let from = match a.opt_str("from")? {
215 Some(c) => find(&c)?,
216 None => revs.iter().find(|r| r.seq < to.seq).ok_or_else(|| {
217 bad(format!(
218 "{} is the first revision; there is nothing before it to compare with.",
219 revisions::label(to)
220 ))
221 })?,
222 };
223 Ok((from.clone(), to.clone()))
224}
225
226pub(super) async fn compare_revisions(st: &AppState, me: &Actor, a: &Args<'_>) -> AppResult<Value> {
227 let (from, to) = revision_pair(st, me, a).await?;
228 let (from, to) = (&from, &to);
229 let old = snapshot(st, me, from).await?;
230 let new = snapshot(st, me, to).await?;
231 let mut value = compare(&old, &new);
232 value["from_state"] = json!(from.state);
233 value["to_state"] = json!(to.state);
234 Ok(value)
235}