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