1use std::{
17 ffi::OsStr,
18 fmt,
19 fs::{self, File},
20 io::Write,
21 path::Path,
22};
23
24use csv;
25use serde::{Deserialize, Serialize};
26use serde_yaml;
27use strum::EnumIter;
28
29use crate::stpa::{ElementLink, LoadError, CYCLE_DETECTED_MSG};
30
31use super::StpaData;
32
33#[derive(Debug, Serialize, Deserialize)]
34pub struct InteractionCsv {
35 #[serde(rename = "Interaction Id")]
36 pub id: String,
37 #[serde(rename = "Diagram Label")]
38 pub diagram_label: String,
39 #[serde(rename = "Interaction description")]
40 pub description: String,
41 #[serde(rename = "Type")]
42 pub interaction_type: String,
43 #[serde(rename = "Provider Id")]
44 pub start: String,
45 #[serde(rename = "Receiver Id")]
46 pub end: String,
47 #[serde(rename = "Category")]
48 pub catigory: String,
49 #[serde(rename = "Notes / Questions")]
50 pub notes: String,
51}
52
53#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, EnumIter)]
54pub enum InteractionType {
55 ControlAction,
56 Feedback,
57 Information,
58 ProcessInOut, OtherInteraction,
60 Missing,
61}
62
63impl fmt::Display for InteractionType {
64 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
65 write!(f, "{self:?}")
66 }
67}
68
69impl<T: AsRef<str>> From<T> for InteractionType {
70 fn from(raw: T) -> Self {
71 match raw.as_ref().trim() {
72 "C" => InteractionType::ControlAction,
73 "F" => InteractionType::Feedback,
74 "I" => InteractionType::Information,
75 "P" => InteractionType::ProcessInOut,
76 "X" => InteractionType::OtherInteraction,
77 _ => InteractionType::Missing,
78 }
79 }
80}
81
82#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Default)]
83pub struct Interactions {
84 pub interactions: Vec<Interaction>,
85}
86
87#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
88pub struct Interaction {
89 pub id: String,
90 pub diagram_label: String,
91 pub description: String,
92 pub interaction_type: InteractionType,
93 pub start: ElementLink,
94 pub end: ElementLink,
95 pub category: InteractionCategory,
96 pub note: String,
97}
98
99impl Interaction {
100 pub fn describe(
101 &self,
102 stpa_data: &StpaData,
103 currently_expanding: &mut Vec<String>,
104 ) -> Result<serde_json::Value, String> {
105 if currently_expanding.contains(&self.id) {
107 return Ok(serde_json::json!({
108 "interaction": {
109 "id": self.id,
110 "message": CYCLE_DETECTED_MSG,
111 }
112 }));
113 } else {
114 currently_expanding.push(self.id.clone());
115 }
116
117 let mut obj = serde_json::json!({
119 "interaction": self,
120 });
121
122 let start = stpa_data
123 .describe_inner(self.start.as_ref(), currently_expanding)
124 .map_err(|e| format!("Failed while resolving interaction.start: {e}"))?;
125 let end = stpa_data
126 .describe_inner(self.end.as_ref(), currently_expanding)
127 .map_err(|e| format!("Failed while resolving interaction.end: {e}"))?;
128
129 obj["interaction"]["start"] = start;
130 obj["interaction"]["end"] = end;
131
132 let popped = currently_expanding.pop();
133 debug_assert_eq!(popped.as_ref(), Some(&self.id));
134 Ok(obj)
135 }
136}
137
138impl From<InteractionCsv> for Interaction {
139 fn from(csv: InteractionCsv) -> Self {
140 Interaction {
141 id: csv.id,
142 diagram_label: csv.diagram_label,
143 description: csv.description,
144 interaction_type: InteractionType::from(csv.interaction_type.as_str()),
145 start: ElementLink::from(csv.start),
146 end: ElementLink::from(csv.end),
147 category: InteractionCategory::from(csv.catigory.as_str()),
148 note: csv.notes,
149 }
150 }
151}
152
153impl Interactions {
154 pub fn from_yaml(text: &str) -> Result<Interactions, LoadError> {
155 serde_yaml::from_str::<Interactions>(text).map_err(LoadError::Yaml)
156 }
157 pub fn to_yaml(&self) -> Result<String, LoadError> {
158 serde_yaml::to_string(&self).map_err(LoadError::Yaml)
159 }
160 pub fn from_csv(filename: &Path) -> Result<Interactions, LoadError> {
161 let mut interactions = vec![];
162 let file = File::open(filename)?;
163 for interaction in csv::ReaderBuilder::new()
164 .from_reader(file)
165 .deserialize::<InteractionCsv>()
166 {
167 interactions.push(interaction?.into());
168 }
169 Ok(Interactions { interactions })
170 }
171 pub fn from_file(filename: &Path) -> Result<Interactions, LoadError> {
172 match filename.extension().and_then(OsStr::to_str) {
173 Some("csv") => Self::from_csv(filename),
174 Some("yml") | Some("yaml") => {
175 let text = fs::read_to_string(filename)?;
176 Self::from_yaml(&text)
177 }
178 _ => Err(LoadError::NotSupportedFormat),
179 }
180 }
181 pub fn to_file(&self, filename: &Path) -> Result<(), LoadError> {
182 match filename.extension().and_then(OsStr::to_str) {
183 Some("yml") | Some("yaml") => {
184 let mut f = File::create(filename)?;
185 f.write_all(self.to_yaml()?.as_bytes())?;
186 Ok(())
187 }
188 _ => Err(LoadError::NotSupportedFormat),
189 }
190 }
191 pub fn find(&self, id: &str) -> Option<&Interaction> {
192 self.interactions
193 .iter()
194 .find(|interaction| interaction.id == id)
195 }
196}
197
198#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, EnumIter)]
199pub enum InteractionCategory {
200 Continuous,
201 Discrete,
202 Missing,
203}
204
205impl fmt::Display for InteractionCategory {
206 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
207 write!(f, "{self:?}")
208 }
209}
210
211impl From<&str> for InteractionCategory {
212 fn from(raw: &str) -> Self {
213 match raw.trim() {
214 "C" => InteractionCategory::Continuous,
215 "D" => InteractionCategory::Discrete,
216 _ => InteractionCategory::Missing,
217 }
218 }
219}
220
221#[cfg(test)]
222mod test {
223 use super::{Interactions, LoadError};
224
225 #[test]
226 fn test_load() -> Result<(), LoadError> {
227 let interactions = Interactions::from_yaml(
228 "interactions:\n- id: bob\n name: bob box\n diagram_label: test\n description: bob\n interaction_type: Feedback\n \
229 start: a\n end: b\n category: Continuous\n note: bobs",
230 ).unwrap();
231 assert_eq!(interactions.interactions.len(), 1);
232 assert_eq!(interactions.interactions[0].id, "bob");
233 assert_eq!(interactions.interactions[0].start.0, "a".into());
234 assert_eq!(interactions.interactions[0].end.0, "b".into());
235 Ok(())
236 }
237}