rafia_stpa/stpa/
elements.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::{
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}