1use crate::markdown::Requirement;
11use crate::parse::Artifact;
12
13#[derive(Debug, Clone)]
16pub struct RequirementChange {
17 pub id: String,
18 pub old_text: String,
19 pub new_text: String,
20}
21
22#[derive(Debug, Clone, Default)]
24pub struct Diff {
25 pub added_requirements: Vec<Requirement>,
26 pub removed_requirements: Vec<Requirement>,
27 pub modified_requirements: Vec<RequirementChange>,
28 pub added_metrics: Vec<String>,
29 pub removed_metrics: Vec<String>,
30 pub added_risks: Vec<String>,
31 pub removed_risks: Vec<String>,
32}
33
34impl Diff {
35 pub fn is_empty(&self) -> bool {
37 self.added_requirements.is_empty()
38 && self.removed_requirements.is_empty()
39 && self.modified_requirements.is_empty()
40 && self.added_metrics.is_empty()
41 && self.removed_metrics.is_empty()
42 && self.added_risks.is_empty()
43 && self.removed_risks.is_empty()
44 }
45}
46
47fn by_id(requirements: &[Requirement]) -> Vec<(&str, &Requirement)> {
51 let mut out: Vec<(&str, &Requirement)> = Vec::new();
52 for r in requirements {
53 if let Some(slot) = out.iter_mut().find(|(id, _)| *id == r.id) {
54 slot.1 = r;
55 } else {
56 out.push((r.id.as_str(), r));
57 }
58 }
59 out
60}
61
62fn ordered_difference(a: &[String], b: &[String]) -> Vec<String> {
65 let mut seen: Vec<&str> = Vec::new();
66 let mut out: Vec<String> = Vec::new();
67 for item in a {
68 if !b.contains(item) && !seen.contains(&item.as_str()) {
69 seen.push(item.as_str());
70 out.push(item.clone());
71 }
72 }
73 out
74}
75
76pub fn diff(old: &Artifact, new: &Artifact) -> Diff {
78 let old_reqs = by_id(&old.product.requirements);
79 let new_reqs = by_id(&new.product.requirements);
80
81 let mut result = Diff::default();
82
83 for (req_id, new_req) in &new_reqs {
85 match old_reqs.iter().find(|(id, _)| id == req_id) {
86 None => result.added_requirements.push((*new_req).clone()),
87 Some((_, old_req)) if old_req.text != new_req.text => {
88 result.modified_requirements.push(RequirementChange {
89 id: (*req_id).to_string(),
90 old_text: old_req.text.clone(),
91 new_text: new_req.text.clone(),
92 });
93 }
94 Some(_) => {}
95 }
96 }
97
98 for (req_id, old_req) in &old_reqs {
100 if !new_reqs.iter().any(|(id, _)| id == req_id) {
101 result.removed_requirements.push((*old_req).clone());
102 }
103 }
104
105 result.added_metrics =
106 ordered_difference(&new.product.success_metrics, &old.product.success_metrics);
107 result.removed_metrics =
108 ordered_difference(&old.product.success_metrics, &new.product.success_metrics);
109 result.added_risks = ordered_difference(&new.product.risks, &old.product.risks);
110 result.removed_risks = ordered_difference(&old.product.risks, &new.product.risks);
111
112 result
113}