1use std::path::PathBuf;
6
7use super::FieldName;
8
9#[derive(Debug, Default, Clone)]
14pub struct EditUpdates {
15 pub name: Option<String>,
17 pub description: Option<String>,
19 pub refines: Option<Vec<String>>,
21 pub derives_from: Option<Vec<String>>,
23 pub satisfies: Option<Vec<String>>,
25 pub specification: Option<String>,
27 pub platform: Option<String>,
29}
30
31impl EditUpdates {
32 pub fn has_updates(&self) -> bool {
34 self.name.is_some()
35 || self.description.is_some()
36 || self.refines.is_some()
37 || self.derives_from.is_some()
38 || self.satisfies.is_some()
39 || self.specification.is_some()
40 || self.platform.is_some()
41 }
42}
43
44#[derive(Debug, Clone)]
46pub struct EditSummary {
47 pub item_id: String,
49 pub file_path: PathBuf,
51 pub changes: Vec<FieldChange>,
53}
54
55impl EditSummary {
56 pub fn has_changes(&self) -> bool {
58 self.changes.iter().any(|c| c.is_changed())
59 }
60
61 pub fn actual_changes(&self) -> Vec<&FieldChange> {
63 self.changes.iter().filter(|c| c.is_changed()).collect()
64 }
65}
66
67#[derive(Debug, Clone)]
69pub struct FieldChange {
70 pub field: FieldName,
72 pub old_value: String,
74 pub new_value: String,
76}
77
78impl FieldChange {
79 pub fn new(
81 field: FieldName,
82 old_value: impl Into<String>,
83 new_value: impl Into<String>,
84 ) -> Self {
85 Self {
86 field,
87 old_value: old_value.into(),
88 new_value: new_value.into(),
89 }
90 }
91
92 pub fn is_changed(&self) -> bool {
94 self.old_value != self.new_value
95 }
96}
97
98#[derive(Debug, Default, Clone)]
104pub struct TraceabilityLinks {
105 pub refines: Vec<String>,
107 pub derives_from: Vec<String>,
109 pub satisfies: Vec<String>,
111}
112
113impl TraceabilityLinks {
114 pub fn is_empty(&self) -> bool {
116 self.refines.is_empty() && self.derives_from.is_empty() && self.satisfies.is_empty()
117 }
118
119 pub fn from_upstream(upstream: &super::UpstreamRefs) -> Self {
121 Self {
122 refines: upstream
123 .refines
124 .iter()
125 .map(|id| id.as_str().to_string())
126 .collect(),
127 derives_from: upstream
128 .derives_from
129 .iter()
130 .map(|id| id.as_str().to_string())
131 .collect(),
132 satisfies: upstream
133 .satisfies
134 .iter()
135 .map(|id| id.as_str().to_string())
136 .collect(),
137 }
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144
145 #[test]
146 fn test_edit_updates_has_updates_empty() {
147 let updates = EditUpdates::default();
148 assert!(!updates.has_updates());
149 }
150
151 #[test]
152 fn test_edit_updates_has_updates_name() {
153 let updates = EditUpdates {
154 name: Some("New Name".to_string()),
155 ..Default::default()
156 };
157 assert!(updates.has_updates());
158 }
159
160 #[test]
161 fn test_edit_updates_has_updates_traceability() {
162 let updates = EditUpdates {
163 derives_from: Some(vec!["SCEN-001".to_string()]),
164 ..Default::default()
165 };
166 assert!(updates.has_updates());
167 }
168
169 #[test]
170 fn test_field_change_is_changed() {
171 let changed = FieldChange::new(FieldName::Name, "Old", "New");
172 assert!(changed.is_changed());
173
174 let unchanged = FieldChange::new(FieldName::Name, "Same", "Same");
175 assert!(!unchanged.is_changed());
176 }
177
178 #[test]
179 fn test_edit_summary_has_changes() {
180 let summary = EditSummary {
181 item_id: "SREQ-001".to_string(),
182 file_path: PathBuf::from("test.md"),
183 changes: vec![
184 FieldChange::new(FieldName::Name, "Old", "New"),
185 FieldChange::new(FieldName::Description, "Same", "Same"),
186 ],
187 };
188 assert!(summary.has_changes());
189 assert_eq!(summary.actual_changes().len(), 1);
190 }
191
192 #[test]
193 fn test_edit_summary_no_changes() {
194 let summary = EditSummary {
195 item_id: "SREQ-001".to_string(),
196 file_path: PathBuf::from("test.md"),
197 changes: vec![FieldChange::new(FieldName::Name, "Same", "Same")],
198 };
199 assert!(!summary.has_changes());
200 assert_eq!(summary.actual_changes().len(), 0);
201 }
202}