Skip to main content

ros2msg/msg/
mod.rs

1//! ROS2 Message/Service/Action Parser Module
2//!
3//! This module provides functionality for parsing ROS2 `.msg`, `.srv`, and `.action` files.
4//! It handles the traditional ROS2 message format with simple field definitions.
5
6// Re-export the core modules for the msg parser
7/// Action parsing functionality
8pub mod action;
9/// Error types and handling
10pub mod errors;
11/// Message parsing functionality
12pub mod message;
13/// Service parsing functionality
14pub mod service;
15/// Core data structures
16pub mod types;
17/// Validation utilities
18pub mod validation;
19
20// Re-export commonly used types and functions
21pub use action::{
22    ActionSpecification, create_feedback_message, parse_action_file, parse_action_string,
23};
24pub use errors::{ParseError, ParseResult};
25pub use message::{MessageSpecification, parse_message_file, parse_message_string};
26pub use service::{
27    ServiceSpecification, create_service_event_message, parse_service_file, parse_service_string,
28};
29pub use types::{AnnotationValue, Annotations, BaseType, Constant, Field, Type, Value};
30pub use validation::{
31    PRIMITIVE_TYPES, PrimitiveValue, is_valid_constant_name, is_valid_field_name,
32    is_valid_message_name, is_valid_package_name, parse_primitive_value_string,
33};
34
35/// Interface specification that can be either a Message, Service, or Action
36#[derive(Debug, Clone, PartialEq)]
37#[allow(clippy::large_enum_variant)]
38pub enum InterfaceSpecification {
39    /// A message specification
40    Message(MessageSpecification),
41    /// A service specification  
42    Service(ServiceSpecification),
43    /// An action specification
44    Action(ActionSpecification),
45}
46
47impl InterfaceSpecification {
48    /// Get the package name
49    #[must_use]
50    pub fn package_name(&self) -> &str {
51        match self {
52            InterfaceSpecification::Message(spec) => &spec.pkg_name,
53            InterfaceSpecification::Service(spec) => &spec.pkg_name,
54            InterfaceSpecification::Action(spec) => &spec.pkg_name,
55        }
56    }
57
58    /// Get the interface name
59    #[must_use]
60    pub fn interface_name(&self) -> &str {
61        match self {
62            InterfaceSpecification::Message(spec) => &spec.msg_name,
63            InterfaceSpecification::Service(spec) => &spec.srv_name,
64            InterfaceSpecification::Action(spec) => &spec.action_name,
65        }
66    }
67
68    /// Get the full interface name (package/interface)
69    #[must_use]
70    pub fn full_name(&self) -> String {
71        format!("{}/{}", self.package_name(), self.interface_name())
72    }
73
74    /// Check if this is a message specification
75    #[must_use]
76    pub fn is_message(&self) -> bool {
77        matches!(self, InterfaceSpecification::Message(_))
78    }
79
80    /// Check if this is a service specification
81    #[must_use]
82    pub fn is_service(&self) -> bool {
83        matches!(self, InterfaceSpecification::Service(_))
84    }
85
86    /// Check if this is an action specification
87    #[must_use]
88    pub fn is_action(&self) -> bool {
89        matches!(self, InterfaceSpecification::Action(_))
90    }
91
92    /// Get as message specification if it is one
93    #[must_use]
94    pub fn as_message(&self) -> Option<&MessageSpecification> {
95        match self {
96            InterfaceSpecification::Message(spec) => Some(spec),
97            _ => None,
98        }
99    }
100
101    /// Get as service specification if it is one
102    #[must_use]
103    pub fn as_service(&self) -> Option<&ServiceSpecification> {
104        match self {
105            InterfaceSpecification::Service(spec) => Some(spec),
106            _ => None,
107        }
108    }
109
110    /// Get as action specification if it is one
111    #[must_use]
112    pub fn as_action(&self) -> Option<&ActionSpecification> {
113        match self {
114            InterfaceSpecification::Action(spec) => Some(spec),
115            _ => None,
116        }
117    }
118}
119
120impl std::fmt::Display for InterfaceSpecification {
121    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
122        match self {
123            InterfaceSpecification::Message(msg) => write!(f, "{msg}"),
124            InterfaceSpecification::Service(srv) => write!(f, "{srv}"),
125            InterfaceSpecification::Action(action) => write!(f, "{action}"),
126        }
127    }
128}
129
130/// Parse any ROS2 interface file based on its extension
131///
132/// Automatically detects the file type based on the extension:
133/// - `.msg` files are parsed as messages
134/// - `.srv` files are parsed as services  
135/// - `.action` files are parsed as actions
136///
137/// # Arguments
138///
139/// * `pkg_name` - The package name containing the interface
140/// * `file_path` - Path to the interface file
141///
142/// # Returns
143///
144/// Returns an `InterfaceSpecification` enum containing the parsed specification
145///
146/// # Errors
147///
148/// Returns an error if:
149/// - The file extension is not recognized (must be .msg, .srv, or .action)
150/// - The file cannot be read
151/// - The content cannot be parsed for the detected file type
152pub fn parse_interface_file(
153    pkg_name: &str,
154    file_path: &std::path::Path,
155) -> ParseResult<InterfaceSpecification> {
156    let extension = file_path
157        .extension()
158        .and_then(|ext| ext.to_str())
159        .ok_or_else(|| ParseError::InvalidType {
160            type_string: "file extension".to_string(),
161            reason: "File must have an extension".to_string(),
162        })?;
163
164    match extension {
165        "msg" => {
166            let msg_spec = parse_message_file(pkg_name, file_path)?;
167            Ok(InterfaceSpecification::Message(msg_spec))
168        }
169        "srv" => {
170            let srv_spec = parse_service_file(pkg_name, file_path)?;
171            Ok(InterfaceSpecification::Service(srv_spec))
172        }
173        "action" => {
174            let action_spec = parse_action_file(pkg_name, file_path)?;
175            Ok(InterfaceSpecification::Action(action_spec))
176        }
177        _ => Err(ParseError::InvalidType {
178            type_string: extension.to_string(),
179            reason: "Expected .msg, .srv, or .action extension".to_string(),
180        }),
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187
188    #[test]
189    fn test_interface_specification_message_methods() {
190        let msg = parse_message_string("test_pkg", "TestMsg", "int32 x\n").unwrap();
191        let iface = InterfaceSpecification::Message(msg);
192
193        assert_eq!(iface.package_name(), "test_pkg");
194        assert_eq!(iface.interface_name(), "TestMsg");
195        assert_eq!(iface.full_name(), "test_pkg/TestMsg");
196        assert!(iface.is_message());
197        assert!(!iface.is_service());
198        assert!(!iface.is_action());
199        assert!(iface.as_message().is_some());
200        assert!(iface.as_service().is_none());
201        assert!(iface.as_action().is_none());
202    }
203
204    #[test]
205    fn test_interface_specification_service_methods() {
206        let srv = parse_service_string("test_pkg", "TestSrv", "int32 a\n---\nint32 b\n").unwrap();
207        let iface = InterfaceSpecification::Service(srv);
208
209        assert_eq!(iface.package_name(), "test_pkg");
210        assert_eq!(iface.interface_name(), "TestSrv");
211        assert_eq!(iface.full_name(), "test_pkg/TestSrv");
212        assert!(!iface.is_message());
213        assert!(iface.is_service());
214        assert!(!iface.is_action());
215        assert!(iface.as_message().is_none());
216        assert!(iface.as_service().is_some());
217        assert!(iface.as_action().is_none());
218    }
219
220    #[test]
221    fn test_interface_specification_action_methods() {
222        let action = parse_action_string(
223            "test_pkg",
224            "TestAction",
225            "int32 x\n---\nint32 y\n---\nint32 z\n",
226        )
227        .unwrap();
228        let iface = InterfaceSpecification::Action(action);
229
230        assert_eq!(iface.package_name(), "test_pkg");
231        assert_eq!(iface.interface_name(), "TestAction");
232        assert_eq!(iface.full_name(), "test_pkg/TestAction");
233        assert!(!iface.is_message());
234        assert!(!iface.is_service());
235        assert!(iface.is_action());
236        assert!(iface.as_message().is_none());
237        assert!(iface.as_service().is_none());
238        assert!(iface.as_action().is_some());
239    }
240
241    #[test]
242    fn test_interface_specification_display() {
243        let msg = parse_message_string("pkg", "Msg", "int32 x\n").unwrap();
244        let iface = InterfaceSpecification::Message(msg);
245        let display = format!("{iface}");
246        assert!(display.contains("Msg"));
247    }
248
249    #[test]
250    fn test_parse_interface_file_msg() {
251        let temp_dir = std::env::temp_dir();
252        let path = temp_dir.join("Test.msg");
253        std::fs::write(&path, "int32 value\n").unwrap();
254
255        let result = parse_interface_file("test_pkg", &path);
256        assert!(result.is_ok());
257        assert!(result.unwrap().is_message());
258
259        std::fs::remove_file(&path).ok();
260    }
261
262    #[test]
263    fn test_parse_interface_file_srv() {
264        let temp_dir = std::env::temp_dir();
265        let path = temp_dir.join("Test.srv");
266        std::fs::write(&path, "int32 a\n---\nint32 b\n").unwrap();
267
268        let result = parse_interface_file("test_pkg", &path);
269        assert!(result.is_ok());
270        assert!(result.unwrap().is_service());
271
272        std::fs::remove_file(&path).ok();
273    }
274
275    #[test]
276    fn test_parse_interface_file_action() {
277        let temp_dir = std::env::temp_dir();
278        let path = temp_dir.join("Test.action");
279        std::fs::write(&path, "int32 x\n---\nint32 y\n---\nint32 z\n").unwrap();
280
281        let result = parse_interface_file("test_pkg", &path);
282        assert!(result.is_ok());
283        assert!(result.unwrap().is_action());
284
285        std::fs::remove_file(&path).ok();
286    }
287
288    #[test]
289    fn test_parse_interface_file_invalid_extension() {
290        let temp_dir = std::env::temp_dir();
291        let path = temp_dir.join("test.txt");
292        std::fs::write(&path, "int32 x\n").unwrap();
293
294        let result = parse_interface_file("test_pkg", &path);
295        assert!(result.is_err());
296
297        std::fs::remove_file(&path).ok();
298    }
299
300    #[test]
301    fn test_parse_interface_file_no_extension() {
302        let temp_dir = std::env::temp_dir();
303        let path = temp_dir.join("test");
304        std::fs::write(&path, "int32 x\n").unwrap();
305
306        let result = parse_interface_file("test_pkg", &path);
307        assert!(result.is_err());
308
309        std::fs::remove_file(&path).ok();
310    }
311}