1use std::collections::HashMap;
3use std::fs;
4use std::path::Path;
5
6#[cfg(feature = "serde")]
7use serde::{Deserialize, Serialize};
8
9use super::errors::{ParseError, ParseResult};
10use crate::msg::types::{AnnotationValue, Annotations, Constant, Field, Type};
11use crate::msg::validation::{
12 COMMENT_DELIMITER, CONSTANT_SEPARATOR, OPTIONAL_ANNOTATION, is_valid_message_name,
13 is_valid_package_name,
14};
15
16#[derive(Debug, Clone, PartialEq)]
18#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
19pub struct MessageSpecification {
20 pub pkg_name: String,
22 pub msg_name: String,
24 pub fields: Vec<Field>,
26 pub constants: Vec<Constant>,
28 pub annotations: Annotations,
30}
31
32impl MessageSpecification {
33 pub fn new(pkg_name: String, msg_name: String) -> ParseResult<Self> {
39 if !is_valid_package_name(&pkg_name) {
40 return Err(ParseError::InvalidResourceName {
41 name: pkg_name,
42 reason: "invalid package name pattern".to_string(),
43 });
44 }
45
46 if !is_valid_message_name(&msg_name) {
47 return Err(ParseError::InvalidResourceName {
48 name: msg_name,
49 reason: "invalid message name pattern".to_string(),
50 });
51 }
52
53 Ok(MessageSpecification {
54 pkg_name,
55 msg_name,
56 fields: Vec::new(),
57 constants: Vec::new(),
58 annotations: HashMap::new(),
59 })
60 }
61
62 pub fn add_field(&mut self, field: Field) {
64 self.fields.push(field);
65 }
66
67 pub fn add_constant(&mut self, constant: Constant) {
69 self.constants.push(constant);
70 }
71
72 #[must_use]
74 pub fn get_field(&self, name: &str) -> Option<&Field> {
75 self.fields.iter().find(|f| f.name == name)
76 }
77
78 #[must_use]
80 pub fn get_constant(&self, name: &str) -> Option<&Constant> {
81 self.constants.iter().find(|c| c.name == name)
82 }
83
84 #[must_use]
86 pub fn has_fields(&self) -> bool {
87 !self.fields.is_empty()
88 }
89
90 #[must_use]
92 pub fn has_constants(&self) -> bool {
93 !self.constants.is_empty()
94 }
95}
96
97impl std::fmt::Display for MessageSpecification {
98 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
99 writeln!(f, "# {}/{}", self.pkg_name, self.msg_name)?;
100
101 for constant in &self.constants {
103 writeln!(f, "{constant}")?;
104 }
105
106 if !self.constants.is_empty() && !self.fields.is_empty() {
107 writeln!(f)?; }
109
110 for field in &self.fields {
112 writeln!(f, "{field}")?;
113 }
114
115 Ok(())
116 }
117}
118
119pub fn parse_message_file<P: AsRef<Path>>(
125 pkg_name: &str,
126 interface_filename: P,
127) -> ParseResult<MessageSpecification> {
128 let path = interface_filename.as_ref();
129 let basename =
130 path.file_name()
131 .and_then(|n| n.to_str())
132 .ok_or_else(|| ParseError::InvalidField {
133 reason: "invalid filename".to_string(),
134 })?;
135
136 let msg_name = basename
137 .strip_suffix(".msg")
138 .unwrap_or(basename)
139 .to_string();
140
141 let content = fs::read_to_string(path)?;
142 parse_message_string(pkg_name, &msg_name, &content)
143}
144
145pub fn parse_message_string(
151 pkg_name: &str,
152 msg_name: &str,
153 message_string: &str,
154) -> ParseResult<MessageSpecification> {
155 let mut spec = MessageSpecification::new(pkg_name.to_string(), msg_name.to_string())?;
156
157 let normalized_content = message_string.replace('\t', " ");
159
160 let (file_level_comments, content_lines) = extract_file_level_comments(&normalized_content);
162
163 if !file_level_comments.is_empty() {
165 spec.annotations.insert(
166 "comment".to_string(),
167 AnnotationValue::StringList(file_level_comments),
168 );
169 }
170
171 let mut current_comments = Vec::<String>::new();
173 let mut is_optional = false;
174
175 for (line_num, line) in content_lines.iter().enumerate() {
176 let line = line.trim_end();
177
178 if line.trim().is_empty() {
180 continue;
181 }
182
183 let (line_content, comment) = extract_line_comment(line);
185
186 if let Some(comment_text) = comment {
187 if line_content.trim().is_empty() {
188 current_comments.push(comment_text);
190 continue;
191 }
192 current_comments.push(comment_text);
194 }
195
196 let line_content = line_content.trim();
197 if line_content.is_empty() {
198 continue;
199 }
200
201 if line_content == OPTIONAL_ANNOTATION {
203 is_optional = true;
204 continue;
205 }
206
207 match parse_line_content(line_content, pkg_name, line_num + 1) {
209 Ok(LineContent::Field(mut field)) => {
210 if !current_comments.is_empty() {
212 field.annotations.insert(
213 "comment".to_string(),
214 AnnotationValue::StringList(current_comments.clone()),
215 );
216 current_comments.clear();
217 }
218
219 if is_optional {
221 field
222 .annotations
223 .insert("optional".to_string(), AnnotationValue::Bool(true));
224 is_optional = false;
225 }
226
227 spec.add_field(field);
228 }
229 Ok(LineContent::Constant(mut constant)) => {
230 if !current_comments.is_empty() {
232 constant.annotations.insert(
233 "comment".to_string(),
234 AnnotationValue::StringList(current_comments.clone()),
235 );
236 current_comments.clear();
237 }
238
239 spec.add_constant(constant);
240 }
241 Err(e) => {
242 return Err(ParseError::LineParseError {
243 line: line_num + 1,
244 message: format!("Error parsing line '{line_content}': {e}"),
245 });
246 }
247 }
248 }
249
250 process_comments(&mut spec);
252
253 Ok(spec)
254}
255
256enum LineContent {
258 Field(Field),
259 Constant(Constant),
260}
261
262fn extract_file_level_comments(message_string: &str) -> (Vec<String>, Vec<String>) {
264 let lines: Vec<String> = message_string
265 .lines()
266 .map(std::string::ToString::to_string)
267 .collect();
268
269 let mut file_level_comments = Vec::new();
270 let mut first_content_index = 0;
271
272 for (i, line) in lines.iter().enumerate() {
274 let trimmed = line.trim();
275
276 if trimmed.is_empty() {
277 first_content_index = i + 1;
279 break;
280 } else if trimmed.starts_with(COMMENT_DELIMITER) {
281 if let Some(comment_text) = trimmed.strip_prefix(COMMENT_DELIMITER) {
283 file_level_comments.push(comment_text.trim_start().to_string());
284 }
285 } else {
286 first_content_index = i;
288 break;
289 }
290 }
291
292 let content_lines = lines[first_content_index..].to_vec();
293
294 (file_level_comments, content_lines)
295}
296
297fn extract_line_comment(line: &str) -> (String, Option<String>) {
299 if let Some(comment_index) = line.find(COMMENT_DELIMITER) {
300 let content = line[..comment_index].to_string();
301 let comment = line[comment_index + 1..].trim_start().to_string();
302 (content, Some(comment))
303 } else {
304 (line.to_string(), None)
305 }
306}
307
308fn parse_line_content(line: &str, pkg_name: &str, _line_num: usize) -> ParseResult<LineContent> {
310 if line.contains(CONSTANT_SEPARATOR) && !is_array_bound_syntax(line) {
312 parse_constant_line(line)
313 } else {
314 parse_field_line(line, pkg_name)
315 }
316}
317
318fn is_array_bound_syntax(line: &str) -> bool {
320 if line.contains("<=") && (line.contains('[') || line.contains(']')) {
322 return true;
323 }
324
325 if line.contains("<=") && (line.contains("string") || line.contains("wstring")) {
327 return true;
328 }
329
330 false
331}
332
333fn parse_constant_line(line: &str) -> ParseResult<LineContent> {
335 let parts: Vec<&str> = line.splitn(2, CONSTANT_SEPARATOR).collect();
336 if parts.len() != 2 {
337 return Err(ParseError::InvalidConstant {
338 reason: "constant must have format: TYPE NAME=VALUE".to_string(),
339 });
340 }
341
342 let left_part = parts[0].trim();
343 let value_part = parts[1].trim();
344
345 let type_name_parts: Vec<&str> = left_part.split_whitespace().collect();
347 if type_name_parts.len() != 2 {
348 return Err(ParseError::InvalidConstant {
349 reason: "constant must have format: TYPE NAME=VALUE".to_string(),
350 });
351 }
352
353 let type_name = type_name_parts[0];
354 let const_name = type_name_parts[1];
355
356 let constant = Constant::new(type_name, const_name, value_part)?;
357 Ok(LineContent::Constant(constant))
358}
359
360fn parse_field_line(line: &str, pkg_name: &str) -> ParseResult<LineContent> {
362 let parts: Vec<&str> = line.split_whitespace().collect();
363 if parts.len() < 2 {
364 return Err(ParseError::InvalidField {
365 reason: "field must have at least type and name".to_string(),
366 });
367 }
368
369 let type_string = parts[0];
370 let field_name = parts[1];
371
372 let default_value = if parts.len() > 2 {
374 Some(parts[2..].join(" "))
375 } else {
376 None
377 };
378
379 let field_type = Type::new(type_string, Some(pkg_name))?;
380 let field = Field::new(field_type, field_name, default_value.as_deref())?;
381
382 Ok(LineContent::Field(field))
383}
384
385fn process_comments(spec: &mut MessageSpecification) {
387 process_element_comments(&mut spec.annotations);
389
390 for field in &mut spec.fields {
392 process_element_comments(&mut field.annotations);
393 }
394
395 for constant in &mut spec.constants {
397 process_element_comments(&mut constant.annotations);
398 }
399}
400
401fn process_element_comments(annotations: &mut Annotations) {
403 if let Some(AnnotationValue::StringList(comments)) = annotations.get("comment").cloned() {
404 let comment_text = comments.join("\n");
406
407 let mut processed_comments = if let Some(unit) = extract_unit_from_comment(&comment_text) {
408 annotations.insert("unit".to_string(), AnnotationValue::String(unit.clone()));
409
410 comments
412 .into_iter()
413 .map(|line| remove_unit_from_line(&line, &unit))
414 .collect()
415 } else {
416 comments
417 };
418
419 processed_comments.retain(|line| !line.trim().is_empty());
421
422 if processed_comments.is_empty() {
423 annotations.remove("comment");
424 } else {
425 annotations.insert(
426 "comment".to_string(),
427 AnnotationValue::StringList(processed_comments),
428 );
429 }
430 }
431}
432
433fn extract_unit_from_comment(comment: &str) -> Option<String> {
435 let re = regex::Regex::new(r"\[([^,\]]+)\]").ok()?;
437 let captures = re.captures(comment)?;
438 captures.get(1).map(|m| m.as_str().trim().to_string())
439}
440
441fn remove_unit_from_line(line: &str, unit: &str) -> String {
443 let pattern = format!("[{unit}]");
444 line.replace(&pattern, "").trim().to_string()
445}
446
447#[cfg(test)]
448mod tests {
449 use super::*;
450 use crate::msg::validation::PrimitiveValue;
451
452 #[test]
453 fn test_parse_simple_message() {
454 let content = r"
455# This is a test message
456int32 x
457int32 y
458string name
459";
460
461 let spec = parse_message_string("test_msgs", "TestMessage", content).unwrap();
462 assert_eq!(spec.pkg_name, "test_msgs");
463 assert_eq!(spec.msg_name, "TestMessage");
464 assert_eq!(spec.fields.len(), 3);
465 assert_eq!(spec.fields[0].name, "x");
466 assert_eq!(spec.fields[1].name, "y");
467 assert_eq!(spec.fields[2].name, "name");
468 }
469
470 #[test]
471 fn test_parse_message_with_constants() {
472 let content = r#"
473# Constants
474int32 MAX_VALUE=100
475string DEFAULT_NAME="test"
476
477# Fields
478int32 value
479string name
480"#;
481
482 let spec = parse_message_string("test_msgs", "TestMessage", content).unwrap();
483 assert_eq!(spec.constants.len(), 2);
484 assert_eq!(spec.fields.len(), 2);
485
486 let max_const = spec.get_constant("MAX_VALUE").unwrap();
487 assert_eq!(max_const.value, PrimitiveValue::Int32(100));
488 }
489
490 #[test]
491 fn test_parse_message_with_arrays() {
492 let content = r"
493int32[] dynamic_array
494int32[5] fixed_array
495int32[<=10] bounded_array
496";
497
498 let spec = parse_message_string("test_msgs", "TestMessage", content).unwrap();
499 assert_eq!(spec.fields.len(), 3);
500
501 assert!(spec.fields[0].field_type.is_dynamic_array());
502 assert_eq!(spec.fields[1].field_type.array_size, Some(5));
503 assert!(spec.fields[2].field_type.is_bounded_array());
504 }
505
506 #[test]
507 fn test_parse_message_with_comments() {
508 let content = r"
509# File level comment
510# Second line
511
512int32 x # X coordinate
513int32 y # Y coordinate
514";
515
516 let spec = parse_message_string("test_msgs", "TestMessage", content).unwrap();
517
518 if let Some(AnnotationValue::StringList(comments)) = spec.annotations.get("comment") {
520 assert!(comments.contains(&"File level comment".to_string()));
521 }
522
523 assert!(spec.fields[0].annotations.contains_key("comment"));
525 assert!(spec.fields[1].annotations.contains_key("comment"));
526 }
527
528 #[test]
529 fn test_parse_message_with_optional_fields() {
530 let content = r"
531int32 required_field
532@optional
533int32 optional_field
534";
535
536 let spec = parse_message_string("test_msgs", "TestMessage", content).unwrap();
537 assert_eq!(spec.fields.len(), 2);
538
539 assert!(!spec.fields[0].annotations.contains_key("optional"));
541
542 if let Some(AnnotationValue::Bool(is_optional)) = spec.fields[1].annotations.get("optional")
544 {
545 assert!(is_optional);
546 }
547 }
548}