sara_core/model/
edit.rs

1//! Edit command types for updating document metadata.
2//!
3//! Provides types for FR-054 through FR-066 (Edit Command).
4
5use std::path::PathBuf;
6
7use super::FieldName;
8
9/// Fields to update via CLI flags (non-interactive mode).
10///
11/// Used for non-interactive editing where the user specifies which
12/// fields to update via command-line flags (FR-057, FR-058).
13#[derive(Debug, Default, Clone)]
14pub struct EditUpdates {
15    /// New name for the item.
16    pub name: Option<String>,
17    /// New description for the item.
18    pub description: Option<String>,
19    /// New refines links (for UseCase, Scenario).
20    pub refines: Option<Vec<String>>,
21    /// New derives_from links (for requirements).
22    pub derives_from: Option<Vec<String>>,
23    /// New satisfies links (for architectures, designs).
24    pub satisfies: Option<Vec<String>>,
25    /// New specification (for requirement types).
26    pub specification: Option<String>,
27    /// New platform (for SystemArchitecture).
28    pub platform: Option<String>,
29}
30
31impl EditUpdates {
32    /// Returns true if any field is set (triggers non-interactive mode).
33    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/// Summary of changes made during an edit operation.
45#[derive(Debug, Clone)]
46pub struct EditSummary {
47    /// The ID of the edited item.
48    pub item_id: String,
49    /// Path to the modified file.
50    pub file_path: PathBuf,
51    /// List of field changes applied.
52    pub changes: Vec<FieldChange>,
53}
54
55impl EditSummary {
56    /// Returns true if any changes were actually made.
57    pub fn has_changes(&self) -> bool {
58        self.changes.iter().any(|c| c.is_changed())
59    }
60
61    /// Returns only the fields that were actually changed.
62    pub fn actual_changes(&self) -> Vec<&FieldChange> {
63        self.changes.iter().filter(|c| c.is_changed()).collect()
64    }
65}
66
67/// A single field change in an edit operation.
68#[derive(Debug, Clone)]
69pub struct FieldChange {
70    /// The field that was changed.
71    pub field: FieldName,
72    /// Previous value (for display in diff).
73    pub old_value: String,
74    /// New value (for display in diff).
75    pub new_value: String,
76}
77
78impl FieldChange {
79    /// Creates a new field change record.
80    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    /// Returns true if the value actually changed.
93    pub fn is_changed(&self) -> bool {
94        self.old_value != self.new_value
95    }
96}
97
98/// Traceability links as string IDs (for user input and editing).
99///
100/// This struct represents traceability links using plain strings,
101/// suitable for CLI input, interactive prompts, and serialization.
102/// Use `UpstreamRefs` for the validated graph model.
103#[derive(Debug, Default, Clone)]
104pub struct TraceabilityLinks {
105    /// Items this item refines (for UseCase, Scenario).
106    pub refines: Vec<String>,
107    /// Items this item derives from (for requirements).
108    pub derives_from: Vec<String>,
109    /// Items this item satisfies (for architectures, designs).
110    pub satisfies: Vec<String>,
111}
112
113impl TraceabilityLinks {
114    /// Returns true if all traceability fields are empty.
115    pub fn is_empty(&self) -> bool {
116        self.refines.is_empty() && self.derives_from.is_empty() && self.satisfies.is_empty()
117    }
118
119    /// Creates from an Item's upstream references.
120    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}