ros2args/types.rs
1//! Type definitions for ROS2 command-line arguments
2
3use std::path::PathBuf;
4use yaml_rust2::Yaml;
5
6#[cfg(feature = "serde")]
7use serde::{Deserialize, Serialize};
8
9/// Represents a name remapping rule
10///
11/// Remapping rules can be either global (applying to all nodes) or node-specific.
12///
13/// # Examples
14///
15/// - Global: `foo:=bar` remaps `foo` to `bar` for all nodes
16/// - Node-specific: `my_node:foo:=bar` remaps `foo` to `bar` only for `my_node`
17#[derive(Debug, Clone, PartialEq, Eq, Hash)]
18#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
19pub struct RemapRule {
20 /// Optional node name to target (None means applies to all nodes)
21 pub node_name: Option<String>,
22 /// The original name to remap from
23 pub from: String,
24 /// The new name to remap to
25 pub to: String,
26}
27
28impl RemapRule {
29 /// Create a new global remapping rule
30 #[must_use]
31 pub fn new_global(from: String, to: String) -> Self {
32 Self {
33 node_name: None,
34 from,
35 to,
36 }
37 }
38
39 /// Create a new node-specific remapping rule
40 #[must_use]
41 pub fn new_node_specific(node_name: String, from: String, to: String) -> Self {
42 Self {
43 node_name: Some(node_name),
44 from,
45 to,
46 }
47 }
48
49 /// Check if this rule applies to a specific node
50 #[must_use]
51 pub fn applies_to_node(&self, node_name: &str) -> bool {
52 self.node_name.as_ref().is_none_or(|n| n == node_name)
53 }
54}
55
56/// Represents a parameter assignment
57///
58/// Parameters can be either global (applying to all nodes) or node-specific.
59/// Values are stored as YAML types to preserve type information.
60///
61/// # Examples
62///
63/// - Global: `use_sim_time:=true`
64/// - Node-specific: `my_node:use_sim_time:=true`
65#[derive(Debug, Clone, PartialEq)]
66pub struct ParamAssignment {
67 /// Optional node name to target (None means applies to all nodes)
68 pub node_name: Option<String>,
69 /// Parameter name
70 pub name: String,
71 /// Parameter value (stored as YAML value to preserve type information)
72 pub value: Yaml,
73}
74
75impl ParamAssignment {
76 /// Create a new global parameter assignment
77 #[must_use]
78 pub fn new_global(name: String, value: Yaml) -> Self {
79 Self {
80 node_name: None,
81 name,
82 value,
83 }
84 }
85
86 /// Create a new node-specific parameter assignment
87 #[must_use]
88 pub fn new_node_specific(node_name: String, name: String, value: Yaml) -> Self {
89 Self {
90 node_name: Some(node_name),
91 name,
92 value,
93 }
94 }
95
96 /// Check if this parameter applies to a specific node
97 #[must_use]
98 pub fn applies_to_node(&self, node_name: &str) -> bool {
99 self.node_name.as_ref().is_none_or(|n| n == node_name)
100 }
101
102 /// Get the value as a boolean, if it is one
103 #[must_use]
104 pub fn as_bool(&self) -> Option<bool> {
105 self.value.as_bool()
106 }
107
108 /// Get the value as an integer, if it is one
109 #[must_use]
110 pub fn as_i64(&self) -> Option<i64> {
111 self.value.as_i64()
112 }
113
114 /// Get the value as a float, if it is one
115 #[must_use]
116 pub fn as_f64(&self) -> Option<f64> {
117 self.value.as_f64()
118 }
119
120 /// Get the value as a string, if it is one
121 #[must_use]
122 pub fn as_str(&self) -> Option<&str> {
123 self.value.as_str()
124 }
125
126 /// Get the value as a YAML array, if it is one
127 #[must_use]
128 pub fn as_vec(&self) -> Option<&Vec<Yaml>> {
129 self.value.as_vec()
130 }
131
132 /// Get the value as a YAML hash/map, if it is one
133 #[must_use]
134 pub fn as_hash(&self) -> Option<&yaml_rust2::yaml::Hash> {
135 self.value.as_hash()
136 }
137
138 /// Check if the value is null
139 #[must_use]
140 pub fn is_null(&self) -> bool {
141 self.value.is_null()
142 }
143
144 /// Get a reference to the underlying YAML value
145 #[must_use]
146 pub fn value(&self) -> &Yaml {
147 &self.value
148 }
149}
150
151/// Log levels supported by ROS2
152#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
153#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
154pub enum LogLevel {
155 /// Debug level logging
156 Debug,
157 /// Info level logging
158 Info,
159 /// Warning level logging
160 Warn,
161 /// Error level logging
162 Error,
163 /// Fatal level logging
164 Fatal,
165}
166
167impl LogLevel {
168 /// Convert the log level to a string
169 #[must_use]
170 pub fn as_str(&self) -> &'static str {
171 match self {
172 Self::Debug => "DEBUG",
173 Self::Info => "INFO",
174 Self::Warn => "WARN",
175 Self::Error => "ERROR",
176 Self::Fatal => "FATAL",
177 }
178 }
179}
180
181impl std::str::FromStr for LogLevel {
182 type Err = String;
183
184 fn from_str(s: &str) -> Result<Self, Self::Err> {
185 match s.to_uppercase().as_str() {
186 "DEBUG" => Ok(Self::Debug),
187 "INFO" => Ok(Self::Info),
188 "WARN" | "WARNING" => Ok(Self::Warn),
189 "ERROR" => Ok(Self::Error),
190 "FATAL" => Ok(Self::Fatal),
191 _ => Err(format!("Invalid log level: {s}")),
192 }
193 }
194}
195
196/// Represents a log level assignment
197///
198/// Can be either a global log level or logger-specific.
199///
200/// # Examples
201///
202/// - Global: `--log-level DEBUG`
203/// - Logger-specific: `--log-level rclcpp:=DEBUG`
204#[derive(Debug, Clone, PartialEq, Eq)]
205#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
206pub struct LogLevelAssignment {
207 /// Optional logger name (None means global)
208 pub logger_name: Option<String>,
209 /// Log level
210 pub level: LogLevel,
211}
212
213impl LogLevelAssignment {
214 /// Create a new global log level assignment
215 #[must_use]
216 pub fn new_global(level: LogLevel) -> Self {
217 Self {
218 logger_name: None,
219 level,
220 }
221 }
222
223 /// Create a new logger-specific log level assignment
224 #[must_use]
225 pub fn new_logger_specific(logger_name: String, level: LogLevel) -> Self {
226 Self {
227 logger_name: Some(logger_name),
228 level,
229 }
230 }
231}
232
233/// Represents logging output configuration flags
234#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
235#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
236pub struct LoggingOutputConfig {
237 /// Enable/disable rosout logging (None means not specified)
238 pub rosout: Option<bool>,
239 /// Enable/disable stdout logging (None means not specified)
240 pub stdout: Option<bool>,
241 /// Enable/disable external library logging (None means not specified)
242 pub external_lib: Option<bool>,
243}
244
245/// Complete set of parsed ROS2 command-line arguments
246#[derive(Debug, Clone, PartialEq, Default)]
247pub struct Ros2Args {
248 /// Name remapping rules
249 pub remap_rules: Vec<RemapRule>,
250 /// Parameter assignments
251 pub param_assignments: Vec<ParamAssignment>,
252 /// Parameter files to load
253 pub param_files: Vec<PathBuf>,
254 /// Log level assignments
255 pub log_levels: Vec<LogLevelAssignment>,
256 /// Log configuration file
257 pub log_config_file: Option<PathBuf>,
258 /// Logging output configuration
259 pub logging_output: LoggingOutputConfig,
260 /// Enclave path for security
261 pub enclave: Option<String>,
262}
263
264impl Ros2Args {
265 /// Create a new empty `Ros2Args`
266 #[must_use]
267 pub fn new() -> Self {
268 Self::default()
269 }
270
271 /// Parse ROS2 arguments from command-line arguments (typically from `std::env::args()`)
272 ///
273 /// This is a convenience method that calls the parser directly with the provided arguments.
274 /// Multiple `--ros-args` sections are supported and will be merged into a single `Ros2Args` structure.
275 ///
276 /// # Arguments
277 ///
278 /// * `args` - Command-line arguments as an iterator of strings
279 ///
280 /// # Returns
281 ///
282 /// Returns a tuple of `(Ros2Args, Vec<String>)` where:
283 /// - `Ros2Args` contains all parsed ROS2 arguments
284 /// - `Vec<String>` contains remaining user-defined arguments
285 ///
286 /// # Errors
287 ///
288 /// Returns an error if any ROS2 argument is malformed or invalid.
289 ///
290 /// # Examples
291 ///
292 /// ```
293 /// use ros2args::Ros2Args;
294 ///
295 /// let args = vec![
296 /// "my_program".to_string(),
297 /// "--ros-args".to_string(),
298 /// "-r".to_string(),
299 /// "foo:=bar".to_string(),
300 /// "-p".to_string(),
301 /// "use_sim_time:=true".to_string(),
302 /// ];
303 ///
304 /// let (ros_args, user_args) = Ros2Args::from_args(&args)?;
305 /// assert_eq!(ros_args.remap_rules.len(), 1);
306 /// assert_eq!(ros_args.param_assignments.len(), 1);
307 /// # Ok::<(), ros2args::Ros2ArgsError>(())
308 /// ```
309 pub fn from_args<I, S>(args: I) -> crate::Ros2ArgsResult<(Self, Vec<String>)>
310 where
311 I: IntoIterator<Item = S>,
312 S: AsRef<str>,
313 {
314 let args_vec: Vec<String> = args.into_iter().map(|s| s.as_ref().to_string()).collect();
315 crate::parse_ros2_args(&args_vec)
316 }
317
318 /// Parse ROS2 arguments from the current process's command-line arguments
319 ///
320 /// This is a convenience method that reads from `std::env::args()` and parses ROS2 arguments.
321 /// Only the parsed ROS2 arguments are returned; user arguments are discarded.
322 ///
323 /// # Returns
324 ///
325 /// Returns the parsed `Ros2Args` structure.
326 ///
327 /// # Errors
328 ///
329 /// Returns an error if any ROS2 argument is malformed or invalid.
330 ///
331 /// # Examples
332 ///
333 /// ```no_run
334 /// use ros2args::Ros2Args;
335 ///
336 /// // Parse arguments from std::env::args()
337 /// let ros_args = Ros2Args::from_env()?;
338 ///
339 /// println!("ROS2 remapping rules: {:?}", ros_args.remap_rules);
340 /// # Ok::<(), ros2args::Ros2ArgsError>(())
341 /// ```
342 pub fn from_env() -> crate::Ros2ArgsResult<Self> {
343 let args: Vec<String> = std::env::args().collect();
344 let (ros_args, _user_args) = crate::parse_ros2_args(&args)?;
345 Ok(ros_args)
346 }
347
348 /// Get all remapping rules that apply to a specific node
349 #[must_use]
350 pub fn get_remap_rules_for_node(&self, node_name: &str) -> Vec<&RemapRule> {
351 self.remap_rules
352 .iter()
353 .filter(|r| r.applies_to_node(node_name))
354 .collect()
355 }
356
357 /// Get all parameter assignments that apply to a specific node
358 ///
359 /// This includes both command-line parameter assignments and parameters from YAML files.
360 /// Returns an error if any parameter file cannot be parsed.
361 ///
362 /// # Errors
363 ///
364 /// Returns an error if any parameter file cannot be read or parsed.
365 pub fn get_params_for_node(
366 &self,
367 node_name: &str,
368 ) -> crate::Ros2ArgsResult<Vec<ParamAssignment>> {
369 let mut params = Vec::new();
370
371 // Add command-line parameter assignments
372 params.extend(
373 self.param_assignments
374 .iter()
375 .filter(|p| p.applies_to_node(node_name))
376 .cloned(),
377 );
378
379 // Parse and add parameters from YAML files
380 for param_file in &self.param_files {
381 let file_params = crate::param_file::parse_param_file(param_file)?;
382 params.extend(
383 file_params
384 .into_iter()
385 .filter(|p| p.applies_to_node(node_name)),
386 );
387 }
388
389 Ok(params)
390 }
391
392 /// Merge another `Ros2Args` into this one
393 pub fn merge(&mut self, other: Ros2Args) {
394 self.remap_rules.extend(other.remap_rules);
395 self.param_assignments.extend(other.param_assignments);
396 self.param_files.extend(other.param_files);
397 self.log_levels.extend(other.log_levels);
398 if other.log_config_file.is_some() {
399 self.log_config_file = other.log_config_file;
400 }
401 if other.logging_output.rosout.is_some() {
402 self.logging_output.rosout = other.logging_output.rosout;
403 }
404 if other.logging_output.stdout.is_some() {
405 self.logging_output.stdout = other.logging_output.stdout;
406 }
407 if other.logging_output.external_lib.is_some() {
408 self.logging_output.external_lib = other.logging_output.external_lib;
409 }
410 if other.enclave.is_some() {
411 self.enclave = other.enclave;
412 }
413 }
414}