1use std::{
17 ffi::OsStr,
18 fs::{self, File},
19 path::Path,
20};
21
22use csv;
23use serde::{Deserialize, Serialize};
24use serde_yaml;
25use strum::EnumIter;
26
27use crate::stpa::{LoadError, CYCLE_DETECTED_MSG};
28
29use super::{constraints::UcaLink, ConstraintLink, ControlLoopSequenceLink, HazardLink, StpaData};
30
31#[derive(Debug, Serialize, Deserialize)]
33pub struct ScenarioCsv {
34 #[serde(rename = "Scenario Id")]
35 pub id: String,
36 #[serde(rename = "Seq Ref")]
37 pub sequence: String,
38 #[serde(rename = "CS Type")]
39 pub causal_scenario: String,
40 #[serde(rename = "Analysis result")]
41 pub analysis_result: String,
42 #[serde(rename = "Causal Scenario Definition")]
43 pub causal_scenario_defintion: String,
44 #[serde(rename = "UCA Links")]
45 pub ucas: String,
46 #[serde(rename = "Link to Hazard(s)")]
47 pub hazards: String,
48 #[serde(rename = "Constraint Id")]
49 pub constraint: String,
50 #[serde(rename = "Notes")]
51 pub notes: String,
52}
53
54#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
55pub struct Scenario {
56 pub id: String,
57 pub sequence: ControlLoopSequenceLink,
58 pub causal_scenario: CausalScenario,
59 pub causal_scenario_prompt: String,
60 pub analysis_result: CausalScenarioResult,
61 pub causal_scenario_definition: String,
62 pub ucas: Vec<UcaLink>,
63 pub hazards: Vec<HazardLink>,
64 pub constraint: Vec<ConstraintLink>,
65 pub notes: String,
66}
67
68impl Scenario {
69 pub fn describe(
70 &self,
71 stpa_data: &StpaData,
72 currently_expanding: &mut Vec<String>,
73 ) -> Result<serde_json::Value, String> {
74 if currently_expanding.contains(&self.id) {
76 return Ok(serde_json::json!({
77 "scenario": {
78 "id": self.id,
79 "message": CYCLE_DETECTED_MSG,
80 }
81 }));
82 } else {
83 currently_expanding.push(self.id.clone());
84 }
85
86 let mut obj = serde_json::json!({"scenario": self});
87
88 obj["scenario"]["sequence"] =
89 stpa_data.describe_inner(self.sequence.0.as_ref(), currently_expanding)?;
90
91 let ucas = self
92 .constraint
93 .iter()
94 .map(|constraint| {
95 stpa_data
96 .describe_inner(constraint.0.as_ref(), currently_expanding)
97 .map_err(|e| {
98 format!(
99 "Failed while resolving constraint id {constraint} of uca {}: {e}",
100 self.id
101 )
102 })
103 })
104 .collect::<Result<Vec<_>, _>>()?;
105
106 obj["scenario"]["constraint"] = serde_json::json!(ucas);
107
108 let hazards = self
109 .hazards
110 .iter()
111 .map(|hazard| {
112 stpa_data
113 .describe_inner(hazard.0.as_ref(), currently_expanding)
114 .map_err(|e| {
115 format!(
116 "Failed while resolving hazard id {hazard:?} of scenario {:?}: {e}",
117 self.id
118 )
119 })
120 })
121 .collect::<Result<Vec<_>, _>>()?;
122 obj["scenario"]["hazards"] = serde_json::json!(hazards);
123
124 let ucas = self
125 .ucas
126 .iter()
127 .map(|uca| {
128 stpa_data
129 .describe_inner(uca.as_ref(), currently_expanding)
130 .map_err(|e| {
131 format!(
132 "Failed while resolving UCA id {uca:?} of scenario {:?}: {e}",
133 self.id
134 )
135 })
136 })
137 .collect::<Result<Vec<_>, _>>()?;
138 obj["scenario"]["ucas"] = serde_json::json!(ucas);
139
140 let popped = currently_expanding.pop();
141 debug_assert_eq!(popped.as_ref(), Some(&self.id));
142 Ok(obj)
143 }
144}
145
146impl From<ScenarioCsv> for Scenario {
147 fn from(csv: ScenarioCsv) -> Self {
148 Scenario {
149 id: csv.id,
150 sequence: ControlLoopSequenceLink::from(csv.sequence),
151 causal_scenario: CausalScenario::from(csv.causal_scenario.as_str()),
152 causal_scenario_prompt: "".to_string(),
153 causal_scenario_definition: csv.causal_scenario_defintion,
154 analysis_result: CausalScenarioResult::from(csv.analysis_result.as_str()),
155 ucas: csv
156 .ucas
157 .split(",")
158 .filter(|e| !e.is_empty())
159 .map(|s| UcaLink::from(s.trim().to_string()))
160 .collect(),
161 hazards: csv
162 .hazards
163 .split(",")
164 .filter(|e| !e.is_empty())
165 .map(|s| HazardLink::from(s.trim().to_string()))
166 .collect(),
167 constraint: csv
168 .constraint
169 .split(",")
170 .filter(|e| !e.is_empty())
171 .map(|s| ConstraintLink::from(s.trim().to_string()))
172 .collect(),
173 notes: csv.notes,
174 }
175 }
176}
177
178#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
179pub struct Scenarios {
180 pub scenarios: Vec<Scenario>,
181}
182
183impl Scenarios {
184 pub fn from_yaml(text: &str) -> Result<Scenarios, LoadError> {
185 serde_yaml::from_str::<Scenarios>(text).map_err(LoadError::Yaml)
186 }
187 pub fn to_yaml(&self) -> Result<String, LoadError> {
188 serde_yaml::to_string(&self).map_err(LoadError::Yaml)
189 }
190 pub fn from_csv(filename: &Path) -> Result<Scenarios, LoadError> {
191 let mut scenarios = vec![];
192 let file = File::open(filename)?;
193 for scenario in csv::ReaderBuilder::new()
194 .from_reader(file)
195 .deserialize::<ScenarioCsv>()
196 {
197 scenarios.push(scenario?.into());
198 }
199 Ok(Scenarios { scenarios })
200 }
201 pub fn from_file(filename: &Path) -> Result<Scenarios, LoadError> {
202 match filename.extension().and_then(OsStr::to_str) {
203 Some("csv") => Self::from_csv(filename),
204 Some("yml") | Some("yaml") => {
205 let text = fs::read_to_string(filename)?;
206 Self::from_yaml(&text)
207 }
208 _ => Err(LoadError::NotSupportedFormat),
209 }
210 }
211 pub fn find(&self, id: &str) -> Option<&Scenario> {
212 self.scenarios.iter().find(|scenario| scenario.id == id)
213 }
214}
215
216#[derive(Debug, Serialize, Deserialize, PartialEq, EnumIter, Clone, Copy)]
217pub enum CausalScenario {
218 Controller,
219 ControlAlgorithm,
220 UnsafetControlInput,
221 ProcessModel,
222 ControllerDisturbance,
223 Feedback,
224 FeedbackPath,
225 UnsafeDate,
226 ControlAction,
227 ControlPath,
228 Process,
229 ConflictingControl,
230 ProcessInputs,
231 ProcessOutputs,
232 ProcessDisturbance,
233 Missing,
234}
235
236impl From<&str> for CausalScenario {
237 fn from(raw: &str) -> Self {
238 match raw.trim() {
239 "CS1-C" => CausalScenario::Controller,
240 "CS1-A" => CausalScenario::ControlAlgorithm,
241 "CS1-I" => CausalScenario::UnsafetControlInput,
242 "CS1-M" => CausalScenario::ProcessModel,
243 "CS1-D" => CausalScenario::ControllerDisturbance,
244 "CS2-F" => CausalScenario::Feedback,
245 "CS2-P" => CausalScenario::FeedbackPath,
246 "CS2-U" => CausalScenario::UnsafeDate,
247 "CS3-A" => CausalScenario::ControlAction,
248 "CS3-P" => CausalScenario::ControlPath,
249 "CS4-C" => CausalScenario::ConflictingControl,
250 "CS4-I" => CausalScenario::ProcessInputs,
251 "CS4-O" => CausalScenario::ProcessOutputs,
252 "CS4-D" => CausalScenario::ProcessDisturbance,
253 _ => CausalScenario::Missing,
254 }
255 }
256}
257
258#[derive(Debug, Serialize, Deserialize, PartialEq, EnumIter, Clone, Copy)]
259pub enum CausalScenarioResult {
260 UCA,
261 Hazard,
262 UcaAndHazard,
263 OutOfScope,
264 ScenarioAlreadyFound,
265 NotApplicable,
266 NotYetAnalysed,
267 Missing,
268}
269
270impl From<&str> for CausalScenarioResult {
271 fn from(raw: &str) -> Self {
272 match raw.trim() {
273 "UCA" => CausalScenarioResult::UCA,
274 "Hazard" => CausalScenarioResult::Hazard,
275 "Both" => CausalScenarioResult::UcaAndHazard,
276 "OOS" => CausalScenarioResult::OutOfScope,
277 "SAF" => CausalScenarioResult::ScenarioAlreadyFound,
278 "N/A" => CausalScenarioResult::NotApplicable,
279 "TBD" | "" => CausalScenarioResult::NotYetAnalysed,
280 _ => CausalScenarioResult::Missing,
281 }
282 }
283}