rafia_stpa/stpa/
losses.rs

1// *****************************************************************************
2// Copyright (c) 2025 Codethink
3//
4// This program and the accompanying materials are made available under the
5// terms of the Eclipse Public License 2.0 which is available at
6// http://www.eclipse.org/legal/epl-2.0.
7//
8// This Source Code may also be made available under the following Secondary
9// Licenses when the conditions for such availability set forth in the Eclipse
10// Public License, v. 2.0 are satisfied: GNU General Public License, version 2
11// with the GNU Classpath Exception which is
12// available at https://www.gnu.org/software/classpath/license.html.
13//
14// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15// *****************************************************************************
16use 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}