rafia_stpa/stpa/
ca_analysis.rs1use 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::{InteractionLink, LoadError, UcaContextLink, UcaType, CYCLE_DETECTED_MSG};
28
29use super::{HazardLink, StpaData};
30
31#[derive(Debug, Serialize, Deserialize)]
32pub struct CaAnalysisCsv {
33 #[serde(rename = "CA Analysis ID")]
34 pub id: String,
35 #[serde(rename = "CA Id")]
36 pub ca_id: String,
37 #[serde(rename = "UCAType")]
38 pub uca_type: String,
39 #[serde(rename = "UCA Context")]
40 pub uca_context: String,
41 #[serde(rename = "Analysis Result")]
42 pub analysis_result: String,
43 #[serde(rename = "Hazard(s)")]
44 pub link_hazards: String,
45 #[serde(rename = "Justification")]
46 pub justification: String,
47}
48
49#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
50pub struct CaAnalysis {
51 pub id: String,
52 pub ca_id: InteractionLink,
53 pub uca_type: UcaType,
54 pub uca_context: UcaContextLink,
55 pub analysis_result: CaResult,
56 pub link_to_hazards: Vec<HazardLink>,
57 pub justification: String,
58}
59
60impl CaAnalysis {
61 pub fn describe(
62 &self,
63 stpa_data: &StpaData,
64 currently_expanding: &mut Vec<String>,
65 ) -> Result<serde_json::Value, String> {
66 if currently_expanding.contains(&self.id) {
68 return Ok(serde_json::json!({
69 "ca_analysis": {
70 "id": self.id,
71 "message": CYCLE_DETECTED_MSG,
72 }
73 }));
74 } else {
75 currently_expanding.push(self.id.clone());
76 }
77
78 let mut obj = serde_json::json!({"ca_analysis": self});
79
80 obj["ca_analysis"]["ca_id"] =
81 stpa_data.describe_inner(self.ca_id.0.as_ref(), currently_expanding)?;
82 obj["ca_analysis"]["uca_context"] =
83 stpa_data.describe_inner(self.uca_context.0.as_ref(), currently_expanding)?;
84
85 let hazards = self
86 .link_to_hazards
87 .iter()
88 .map(|hazard| {
89 stpa_data
90 .describe_inner(hazard.0.as_ref(), currently_expanding)
91 .map_err(|e| {
92 format!(
93 "Failed while resolving hazard id {hazard} of ca_analysis {}: {e}",
94 self.id
95 )
96 })
97 })
98 .collect::<Result<Vec<_>, _>>()?;
99 obj["ca_analysis"]["link_to_hazards"] = serde_json::json!(hazards);
100
101 let popped = currently_expanding.pop();
102 debug_assert_eq!(popped.as_ref(), Some(&self.id));
103 Ok(obj)
104 }
105}
106
107impl From<CaAnalysisCsv> for CaAnalysis {
108 fn from(csv: CaAnalysisCsv) -> Self {
109 CaAnalysis {
110 id: csv.id,
111 ca_id: InteractionLink::from(csv.ca_id),
112 uca_type: UcaType::from(csv.uca_type.as_str()),
113 uca_context: UcaContextLink::from(csv.uca_context),
114 analysis_result: CaResult::from(csv.analysis_result.as_str()),
115 link_to_hazards: csv
116 .link_hazards
117 .split(",")
118 .filter(|e| !e.is_empty())
119 .map(|s| HazardLink::from(s.trim().to_string()))
120 .collect(),
121 justification: csv.justification,
122 }
123 }
124}
125
126#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Default)]
127pub struct CaAnalyses {
128 pub ca_analyses: Vec<CaAnalysis>,
129}
130
131impl CaAnalyses {
132 pub fn from_yaml(text: &str) -> Result<CaAnalyses, LoadError> {
133 serde_yaml::from_str::<CaAnalyses>(text).map_err(LoadError::Yaml)
134 }
135 pub fn to_yaml(&self) -> Result<String, LoadError> {
136 serde_yaml::to_string(&self).map_err(LoadError::Yaml)
137 }
138 pub fn from_csv(filename: &Path) -> Result<CaAnalyses, LoadError> {
139 let mut ca_analyses = vec![];
140 let file = File::open(filename)?;
141 for ca_analysis in csv::ReaderBuilder::new()
142 .from_reader(file)
143 .deserialize::<CaAnalysisCsv>()
144 {
145 ca_analyses.push(ca_analysis?.into());
146 }
147 Ok(CaAnalyses { ca_analyses })
148 }
149 pub fn from_file(filename: &Path) -> Result<CaAnalyses, LoadError> {
150 match filename.extension().and_then(OsStr::to_str) {
151 Some("csv") => Self::from_csv(filename),
152 Some("yml") | Some("yaml") => {
153 let text = fs::read_to_string(filename)?;
154 Self::from_yaml(&text)
155 }
156 _ => Err(LoadError::NotSupportedFormat),
157 }
158 }
159 pub fn find(&self, id: &str) -> Option<&CaAnalysis> {
160 self.ca_analyses.iter().find(|ca| ca.id == id)
161 }
162}
163
164#[derive(Debug, Serialize, Deserialize, PartialEq, EnumIter, Clone, Copy)]
165pub enum CaResult {
166 UCA,
167 Safe,
168 NA,
169 NotYetAnalysed,
170 Missing,
171}
172
173impl From<&str> for CaResult {
174 fn from(raw: &str) -> Self {
175 match raw.trim() {
176 "UCA" => CaResult::UCA,
177 "Safe" => CaResult::Safe,
178 "N/A" => CaResult::NA,
179 "TBD" | "" => CaResult::NotYetAnalysed,
180 _ => CaResult::Missing,
181 }
182 }
183}