use std::collections::HashMap;
use std::fs;
use std::path::Path;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use super::errors::{ParseError, ParseResult};
use crate::msg::types::{AnnotationValue, Annotations, Constant, Field, Type};
use crate::msg::validation::{
COMMENT_DELIMITER, CONSTANT_SEPARATOR, OPTIONAL_ANNOTATION, is_valid_message_name,
is_valid_package_name,
};
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct MessageSpecification {
pub pkg_name: String,
pub msg_name: String,
pub fields: Vec<Field>,
pub constants: Vec<Constant>,
pub annotations: Annotations,
}
impl MessageSpecification {
pub fn new(pkg_name: String, msg_name: String) -> ParseResult<Self> {
if !is_valid_package_name(&pkg_name) {
return Err(ParseError::InvalidResourceName {
name: pkg_name,
reason: "invalid package name pattern".to_string(),
});
}
if !is_valid_message_name(&msg_name) {
return Err(ParseError::InvalidResourceName {
name: msg_name,
reason: "invalid message name pattern".to_string(),
});
}
Ok(MessageSpecification {
pkg_name,
msg_name,
fields: Vec::new(),
constants: Vec::new(),
annotations: HashMap::new(),
})
}
pub fn add_field(&mut self, field: Field) {
self.fields.push(field);
}
pub fn add_constant(&mut self, constant: Constant) {
self.constants.push(constant);
}
#[must_use]
pub fn get_field(&self, name: &str) -> Option<&Field> {
self.fields.iter().find(|f| f.name == name)
}
#[must_use]
pub fn get_constant(&self, name: &str) -> Option<&Constant> {
self.constants.iter().find(|c| c.name == name)
}
#[must_use]
pub fn has_fields(&self) -> bool {
!self.fields.is_empty()
}
#[must_use]
pub fn has_constants(&self) -> bool {
!self.constants.is_empty()
}
}
impl std::fmt::Display for MessageSpecification {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "# {}/{}", self.pkg_name, self.msg_name)?;
for constant in &self.constants {
writeln!(f, "{constant}")?;
}
if !self.constants.is_empty() && !self.fields.is_empty() {
writeln!(f)?; }
for field in &self.fields {
writeln!(f, "{field}")?;
}
Ok(())
}
}
pub fn parse_message_file<P: AsRef<Path>>(
pkg_name: &str,
interface_filename: P,
) -> ParseResult<MessageSpecification> {
let path = interface_filename.as_ref();
let basename =
path.file_name()
.and_then(|n| n.to_str())
.ok_or_else(|| ParseError::InvalidField {
reason: "invalid filename".to_string(),
})?;
let msg_name = basename
.strip_suffix(".msg")
.unwrap_or(basename)
.to_string();
let content = fs::read_to_string(path)?;
parse_message_string(pkg_name, &msg_name, &content)
}
pub fn parse_message_string(
pkg_name: &str,
msg_name: &str,
message_string: &str,
) -> ParseResult<MessageSpecification> {
let mut spec = MessageSpecification::new(pkg_name.to_string(), msg_name.to_string())?;
let normalized_content = message_string.replace('\t', " ");
let (file_level_comments, content_lines) = extract_file_level_comments(&normalized_content);
if !file_level_comments.is_empty() {
spec.annotations.insert(
"comment".to_string(),
AnnotationValue::StringList(file_level_comments),
);
}
let mut current_comments = Vec::<String>::new();
let mut is_optional = false;
for (line_num, line) in content_lines.iter().enumerate() {
let line = line.trim_end();
if line.trim().is_empty() {
continue;
}
let (line_content, comment) = extract_line_comment(line);
if let Some(comment_text) = comment {
if line_content.trim().is_empty() {
current_comments.push(comment_text);
continue;
}
current_comments.push(comment_text);
}
let line_content = line_content.trim();
if line_content.is_empty() {
continue;
}
if line_content == OPTIONAL_ANNOTATION {
is_optional = true;
continue;
}
match parse_line_content(line_content, pkg_name, line_num + 1) {
Ok(LineContent::Field(mut field)) => {
if !current_comments.is_empty() {
field.annotations.insert(
"comment".to_string(),
AnnotationValue::StringList(current_comments.clone()),
);
current_comments.clear();
}
if is_optional {
field
.annotations
.insert("optional".to_string(), AnnotationValue::Bool(true));
is_optional = false;
}
spec.add_field(field);
}
Ok(LineContent::Constant(mut constant)) => {
if !current_comments.is_empty() {
constant.annotations.insert(
"comment".to_string(),
AnnotationValue::StringList(current_comments.clone()),
);
current_comments.clear();
}
spec.add_constant(constant);
}
Err(e) => {
return Err(ParseError::LineParseError {
line: line_num + 1,
message: format!("Error parsing line '{line_content}': {e}"),
});
}
}
}
process_comments(&mut spec);
Ok(spec)
}
enum LineContent {
Field(Field),
Constant(Constant),
}
fn extract_file_level_comments(message_string: &str) -> (Vec<String>, Vec<String>) {
let lines: Vec<String> = message_string
.lines()
.map(std::string::ToString::to_string)
.collect();
let mut file_level_comments = Vec::new();
let mut first_content_index = 0;
for (i, line) in lines.iter().enumerate() {
let trimmed = line.trim();
if trimmed.is_empty() {
first_content_index = i + 1;
break;
} else if trimmed.starts_with(COMMENT_DELIMITER) {
if let Some(comment_text) = trimmed.strip_prefix(COMMENT_DELIMITER) {
file_level_comments.push(comment_text.trim_start().to_string());
}
} else {
first_content_index = i;
break;
}
}
let content_lines = lines[first_content_index..].to_vec();
(file_level_comments, content_lines)
}
fn extract_line_comment(line: &str) -> (String, Option<String>) {
if let Some(comment_index) = line.find(COMMENT_DELIMITER) {
let content = line[..comment_index].to_string();
let comment = line[comment_index + 1..].trim_start().to_string();
(content, Some(comment))
} else {
(line.to_string(), None)
}
}
fn parse_line_content(line: &str, pkg_name: &str, _line_num: usize) -> ParseResult<LineContent> {
if line.contains(CONSTANT_SEPARATOR) && !is_array_bound_syntax(line) {
parse_constant_line(line)
} else {
parse_field_line(line, pkg_name)
}
}
fn is_array_bound_syntax(line: &str) -> bool {
if line.contains("<=") && (line.contains('[') || line.contains(']')) {
return true;
}
if line.contains("<=") && (line.contains("string") || line.contains("wstring")) {
return true;
}
false
}
fn parse_constant_line(line: &str) -> ParseResult<LineContent> {
let parts: Vec<&str> = line.splitn(2, CONSTANT_SEPARATOR).collect();
if parts.len() != 2 {
return Err(ParseError::InvalidConstant {
reason: "constant must have format: TYPE NAME=VALUE".to_string(),
});
}
let left_part = parts[0].trim();
let value_part = parts[1].trim();
let type_name_parts: Vec<&str> = left_part.split_whitespace().collect();
if type_name_parts.len() != 2 {
return Err(ParseError::InvalidConstant {
reason: "constant must have format: TYPE NAME=VALUE".to_string(),
});
}
let type_name = type_name_parts[0];
let const_name = type_name_parts[1];
let constant = Constant::new(type_name, const_name, value_part)?;
Ok(LineContent::Constant(constant))
}
fn parse_field_line(line: &str, pkg_name: &str) -> ParseResult<LineContent> {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() < 2 {
return Err(ParseError::InvalidField {
reason: "field must have at least type and name".to_string(),
});
}
let type_string = parts[0];
let field_name = parts[1];
let default_value = if parts.len() > 2 {
Some(parts[2..].join(" "))
} else {
None
};
let field_type = Type::new(type_string, Some(pkg_name))?;
let field = Field::new(field_type, field_name, default_value.as_deref())?;
Ok(LineContent::Field(field))
}
fn process_comments(spec: &mut MessageSpecification) {
process_element_comments(&mut spec.annotations);
for field in &mut spec.fields {
process_element_comments(&mut field.annotations);
}
for constant in &mut spec.constants {
process_element_comments(&mut constant.annotations);
}
}
fn process_element_comments(annotations: &mut Annotations) {
if let Some(AnnotationValue::StringList(comments)) = annotations.get("comment").cloned() {
let comment_text = comments.join("\n");
let mut processed_comments = if let Some(unit) = extract_unit_from_comment(&comment_text) {
annotations.insert("unit".to_string(), AnnotationValue::String(unit.clone()));
comments
.into_iter()
.map(|line| remove_unit_from_line(&line, &unit))
.collect()
} else {
comments
};
processed_comments.retain(|line| !line.trim().is_empty());
if processed_comments.is_empty() {
annotations.remove("comment");
} else {
annotations.insert(
"comment".to_string(),
AnnotationValue::StringList(processed_comments),
);
}
}
}
fn extract_unit_from_comment(comment: &str) -> Option<String> {
let re = regex::Regex::new(r"\[([^,\]]+)\]").ok()?;
let captures = re.captures(comment)?;
captures.get(1).map(|m| m.as_str().trim().to_string())
}
fn remove_unit_from_line(line: &str, unit: &str) -> String {
let pattern = format!("[{unit}]");
line.replace(&pattern, "").trim().to_string()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::msg::validation::PrimitiveValue;
#[test]
fn test_parse_simple_message() {
let content = r"
# This is a test message
int32 x
int32 y
string name
";
let spec = parse_message_string("test_msgs", "TestMessage", content).unwrap();
assert_eq!(spec.pkg_name, "test_msgs");
assert_eq!(spec.msg_name, "TestMessage");
assert_eq!(spec.fields.len(), 3);
assert_eq!(spec.fields[0].name, "x");
assert_eq!(spec.fields[1].name, "y");
assert_eq!(spec.fields[2].name, "name");
}
#[test]
fn test_parse_message_with_constants() {
let content = r#"
# Constants
int32 MAX_VALUE=100
string DEFAULT_NAME="test"
# Fields
int32 value
string name
"#;
let spec = parse_message_string("test_msgs", "TestMessage", content).unwrap();
assert_eq!(spec.constants.len(), 2);
assert_eq!(spec.fields.len(), 2);
let max_const = spec.get_constant("MAX_VALUE").unwrap();
assert_eq!(max_const.value, PrimitiveValue::Int32(100));
}
#[test]
fn test_parse_message_with_arrays() {
let content = r"
int32[] dynamic_array
int32[5] fixed_array
int32[<=10] bounded_array
";
let spec = parse_message_string("test_msgs", "TestMessage", content).unwrap();
assert_eq!(spec.fields.len(), 3);
assert!(spec.fields[0].field_type.is_dynamic_array());
assert_eq!(spec.fields[1].field_type.array_size, Some(5));
assert!(spec.fields[2].field_type.is_bounded_array());
}
#[test]
fn test_parse_message_with_comments() {
let content = r"
# File level comment
# Second line
int32 x # X coordinate
int32 y # Y coordinate
";
let spec = parse_message_string("test_msgs", "TestMessage", content).unwrap();
if let Some(AnnotationValue::StringList(comments)) = spec.annotations.get("comment") {
assert!(comments.contains(&"File level comment".to_string()));
}
assert!(spec.fields[0].annotations.contains_key("comment"));
assert!(spec.fields[1].annotations.contains_key("comment"));
}
#[test]
fn test_parse_message_with_optional_fields() {
let content = r"
int32 required_field
@optional
int32 optional_field
";
let spec = parse_message_string("test_msgs", "TestMessage", content).unwrap();
assert_eq!(spec.fields.len(), 2);
assert!(!spec.fields[0].annotations.contains_key("optional"));
if let Some(AnnotationValue::Bool(is_optional)) = spec.fields[1].annotations.get("optional")
{
assert!(is_optional);
}
}
}