rafia_stpa/stpa/
elements.rs1use std::{
17 ffi::OsStr,
18 fs::{self, File},
19 path::Path,
20};
21
22use csv;
23use indexmap::IndexSet;
24use serde::{Deserialize, Serialize};
25use serde_yaml;
26use strum::EnumIter;
27
28use crate::stpa::LoadError;
29
30#[derive(Debug, Serialize, Deserialize)]
31pub struct ElementCsv {
32 #[serde(rename = "Element Id")]
33 pub id: String,
34 #[serde(rename = "Element Name")]
35 pub name: String,
36 #[serde(rename = "Responsibilities")]
37 pub responsibilities: String,
38 #[serde(rename = "Roles")]
39 pub roles: String,
40 #[serde(rename = "Notes")]
41 pub notes: String,
42}
43
44#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
45pub struct Element {
46 pub id: String,
47 pub name: String,
48 pub responsibilities: Vec<String>,
49 pub roles: IndexSet<RoleTypes>,
50 pub notes: String,
51}
52
53impl Element {
54 pub fn describe(&self) -> serde_json::Value {
55 serde_json::json!({"element": self})
56 }
57}
58
59impl From<ElementCsv> for Element {
60 fn from(csv: ElementCsv) -> Self {
61 Element {
62 id: csv.id,
63 name: csv.name,
64 responsibilities: csv
65 .responsibilities
66 .lines()
67 .map(|s| s.to_string())
68 .collect::<Vec<_>>(),
69 roles: csv.roles.split(",").map(|s| s.into()).collect(),
70 notes: csv.notes,
71 }
72 }
73}
74
75#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
76pub struct Elements {
77 pub elements: Vec<Element>,
78}
79
80impl Elements {
81 pub fn from_yaml(text: &str) -> Result<Elements, LoadError> {
82 serde_yaml::from_str::<Elements>(text).map_err(LoadError::Yaml)
83 }
84 pub fn to_yaml(&self) -> Result<String, LoadError> {
85 serde_yaml::to_string(&self).map_err(LoadError::Yaml)
86 }
87 pub fn from_csv(filename: &Path) -> Result<Elements, LoadError> {
88 let mut elements = vec![];
89 let file = File::open(filename)?;
90 for element in csv::ReaderBuilder::new()
91 .from_reader(file)
92 .deserialize::<ElementCsv>()
93 {
94 elements.push(element?.into());
95 }
96 Ok(Elements { elements })
97 }
98 pub fn from_file(filename: &Path) -> Result<Elements, LoadError> {
99 match filename.extension().and_then(OsStr::to_str) {
100 Some("csv") => Self::from_csv(filename),
101 Some("yml") | Some("yaml") => {
102 let text = fs::read_to_string(filename)?;
103 Self::from_yaml(&text)
104 }
105 _ => Err(LoadError::NotSupportedFormat),
106 }
107 }
108 pub fn elements(&self) -> &Vec<Element> {
109 &self.elements
110 }
111 pub fn find(&self, id: &str) -> Option<&Element> {
112 self.elements.iter().find(|element| element.id == id)
113 }
114}
115
116#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash, EnumIter)]
117pub enum RoleTypes {
118 Controller,
119 ControlledProcess,
120 Actuator,
121 Sensor,
122 Interference,
123 ControlPath,
124 FeedbackPath,
125 OutOfScope,
126 Missing,
127}
128
129impl From<&str> for RoleTypes {
130 fn from(raw: &str) -> Self {
131 match raw.trim().replace(" ", "").to_lowercase().as_str() {
132 "controller" => RoleTypes::Controller,
133 "controlledprocess" => RoleTypes::ControlledProcess,
134 "actuator" => RoleTypes::Actuator,
135 "sensor" => RoleTypes::Sensor,
136 "interference" => RoleTypes::Interference,
137 "controlpath" => RoleTypes::ControlPath,
138 "feedbackpath" => RoleTypes::FeedbackPath,
139 "outofscope" => RoleTypes::OutOfScope,
140 _ => RoleTypes::Missing,
141 }
142 }
143}
144
145#[cfg(test)]
146mod test {
147 use super::{Elements, LoadError};
148
149 #[test]
150 fn test_load() -> Result<(), LoadError> {
151 let elements = Elements::from_yaml(
152 "elements:\n- id: bob\n name: bob box\n description: I lost the plot\n responsibilities: []\n roles: [Controller]\n notes: \"\"",
153 )?;
154 assert_eq!(elements.elements.len(), 1);
155 assert_eq!(elements.elements[0].id, "bob");
156 Ok(())
157 }
158}