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