re_ros_msg/
lib.rs

1//! Minimal ROS 2 `.msg` reflection parser (messages only).
2//!
3//! This module parses the textual ROS 2 message definition format (aka `.msg`)
4//! into a typed, reflection-friendly representation. It is intentionally kept
5//! generic and does not rely on any pre-baked message definitions, so it can be
6//! used to parse unknown types and still extract semantic meaning (types,
7//! arrays, names, constants, default values).
8use anyhow::Context as _;
9
10use crate::message_spec::MessageSpecification;
11
12pub mod deserialize;
13pub mod message_spec;
14
15/// Parse a schema name from a line starting with "MSG: ".
16fn parse_schema_name(line: &str) -> Option<&str> {
17    line.trim().strip_prefix("MSG: ").map(str::trim)
18}
19
20#[derive(Debug, Clone, PartialEq)]
21pub struct MessageSchema {
22    /// Specification of the main message type.
23    pub spec: MessageSpecification,
24
25    /// Dependent message types referenced by the main type.
26    pub dependencies: Vec<MessageSpecification>, // Other message types referenced by this one.
27}
28
29impl MessageSchema {
30    pub fn parse(name: &str, input: &str) -> anyhow::Result<Self> {
31        let main_spec_content = extract_main_msg_spec(input);
32        let specs = extract_msg_specs(input);
33
34        let main_spec = MessageSpecification::parse(name, &main_spec_content)
35            .with_context(|| format!("failed to parse main message spec `{name}`"))?;
36
37        let mut dependencies = Vec::new();
38        for (dep_name, dep_content) in specs {
39            let dep_spec = MessageSpecification::parse(&dep_name, &dep_content)
40                .with_context(|| format!("failed to parse dependent message spec `{dep_name}`"))?;
41            dependencies.push(dep_spec);
42        }
43
44        Ok(Self {
45            spec: main_spec,
46            dependencies,
47        })
48    }
49}
50
51/// Check if a line is a schema separator (a line of at least 3 '=' characters).
52pub fn is_schema_separator(line: &str) -> bool {
53    let line = line.trim();
54    line.len() >= 3 && line.chars().all(|c| c == '=')
55}
56
57/// Extract the main message specification from input, stopping at the first schema separator.
58///
59/// The main spec is everything before the first "====" separator line.
60fn extract_main_msg_spec(input: &str) -> String {
61    input
62        .lines()
63        .take_while(|line| !is_schema_separator(line))
64        .filter(|line| !line.is_empty())
65        .collect::<Vec<_>>()
66        .join("\n")
67}
68
69/// Find "MSG: `<name>`" and take the rest as content
70/// Extract all message specifications from input that are separated by schema separators.
71///
72/// Returns a vector of `(message_name, message_body)` pairs for each schema found.
73fn extract_msg_specs(input: &str) -> Vec<(String, String)> {
74    let mut specs = Vec::new();
75    let mut current_section = Vec::new();
76
77    for line in input.lines() {
78        if is_schema_separator(line) {
79            if let Some(spec) = parse_section(&current_section) {
80                specs.push(spec);
81            }
82            current_section.clear();
83        } else {
84            current_section.push(line);
85        }
86    }
87
88    // Handle the final section if it doesn't end with a separator
89    if let Some(spec) = parse_section(&current_section) {
90        specs.push(spec);
91    }
92
93    specs
94}
95
96/// Parse a section of lines into a (name, body) pair.
97///
98/// The first line should contain "MSG: `<name>`" and subsequent lines form the message body.
99fn parse_section(lines: &[&str]) -> Option<(String, String)> {
100    if lines.len() < 2 {
101        return None;
102    }
103
104    let first_line = lines[0].trim();
105    let name = parse_schema_name(first_line)?;
106    let body = lines[1..].join("\n");
107
108    Some((name.to_owned(), body))
109}