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, PartialEq, Eq)]
31pub enum MsgTypeRef {
32 Primitive(String),
37 Named {
38 package: String,
39 name: String,
40 },
41}
42
43#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
54pub enum RosPrimitive {
55 Bool,
56 Byte, Char, Int8,
59 Uint8,
60 Int16,
61 Uint16,
62 Int32,
63 Uint32,
64 Int64,
65 Uint64,
66 Float32,
67 Float64,
68 String,
69 Wstring,
70}
71
72impl RosPrimitive {
73 pub fn parse(s: &str) -> Option<Self> {
74 Some(match s {
75 "bool" => Self::Bool,
76 "byte" => Self::Byte,
77 "char" => Self::Char,
78 "int8" => Self::Int8,
79 "uint8" => Self::Uint8,
80 "int16" => Self::Int16,
81 "uint16" => Self::Uint16,
82 "int32" => Self::Int32,
83 "uint32" => Self::Uint32,
84 "int64" => Self::Int64,
85 "uint64" => Self::Uint64,
86 "float32" => Self::Float32,
87 "float64" => Self::Float64,
88 "string" => Self::String,
89 "wstring" => Self::Wstring,
90 _ => return None,
91 })
92 }
93
94 pub fn as_ros_str(&self) -> &'static str {
95 match self {
96 Self::Bool => "bool",
97 Self::Byte => "byte",
98 Self::Char => "char",
99 Self::Int8 => "int8",
100 Self::Uint8 => "uint8",
101 Self::Int16 => "int16",
102 Self::Uint16 => "uint16",
103 Self::Int32 => "int32",
104 Self::Uint32 => "uint32",
105 Self::Int64 => "int64",
106 Self::Uint64 => "uint64",
107 Self::Float32 => "float32",
108 Self::Float64 => "float64",
109 Self::String => "string",
110 Self::Wstring => "wstring",
111 }
112 }
113
114 pub fn proto_type(&self) -> &'static str {
116 match self {
117 Self::Bool => "bool",
118 Self::Byte | Self::Int8 => "int32",
122 Self::Char | Self::Uint8 => "uint32",
123 Self::Int16 => "int32",
124 Self::Uint16 => "uint32",
125 Self::Int32 => "int32",
126 Self::Uint32 => "uint32",
127 Self::Int64 => "int64",
128 Self::Uint64 => "uint64",
129 Self::Float32 => "float",
130 Self::Float64 => "double",
131 Self::String | Self::Wstring => "string",
132 }
133 }
134
135 pub fn python_type(&self) -> &'static str {
137 match self {
138 Self::Bool => "bool",
139 Self::Float32 | Self::Float64 => "float",
140 Self::String | Self::Wstring => "str",
141 Self::Byte
142 | Self::Char
143 | Self::Int8
144 | Self::Uint8
145 | Self::Int16
146 | Self::Uint16
147 | Self::Int32
148 | Self::Uint32
149 | Self::Int64
150 | Self::Uint64 => "int",
151 }
152 }
153
154 pub fn python_default(&self) -> &'static str {
156 match self {
157 Self::Bool => "False",
158 Self::Float32 | Self::Float64 => "0.0",
159 Self::String | Self::Wstring => "\"\"",
160 _ => "0",
161 }
162 }
163
164 pub fn python_cast(&self) -> &'static str {
167 self.python_type()
168 }
169
170 pub fn json_schema_type(&self) -> &'static str {
172 match self {
173 Self::Bool => "boolean",
174 Self::Float32 | Self::Float64 => "number",
175 Self::String | Self::Wstring => "string",
176 _ => "integer",
177 }
178 }
179
180 pub fn integer_range(&self) -> Option<(i128, i128)> {
185 Some(match self {
186 Self::Byte | Self::Int8 => (i8::MIN as i128, i8::MAX as i128),
187 Self::Char | Self::Uint8 => (0, u8::MAX as i128),
188 Self::Int16 => (i16::MIN as i128, i16::MAX as i128),
189 Self::Uint16 => (0, u16::MAX as i128),
190 Self::Int32 => (i32::MIN as i128, i32::MAX as i128),
191 Self::Uint32 => (0, u32::MAX as i128),
192 Self::Int64 => (i64::MIN as i128, i64::MAX as i128),
193 Self::Uint64 => (0, u64::MAX as i128),
196 _ => return None,
197 })
198 }
199
200 pub fn is_blob_element(&self) -> bool {
206 matches!(self, Self::Uint8)
207 }
208}
209
210#[derive(Clone, Debug)]
211pub struct MsgSpec {
212 pub package: String,
213 pub name: String,
214 pub fields: Vec<MsgField>,
215}
216
217#[derive(Clone, Debug)]
218pub struct SrvSpec {
219 pub package: String,
220 pub name: String,
221 pub request: MsgSpec,
222 pub response: MsgSpec,
223}
224
225#[derive(Clone, Debug, Default)]
226pub struct ResolveContext {
227 pub namespace: Option<String>,
228 pub interface_kind: Option<&'static str>,
229 pub interface_name: Option<String>,
230 pub field_name: Option<String>,
231}
232
233pub struct MsgResolver {
234 pub index: HashMap<(String, String), PathBuf>,
235 pub cache: HashMap<(String, String), MsgSpec>,
236 pub srv_index: HashMap<(String, String), PathBuf>,
237 pub srv_cache: HashMap<(String, String), SrvSpec>,
238 pub include_paths: Vec<PathBuf>,
239}
240
241impl MsgResolver {
242 pub fn new(include_paths: &[PathBuf]) -> Result<Self> {
243 let mut index = HashMap::new();
244 let mut srv_index = HashMap::new();
245 for root in include_paths {
246 index_msg_files(root, &mut index)?;
247 index_srv_files(root, &mut srv_index)?;
248 }
249 Ok(Self {
250 index,
251 cache: HashMap::new(),
252 srv_index,
253 srv_cache: HashMap::new(),
254 include_paths: include_paths.to_vec(),
255 })
256 }
257
258 pub fn resolve_ridl_type(&mut self, type_ref: &str, ctx: &ResolveContext) -> Result<()> {
259 if let Some((package, name)) = parse_ridl_type_ref(type_ref) {
260 self.resolve_named_type(&package, &name, Some((type_ref, ctx)))?;
261 }
262 Ok(())
263 }
264
265 pub fn resolve_named_type(
266 &mut self,
267 package: &str,
268 name: &str,
269 from: Option<(&str, &ResolveContext)>,
270 ) -> Result<()> {
271 let mut visiting: BTreeSet<(String, String)> = BTreeSet::new();
272 self.resolve_named_type_inner(package, name, from, &mut visiting)
273 }
274
275 fn resolve_named_type_inner(
281 &mut self,
282 package: &str,
283 name: &str,
284 from: Option<(&str, &ResolveContext)>,
285 visiting: &mut BTreeSet<(String, String)>,
286 ) -> Result<()> {
287 let key = (package.to_string(), name.to_string());
288 if self.cache.contains_key(&key) {
289 return Ok(());
290 }
291 if !visiting.insert(key.clone()) {
292 return Ok(());
296 }
297 let full_type = ros_msg_type_fmt(package, name);
298 let path = self
299 .find_msg_path(package, name)
300 .with_context(|| format_resolve_error(&full_type, from, &self.include_paths))?;
301 let spec = parse_msg_file(package, name, &path)?;
302 for field in &spec.fields {
303 if let MsgTypeRef::Named { package, name } = &field.type_ref {
304 let dep_ctx = ResolveContext {
305 namespace: Some(spec.package.clone()),
306 interface_kind: Some("msg"),
307 interface_name: Some(spec.name.clone()),
308 field_name: Some(field.name.clone()),
309 };
310 self.resolve_named_type_inner(
311 package,
312 name,
313 Some((&ros_msg_type_fmt(package, name), &dep_ctx)),
314 visiting,
315 )?;
316 }
317 }
318 visiting.remove(&key);
319 self.cache.insert(key, spec);
320 Ok(())
321 }
322
323 pub fn resolve_all_in_index(&mut self, verbose: bool, skip_count: &mut usize) -> Result<()> {
324 let keys: Vec<_> = self.index.keys().cloned().collect();
325 for (package, name) in keys {
326 if let Err(e) = self.resolve_named_type(&package, &name, None) {
327 if verbose {
328 eprintln!(
329 "[robonix-codegen] warning: skipping {}/{}: {:#}",
330 package, name, e
331 );
332 } else {
333 *skip_count += 1;
334 }
335 }
336 }
337 Ok(())
338 }
339
340 pub fn find_msg_path(&self, package: &str, name: &str) -> Option<PathBuf> {
341 let candidates = package_aliases(package);
342 for candidate in candidates {
343 if let Some(path) = self.index.get(&(candidate.to_string(), name.to_string())) {
344 return Some(path.clone());
345 }
346 }
347 None
348 }
349
350 pub fn ordered_specs(&self) -> Vec<&MsgSpec> {
351 let mut keys: Vec<_> = self.cache.keys().cloned().collect();
352 keys.sort();
353 keys.iter()
354 .filter_map(|key| self.cache.get(key))
355 .collect::<Vec<_>>()
356 }
357
358 pub fn resolve_all_srv(&mut self, verbose: bool, skip_count: &mut usize) -> Result<()> {
359 let keys: Vec<_> = self.srv_index.keys().cloned().collect();
360 for (package, name) in keys {
361 if let Err(e) = self.resolve_srv(&package, &name) {
362 if verbose {
363 eprintln!(
364 "[robonix-codegen] warning: skipping srv {}/{}: {:#}",
365 package, name, e
366 );
367 } else {
368 *skip_count += 1;
369 }
370 }
371 }
372 Ok(())
373 }
374
375 pub fn resolve_srv(&mut self, package: &str, name: &str) -> Result<()> {
376 let key = (package.to_string(), name.to_string());
377 if self.srv_cache.contains_key(&key) {
378 return Ok(());
379 }
380 let path = self.find_srv_path(package, name).with_context(|| {
381 format!("{RIDLC_ERR_PREFIX} .srv file not found: {package}/srv/{name}")
382 })?;
383 let spec = parse_srv_file(package, name, &path)?;
384 for field in spec
386 .request
387 .fields
388 .iter()
389 .chain(spec.response.fields.iter())
390 {
391 if let MsgTypeRef::Named {
392 package: pkg,
393 name: nm,
394 } = &field.type_ref
395 {
396 let dep_ctx = ResolveContext {
397 namespace: Some(spec.package.clone()),
398 interface_kind: Some("srv"),
399 interface_name: Some(spec.name.clone()),
400 field_name: Some(field.name.clone()),
401 };
402 self.resolve_named_type(pkg, nm, Some((&ros_msg_type_fmt(pkg, nm), &dep_ctx)))?;
403 }
404 }
405 self.srv_cache.insert(key, spec);
406 Ok(())
407 }
408
409 pub fn find_srv_path(&self, package: &str, name: &str) -> Option<PathBuf> {
410 let candidates = package_aliases(package);
411 for candidate in candidates {
412 if let Some(path) = self
413 .srv_index
414 .get(&(candidate.to_string(), name.to_string()))
415 {
416 return Some(path.clone());
417 }
418 }
419 None
420 }
421
422 pub fn ordered_srvs(&self) -> Vec<&SrvSpec> {
423 let mut keys: Vec<_> = self.srv_cache.keys().cloned().collect();
424 keys.sort();
425 keys.iter()
426 .filter_map(|key| self.srv_cache.get(key))
427 .collect::<Vec<_>>()
428 }
429
430 pub fn srv_spec(&self, package: &str, name: &str) -> Option<&SrvSpec> {
431 self.srv_cache.get(&(package.to_string(), name.to_string()))
432 }
433}
434
435pub fn index_msg_files(root: &Path, index: &mut HashMap<(String, String), PathBuf>) -> Result<()> {
436 if !root.exists() {
437 return Ok(());
438 }
439 for entry in fs::read_dir(root).with_context(|| {
440 format!(
441 "{RIDLC_ERR_PREFIX} failed to read include directory '{}' (check that path exists)",
442 root.display()
443 )
444 })? {
445 let entry = entry?;
446 let path = entry.path();
447 if path.is_dir() {
448 index_msg_files(&path, index)?;
449 continue;
450 }
451 if path.extension().and_then(|s| s.to_str()) != Some("msg") {
452 continue;
453 }
454 let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
455 continue;
456 };
457 let Some(package) = infer_package_name(&path) else {
458 continue;
459 };
460 index.entry((package, stem.to_string())).or_insert(path);
461 }
462 Ok(())
463}
464
465pub fn index_srv_files(root: &Path, index: &mut HashMap<(String, String), PathBuf>) -> Result<()> {
466 if !root.exists() {
467 return Ok(());
468 }
469 for entry in fs::read_dir(root).with_context(|| {
470 format!(
471 "{RIDLC_ERR_PREFIX} failed to read include directory '{}' (check that path exists)",
472 root.display()
473 )
474 })? {
475 let entry = entry?;
476 let path = entry.path();
477 if path.is_dir() {
478 index_srv_files(&path, index)?;
479 continue;
480 }
481 if path.extension().and_then(|s| s.to_str()) != Some("srv") {
482 continue;
483 }
484 let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
485 continue;
486 };
487 let Some(package) = infer_srv_package_name(&path) else {
488 continue;
489 };
490 index.entry((package, stem.to_string())).or_insert(path);
491 }
492 Ok(())
493}
494
495pub fn infer_srv_package_name(path: &Path) -> Option<String> {
496 let parent = path.parent()?;
497 let parent_name = parent.file_name()?.to_str()?;
498 if parent_name == "srv" {
499 return parent
500 .parent()?
501 .file_name()?
502 .to_str()
503 .map(|s| s.to_string());
504 }
505 Some(parent_name.to_string())
506}
507
508fn strip_leading_robonix_grpc_lines(src: &str) -> String {
510 let mut s = src.to_string();
511 loop {
512 let t = s.trim_start();
513 let first = t.lines().next().unwrap_or("").trim();
514 if first.starts_with("# @robonix.grpc") {
515 if let Some(pos) = s.find('\n') {
516 s = s[pos + 1..].to_string();
517 continue;
518 }
519 s.clear();
520 break;
521 }
522 break;
523 }
524 s
525}
526
527pub fn parse_srv_file(package: &str, name: &str, path: &Path) -> Result<SrvSpec> {
528 let src = fs::read_to_string(path).with_context(|| {
529 format!(
530 "{RIDLC_ERR_PREFIX} failed to read .srv file '{}'",
531 path.display()
532 )
533 })?;
534 let body = strip_leading_robonix_grpc_lines(&src);
535
536 let mut request_lines: Vec<&str> = Vec::new();
541 let mut response_lines: Vec<&str> = Vec::new();
542 let mut in_response = false;
543 for line in body.lines() {
544 if !in_response && line.trim() == "---" {
545 in_response = true;
546 continue;
547 }
548 if in_response {
549 response_lines.push(line);
550 } else {
551 request_lines.push(line);
552 }
553 }
554 let request_src = request_lines.join("\n");
555 let response_src = response_lines.join("\n");
556
557 let request = parse_msg_section(package, &format!("{name}_Request"), &request_src)
558 .with_context(|| format!("{RIDLC_ERR_PREFIX} parsing request of '{}'", path.display()))?;
559 let response = parse_msg_section(package, &format!("{name}_Response"), &response_src)
560 .with_context(|| {
561 format!(
562 "{RIDLC_ERR_PREFIX} parsing response of '{}'",
563 path.display()
564 )
565 })?;
566
567 Ok(SrvSpec {
568 package: package.to_string(),
569 name: name.to_string(),
570 request,
571 response,
572 })
573}
574
575fn parse_msg_section(package: &str, name: &str, src: &str) -> Result<MsgSpec> {
576 let fields = parse_fields_from_lines(package, src, None)?;
577 Ok(MsgSpec {
578 package: package.to_string(),
579 name: name.to_string(),
580 fields,
581 })
582}
583
584fn parse_fields_from_lines(
594 package: &str,
595 src: &str,
596 path_hint: Option<&Path>,
597) -> Result<Vec<MsgField>> {
598 let mut fields = Vec::new();
599 for raw_line in src.lines() {
600 let (code, comment) = match raw_line.find('#') {
604 Some(idx) => (&raw_line[..idx], raw_line[idx + 1..].trim()),
605 None => (raw_line, ""),
606 };
607 let code = code.trim();
608 if code.is_empty() {
609 continue;
610 }
611 if code.contains('=') {
614 continue;
615 }
616 let mut parts = code.split_whitespace();
617 let Some(raw_type) = parts.next() else {
618 continue;
619 };
620 let Some(raw_name) = parts.next() else {
621 continue;
622 };
623 let (type_ref, is_array, array_size, string_max_len) =
624 parse_msg_field_type(package, raw_type).with_context(|| match path_hint {
625 Some(p) => format!(
626 "{RIDLC_ERR_PREFIX} invalid field in '{}' at line with type '{}'",
627 p.display(),
628 raw_type
629 ),
630 None => format!(
631 "{RIDLC_ERR_PREFIX} invalid field at line with type '{}'",
632 raw_type
633 ),
634 })?;
635 fields.push(MsgField {
636 name: raw_name.to_string(),
637 type_ref,
638 is_array,
639 array_size,
640 string_max_len,
641 description: comment.to_string(),
642 });
643 }
644 Ok(fields)
645}
646
647pub fn infer_package_name(path: &Path) -> Option<String> {
648 let parent = path.parent()?;
649 let parent_name = parent.file_name()?.to_str()?;
650 if parent_name == "msg" {
651 return parent
652 .parent()?
653 .file_name()?
654 .to_str()
655 .map(|s| s.to_string());
656 }
657 Some(parent_name.to_string())
658}
659
660pub fn parse_msg_file(package: &str, name: &str, path: &Path) -> Result<MsgSpec> {
661 let src = fs::read_to_string(path).with_context(|| {
662 format!(
663 "{RIDLC_ERR_PREFIX} failed to read .msg file '{}'",
664 path.display()
665 )
666 })?;
667 let fields = parse_fields_from_lines(package, &src, Some(path))?;
668 Ok(MsgSpec {
669 package: package.to_string(),
670 name: name.to_string(),
671 fields,
672 })
673}
674
675pub fn parse_msg_field_type(
682 current_package: &str,
683 raw_type: &str,
684) -> Result<(MsgTypeRef, bool, Option<usize>, Option<usize>)> {
685 let (without_bound, bound) = match raw_type.split_once("<=") {
689 Some((lhs, rhs)) => {
690 let n = rhs
691 .trim_matches(|c: char| c.is_whitespace() || c == ']' || c == '[')
692 .split(|c: char| !c.is_ascii_digit())
693 .next()
694 .unwrap_or("")
695 .parse::<usize>()
696 .ok();
697 (lhs.trim(), n)
698 }
699 None => (raw_type.trim(), None),
700 };
701 let normalized = without_bound;
702 let (base_type, is_array, array_size) = if let Some(idx) = normalized.find('[') {
703 let bracket = &normalized[idx..]; let size = bracket
705 .trim_matches(|c| c == '[' || c == ']')
706 .parse::<usize>()
707 .ok(); (&normalized[..idx], true, size)
709 } else {
710 (normalized, false, None)
711 };
712 let base_type = base_type.trim();
713 if base_type.is_empty() {
714 bail!("{RIDLC_ERR_PREFIX} empty message field type in .msg file");
715 }
716 let string_max_len = match base_type {
717 "string" | "wstring" => bound,
718 _ => None,
719 };
720 if RosPrimitive::parse(base_type).is_some() {
721 return Ok((
722 MsgTypeRef::Primitive(base_type.to_string()),
723 is_array,
724 array_size,
725 string_max_len,
726 ));
727 }
728 let segs: Vec<&str> = base_type.split('/').collect();
730 if segs.len() == 3 && segs[1] == "msg" {
731 let pkg = segs[0].trim();
732 let nm = segs[2].trim();
733 if pkg.is_empty() || nm.is_empty() {
734 bail!(
735 "{RIDLC_ERR_PREFIX} invalid pkg/msg/Name reference '{}': empty package or name",
736 base_type
737 );
738 }
739 return Ok((
740 MsgTypeRef::Named {
741 package: pkg.to_string(),
742 name: nm.to_string(),
743 },
744 is_array,
745 array_size,
746 string_max_len,
747 ));
748 }
749 if let Some((package, name)) = base_type.split_once('/') {
750 let pkg = package.trim();
751 let nm = name.trim();
752 if pkg.is_empty() || nm.is_empty() {
753 bail!(
754 "{RIDLC_ERR_PREFIX} invalid pkg/Name reference '{}': empty package or name",
755 base_type
756 );
757 }
758 return Ok((
759 MsgTypeRef::Named {
760 package: pkg.to_string(),
761 name: nm.to_string(),
762 },
763 is_array,
764 array_size,
765 string_max_len,
766 ));
767 }
768 Ok((
769 MsgTypeRef::Named {
770 package: current_package.to_string(),
771 name: base_type.to_string(),
772 },
773 is_array,
774 array_size,
775 string_max_len,
776 ))
777}
778
779pub fn is_ros_primitive(raw: &str) -> bool {
782 RosPrimitive::parse(raw).is_some()
783}
784
785pub fn parse_ridl_type_ref(type_ref: &str) -> Option<(String, String)> {
792 parse_qualified_type_ref(type_ref).map(|(_, p, n)| (p, n))
793}
794
795pub fn parse_qualified_type_ref(type_ref: &str) -> Option<(IdlKind, String, String)> {
799 let trimmed = type_ref.trim();
800 let mut parts = trimmed.split('/');
801 let package = parts.next()?;
802 let middle = parts.next()?;
803 let mut name = parts.next()?.to_string();
804 if parts.next().is_some() {
805 return None;
806 }
807 let kind = match middle {
808 "msg" => IdlKind::Msg,
809 "srv" => IdlKind::Srv,
810 _ => return None,
811 };
812 if package.is_empty() || name.is_empty() {
813 return None;
814 }
815 if name.ends_with("[]") {
816 name.truncate(name.len().saturating_sub(2));
817 }
818 Some((kind, package.to_string(), name))
819}
820
821#[derive(Clone, Copy, Debug, PartialEq, Eq)]
822pub enum IdlKind {
823 Msg,
824 Srv,
825}
826
827pub fn ros_msg_type_fmt(package: &str, name: &str) -> String {
828 format!("{package}/msg/{name}")
829}
830
831pub fn package_aliases(package: &str) -> Vec<&str> {
832 match package {
833 "robonix_msgs" => vec!["robonix_msgs", "robonix_msg"],
834 _ => vec![package],
835 }
836}
837
838pub fn format_resolve_error(
839 full_type: &str,
840 from: Option<(&str, &ResolveContext)>,
841 include_paths: &[PathBuf],
842) -> String {
843 let mut msg = format!(
844 "{RIDLC_ERR_PREFIX} failed to resolve ROS message type '{}'",
845 full_type
846 );
847 if let Some((_type_ref, ctx)) = from {
848 let mut parts = Vec::new();
849 if let Some(ref ns) = ctx.namespace {
850 parts.push(format!("namespace '{ns}'"));
851 }
852 if let (Some(kind), Some(ref name)) = (ctx.interface_kind, ctx.interface_name.as_ref()) {
853 parts.push(format!("{kind} '{name}'"));
854 }
855 if let Some(ref f) = ctx.field_name {
856 parts.push(format!("field '{f}'"));
857 }
858 if !parts.is_empty() {
859 msg.push_str(&format!("\n referenced from: {}", parts.join(", ")));
860 }
861 }
862 msg.push_str("\n searched include paths:");
863 for p in include_paths {
864 msg.push_str(&format!("\n - {}", p.display()));
865 }
866 msg.push_str("\n hint: ensure the .msg file exists (e.g. <include>/common_interfaces/std_msgs/msg/Float64.msg)");
867 msg
868}