rafia_stpa/stpa/
hazards.rs1use std::{
17 ffi::OsStr,
18 fs::{self, File},
19 path::Path,
20 sync::Arc,
21};
22
23use csv;
24use serde::{Deserialize, Serialize};
25use serde_yaml;
26
27use crate::{
28 define_default_link_type_impls,
29 stpa::{LoadError, CYCLE_DETECTED_MSG},
30};
31
32use super::StpaData;
33
34#[derive(Debug, Serialize, Deserialize)]
35pub struct HazardCsv {
36 #[serde(rename = "Hazard Id")]
37 pub id: String,
38 #[serde(rename = "Hazard description: behaviour, state (condition) / event (time limited)")]
39 pub description: String,
40 #[serde(rename = "Hazard Link To Loss(es)")]
41 pub losses: String,
42 #[serde(rename = "Notes")]
43 pub notes: String,
44}
45
46#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
47pub struct LossLink(Arc<str>);
48
49define_default_link_type_impls!(LossLink);
50
51#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
52pub struct Hazard {
53 pub id: String,
54 pub description: String,
55 pub losses: Vec<LossLink>,
56 pub notes: String,
57}
58
59impl Hazard {
60 pub fn describe(
61 &self,
62 stpa_data: &StpaData,
63 currently_expanding: &mut Vec<String>,
64 ) -> Result<serde_json::Value, String> {
65 if currently_expanding.contains(&self.id) {
67 return Ok(serde_json::json!({
68 "hazard": {
69 "id": self.id,
70 "message": CYCLE_DETECTED_MSG,
71 }
72 }));
73 } else {
74 currently_expanding.push(self.id.clone());
75 }
76
77 let mut obj = serde_json::json!({"hazard": self});
78
79 let losses = self
80 .losses
81 .iter()
82 .map(|loss| {
83 stpa_data
84 .describe_inner(loss.0.as_ref(), currently_expanding)
85 .map_err(|e| {
86 format!(
87 "Failed while resolving loss id {loss} of hazard {}: {e}",
88 self.id
89 )
90 })
91 })
92 .collect::<Result<Vec<_>, _>>()?;
93 obj["hazard"]["losses"] = serde_json::json!(losses);
94
95 let popped = currently_expanding.pop();
96 debug_assert_eq!(popped.as_ref(), Some(&self.id));
97 Ok(obj)
98 }
99}
100
101impl From<HazardCsv> for Hazard {
102 fn from(csv: HazardCsv) -> Self {
103 Hazard {
104 id: csv.id,
105 description: csv.description,
106 losses: csv
107 .losses
108 .split(",")
109 .map(|s| LossLink(Arc::from(s.trim())))
110 .collect(),
111 notes: csv.notes,
112 }
113 }
114}
115
116#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Default)]
117pub struct Hazards {
118 pub hazards: Vec<Hazard>,
119}
120
121impl Hazards {
122 pub fn from_yaml(text: &str) -> Result<Hazards, LoadError> {
123 serde_yaml::from_str::<Hazards>(text).map_err(LoadError::Yaml)
124 }
125 pub fn to_yaml(&self) -> Result<String, LoadError> {
126 serde_yaml::to_string(&self).map_err(LoadError::Yaml)
127 }
128 pub fn from_csv(filename: &Path) -> Result<Hazards, LoadError> {
129 let mut hazards = vec![];
130 let file = File::open(filename)?;
131 for hazard in csv::ReaderBuilder::new()
132 .from_reader(file)
133 .deserialize::<HazardCsv>()
134 {
135 hazards.push(hazard?.into());
136 }
137 Ok(Hazards { hazards })
138 }
139 pub fn from_file(filename: &Path) -> Result<Hazards, LoadError> {
140 match filename.extension().and_then(OsStr::to_str) {
141 Some("csv") => Self::from_csv(filename),
142 Some("yml") | Some("yaml") => {
143 let text = fs::read_to_string(filename)?;
144 Self::from_yaml(&text)
145 }
146 _ => Err(LoadError::NotSupportedFormat),
147 }
148 }
149 pub fn find(&self, id: &str) -> Option<&Hazard> {
150 self.hazards.iter().find(|hazard| hazard.id == id)
151 }
152}