1use std::{
17 ffi::OsStr,
18 fs::{self, File},
19 io::Write,
20 path::{Path, PathBuf},
21 sync::Arc,
22};
23
24#[cfg(feature = "pyo3")]
25use pyo3::{exceptions::PyRuntimeError, PyErr};
26use serde::{Deserialize, Serialize};
27use strum::EnumIter;
28use thiserror::Error;
29
30pub mod ca_analysis;
31pub mod constraints;
32pub mod control_loop_sequences;
33pub mod control_loops;
34pub mod elements;
35pub mod hazards;
36pub mod interactions;
37pub mod losses;
38pub mod scenarios;
39pub mod uca_contexts;
40pub mod ucas;
41
42use ca_analysis::CaAnalyses;
43use constraints::Constraints;
44use control_loop_sequences::ControlLoopSequences;
45use control_loops::ControlLoops;
46use elements::Elements;
47use hazards::Hazards;
48use interactions::Interactions;
49use losses::Losses;
50use scenarios::Scenarios;
51use uca_contexts::UcaContexts;
52use ucas::Ucas;
53
54use crate::define_default_link_type_impls;
55
56#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
57pub struct CausalScenarioLink(Arc<str>);
58
59#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
60pub struct ControlLoopLink(Arc<str>);
61
62#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
63pub struct ControlLoopSequenceLink(Arc<str>);
64
65#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
66pub struct InteractionLink(Arc<str>);
67
68#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
69pub struct ElementLink(Arc<str>);
70
71#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
72pub struct HazardLink(Arc<str>);
73
74#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
75pub struct ConstraintLink(Arc<str>);
76
77#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
78pub struct UcaContextLink(Arc<str>);
79
80define_default_link_type_impls!(CausalScenarioLink);
81define_default_link_type_impls!(ControlLoopLink);
82define_default_link_type_impls!(ControlLoopSequenceLink);
83define_default_link_type_impls!(InteractionLink);
84define_default_link_type_impls!(ElementLink);
85define_default_link_type_impls!(HazardLink);
86define_default_link_type_impls!(ConstraintLink);
87define_default_link_type_impls!(UcaContextLink);
88
89#[derive(Error, Debug)]
90pub enum LoadError {
91 #[error("Could not read from file")]
92 Io(#[from] std::io::Error),
93 #[error("Could not parse YAML")]
94 Yaml(#[from] serde_yaml::Error),
95 #[error("Could read from file with that format")]
96 NotSupportedFormat,
97 #[error("Could not read from CSV")]
98 Csv(#[from] csv::Error),
99}
100
101#[cfg(feature = "pyo3")]
102impl From<LoadError> for PyErr {
103 fn from(value: LoadError) -> Self {
104 PyRuntimeError::new_err(value.to_string())
105 }
106}
107
108#[derive(Debug, Serialize, Deserialize, PartialEq, EnumIter, Clone, Copy)]
109pub enum UcaType {
110 NotProvided,
111 Provided,
112 MagnitudeLess,
113 MagnitudeMore,
114 DurationShort,
115 DurationLong,
116 TimingEarly,
117 TimingLate,
118 SequenceOrder,
119 Missing,
120}
121
122impl From<&str> for UcaType {
123 fn from(raw: &str) -> Self {
124 match raw.trim() {
125 "NP" => UcaType::NotProvided,
126 "PR" => UcaType::Provided,
127 "ML" => UcaType::MagnitudeLess,
128 "MM" => UcaType::MagnitudeMore,
129 "DS" => UcaType::DurationShort,
130 "DL" => UcaType::DurationLong,
131 "TE" => UcaType::TimingEarly,
132 "TL" => UcaType::TimingLate,
133 "SO" => UcaType::SequenceOrder,
134 _ => UcaType::Missing,
135 }
136 }
137}
138
139#[derive(Debug, Serialize, Deserialize, Clone)]
140pub struct StpaProject {
141 pub root: Option<PathBuf>,
142 pub project_file: Option<PathBuf>,
143 pub losses: Option<PathBuf>,
144 pub hazards: Option<PathBuf>,
145 pub elements: Option<PathBuf>,
146 pub interactions: Option<PathBuf>,
147 pub constraints: Option<PathBuf>,
148 #[serde(rename = "constraints-folder")]
149 pub constraints_folder: Option<PathBuf>,
150 pub ca_analysis: Option<PathBuf>,
151 pub uca_contexts: Option<PathBuf>,
152 pub uca: Option<PathBuf>,
153 pub control_loops: Option<PathBuf>,
154 pub cl_sequences: Option<PathBuf>,
155 pub scenarios: Option<PathBuf>,
156 pub auto: Option<Vec<PathBuf>>,
157 diagram: Option<PathBuf>,
158}
159
160impl StpaProject {
161 pub fn from_yaml(text: &str) -> Result<StpaProject, LoadError> {
162 serde_yaml::from_str::<StpaProject>(text).map_err(LoadError::Yaml)
163 }
164 pub fn to_yaml(&self) -> Result<String, LoadError> {
165 serde_yaml::to_string(&self).map_err(LoadError::Yaml)
166 }
167 pub fn from_file(filename: &Path) -> Result<StpaProject, LoadError> {
168 let mut new = match filename.extension().and_then(OsStr::to_str) {
169 Some("yml") | Some("yaml") => {
170 let text = fs::read_to_string(filename)?;
171 Self::from_yaml(&text)
172 }
173 _ => Err(LoadError::NotSupportedFormat),
174 }?;
175 if new.root.is_none() {
176 if let Some(new_root) = filename.parent() {
177 new.root = Some(new_root.to_owned());
178 }
179 }
180 new.project_file = Some(filename.to_owned());
181 Ok(new)
182 }
183}
184
185#[derive(Debug, Default, Clone, PartialEq)]
186pub struct StpaData {
187 pub losses: Losses,
188 pub elements: Elements,
189 pub interactions: Interactions,
190 pub hazards: Hazards,
191 pub constraints: Constraints,
192 pub ca_analyses: CaAnalyses,
193 pub uca_contexts: UcaContexts,
194 pub ucas: Ucas,
195 pub control_loops: ControlLoops,
196 pub control_loop_sequences: ControlLoopSequences,
197 pub scenarios: Scenarios,
198 pub diagram: Option<url::Url>,
199}
200
201#[derive(Error, Debug)]
202pub enum ProjectError {
203 #[error("Could not parse path")]
204 Io(#[from] std::io::Error),
205 #[error("Could not load losses")]
206 Losses(LoadError, PathBuf),
207 #[error("Could not load hazards")]
208 Hazards(LoadError, PathBuf),
209 #[error("Could not load interactions")]
210 Interactions(LoadError, PathBuf),
211 #[error("Could not load elements")]
212 Elements(LoadError, PathBuf),
213 #[error("Could not load constraints")]
214 Constraints(LoadError, PathBuf),
215 #[error("Could not load CA analysis")]
216 CaAnalyses(LoadError, PathBuf),
217 #[error("Could not load UCAs")]
218 Ucas(LoadError, PathBuf),
219 #[error("Could not load UCA contexts")]
220 UcaContexts(LoadError, PathBuf),
221 #[error("Could not load control loops")]
222 ControlLoops(LoadError, PathBuf),
223 #[error("Could not load control loop sequences")]
224 ControlLoopSequences(LoadError, PathBuf),
225 #[error("Could not load scenarios")]
226 Scenarios(LoadError, PathBuf),
227}
228
229#[cfg(feature = "pyo3")]
230impl From<ProjectError> for PyErr {
231 fn from(value: ProjectError) -> Self {
232 PyRuntimeError::new_err(value.to_string())
233 }
234}
235
236const CYCLE_DETECTED_MSG: &str = "Detected a reference cycle, not expanding further.";
237
238impl StpaData {
239 pub fn save_project(&self, root: &Path) {
240 let mut f = File::create(root.join("stpa-losses.yml")).expect("Unable to create file");
241 f.write_all(self.losses.to_yaml().unwrap().as_bytes())
242 .expect("Unable to write data");
243 let mut f = File::create(root.join("stpa-elements.yml")).expect("Unable to create file");
244 f.write_all(self.elements.to_yaml().unwrap().as_bytes())
245 .expect("Unable to write data");
246 let mut f =
247 File::create(root.join("stpa-interactions.yml")).expect("Unable to create file");
248 f.write_all(self.interactions.to_yaml().unwrap().as_bytes())
249 .expect("Unable to write data");
250 let mut f = File::create(root.join("stpa-hazards.yml")).expect("Unable to create file");
251 f.write_all(self.hazards.to_yaml().unwrap().as_bytes())
252 .expect("Unable to write data");
253 let mut f = File::create(root.join("stpa-constraints.yml")).expect("Unable to create file");
254 f.write_all(self.constraints.to_yaml().unwrap().as_bytes())
255 .expect("Unable to write data");
256 let mut f = File::create(root.join("stpa-ca_analyses.yml")).expect("Unable to create file");
257 f.write_all(self.ca_analyses.to_yaml().unwrap().as_bytes())
258 .expect("Unable to write data");
259 let mut f =
260 File::create(root.join("stpa-uca_contexts.yml")).expect("Unable to create file");
261 f.write_all(self.uca_contexts.to_yaml().unwrap().as_bytes())
262 .expect("Unable to write data");
263 let mut f = File::create(root.join("stpa-ucas.yml")).expect("Unable to create file");
264 f.write_all(self.ucas.to_yaml().unwrap().as_bytes())
265 .expect("Unable to write data");
266 let mut f =
267 File::create(root.join("stpa-control_loops.yml")).expect("Unable to create file");
268 f.write_all(self.control_loops.to_yaml().unwrap().as_bytes())
269 .expect("Unable to write data");
270 let mut f = File::create(root.join("stpa-control_loop_sequences.yml"))
271 .expect("Unable to create file");
272 f.write_all(self.control_loop_sequences.to_yaml().unwrap().as_bytes())
273 .expect("Unable to write data");
274 let mut f = File::create(root.join("stpa-scenarios.yml")).expect("Unable to create file");
275 f.write_all(self.scenarios.to_yaml().unwrap().as_bytes())
276 .expect("Unable to write data");
277 }
278
279 pub fn describe(&self, element_id: &str) -> Result<serde_json::Value, String> {
280 let mut stack = Vec::new();
282 let desc = self.describe_inner(element_id, &mut stack)?;
283 debug_assert!(stack.is_empty());
284 Ok(desc)
285 }
286
287 fn describe_inner(
288 &self,
289 element_id: &str,
290 currently_expanding: &mut Vec<String>,
291 ) -> Result<serde_json::Value, String> {
292 if let Some(e) = self.losses.find(element_id) {
293 return Ok(e.describe());
294 }
295 if let Some(e) = self.elements.find(element_id) {
296 return Ok(e.describe());
297 }
298 if let Some(e) = self.interactions.find(element_id) {
299 return e.describe(self, currently_expanding);
300 }
301 if let Some(e) = self.hazards.find(element_id) {
302 return e.describe(self, currently_expanding);
303 }
304 if let Some(e) = self.constraints.find(element_id) {
305 return e.describe(self, currently_expanding);
306 }
307 if let Some(e) = self.ca_analyses.find(element_id) {
308 return e.describe(self, currently_expanding);
309 }
310 if let Some(e) = self.uca_contexts.find(element_id) {
311 return Ok(e.describe());
312 }
313 if let Some(e) = self.ucas.find(element_id) {
314 return e.describe(self, currently_expanding);
315 }
316 if let Some(e) = self.control_loops.find(element_id) {
317 return e.describe(self, currently_expanding);
318 }
319 if let Some(e) = self.control_loop_sequences.find(element_id) {
320 return e.describe(self, currently_expanding);
321 }
322 let e = self
323 .scenarios
324 .find(element_id)
325 .ok_or_else(|| format!("Failed to find any item matching {element_id}"))?;
326 e.describe(self, currently_expanding)
327 }
328}
329
330impl TryFrom<StpaProject> for StpaData {
331 type Error = ProjectError;
332
333 fn try_from(project: StpaProject) -> Result<Self, Self::Error> {
334 let mut stpa_data = StpaData::default();
335
336 if let Some(mut losses) = project.losses {
337 if let Some(root) = &project.root {
338 losses = root.join(losses);
339 }
340 let losses = losses.canonicalize().unwrap_or(losses);
341 stpa_data.losses =
342 Losses::from_file(&losses).map_err(|err| ProjectError::Losses(err, losses))?;
343 }
344 if let Some(mut elements) = project.elements {
345 if let Some(root) = &project.root {
346 elements = root.join(elements);
347 }
348 let elements = elements.canonicalize().unwrap_or(elements);
349 stpa_data.elements = Elements::from_file(&elements)
350 .map_err(|err| ProjectError::Elements(err, elements))?;
351 };
352 if let Some(mut interactions) = project.interactions {
353 if let Some(root) = &project.root {
354 interactions = root.join(interactions);
355 }
356 interactions = interactions.canonicalize().unwrap_or(interactions);
357 stpa_data.interactions = Interactions::from_file(&interactions)
358 .map_err(|err| ProjectError::Interactions(err, interactions))?;
359 };
360 if let Some(mut hazards) = project.hazards {
361 if let Some(root) = &project.root {
362 hazards = root.join(hazards);
363 }
364 let hazards = hazards.canonicalize().unwrap_or(hazards);
365 stpa_data.hazards =
366 Hazards::from_file(&hazards).map_err(|err| ProjectError::Hazards(err, hazards))?;
367 };
368 if let Some(mut constraints) = project.constraints {
369 if let Some(root) = &project.root {
370 constraints = root.join(constraints);
371 }
372 let constraints = constraints.canonicalize().unwrap_or(constraints);
373 stpa_data.constraints = Constraints::from_file(&constraints)
374 .map_err(|err| ProjectError::Constraints(err, constraints))?;
375 };
376 if let Some(mut ca_analyses) = project.ca_analysis {
377 if let Some(root) = &project.root {
378 ca_analyses = root.join(ca_analyses);
379 }
380 let ca_analyses = ca_analyses.canonicalize().unwrap_or(ca_analyses);
381 stpa_data.ca_analyses = CaAnalyses::from_file(&ca_analyses)
382 .map_err(|err| ProjectError::CaAnalyses(err, ca_analyses))?;
383 };
384 if let Some(mut uca_contexts) = project.uca_contexts {
385 if let Some(root) = &project.root {
386 uca_contexts = root.join(uca_contexts);
387 }
388 let uca_contexts = uca_contexts.canonicalize().unwrap_or(uca_contexts);
389 stpa_data.uca_contexts = UcaContexts::from_file(&uca_contexts)
390 .map_err(|err| ProjectError::UcaContexts(err, uca_contexts))?;
391 };
392 if let Some(mut ucas) = project.uca {
393 if let Some(root) = &project.root {
394 ucas = root.join(ucas);
395 }
396 let ucas = ucas.canonicalize().unwrap_or(ucas);
397 stpa_data.ucas = Ucas::from_file(&ucas).map_err(|err| ProjectError::Ucas(err, ucas))?;
398 };
399 if let Some(mut control_loops) = project.control_loops {
400 if let Some(root) = &project.root {
401 control_loops = root.join(control_loops);
402 }
403 let control_loops = control_loops.canonicalize().unwrap_or(control_loops);
404 stpa_data.control_loops = ControlLoops::from_file(&control_loops)
405 .map_err(|err| ProjectError::ControlLoops(err, control_loops))?;
406 };
407 if let Some(mut control_loop_sequences) = project.cl_sequences {
408 if let Some(root) = &project.root {
409 control_loop_sequences = root.join(control_loop_sequences);
410 }
411 control_loop_sequences = control_loop_sequences
412 .canonicalize()
413 .unwrap_or(control_loop_sequences);
414 stpa_data.control_loop_sequences =
415 ControlLoopSequences::from_file(&control_loop_sequences).map_err(|err| {
416 ProjectError::ControlLoopSequences(err, control_loop_sequences)
417 })?;
418 };
419 if let Some(mut scenarios) = project.scenarios {
420 if let Some(root) = &project.root {
421 scenarios = root.join(scenarios);
422 }
423 let scenarios = scenarios.canonicalize().unwrap_or(scenarios);
424 stpa_data.scenarios = Scenarios::from_file(&scenarios)
425 .map_err(|err| ProjectError::Scenarios(err, scenarios))?;
426 };
427 if let Some(mut diagram) = project.diagram {
428 if let Some(root) = &project.root {
429 diagram = root.join(diagram);
430 }
431 stpa_data.diagram = Some(
432 url::Url::from_file_path(diagram.canonicalize()?)
433 .expect("This should never fail with a canonicalized path"),
434 );
435 };
436 Ok(stpa_data)
437 }
438}