1use anyhow::{Context, Result, bail};
6use std::collections::{BTreeSet, HashMap};
7use std::fs;
8use std::path::{Path, PathBuf};
9
10use super::err::RIDLC_ERR_PREFIX;
11
12#[derive(Clone, Debug)]
13pub struct MsgField {
14 pub name: String,
15 pub type_ref: MsgTypeRef,
16 pub is_array: bool,
17 pub array_size: Option<usize>,
19 pub string_max_len: Option<usize>,
22 pub description: String,
28}
29
30#[derive(Clone, Debug)]
31pub struct MsgConstant {
32 pub name: String,
33 pub type_name: String,
34 pub value: i32,
35}
36
37#[derive(Clone, Debug, PartialEq, Eq)]
38pub enum MsgTypeRef {
39 Primitive(String),
44 Named {
45 package: String,
46 name: String,
47 },
48}
49
50#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
61pub enum RosPrimitive {
62 Bool,
63 Byte, Char, Int8,
66 Uint8,
67 Int16,
68 Uint16,
69 Int32,
70 Uint32,
71 Int64,
72 Uint64,
73 Float32,
74 Float64,
75 String,
76 Wstring,
77}
78
79impl RosPrimitive {
80 pub fn parse(s: &str) -> Option<Self> {
81 Some(match s {
82 "bool" => Self::Bool,
83 "byte" => Self::Byte,
84 "char" => Self::Char,
85 "int8" => Self::Int8,
86 "uint8" => Self::Uint8,
87 "int16" => Self::Int16,
88 "uint16" => Self::Uint16,
89 "int32" => Self::Int32,
90 "uint32" => Self::Uint32,
91 "int64" => Self::Int64,
92 "uint64" => Self::Uint64,
93 "float32" => Self::Float32,
94 "float64" => Self::Float64,
95 "string" => Self::String,
96 "wstring" => Self::Wstring,
97 _ => return None,
98 })
99 }
100
101 pub fn as_ros_str(&self) -> &'static str {
102 match self {
103 Self::Bool => "bool",
104 Self::Byte => "byte",
105 Self::Char => "char",
106 Self::Int8 => "int8",
107 Self::Uint8 => "uint8",
108 Self::Int16 => "int16",
109 Self::Uint16 => "uint16",
110 Self::Int32 => "int32",
111 Self::Uint32 => "uint32",
112 Self::Int64 => "int64",
113 Self::Uint64 => "uint64",
114 Self::Float32 => "float32",
115 Self::Float64 => "float64",
116 Self::String => "string",
117 Self::Wstring => "wstring",
118 }
119 }
120
121 pub fn proto_type(&self) -> &'static str {
123 match self {
124 Self::Bool => "bool",
125 Self::Byte | Self::Int8 => "int32",
129 Self::Char | Self::Uint8 => "uint32",
130 Self::Int16 => "int32",
131 Self::Uint16 => "uint32",
132 Self::Int32 => "int32",
133 Self::Uint32 => "uint32",
134 Self::Int64 => "int64",
135 Self::Uint64 => "uint64",
136 Self::Float32 => "float",
137 Self::Float64 => "double",
138 Self::String | Self::Wstring => "string",
139 }
140 }
141
142 pub fn python_type(&self) -> &'static str {
144 match self {
145 Self::Bool => "bool",
146 Self::Float32 | Self::Float64 => "float",
147 Self::String | Self::Wstring => "str",
148 Self::Byte
149 | Self::Char
150 | Self::Int8
151 | Self::Uint8
152 | Self::Int16
153 | Self::Uint16
154 | Self::Int32
155 | Self::Uint32
156 | Self::Int64
157 | Self::Uint64 => "int",
158 }
159 }
160
161 pub fn python_default(&self) -> &'static str {
163 match self {
164 Self::Bool => "False",
165 Self::Float32 | Self::Float64 => "0.0",
166 Self::String | Self::Wstring => "\"\"",
167 _ => "0",
168 }
169 }
170
171 pub fn python_cast(&self) -> &'static str {
174 self.python_type()
175 }
176
177 pub fn json_schema_type(&self) -> &'static str {
179 match self {
180 Self::Bool => "boolean",
181 Self::Float32 | Self::Float64 => "number",
182 Self::String | Self::Wstring => "string",
183 _ => "integer",
184 }
185 }
186
187 pub fn integer_range(&self) -> Option<(i128, i128)> {
192 Some(match self {
193 Self::Byte | Self::Int8 => (i8::MIN as i128, i8::MAX as i128),
194 Self::Char | Self::Uint8 => (0, u8::MAX as i128),
195 Self::Int16 => (i16::MIN as i128, i16::MAX as i128),
196 Self::Uint16 => (0, u16::MAX as i128),
197 Self::Int32 => (i32::MIN as i128, i32::MAX as i128),
198 Self::Uint32 => (0, u32::MAX as i128),
199 Self::Int64 => (i64::MIN as i128, i64::MAX as i128),
200 Self::Uint64 => (0, u64::MAX as i128),
203 _ => return None,
204 })
205 }
206
207 pub fn is_blob_element(&self) -> bool {
213 matches!(self, Self::Uint8)
214 }
215}
216
217#[derive(Clone, Debug)]
218pub struct MsgSpec {
219 pub package: String,
220 pub name: String,
221 pub constants: Vec<MsgConstant>,
222 pub fields: Vec<MsgField>,
223}
224
225#[derive(Clone, Debug)]
226pub struct SrvSpec {
227 pub package: String,
228 pub name: String,
229 pub request: MsgSpec,
230 pub response: MsgSpec,
231}
232
233#[derive(Clone, Debug, Default)]
234pub struct ResolveContext {
235 pub namespace: Option<String>,
236 pub interface_kind: Option<&'static str>,
237 pub interface_name: Option<String>,
238 pub field_name: Option<String>,
239}
240
241pub struct MsgResolver {
242 pub index: HashMap<(String, String), PathBuf>,
243 pub cache: HashMap<(String, String), MsgSpec>,
244 pub srv_index: HashMap<(String, String), PathBuf>,
245 pub srv_cache: HashMap<(String, String), SrvSpec>,
246 pub include_paths: Vec<PathBuf>,
247}
248
249impl MsgResolver {
250 pub fn new(include_paths: &[PathBuf]) -> Result<Self> {
251 let mut index = HashMap::new();
252 let mut srv_index = HashMap::new();
253 for root in include_paths {
254 index_msg_files(root, &mut index)?;
255 index_srv_files(root, &mut srv_index)?;
256 }
257 Ok(Self {
258 index,
259 cache: HashMap::new(),
260 srv_index,
261 srv_cache: HashMap::new(),
262 include_paths: include_paths.to_vec(),
263 })
264 }
265
266 pub fn resolve_ridl_type(&mut self, type_ref: &str, ctx: &ResolveContext) -> Result<()> {
267 if let Some((package, name)) = parse_ridl_type_ref(type_ref) {
268 self.resolve_named_type(&package, &name, Some((type_ref, ctx)))?;
269 }
270 Ok(())
271 }
272
273 pub fn resolve_named_type(
274 &mut self,
275 package: &str,
276 name: &str,
277 from: Option<(&str, &ResolveContext)>,
278 ) -> Result<()> {
279 let mut visiting: BTreeSet<(String, String)> = BTreeSet::new();
280 self.resolve_named_type_inner(package, name, from, &mut visiting)
281 }
282
283 fn resolve_named_type_inner(
289 &mut self,
290 package: &str,
291 name: &str,
292 from: Option<(&str, &ResolveContext)>,
293 visiting: &mut BTreeSet<(String, String)>,
294 ) -> Result<()> {
295 let key = (package.to_string(), name.to_string());
296 if self.cache.contains_key(&key) {
297 return Ok(());
298 }
299 if !visiting.insert(key.clone()) {
300 return Ok(());
304 }
305 let full_type = ros_msg_type_fmt(package, name);
306 let path = self
307 .find_msg_path(package, name)
308 .with_context(|| format_resolve_error(&full_type, from, &self.include_paths))?;
309 let spec = parse_msg_file(package, name, &path)?;
310 for field in &spec.fields {
311 if let MsgTypeRef::Named { package, name } = &field.type_ref {
312 let dep_ctx = ResolveContext {
313 namespace: Some(spec.package.clone()),
314 interface_kind: Some("msg"),
315 interface_name: Some(spec.name.clone()),
316 field_name: Some(field.name.clone()),
317 };
318 self.resolve_named_type_inner(
319 package,
320 name,
321 Some((&ros_msg_type_fmt(package, name), &dep_ctx)),
322 visiting,
323 )?;
324 }
325 }
326 visiting.remove(&key);
327 self.cache.insert(key, spec);
328 Ok(())
329 }
330
331 pub fn resolve_all_in_index(&mut self, verbose: bool, skip_count: &mut usize) -> Result<()> {
332 let keys: Vec<_> = self.index.keys().cloned().collect();
333 for (package, name) in keys {
334 if let Err(e) = self.resolve_named_type(&package, &name, None) {
335 if verbose {
336 eprintln!(
337 "[robonix-codegen] warning: skipping {}/{}: {:#}",
338 package, name, e
339 );
340 } else {
341 *skip_count += 1;
342 }
343 }
344 }
345 Ok(())
346 }
347
348 pub fn find_msg_path(&self, package: &str, name: &str) -> Option<PathBuf> {
349 let candidates = package_aliases(package);
350 for candidate in candidates {
351 if let Some(path) = self.index.get(&(candidate.to_string(), name.to_string())) {
352 return Some(path.clone());
353 }
354 }
355 None
356 }
357
358 pub fn ordered_specs(&self) -> Vec<&MsgSpec> {
359 let mut keys: Vec<_> = self.cache.keys().cloned().collect();
360 keys.sort();
361 keys.iter()
362 .filter_map(|key| self.cache.get(key))
363 .collect::<Vec<_>>()
364 }
365
366 pub fn resolve_all_srv(&mut self, verbose: bool, skip_count: &mut usize) -> Result<()> {
367 let keys: Vec<_> = self.srv_index.keys().cloned().collect();
368 for (package, name) in keys {
369 if let Err(e) = self.resolve_srv(&package, &name) {
370 if verbose {
371 eprintln!(
372 "[robonix-codegen] warning: skipping srv {}/{}: {:#}",
373 package, name, e
374 );
375 } else {
376 *skip_count += 1;
377 }
378 }
379 }
380 Ok(())
381 }
382
383 pub fn resolve_srv(&mut self, package: &str, name: &str) -> Result<()> {
384 let key = (package.to_string(), name.to_string());
385 if self.srv_cache.contains_key(&key) {
386 return Ok(());
387 }
388 let path = self.find_srv_path(package, name).with_context(|| {
389 format!("{RIDLC_ERR_PREFIX} .srv file not found: {package}/srv/{name}")
390 })?;
391 let spec = parse_srv_file(package, name, &path)?;
392 for field in spec
394 .request
395 .fields
396 .iter()
397 .chain(spec.response.fields.iter())
398 {
399 if let MsgTypeRef::Named {
400 package: pkg,
401 name: nm,
402 } = &field.type_ref
403 {
404 let dep_ctx = ResolveContext {
405 namespace: Some(spec.package.clone()),
406 interface_kind: Some("srv"),
407 interface_name: Some(spec.name.clone()),
408 field_name: Some(field.name.clone()),
409 };
410 self.resolve_named_type(pkg, nm, Some((&ros_msg_type_fmt(pkg, nm), &dep_ctx)))?;
411 }
412 }
413 self.srv_cache.insert(key, spec);
414 Ok(())
415 }
416
417 pub fn find_srv_path(&self, package: &str, name: &str) -> Option<PathBuf> {
418 let candidates = package_aliases(package);
419 for candidate in candidates {
420 if let Some(path) = self
421 .srv_index
422 .get(&(candidate.to_string(), name.to_string()))
423 {
424 return Some(path.clone());
425 }
426 }
427 None
428 }
429
430 pub fn ordered_srvs(&self) -> Vec<&SrvSpec> {
431 let mut keys: Vec<_> = self.srv_cache.keys().cloned().collect();
432 keys.sort();
433 keys.iter()
434 .filter_map(|key| self.srv_cache.get(key))
435 .collect::<Vec<_>>()
436 }
437
438 pub fn srv_spec(&self, package: &str, name: &str) -> Option<&SrvSpec> {
439 self.srv_cache.get(&(package.to_string(), name.to_string()))
440 }
441}
442
443pub fn index_msg_files(root: &Path, index: &mut HashMap<(String, String), PathBuf>) -> Result<()> {
444 if !root.exists() {
445 return Ok(());
446 }
447 for entry in fs::read_dir(root).with_context(|| {
448 format!(
449 "{RIDLC_ERR_PREFIX} failed to read include directory '{}' (check that path exists)",
450 root.display()
451 )
452 })? {
453 let entry = entry?;
454 let path = entry.path();
455 if path.is_dir() {
456 index_msg_files(&path, index)?;
457 continue;
458 }
459 if path.extension().and_then(|s| s.to_str()) != Some("msg") {
460 continue;
461 }
462 let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
463 continue;
464 };
465 let Some(package) = infer_package_name(&path) else {
466 continue;
467 };
468 index.entry((package, stem.to_string())).or_insert(path);
469 }
470 Ok(())
471}
472
473pub fn index_srv_files(root: &Path, index: &mut HashMap<(String, String), PathBuf>) -> Result<()> {
474 if !root.exists() {
475 return Ok(());
476 }
477 for entry in fs::read_dir(root).with_context(|| {
478 format!(
479 "{RIDLC_ERR_PREFIX} failed to read include directory '{}' (check that path exists)",
480 root.display()
481 )
482 })? {
483 let entry = entry?;
484 let path = entry.path();
485 if path.is_dir() {
486 index_srv_files(&path, index)?;
487 continue;
488 }
489 if path.extension().and_then(|s| s.to_str()) != Some("srv") {
490 continue;
491 }
492 let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
493 continue;
494 };
495 let Some(package) = infer_srv_package_name(&path) else {
496 continue;
497 };
498 index.entry((package, stem.to_string())).or_insert(path);
499 }
500 Ok(())
501}
502
503pub fn infer_srv_package_name(path: &Path) -> Option<String> {
504 let parent = path.parent()?;
505 let parent_name = parent.file_name()?.to_str()?;
506 if parent_name == "srv" {
507 return parent
508 .parent()?
509 .file_name()?
510 .to_str()
511 .map(|s| s.to_string());
512 }
513 Some(parent_name.to_string())
514}
515
516fn strip_leading_robonix_grpc_lines(src: &str) -> String {
518 let mut s = src.to_string();
519 loop {
520 let t = s.trim_start();
521 let first = t.lines().next().unwrap_or("").trim();
522 if first.starts_with("# @robonix.grpc") {
523 if let Some(pos) = s.find('\n') {
524 s = s[pos + 1..].to_string();
525 continue;
526 }
527 s.clear();
528 break;
529 }
530 break;
531 }
532 s
533}
534
535pub fn parse_srv_file(package: &str, name: &str, path: &Path) -> Result<SrvSpec> {
536 let src = fs::read_to_string(path).with_context(|| {
537 format!(
538 "{RIDLC_ERR_PREFIX} failed to read .srv file '{}'",
539 path.display()
540 )
541 })?;
542 let body = strip_leading_robonix_grpc_lines(&src);
543
544 let mut request_lines: Vec<&str> = Vec::new();
549 let mut response_lines: Vec<&str> = Vec::new();
550 let mut in_response = false;
551 for line in body.lines() {
552 if !in_response && line.trim() == "---" {
553 in_response = true;
554 continue;
555 }
556 if in_response {
557 response_lines.push(line);
558 } else {
559 request_lines.push(line);
560 }
561 }
562 let request_src = request_lines.join("\n");
563 let response_src = response_lines.join("\n");
564
565 let request = parse_msg_section(package, &format!("{name}_Request"), &request_src)
566 .with_context(|| format!("{RIDLC_ERR_PREFIX} parsing request of '{}'", path.display()))?;
567 let response = parse_msg_section(package, &format!("{name}_Response"), &response_src)
568 .with_context(|| {
569 format!(
570 "{RIDLC_ERR_PREFIX} parsing response of '{}'",
571 path.display()
572 )
573 })?;
574
575 Ok(SrvSpec {
576 package: package.to_string(),
577 name: name.to_string(),
578 request,
579 response,
580 })
581}
582
583fn parse_msg_section(package: &str, name: &str, src: &str) -> Result<MsgSpec> {
584 let (constants, fields) = parse_msg_members_from_lines(package, src, None)?;
585 Ok(MsgSpec {
586 package: package.to_string(),
587 name: name.to_string(),
588 constants,
589 fields,
590 })
591}
592
593fn parse_msg_members_from_lines(
602 package: &str,
603 src: &str,
604 path_hint: Option<&Path>,
605) -> Result<(Vec<MsgConstant>, Vec<MsgField>)> {
606 let mut constants = Vec::new();
607 let mut fields = Vec::new();
608 for raw_line in src.lines() {
609 let (code, comment) = match raw_line.find('#') {
613 Some(idx) => (&raw_line[..idx], raw_line[idx + 1..].trim()),
614 None => (raw_line, ""),
615 };
616 let code = code.trim();
617 if code.is_empty() {
618 continue;
619 }
620 if let Some(constant) = parse_constant_line(code).with_context(|| match path_hint {
621 Some(p) => format!(
622 "{RIDLC_ERR_PREFIX} invalid constant in '{}' at line '{}'",
623 p.display(),
624 code
625 ),
626 None => format!("{RIDLC_ERR_PREFIX} invalid constant at line '{code}'"),
627 })? {
628 constants.push(constant);
629 continue;
630 }
631 let mut parts = code.split_whitespace();
632 let Some(raw_type) = parts.next() else {
633 continue;
634 };
635 let Some(raw_name) = parts.next() else {
636 continue;
637 };
638 let (type_ref, is_array, array_size, string_max_len) =
639 parse_msg_field_type(package, raw_type).with_context(|| match path_hint {
640 Some(p) => format!(
641 "{RIDLC_ERR_PREFIX} invalid field in '{}' at line with type '{}'",
642 p.display(),
643 raw_type
644 ),
645 None => format!(
646 "{RIDLC_ERR_PREFIX} invalid field at line with type '{}'",
647 raw_type
648 ),
649 })?;
650 fields.push(MsgField {
651 name: raw_name.to_string(),
652 type_ref,
653 is_array,
654 array_size,
655 string_max_len,
656 description: comment.to_string(),
657 });
658 }
659 Ok((constants, fields))
660}
661
662fn parse_constant_line(code: &str) -> Result<Option<MsgConstant>> {
667 let Some((lhs, rhs)) = code.split_once('=') else {
668 return Ok(None);
669 };
670 let mut parts = lhs.split_whitespace();
671 let Some(type_name) = parts.next() else {
672 return Ok(None);
673 };
674 let Some(name) = parts.next() else {
675 return Ok(None);
676 };
677 let Some(primitive) = RosPrimitive::parse(type_name) else {
678 return Ok(None);
679 };
680 if !matches!(
681 primitive,
682 RosPrimitive::Byte
683 | RosPrimitive::Char
684 | RosPrimitive::Int8
685 | RosPrimitive::Uint8
686 | RosPrimitive::Int16
687 | RosPrimitive::Uint16
688 | RosPrimitive::Int32
689 | RosPrimitive::Uint32
690 | RosPrimitive::Int64
691 | RosPrimitive::Uint64
692 ) {
693 return Ok(None);
694 }
695 let value_text = rhs.split_whitespace().next().unwrap_or("").trim();
696 let value = value_text
697 .parse::<i32>()
698 .with_context(|| format!("constant '{name}' value '{value_text}' is not an i32"))?;
699 Ok(Some(MsgConstant {
700 name: name.to_string(),
701 type_name: type_name.to_string(),
702 value,
703 }))
704}
705
706pub fn infer_package_name(path: &Path) -> Option<String> {
707 let parent = path.parent()?;
708 let parent_name = parent.file_name()?.to_str()?;
709 if parent_name == "msg" {
710 return parent
711 .parent()?
712 .file_name()?
713 .to_str()
714 .map(|s| s.to_string());
715 }
716 Some(parent_name.to_string())
717}
718
719pub fn parse_msg_file(package: &str, name: &str, path: &Path) -> Result<MsgSpec> {
720 let src = fs::read_to_string(path).with_context(|| {
721 format!(
722 "{RIDLC_ERR_PREFIX} failed to read .msg file '{}'",
723 path.display()
724 )
725 })?;
726 let (constants, fields) = parse_msg_members_from_lines(package, &src, Some(path))?;
727 Ok(MsgSpec {
728 package: package.to_string(),
729 name: name.to_string(),
730 constants,
731 fields,
732 })
733}
734
735pub fn parse_msg_field_type(
742 current_package: &str,
743 raw_type: &str,
744) -> Result<(MsgTypeRef, bool, Option<usize>, Option<usize>)> {
745 let (without_bound, bound) = match raw_type.split_once("<=") {
749 Some((lhs, rhs)) => {
750 let n = rhs
751 .trim_matches(|c: char| c.is_whitespace() || c == ']' || c == '[')
752 .split(|c: char| !c.is_ascii_digit())
753 .next()
754 .unwrap_or("")
755 .parse::<usize>()
756 .ok();
757 (lhs.trim(), n)
758 }
759 None => (raw_type.trim(), None),
760 };
761 let normalized = without_bound;
762 let (base_type, is_array, array_size) = if let Some(idx) = normalized.find('[') {
763 let bracket = &normalized[idx..]; let size = bracket
765 .trim_matches(|c| c == '[' || c == ']')
766 .parse::<usize>()
767 .ok(); (&normalized[..idx], true, size)
769 } else {
770 (normalized, false, None)
771 };
772 let base_type = base_type.trim();
773 if base_type.is_empty() {
774 bail!("{RIDLC_ERR_PREFIX} empty message field type in .msg file");
775 }
776 let string_max_len = match base_type {
777 "string" | "wstring" => bound,
778 _ => None,
779 };
780 if RosPrimitive::parse(base_type).is_some() {
781 return Ok((
782 MsgTypeRef::Primitive(base_type.to_string()),
783 is_array,
784 array_size,
785 string_max_len,
786 ));
787 }
788 let segs: Vec<&str> = base_type.split('/').collect();
790 if segs.len() == 3 && segs[1] == "msg" {
791 let pkg = segs[0].trim();
792 let nm = segs[2].trim();
793 if pkg.is_empty() || nm.is_empty() {
794 bail!(
795 "{RIDLC_ERR_PREFIX} invalid pkg/msg/Name reference '{}': empty package or name",
796 base_type
797 );
798 }
799 return Ok((
800 MsgTypeRef::Named {
801 package: pkg.to_string(),
802 name: nm.to_string(),
803 },
804 is_array,
805 array_size,
806 string_max_len,
807 ));
808 }
809 if let Some((package, name)) = base_type.split_once('/') {
810 let pkg = package.trim();
811 let nm = name.trim();
812 if pkg.is_empty() || nm.is_empty() {
813 bail!(
814 "{RIDLC_ERR_PREFIX} invalid pkg/Name reference '{}': empty package or name",
815 base_type
816 );
817 }
818 return Ok((
819 MsgTypeRef::Named {
820 package: pkg.to_string(),
821 name: nm.to_string(),
822 },
823 is_array,
824 array_size,
825 string_max_len,
826 ));
827 }
828 Ok((
829 MsgTypeRef::Named {
830 package: current_package.to_string(),
831 name: base_type.to_string(),
832 },
833 is_array,
834 array_size,
835 string_max_len,
836 ))
837}
838
839pub fn is_ros_primitive(raw: &str) -> bool {
842 RosPrimitive::parse(raw).is_some()
843}
844
845pub fn parse_ridl_type_ref(type_ref: &str) -> Option<(String, String)> {
852 parse_qualified_type_ref(type_ref).map(|(_, p, n)| (p, n))
853}
854
855pub fn parse_qualified_type_ref(type_ref: &str) -> Option<(IdlKind, String, String)> {
859 let trimmed = type_ref.trim();
860 let mut parts = trimmed.split('/');
861 let package = parts.next()?;
862 let middle = parts.next()?;
863 let mut name = parts.next()?.to_string();
864 if parts.next().is_some() {
865 return None;
866 }
867 let kind = match middle {
868 "msg" => IdlKind::Msg,
869 "srv" => IdlKind::Srv,
870 _ => return None,
871 };
872 if package.is_empty() || name.is_empty() {
873 return None;
874 }
875 if name.ends_with("[]") {
876 name.truncate(name.len().saturating_sub(2));
877 }
878 Some((kind, package.to_string(), name))
879}
880
881#[derive(Clone, Copy, Debug, PartialEq, Eq)]
882pub enum IdlKind {
883 Msg,
884 Srv,
885}
886
887pub fn ros_msg_type_fmt(package: &str, name: &str) -> String {
888 format!("{package}/msg/{name}")
889}
890
891pub fn package_aliases(package: &str) -> Vec<&str> {
892 match package {
893 "robonix_msgs" => vec!["robonix_msgs", "robonix_msg"],
894 _ => vec![package],
895 }
896}
897
898pub fn format_resolve_error(
899 full_type: &str,
900 from: Option<(&str, &ResolveContext)>,
901 include_paths: &[PathBuf],
902) -> String {
903 let mut msg = format!(
904 "{RIDLC_ERR_PREFIX} failed to resolve ROS message type '{}'",
905 full_type
906 );
907 if let Some((_type_ref, ctx)) = from {
908 let mut parts = Vec::new();
909 if let Some(ref ns) = ctx.namespace {
910 parts.push(format!("namespace '{ns}'"));
911 }
912 if let (Some(kind), Some(ref name)) = (ctx.interface_kind, ctx.interface_name.as_ref()) {
913 parts.push(format!("{kind} '{name}'"));
914 }
915 if let Some(ref f) = ctx.field_name {
916 parts.push(format!("field '{f}'"));
917 }
918 if !parts.is_empty() {
919 msg.push_str(&format!("\n referenced from: {}", parts.join(", ")));
920 }
921 }
922 msg.push_str("\n searched include paths:");
923 for p in include_paths {
924 msg.push_str(&format!("\n - {}", p.display()));
925 }
926 msg.push_str("\n hint: ensure the .msg file exists (e.g. <include>/common_interfaces/std_msgs/msg/Float64.msg)");
927 msg
928}
929
930#[cfg(test)]
931mod tests {
932 use super::*;
933
934 #[test]
935 fn canonical_idl_tree_resolves_ros_action_interfaces() -> Result<()> {
936 let include = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
937 .join("../../capabilities/lib")
938 .canonicalize()?;
939 let mut resolver = MsgResolver::new(&[include])?;
940 let mut skipped = 0;
941
942 resolver.resolve_all_in_index(false, &mut skipped)?;
943 resolver.resolve_all_srv(false, &mut skipped)?;
944
945 assert_eq!(skipped, 0, "the canonical IDL tree must resolve completely");
946 assert!(
947 resolver
948 .cache
949 .contains_key(&("unique_identifier_msgs".to_string(), "UUID".to_string()))
950 );
951 assert!(
952 resolver
953 .cache
954 .contains_key(&("action_msgs".to_string(), "GoalStatusArray".to_string()))
955 );
956 assert!(
957 resolver
958 .srv_cache
959 .contains_key(&("action_msgs".to_string(), "CancelGoal".to_string()))
960 );
961 Ok(())
962 }
963}