1use crate::ParameterType;
20
21pub fn is_exempt(name: &str) -> bool {
27 name == "use_sim_time"
28 || name == "start_type_description_service"
29 || name.starts_with("qos_overrides.")
30}
31
32#[derive(Debug, Clone, Copy, Default)]
38pub struct DeclaredParams {
39 nodes: &'static [(&'static str, &'static str)],
40 params: &'static [(&'static str, &'static str, u8)],
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub struct DeclaredParamMismatch {
46 pub node: &'static str,
48 pub contract: &'static str,
50 pub declared: Option<ParameterType>,
53}
54
55impl DeclaredParams {
56 pub const EMPTY: Self = Self {
58 nodes: &[],
59 params: &[],
60 };
61
62 pub const fn new(
63 nodes: &'static [(&'static str, &'static str)],
64 params: &'static [(&'static str, &'static str, u8)],
65 ) -> Self {
66 Self { nodes, params }
67 }
68
69 pub fn is_empty(&self) -> bool {
70 self.nodes.is_empty()
71 }
72
73 pub fn check(
79 &self,
80 namespace: &str,
81 node_name: &str,
82 param: &str,
83 ty: ParameterType,
84 ) -> Result<(), DeclaredParamMismatch> {
85 let Some(&(node, contract)) = self
86 .nodes
87 .iter()
88 .find(|(fqn, _)| fqn_matches(fqn, namespace, node_name))
89 else {
90 return Ok(());
91 };
92 if is_exempt(param) {
93 return Ok(());
94 }
95 let declared = self
96 .params
97 .iter()
98 .find(|(fqn, name, _)| *fqn == node && *name == param)
99 .map(|&(_, _, code)| ros_type(code));
100 if declared == Some(ty) {
101 return Ok(());
102 }
103 Err(DeclaredParamMismatch {
104 node,
105 contract,
106 declared,
107 })
108 }
109}
110
111fn fqn_matches(fqn: &str, namespace: &str, name: &str) -> bool {
113 let ns = namespace.trim_end_matches('/');
114 fqn.strip_prefix(ns).and_then(|rest| rest.strip_prefix('/')) == Some(name)
115}
116
117pub fn ros_type(code: u8) -> ParameterType {
119 match code {
120 1 => ParameterType::Bool,
121 2 => ParameterType::Integer,
122 3 => ParameterType::Double,
123 4 => ParameterType::String,
124 5 => ParameterType::ByteArray,
125 6 => ParameterType::BoolArray,
126 7 => ParameterType::IntegerArray,
127 8 => ParameterType::DoubleArray,
128 9 => ParameterType::StringArray,
129 _ => ParameterType::NotSet,
130 }
131}
132
133pub fn type_name(ty: ParameterType) -> &'static str {
135 match ty {
136 ParameterType::NotSet => "not_set",
137 ParameterType::Bool => "bool",
138 ParameterType::Integer => "integer",
139 ParameterType::Double => "double",
140 ParameterType::String => "string",
141 ParameterType::ByteArray => "byte_array",
142 ParameterType::BoolArray => "bool_array",
143 ParameterType::IntegerArray => "integer_array",
144 ParameterType::DoubleArray => "double_array",
145 ParameterType::StringArray => "string_array",
146 }
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152
153 static NODES: &[(&str, &str)] = &[("/system/mrm_handler", "island.contract.yaml")];
154 static PARAMS: &[(&str, &str, u8)] = &[
155 ("/system/mrm_handler", "update_rate", 2),
156 ("/system/mrm_handler", "use_emergency_holding", 1),
157 ];
158 const TABLE: DeclaredParams = DeclaredParams::new(NODES, PARAMS);
159
160 #[test]
161 fn a_declaration_that_matches_the_contract_is_accepted() {
162 assert_eq!(
163 TABLE.check(
164 "/system",
165 "mrm_handler",
166 "update_rate",
167 ParameterType::Integer
168 ),
169 Ok(())
170 );
171 assert_eq!(
173 TABLE.check(
174 "/system/",
175 "mrm_handler",
176 "use_emergency_holding",
177 ParameterType::Bool
178 ),
179 Ok(())
180 );
181 }
182
183 #[test]
184 fn a_name_the_contract_does_not_declare_is_refused_naming_the_contract() {
185 let e = TABLE
186 .check("/system", "mrm_handler", "timeout", ParameterType::Double)
187 .unwrap_err();
188 assert_eq!(e.node, "/system/mrm_handler");
189 assert_eq!(e.contract, "island.contract.yaml");
190 assert_eq!(e.declared, None);
191 }
192
193 #[test]
194 fn a_type_that_differs_is_refused_with_the_declared_type() {
195 let e = TABLE
196 .check(
197 "/system",
198 "mrm_handler",
199 "update_rate",
200 ParameterType::Double,
201 )
202 .unwrap_err();
203 assert_eq!(e.declared, Some(ParameterType::Integer));
204 assert_eq!(type_name(ParameterType::Integer), "integer");
205 }
206
207 #[test]
210 fn an_undeclaring_node_and_an_empty_table_check_nothing() {
211 assert_eq!(
212 TABLE.check("/", "talker", "anything", ParameterType::String),
213 Ok(())
214 );
215 assert_eq!(
216 DeclaredParams::EMPTY.check("/system", "mrm_handler", "x", ParameterType::Bool),
217 Ok(())
218 );
219 assert_eq!(
221 TABLE.check("/other", "mrm_handler", "x", ParameterType::Bool),
222 Ok(())
223 );
224 }
225
226 #[test]
228 fn the_names_play_launch_exempts_are_exempt_here_too() {
229 for name in [
230 "use_sim_time",
231 "start_type_description_service",
232 "qos_overrides./chatter.subscription.depth",
233 ] {
234 assert_eq!(
235 TABLE.check("/system", "mrm_handler", name, ParameterType::Bool),
236 Ok(()),
237 "{name}"
238 );
239 }
240 }
241
242 #[test]
243 fn every_ros_type_code_round_trips() {
244 for code in 1..=9u8 {
245 assert_eq!(ros_type(code) as u8, code);
246 }
247 }
248}