Skip to main content

nros_params/
declared.rs

1//! phase-446 W6 -- the parameters a node's contract DECLARES, checked when the
2//! code declares one.
3//!
4//! The launch contract states each node's parameters by name and type, and the
5//! build sizes the parameter store from that statement (phase-446 W4). A store
6//! sized from a declaration is only right while the code declares what the
7//! contract says, so a declaration the contract does not make -- an unknown
8//! name, or a known name with another type -- refuses the boot instead of
9//! taking a slot nobody counted.
10//!
11//! The table is `'static` data an entry bakes from the SystemModel
12//! (`nros_orchestration_ir::declared_params`); this module only looks names up
13//! in it. A node with no row has no `params:` in its contract and is NOT
14//! checked, which is the behaviour before the contract could say anything.
15//!
16//! The C++ `ComponentNode` does the same lookup in `rclcpp/detail/declared_params.hpp`,
17//! over a header rendered from the same model by the same function.
18
19use crate::ParameterType;
20
21/// Names no contract has to declare: `use_sim_time` and
22/// `start_type_description_service`, which rclcpp declares on every node, and
23/// `qos_overrides.*`, which is derived from the contract itself. These are the
24/// names play_launch exempts when it holds a launch file to the same contract,
25/// so the launch check and this one agree about what a node may carry.
26pub 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/// The declared parameters of every node in an image whose contract has
33/// `params:`.
34///
35/// `nodes` is `(node FQN, contract)`, where the contract is what a refusal
36/// names; `params` is `(node FQN, parameter name, rcl_interfaces type code)`.
37#[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/// A declaration the contract does not make.
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub struct DeclaredParamMismatch {
46    /// The node's FQN, as the contract keys it.
47    pub node: &'static str,
48    /// The contract the node's parameters are declared in.
49    pub contract: &'static str,
50    /// What the contract declares the name as; `None` when it does not
51    /// declare the name at all.
52    pub declared: Option<ParameterType>,
53}
54
55impl DeclaredParams {
56    /// No table: nothing is checked.
57    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    /// Check one declaration by the node `namespace` + `node_name`.
74    ///
75    /// `Ok` when the node's contract has no `params:`, when the name is
76    /// [exempt](is_exempt), or when the contract declares the name with this
77    /// type.
78    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
111/// `fqn == namespace + "/" + name`, where a namespace of `/` or `` is the root.
112fn 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
117/// The [`ParameterType`] for an `rcl_interfaces/msg/ParameterType` code.
118pub 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
133/// The contract's spelling of a type, for a diagnostic.
134pub 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        // A trailing `/` on the namespace is the same namespace.
172        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    /// A node with no `params:` is not checked, and neither is an image with
208    /// no table: absence is not a declaration of nothing.
209    #[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        // A node in ANOTHER namespace with the same name is another node.
220        assert_eq!(
221            TABLE.check("/other", "mrm_handler", "x", ParameterType::Bool),
222            Ok(())
223        );
224    }
225
226    /// The names every node carries are never refused, whatever their type.
227    #[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}