rafia_stpa/stpa/
control_loop_sequences.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::{InteractionLink, LoadError, CYCLE_DETECTED_MSG};
27
28use super::{ControlLoopLink, StpaData};
29
30//"CL-Sequence Id","Loop","Step","Interaction Id","Provider process model","Provider logic","Expected target behaviour"
31#[derive(Debug, Serialize, Deserialize)]
32pub struct ControlLoopSequenceCsv {
33    #[serde(rename = "CL-Sequence Id")]
34    pub id: String,
35    #[serde(rename = "Loop")]
36    pub control_loop: String,
37    #[serde(rename = "Step")]
38    pub step: String,
39    #[serde(rename = "Interaction Id")]
40    pub interaction: String,
41    #[serde(rename = "Provider process model")]
42    pub provider_process_model: String,
43    #[serde(rename = "Provider logic")]
44    pub provider_logic: String,
45    #[serde(rename = "Expected Receiver behaviour")]
46    pub expected_target_behaviour: String,
47}
48
49#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
50pub struct ControlLoopSequence {
51    pub id: String,
52    pub control_loop: ControlLoopLink,
53    pub step: String,
54    pub interaction: InteractionLink,
55    pub provider_process_model: String,
56    pub provider_logic: String,
57    pub expected_target_behaviour: String,
58}
59
60impl ControlLoopSequence {
61    pub fn describe(
62        &self,
63        stpa_data: &StpaData,
64        currently_expanding: &mut Vec<String>,
65    ) -> Result<serde_json::Value, String> {
66        // check we aren't currently expanding this control_loop_sequence
67        if currently_expanding.contains(&self.id) {
68            return Ok(serde_json::json!({
69                "control_loop_sequence": {
70                    "id": self.id,
71                    "message": CYCLE_DETECTED_MSG,
72                }
73            }));
74        } else {
75            currently_expanding.push(self.id.clone());
76        }
77
78        let mut obj = serde_json::json!({"control_loop_sequence": self});
79        obj["control_loop_sequence"]["control_loop"] =
80            stpa_data.describe_inner(self.control_loop.0.as_ref(), currently_expanding)?;
81        obj["control_loop_sequence"]["interaction"] =
82            stpa_data.describe_inner(self.interaction.0.as_ref(), currently_expanding)?;
83
84        let popped = currently_expanding.pop();
85        debug_assert_eq!(popped.as_ref(), Some(&self.id));
86        Ok(obj)
87    }
88}
89
90impl From<ControlLoopSequenceCsv> for ControlLoopSequence {
91    fn from(csv: ControlLoopSequenceCsv) -> Self {
92        ControlLoopSequence {
93            id: csv.id,
94            control_loop: ControlLoopLink::from(csv.control_loop),
95            step: csv.step,
96            interaction: InteractionLink::from(csv.interaction),
97            provider_process_model: csv.provider_process_model,
98            provider_logic: csv.provider_logic,
99            expected_target_behaviour: csv.expected_target_behaviour,
100        }
101    }
102}
103
104#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Default)]
105pub struct ControlLoopSequences {
106    pub control_loop_sequences: Vec<ControlLoopSequence>,
107}
108
109impl ControlLoopSequences {
110    pub fn from_yaml(text: &str) -> Result<ControlLoopSequences, LoadError> {
111        serde_yaml::from_str::<ControlLoopSequences>(text).map_err(LoadError::Yaml)
112    }
113    pub fn to_yaml(&self) -> Result<String, LoadError> {
114        serde_yaml::to_string(&self).map_err(LoadError::Yaml)
115    }
116    pub fn from_csv(filename: &Path) -> Result<ControlLoopSequences, LoadError> {
117        let mut control_loop_sequences = vec![];
118        let file = File::open(filename)?;
119        for control_loop_sequence in csv::ReaderBuilder::new()
120            .from_reader(file)
121            .deserialize::<ControlLoopSequenceCsv>()
122        {
123            control_loop_sequences.push(control_loop_sequence?.into());
124        }
125        Ok(ControlLoopSequences {
126            control_loop_sequences,
127        })
128    }
129    pub fn from_file(filename: &Path) -> Result<ControlLoopSequences, LoadError> {
130        match filename.extension().and_then(OsStr::to_str) {
131            Some("csv") => Self::from_csv(filename),
132            Some("yml") | Some("yaml") => {
133                let text = fs::read_to_string(filename)?;
134                Self::from_yaml(&text)
135            }
136            _ => Err(LoadError::NotSupportedFormat),
137        }
138    }
139    pub fn find(&self, id: &str) -> Option<&ControlLoopSequence> {
140        self.control_loop_sequences
141            .iter()
142            .find(|control_loop_sequence| control_loop_sequence.id == id)
143    }
144}