1use std::collections::{HashMap, HashSet};
2
3use crate::{Error, fmi3::variable::AbstractVariableTrait};
4
5use super::{Annotations, Fmi3ModelDescription};
6
7#[derive(Default, Debug, PartialEq, hard_xml::XmlRead, hard_xml::XmlWrite)]
8#[xml(
9 tag = "fmiTerminalsAndIcons",
10 strict(unknown_attribute, unknown_element)
11)]
12pub struct Fmi3TerminalsAndIcons {
13 #[xml(attr = "fmiVersion")]
14 pub fmi_version: String,
15 #[xml(child = "Terminals")]
16 pub terminals: Option<Terminals>,
17 #[xml(child = "Annotations")]
18 pub annotations: Option<Annotations>,
19}
20
21#[derive(Default, Debug, PartialEq, hard_xml::XmlRead, hard_xml::XmlWrite)]
22#[xml(tag = "Terminals", strict(unknown_attribute, unknown_element))]
23pub struct Terminals {
24 #[xml(child = "Terminal")]
25 pub terminals: Vec<Terminal>,
26}
27
28#[derive(Default, Debug, PartialEq, hard_xml::XmlRead, hard_xml::XmlWrite)]
29#[xml(tag = "Terminal", strict(unknown_attribute, unknown_element))]
30pub struct Terminal {
31 #[xml(child = "TerminalMemberVariable")]
32 pub terminal_member_variables: Vec<TerminalMemberVariable>,
33 #[xml(child = "TerminalStreamMemberVariable")]
34 pub terminal_stream_member_variables: Vec<TerminalStreamMemberVariable>,
35 #[xml(child = "Terminal")]
36 pub terminals: Vec<Terminal>,
37 #[xml(child = "Annotations")]
38 pub annotations: Option<Annotations>,
39 #[xml(attr = "name")]
40 pub name: String,
41 #[xml(attr = "matchingRule")]
42 pub matching_rule: String,
43 #[xml(attr = "terminalKind")]
44 pub terminal_kind: Option<String>,
45 #[xml(attr = "description")]
46 pub description: Option<String>,
47}
48
49#[derive(Default, Debug, PartialEq, hard_xml::XmlRead, hard_xml::XmlWrite)]
50#[xml(
51 tag = "TerminalMemberVariable",
52 strict(unknown_attribute, unknown_element)
53)]
54pub struct TerminalMemberVariable {
55 #[xml(child = "Annotations")]
56 pub annotations: Option<Annotations>,
57 #[xml(attr = "variableName")]
58 pub variable_name: String,
59 #[xml(attr = "memberName")]
60 pub member_name: Option<String>,
61 #[xml(attr = "variableKind")]
62 pub variable_kind: String,
63}
64
65#[derive(Default, Debug, PartialEq, hard_xml::XmlRead, hard_xml::XmlWrite)]
66#[xml(
67 tag = "TerminalStreamMemberVariable",
68 strict(unknown_attribute, unknown_element)
69)]
70pub struct TerminalStreamMemberVariable {
71 #[xml(child = "Annotations")]
72 pub annotations: Option<Annotations>,
73 #[xml(attr = "inStreamMemberName")]
74 pub in_stream_member_name: String,
75 #[xml(attr = "outStreamMemberName")]
76 pub out_stream_member_name: String,
77 #[xml(attr = "inStreamVariableName")]
78 pub in_stream_variable_name: String,
79 #[xml(attr = "outStreamVariableName")]
80 pub out_stream_variable_name: String,
81}
82
83#[derive(Debug, PartialEq, Eq)]
84pub enum MatchingRule {
85 Plug,
86 Bus,
87 Sequence,
88 Other(String),
89}
90
91impl Terminal {
92 pub fn matching_rule_kind(&self) -> MatchingRule {
93 match self.matching_rule.as_str() {
94 "plug" => MatchingRule::Plug,
95 "bus" => MatchingRule::Bus,
96 "sequence" => MatchingRule::Sequence,
97 other => MatchingRule::Other(other.to_string()),
98 }
99 }
100
101 pub fn validate_matching_rule(&self) -> Result<(), Error> {
102 match self.matching_rule_kind() {
103 MatchingRule::Plug | MatchingRule::Bus => {
104 let mut seen = HashSet::new();
105 for member in &self.terminal_member_variables {
106 let member_name = member.member_name.as_deref().ok_or_else(|| {
107 Error::Model(format!(
108 "Terminal '{}' requires memberName for matchingRule '{}'",
109 self.name, self.matching_rule
110 ))
111 })?;
112 if !seen.insert(member_name) {
113 return Err(Error::Model(format!(
114 "Terminal '{}' has duplicate memberName '{}' for matchingRule '{}'",
115 self.name, member_name, self.matching_rule
116 )));
117 }
118 }
119 }
120 MatchingRule::Sequence | MatchingRule::Other(_) => {}
121 }
122 Ok(())
123 }
124}
125
126pub struct ResolvedTerminals<'a> {
127 pub terminals: Vec<ResolvedTerminal<'a>>,
128}
129
130pub struct ResolvedTerminal<'a> {
131 pub terminal: &'a Terminal,
132 pub members: Vec<ResolvedTerminalMemberVariable<'a>>,
133 pub stream_members: Vec<ResolvedTerminalStreamMemberVariable<'a>>,
134 pub terminals: Vec<ResolvedTerminal<'a>>,
135}
136
137pub struct ResolvedTerminalMemberVariable<'a> {
138 pub member: &'a TerminalMemberVariable,
139 pub variable: &'a dyn AbstractVariableTrait,
140}
141
142pub struct ResolvedTerminalStreamMemberVariable<'a> {
143 pub member: &'a TerminalStreamMemberVariable,
144 pub in_stream_variable: &'a dyn AbstractVariableTrait,
145 pub out_stream_variable: &'a dyn AbstractVariableTrait,
146}
147
148#[derive(Debug, PartialEq, Eq)]
149pub enum TerminalResolutionError {
150 MissingVariable {
151 terminal_path: String,
152 variable_name: String,
153 },
154}
155
156pub fn resolve_terminals<'a>(
157 terminals: &'a Fmi3TerminalsAndIcons,
158 model: &'a Fmi3ModelDescription,
159) -> Result<ResolvedTerminals<'a>, TerminalResolutionError> {
160 let Some(root) = terminals.terminals.as_ref() else {
161 return Ok(ResolvedTerminals {
162 terminals: Vec::new(),
163 });
164 };
165
166 let lookup = build_variable_lookup(&model.model_variables);
167 let mut resolved = Vec::with_capacity(root.terminals.len());
168 for terminal in &root.terminals {
169 resolved.push(resolve_terminal(terminal, &lookup, terminal.name.as_str())?);
170 }
171 Ok(ResolvedTerminals {
172 terminals: resolved,
173 })
174}
175
176fn build_variable_lookup<'a>(
177 model_variables: &'a super::variable::ModelVariables,
178) -> HashMap<&'a str, &'a dyn AbstractVariableTrait> {
179 let mut lookup = HashMap::new();
180 for var in model_variables.iter_abstract() {
181 lookup.insert(var.name(), var);
182 }
183 lookup
184}
185
186fn resolve_terminal<'a>(
187 terminal: &'a Terminal,
188 lookup: &HashMap<&'a str, &'a dyn AbstractVariableTrait>,
189 terminal_path: &str,
190) -> Result<ResolvedTerminal<'a>, TerminalResolutionError> {
191 let mut members = Vec::with_capacity(terminal.terminal_member_variables.len());
192 for member in &terminal.terminal_member_variables {
193 let variable = *lookup.get(member.variable_name.as_str()).ok_or_else(|| {
194 TerminalResolutionError::MissingVariable {
195 terminal_path: terminal_path.to_string(),
196 variable_name: member.variable_name.clone(),
197 }
198 })?;
199 members.push(ResolvedTerminalMemberVariable { member, variable });
200 }
201
202 let mut stream_members = Vec::with_capacity(terminal.terminal_stream_member_variables.len());
203 for member in &terminal.terminal_stream_member_variables {
204 let in_stream_variable = *lookup
205 .get(member.in_stream_variable_name.as_str())
206 .ok_or_else(|| TerminalResolutionError::MissingVariable {
207 terminal_path: terminal_path.to_string(),
208 variable_name: member.in_stream_variable_name.clone(),
209 })?;
210 let out_stream_variable = *lookup
211 .get(member.out_stream_variable_name.as_str())
212 .ok_or_else(|| TerminalResolutionError::MissingVariable {
213 terminal_path: terminal_path.to_string(),
214 variable_name: member.out_stream_variable_name.clone(),
215 })?;
216 stream_members.push(ResolvedTerminalStreamMemberVariable {
217 member,
218 in_stream_variable,
219 out_stream_variable,
220 });
221 }
222
223 let mut terminals = Vec::with_capacity(terminal.terminals.len());
224 for child in &terminal.terminals {
225 let child_path = format!("{}/{}", terminal_path, child.name);
226 terminals.push(resolve_terminal(child, lookup, &child_path)?);
227 }
228
229 Ok(ResolvedTerminal {
230 terminal,
231 members,
232 stream_members,
233 terminals,
234 })
235}