Skip to main content

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}