rafia_stpa/stpa/
control_loops.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 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        // check we aren't currently expanding this ca_analysis
57        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}