rafia_stpa/stpa/
losses.rs1use std::{ffi::OsStr, fs, fs::File, io::Write, path::Path};
17
18use indexmap::IndexSet;
19use serde::{Deserialize, Serialize};
20use serde_yaml;
21use strum::EnumIter;
22
23use super::LoadError;
24
25#[derive(Debug, Serialize, Deserialize)]
26pub struct LossCsv {
27 #[serde(rename = "Loss Id")]
28 pub id: String,
29 #[serde(rename = "Loss Description")]
30 pub description: String,
31 #[serde(rename = "Loss Categories")]
32 pub categories: String,
33}
34
35#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
36pub struct Loss {
37 #[serde(rename = "Loss Id")]
38 pub id: String,
39 #[serde(rename = "Loss Description")]
40 pub description: String,
41 #[serde(rename = "Loss Categories")]
42 pub category: IndexSet<LossCategory>,
43}
44
45impl Loss {
46 pub fn describe(&self) -> serde_json::Value {
47 serde_json::json!({"loss": self})
48 }
49}
50
51impl From<LossCsv> for Loss {
52 fn from(csv: LossCsv) -> Self {
53 Loss {
54 id: csv.id,
55 description: csv.description,
56 category: csv.categories.split(",").map(|s| s.into()).collect(),
57 }
58 }
59}
60
61#[derive(Debug, Serialize, Deserialize, Default, Clone, PartialEq)]
62pub struct Losses {
63 pub losses: Vec<Loss>,
64}
65
66impl Losses {
67 pub fn from_yaml(text: &str) -> Result<Losses, LoadError> {
68 serde_yaml::from_str::<Losses>(text).map_err(LoadError::Yaml)
69 }
70 pub fn to_yaml(&self) -> Result<String, LoadError> {
71 serde_yaml::to_string(&self).map_err(LoadError::Yaml)
72 }
73 pub fn from_csv(filename: &Path) -> Result<Losses, LoadError> {
74 let mut losses = vec![];
75 let file = File::open(filename)?;
76 for element in csv::ReaderBuilder::new()
77 .from_reader(file)
78 .deserialize::<LossCsv>()
79 {
80 losses.push(element?.into());
81 }
82 Ok(Losses { losses })
83 }
84 pub fn from_file(filename: &Path) -> Result<Losses, LoadError> {
85 match filename.extension().and_then(OsStr::to_str) {
86 Some("csv") => Self::from_csv(filename),
87 Some("yml") | Some("yaml") => {
88 let text = fs::read_to_string(filename)?;
89 Self::from_yaml(&text)
90 }
91 _ => Err(LoadError::NotSupportedFormat),
92 }
93 }
94 pub fn to_file(&self, filename: &Path) -> Result<(), LoadError> {
95 match filename.extension().and_then(OsStr::to_str) {
96 Some("yml") | Some("yaml") => {
97 let mut f = File::create(filename)?;
98 f.write_all(self.to_yaml()?.as_bytes())?;
99 Ok(())
100 }
101 _ => Err(LoadError::NotSupportedFormat),
102 }
103 }
104 pub fn find(&self, id: &str) -> Option<&Loss> {
105 self.losses.iter().find(|loss| loss.id == id)
106 }
107}
108
109#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash, EnumIter)]
110pub enum LossCategory {
111 Assets,
112 Commercial,
113 Safety,
114 Security,
115 User,
116 Missing,
117}
118
119impl From<&str> for LossCategory {
120 fn from(raw: &str) -> Self {
121 match raw.trim() {
122 "Assets" => LossCategory::Assets,
123 "Commercial" => LossCategory::Commercial,
124 "Safety" => LossCategory::Safety,
125 "Security" => LossCategory::Security,
126 "User" => LossCategory::User,
127 _ => LossCategory::Missing,
128 }
129 }
130}
131
132#[cfg(test)]
133mod test {
134 use super::{LoadError, Losses};
135
136 #[test]
137 fn test_load() -> Result<(), LoadError> {
138 let losses = Losses::from_yaml(
139 "losses:\n- Loss Id: bob\n Loss Description: I lost the plot\n Loss Categories: [User, Assets]\n",
140 )?;
141 assert_eq!(losses.losses.len(), 1);
142 assert_eq!(losses.losses[0].id, "bob");
143 Ok(())
144 }
145}