1use std::{
12 collections::{BTreeMap, BTreeSet, VecDeque},
13 fmt::{Debug, Display},
14 path::{Path, PathBuf},
15};
16
17use log::*;
18use proc_macro2::TokenStream;
19use quote::{quote, ToTokens};
20use simple_error::{bail, SimpleError as Error};
21use utils::Package;
22
23mod gen;
24pub use gen::CodegenOptions;
25use gen::*;
26mod parse;
27use parse::*;
28pub mod utils;
29use utils::RosVersion;
30mod ros2_hashing;
31use ros2_hashing::*;
32mod ros2_builtin_interfaces;
33
34pub mod integral_types;
35pub use integral_types::*;
36
37pub mod serde_rosmsg_bytes;
39
40pub use ::serde;
45pub use serde::{de::DeserializeOwned, Deserialize, Serialize};
46pub use serde_big_array::BigArray; pub use serde_bytes;
48pub use smart_default::SmartDefault; #[derive(Clone, Debug, Default)]
52pub struct Ros2Hash([u8; 32]);
53
54impl Ros2Hash {
55 pub fn to_hash_string(&self) -> String {
56 format!("RIHS01_{}", hex::encode(self.0))
57 }
58
59 pub fn from_string(hash_str: &str) -> Self {
60 let hex_str = hash_str.trim_start_matches("RIHS01_");
62 let mut bytes = [0u8; 32];
63 hex::decode_to_slice(hex_str, &mut bytes).expect("Invalid hex string");
64 Ros2Hash(bytes)
65 }
66}
67
68impl ToTokens for Ros2Hash {
70 fn to_tokens(&self, tokens: &mut TokenStream) {
71 let bytes = self.0;
72 let arr_tokens = bytes
73 .iter()
74 .map(|b| syn::LitInt::new(&format!("0x{:02x}", b), proc_macro2::Span::call_site()));
75
76 tokens.extend(quote! { [ #(#arr_tokens,)* ] });
77 }
78}
79
80#[derive(Clone, Debug)]
81pub struct MessageFile {
82 pub parsed: ParsedMessageFile,
83 pub md5sum: String,
84 pub ros2_hash: Ros2Hash,
86 pub definition: String,
91 pub is_fixed_encoding_length: bool,
93}
94
95impl MessageFile {
96 fn resolve(parsed: ParsedMessageFile, graph: &BTreeMap<String, MessageFile>) -> Option<Self> {
97 let md5sum = Self::compute_md5sum(&parsed, graph).or_else(|| {
98 log::error!("Failed to calculate md5sum for message: {parsed:#?}");
99 None
100 })?;
101 let ros2_hash = calculate_ros2_hash(&parsed, graph);
102 let definition = Self::compute_full_definition(&parsed, graph).or_else(|| {
103 log::error!("Failed to calculate full definition for message: {parsed:#?}");
104 None
105 })?;
106 let is_fixed_length = Self::determine_if_fixed_length(&parsed, graph).or_else(|| {
107 log::error!("Failed to determine if message is fixed length: {parsed:#?}");
108 None
109 })?;
110 Some(MessageFile {
111 parsed,
112 md5sum,
113 ros2_hash,
114 definition,
115 is_fixed_encoding_length: is_fixed_length,
116 })
117 }
118
119 pub fn get_package_name(&self) -> String {
120 self.parsed.package.clone()
121 }
122
123 pub fn get_short_name(&self) -> String {
124 self.parsed.name.clone()
125 }
126
127 pub fn get_full_name(&self) -> String {
128 format!("{}/{}", self.parsed.package, self.parsed.name)
129 }
130
131 pub fn get_md5sum(&self) -> &str {
132 self.md5sum.as_str()
133 }
134
135 pub fn get_fields(&self) -> &[FieldInfo] {
136 &self.parsed.fields
137 }
138
139 pub fn get_constants(&self) -> &[ConstantInfo] {
140 &self.parsed.constants
141 }
142
143 pub fn is_fixed_length(&self) -> bool {
144 self.is_fixed_encoding_length
145 }
146
147 pub fn get_definition(&self) -> &str {
148 &self.definition
149 }
150
151 fn compute_md5sum(
152 parsed: &ParsedMessageFile,
153 graph: &BTreeMap<String, MessageFile>,
154 ) -> Option<String> {
155 let md5sum_content = Self::_compute_md5sum(parsed, graph)?;
156 let md5sum = md5::compute(md5sum_content.trim_end().as_bytes());
158 log::trace!(
159 "Message type: {} calculated with md5sum: {md5sum:x}",
160 parsed.get_full_name()
161 );
162 Some(format!("{md5sum:x}"))
163 }
164
165 fn _compute_md5sum(
166 parsed: &ParsedMessageFile,
167 graph: &BTreeMap<String, MessageFile>,
168 ) -> Option<String> {
169 let mut md5sum_content = String::new();
170 for constant in &parsed.constants {
171 md5sum_content.push_str(&format!(
172 "{} {}={}\n",
173 constant.constant_type, constant.constant_name, constant.constant_value
174 ));
175 }
176 for field in &parsed.fields {
177 let field_type = field.field_type.field_type.as_str();
178 if is_intrinsic_type(parsed.version.unwrap_or(RosVersion::ROS1), field_type) {
179 md5sum_content.push_str(&format!("{} {}\n", field.field_type, field.field_name));
180 } else {
181 let field_package = field
182 .field_type
183 .package_name
184 .as_ref()
185 .unwrap_or_else(|| panic!("Expected package name for field {field:#?}"));
186 let field_full_name = format!("{field_package}/{field_type}");
187 let sub_message = graph.get(field_full_name.as_str())?;
188 let sub_md5sum = Self::compute_md5sum(&sub_message.parsed, graph)?;
189 md5sum_content.push_str(&format!("{} {}\n", sub_md5sum, field.field_name));
190 }
191 }
192
193 Some(md5sum_content)
194 }
195
196 fn get_unique_field_types(
198 parsed: &ParsedMessageFile,
199 graph: &BTreeMap<String, MessageFile>,
200 ) -> Option<BTreeSet<String>> {
201 let mut unique_field_types = BTreeSet::new();
202 for field in &parsed.fields {
203 let field_type = field.field_type.field_type.as_str();
204 if is_intrinsic_type(parsed.version.unwrap_or(RosVersion::ROS1), field_type) {
205 continue;
206 }
207 let sub_message = graph.get(field.get_full_type_name().as_str())?;
208 unique_field_types.insert(field.get_full_type_name());
210 let mut sub_deps = Self::get_unique_field_types(&sub_message.parsed, graph)?;
211 unique_field_types.append(&mut sub_deps);
212 }
213 Some(unique_field_types)
214 }
215
216 fn compute_full_definition(
220 parsed: &ParsedMessageFile,
221 graph: &BTreeMap<String, MessageFile>,
222 ) -> Option<String> {
223 let mut definition_content = String::new();
224 definition_content.push_str(&format!("{}\n", parsed.source.trim()));
225 let sep: &str =
226 "================================================================================\n";
227 for field in Self::get_unique_field_types(parsed, graph)? {
228 let Some(sub_message) = graph.get(&field) else {
229 log::error!(
230 "Unable to find message type: {field:?}, while computing full definition of {}",
231 parsed.get_full_name()
232 );
233 return None;
234 };
235 definition_content.push_str(sep);
236 definition_content.push_str(&format!("MSG: {}\n", sub_message.get_full_name()));
237 definition_content.push_str(&format!("{}\n", sub_message.get_definition().trim()));
238 }
239 definition_content.pop();
241 Some(definition_content)
242 }
243
244 fn determine_if_fixed_length(
246 parsed: &ParsedMessageFile,
247 graph: &BTreeMap<String, MessageFile>,
248 ) -> Option<bool> {
249 for field in &parsed.fields {
250 match field.field_type.array_info {
252 ArrayType::Unbounded | ArrayType::Bounded(_) => return Some(false),
253 _ => {}
254 }
255 if field.field_type.package_name.is_none() {
256 if field.field_type.field_type == "string" {
258 return Some(false);
259 }
260 } else {
261 let field_msg = graph.get(field.get_full_type_name().as_str())?;
262 let field_is_fixed_length =
263 Self::determine_if_fixed_length(&field_msg.parsed, graph)?;
264 if !field_is_fixed_length {
265 return Some(false);
266 }
267 }
268 }
269 Some(true)
270 }
271}
272
273#[derive(Clone, Debug)]
274pub struct ServiceFile {
275 pub(crate) parsed: ParsedServiceFile,
276 pub(crate) request: MessageFile,
277 pub(crate) response: MessageFile,
278 pub(crate) md5sum: String,
279 pub(crate) ros2_hash: Ros2Hash,
280}
281
282impl ServiceFile {
283 fn resolve(parsed: ParsedServiceFile, graph: &BTreeMap<String, MessageFile>) -> Option<Self> {
286 if let (Some(request), Some(response)) = (
287 MessageFile::resolve(parsed.request_type.clone(), graph),
288 MessageFile::resolve(parsed.response_type.clone(), graph),
289 ) {
290 let md5sum = Self::compute_md5sum(&parsed, graph)?;
291 let ros2_hash = calculate_ros2_srv_hash(&parsed, graph);
292 Some(ServiceFile {
293 parsed,
294 request,
295 response,
296 md5sum,
297 ros2_hash,
298 })
299 } else {
300 log::error!("Unable to resolve dependencies in service: {parsed:#?}");
301 None
302 }
303 }
304
305 pub fn get_full_name(&self) -> String {
306 format!("{}/{}", self.parsed.package, self.parsed.name)
307 }
308
309 pub fn get_short_name(&self) -> String {
310 self.parsed.name.clone()
311 }
312
313 pub fn get_package_name(&self) -> String {
314 self.parsed.package.clone()
315 }
316
317 pub fn request(&self) -> &MessageFile {
318 &self.request
319 }
320
321 pub fn response(&self) -> &MessageFile {
322 &self.response
323 }
324
325 pub fn get_md5sum(&self) -> String {
326 self.md5sum.clone()
327 }
328
329 pub fn get_ros2_hash(&self) -> &Ros2Hash {
330 &self.ros2_hash
331 }
332
333 fn compute_md5sum(
334 parsed: &ParsedServiceFile,
335 graph: &BTreeMap<String, MessageFile>,
336 ) -> Option<String> {
337 let request_content = MessageFile::_compute_md5sum(&parsed.request_type, graph)?;
338 let response_content = MessageFile::_compute_md5sum(&parsed.response_type, graph)?;
339 let mut md5sum_context = md5::Context::new();
340 md5sum_context.consume(request_content.trim_end().as_bytes());
341 md5sum_context.consume(response_content.trim_end().as_bytes());
342
343 let md5sum = md5sum_context.compute();
344 log::trace!(
345 "Message type: {} calculated with md5sum: {md5sum:x}",
346 parsed.get_full_name()
347 );
348 Some(format!("{md5sum:x}"))
349 }
350}
351
352pub struct ActionWithHashes {
354 pub parsed: ParsedActionFile,
355 pub send_goal_hash: Ros2Hash,
356 pub get_result_hash: Ros2Hash,
357 pub feedback_message_hash: Ros2Hash,
358}
359
360impl ActionWithHashes {
361 pub fn from_json_metadata(parsed: ParsedActionFile, json_path: &Path) -> Option<Self> {
363 use std::fs;
364
365 let json_content = fs::read_to_string(json_path).ok()?;
366 let json: serde_json::Value = serde_json::from_str(&json_content).ok()?;
367
368 let type_hashes = json.get("type_hashes")?.as_array()?;
369
370 let find_hash = |suffix: &str| -> Option<Ros2Hash> {
372 type_hashes.iter().find_map(|type_hash| {
373 let type_name = type_hash.get("type_name")?.as_str()?;
374 let hash_string = type_hash.get("hash_string")?.as_str()?;
375
376 type_name
377 .ends_with(suffix)
378 .then(|| Ros2Hash::from_string(hash_string))
379 })
380 };
381
382 Some(ActionWithHashes {
383 parsed,
384 send_goal_hash: find_hash("_SendGoal")?,
385 get_result_hash: find_hash("_GetResult")?,
386 feedback_message_hash: find_hash("_FeedbackMessage")?,
387 })
388 }
389
390 pub fn get_package_name(&self) -> String {
391 self.parsed.package.clone()
392 }
393
394 pub fn get_short_name(&self) -> String {
395 self.parsed.name.clone()
396 }
397}
398
399#[derive(Clone, Debug)]
401pub struct RosLiteral {
402 pub inner: String,
403}
404
405impl Display for RosLiteral {
406 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
407 std::fmt::Display::fmt(&self.inner, f)
408 }
409}
410
411impl From<String> for RosLiteral {
412 fn from(value: String) -> Self {
413 Self { inner: value }
414 }
415}
416
417#[derive(PartialEq, Eq, Hash, Debug, Clone)]
419pub enum ArrayType {
420 NotArray,
421 FixedLength(usize),
422 Bounded(usize),
424 Unbounded,
425}
426
427#[derive(PartialEq, Eq, Hash, Debug, Clone)]
429pub struct FieldType {
430 pub package_name: Option<String>,
432 pub source_package: String,
436 pub field_type: String,
440 pub array_info: ArrayType,
442
443 pub string_capacity: Option<usize>,
446}
447
448impl std::fmt::Display for FieldType {
450 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
451 match self.array_info {
452 ArrayType::FixedLength(n) => f.write_fmt(format_args!("{}[{}]", self.field_type, n)),
453 ArrayType::Unbounded => f.write_fmt(format_args!("{}[]", self.field_type)),
454 ArrayType::NotArray => f.write_fmt(format_args!("{}", self.field_type)),
455 ArrayType::Bounded(n) => f.write_fmt(format_args!("{}[<={}]", self.field_type, n)),
456 }
457 }
458}
459
460impl FieldType {
461 pub fn is_primitive(&self) -> bool {
464 crate::parse::ROS_PRIMITIVE_TYPE_LIST.contains(&self.field_type.as_str())
465 }
466}
467
468#[derive(Clone, Debug)]
470pub struct FieldInfo {
471 pub field_type: FieldType,
472 pub field_name: String,
473 pub default: Option<RosLiteral>,
475}
476
477impl PartialEq for FieldInfo {
479 fn eq(&self, other: &Self) -> bool {
480 self.field_type == other.field_type && self.field_name == other.field_name
481 }
483}
484
485impl FieldInfo {
486 pub fn get_full_type_name(&self) -> String {
488 let field_package = self
489 .field_type
490 .package_name
491 .as_ref()
492 .unwrap_or(&self.field_type.source_package);
493 format!("{field_package}/{}", self.field_type.field_type)
494 }
495
496 pub fn get_ros2_full_type_name(&self) -> String {
498 let field_package = self
499 .field_type
500 .package_name
501 .as_ref()
502 .unwrap_or(&self.field_type.source_package);
503 format!("{field_package}/msg/{}", self.field_type.field_type)
505 }
506}
507
508#[derive(Clone, Debug)]
511pub struct ConstantInfo {
512 pub constant_type: String,
513 pub constant_name: String,
514 pub constant_value: RosLiteral,
515}
516
517impl PartialEq for ConstantInfo {
519 fn eq(&self, other: &Self) -> bool {
520 self.constant_type == other.constant_type && self.constant_name == other.constant_name
521 }
523}
524
525pub fn find_and_generate_ros_messages(
533 additional_search_paths: Vec<PathBuf>,
534) -> Result<(TokenStream, Vec<PathBuf>), Error> {
535 let mut ros_package_paths = utils::get_search_paths();
536 ros_package_paths.extend(additional_search_paths);
537 find_and_generate_ros_messages_without_ros_package_path(ros_package_paths)
538}
539
540pub fn find_and_generate_ros_messages_without_ros_package_path(
548 search_paths: Vec<PathBuf>,
549) -> Result<(TokenStream, Vec<PathBuf>), Error> {
550 let (messages, services, actions) = find_and_parse_ros_messages(&search_paths)?;
551 if messages.is_empty() && services.is_empty() {
552 bail!("Failed to find any services or messages while generating ROS message definitions, paths searched: {search_paths:?}");
555 }
556 tokenize_messages_and_services(messages, services, actions)
557}
558
559fn tokenize_messages_and_services(
561 messages: Vec<ParsedMessageFile>,
562 services: Vec<ParsedServiceFile>,
563 actions: Vec<ParsedActionFile>,
564) -> Result<(TokenStream, Vec<PathBuf>), Error> {
565 let (messages, services) = resolve_dependency_graph(messages, services)?;
566 let msg_iter = messages.iter().map(|m| m.parsed.path.clone());
567 let srv_iter = services.iter().map(|s| s.parsed.path.clone());
568 let action_iter = actions.iter().map(|a| a.path.clone());
569 let dependent_paths = msg_iter.chain(srv_iter).chain(action_iter).collect();
570 let source =
571 generate_rust_ros_message_definitions(messages, services, &CodegenOptions::default())?;
572 Ok((source, dependent_paths))
573}
574
575pub fn generate_ros_messages_for_packages(
578 packages: Vec<Package>,
579) -> Result<(TokenStream, Vec<PathBuf>), Error> {
580 let msg_paths = packages
581 .iter()
582 .flat_map(|package| {
583 utils::get_message_files(package).map(|msgs| {
584 msgs.into_iter()
585 .map(|msg| (package.clone(), msg))
586 .collect::<Vec<_>>()
587 })
588 })
589 .flatten()
590 .collect();
591 let (messages, services, actions) = parse_ros_files(msg_paths)?;
592 if messages.is_empty() && services.is_empty() {
593 bail!("Failed to find any services or messages while generating ROS message definitions, packages searched: {packages:?}")
594 }
595 tokenize_messages_and_services(messages, services, actions)
596}
597
598#[allow(clippy::type_complexity)]
606pub fn find_and_parse_ros_messages(
607 search_paths: &[PathBuf],
608) -> Result<
609 (
610 Vec<ParsedMessageFile>,
611 Vec<ParsedServiceFile>,
612 Vec<ParsedActionFile>,
613 ),
614 Error,
615> {
616 let search_paths = search_paths
617 .iter()
618 .map(|path| {
619 path.canonicalize().map_err(
620 |e| {
621 Error::with(format!("Codegen was instructed to search a path that could not be canonicalized relative to {:?}: {path:?}", std::env::current_dir().unwrap()).as_str(), e)
622 })
623 })
624 .collect::<Result<Vec<_>, Error>>()?;
625 debug!(
626 "Codegen is looking in following paths for files: {:?}",
627 &search_paths
628 );
629 let packages = utils::crawl(&search_paths);
630 let packages = utils::deduplicate_packages(packages);
632 if packages.is_empty() {
633 bail!(
634 "No ROS packages found while searching in: {search_paths:?}, relative to {:?}",
635 std::env::current_dir().unwrap()
636 );
637 }
638 debug!("After deduplication {:?} packages remain.", packages.len());
639
640 let message_files = packages
641 .iter()
642 .flat_map(|pkg| {
643 let files = utils::get_message_files(pkg).map_err(|err| {
644 Error::with(
645 format!("Unable to get paths to message files for {pkg:?}:").as_str(),
646 err,
647 )
648 });
649 match files {
651 Ok(files) => {
652 debug!(
653 "Found {:?} interface files in package: {:?}",
654 files.len(),
655 pkg.name
656 );
657 files
658 .into_iter()
659 .map(|path| Ok((pkg.clone(), path)))
660 .collect()
661 }
662 Err(e) => vec![Err(e)],
663 }
664 })
665 .collect::<Result<Vec<(Package, PathBuf)>, Error>>()?;
666
667 parse_ros_files(message_files)
668}
669
670pub fn generate_rust_ros_message_definitions(
680 messages: Vec<MessageFile>,
681 services: Vec<ServiceFile>,
682 options: &CodegenOptions,
683) -> Result<TokenStream, Error> {
684 let mut modules_to_struct_definitions: BTreeMap<String, Vec<TokenStream>> = BTreeMap::new();
685
686 messages.into_iter().try_for_each(|message| {
688 let pkg_name = message.parsed.package.clone();
689 let definition = generate_struct(message, Some(options))?;
690 if let Some(entry) = modules_to_struct_definitions.get_mut(&pkg_name) {
691 entry.push(definition);
692 } else {
693 modules_to_struct_definitions.insert(pkg_name, vec![definition]);
694 }
695 Ok::<(), Error>(())
696 })?;
697 services.into_iter().try_for_each(|service| {
699 let pkg_name = service.parsed.package.clone();
700 let definition = generate_service(service, Some(options))?;
701 if let Some(entry) = modules_to_struct_definitions.get_mut(&pkg_name) {
702 entry.push(definition);
703 } else {
704 modules_to_struct_definitions.insert(pkg_name, vec![definition]);
705 }
706 Ok::<(), Error>(())
707 })?;
708 let all_pkgs = modules_to_struct_definitions
710 .keys()
711 .cloned()
712 .collect::<Vec<String>>();
713 let module_definitions = modules_to_struct_definitions
714 .into_iter()
715 .map(|(pkg, struct_defs)| generate_mod(pkg, struct_defs, &all_pkgs[..]))
716 .collect::<Vec<TokenStream>>();
717
718 Ok(quote! {
719 #(#module_definitions)*
720
721 })
722}
723
724struct MessageMetadata {
725 msg: ParsedMessageFile,
726 seen_count: u32,
727}
728
729pub fn resolve_dependency_graph(
730 messages: Vec<ParsedMessageFile>,
731 services: Vec<ParsedServiceFile>,
732) -> Result<(Vec<MessageFile>, Vec<ServiceFile>), Error> {
733 const MAX_PARSE_ITER_LIMIT: u32 = 2048;
734 let mut unresolved_messages = messages
735 .into_iter()
736 .map(|msg| MessageMetadata { msg, seen_count: 0 })
737 .collect::<VecDeque<_>>();
738
739 let mut resolved_messages = ros2_builtin_interfaces::get_builtin_interfaces();
741
742 while let Some(MessageMetadata { msg, seen_count }) = unresolved_messages.pop_front() {
744 let fully_resolved = msg.fields.iter().all(|field| {
746 let is_primitive = field.field_type.is_primitive();
747 if !is_primitive {
748 let is_resolved =
749 resolved_messages.contains_key(field.get_full_type_name().as_str());
750 is_resolved
751 } else {
752 true
753 }
754 });
755
756 if fully_resolved {
757 let debug_name = msg.get_full_name();
758 let msg_file = MessageFile::resolve(msg, &resolved_messages).ok_or(
759 Error::new(format!("Failed to correctly resolve message {debug_name:?}, either md5sum could not be calculated, or fixed length was indeterminate"))
760 )?;
761 resolved_messages.insert(msg_file.get_full_name(), msg_file);
762 } else {
763 unresolved_messages.push_back(MessageMetadata {
764 seen_count: seen_count + 1,
765 msg,
766 });
767 }
768
769 if seen_count > MAX_PARSE_ITER_LIMIT {
770 let msg_names = unresolved_messages
771 .iter()
772 .map(|item| format!("{}/{}", item.msg.package, item.msg.name))
773 .collect::<Vec<_>>();
774
775 let mut unresolved_fields = unresolved_messages
777 .iter()
778 .flat_map(|item| {
779 item.msg
780 .fields
781 .iter()
782 .filter_map(|field| {
783 if !field.field_type.is_primitive() {
784 if resolved_messages
785 .contains_key(field.get_full_type_name().as_str())
786 {
787 None
788 } else {
789 Some(field.get_full_type_name())
791 }
792 } else {
793 None
794 }
795 })
796 .collect::<Vec<_>>()
797 })
798 .collect::<Vec<_>>();
799 unresolved_fields.sort();
800 unresolved_fields.dedup();
801 let unresolved_fields = unresolved_fields
802 .into_iter()
803 .filter(|f| !msg_names.contains(f))
804 .collect::<Vec<_>>();
805
806 bail!(
807 "Unable to resolve ROS message dependencies after reaching search limit.\n\
808 The following types are still unresolved:\n{unresolved_fields:#?}\n
809 This is preventing full resolution for the following messages:\n{msg_names:#?}"
810 );
811 }
812 }
813
814 let mut resolved_services: Vec<_> = services
816 .into_iter()
817 .map(|srv| {
818 let name = srv.path.clone();
819 ServiceFile::resolve(srv, &resolved_messages).ok_or(Error::new(format!(
820 "Failed to correctly resolve service: {:?}",
821 &name
822 )))
823 })
824 .collect::<Result<Vec<_>, Error>>()?;
825 resolved_services.sort_by(|a: &ServiceFile, b: &ServiceFile| a.parsed.name.cmp(&b.parsed.name));
826
827 Ok((resolved_messages.into_values().collect(), resolved_services))
828}
829
830#[allow(clippy::type_complexity)]
836pub(crate) fn parse_ros_files(
837 msg_paths: Vec<(Package, PathBuf)>,
838) -> Result<
839 (
840 Vec<ParsedMessageFile>,
841 Vec<ParsedServiceFile>,
842 Vec<ParsedActionFile>,
843 ),
844 Error,
845> {
846 let mut parsed_messages = Vec::new();
847 let mut parsed_services = Vec::new();
848 let mut parsed_actions = Vec::new();
849 for (pkg, path) in msg_paths {
850 let contents = std::fs::read_to_string(&path).map_err(|e| {
851 Error::with(
852 format!("Codgen failed while attempting to read file {path:?} from disk:").as_str(),
853 e,
854 )
855 })?;
856 let name = path
858 .file_stem()
859 .ok_or(Error::new(format!(
860 "Failed to extract valid file stem for file at {path:?}"
861 )))?
862 .to_str()
863 .ok_or(Error::new(format!(
864 "File stem for file at path {path:?} was not valid unicode?"
865 )))?;
866 match path.extension().unwrap().to_str().unwrap() {
867 "srv" => {
868 let srv_file = parse_ros_service_file(&contents, name, &pkg, &path)?;
869 parsed_services.push(srv_file);
870 }
871 "msg" => {
872 let msg = parse_ros_message_file(&contents, name, &pkg, &path)?;
873 parsed_messages.push(msg);
874 }
875 "action" => {
876 let action = parse_ros_action_file(&contents, name, &pkg, &path)?;
877 parsed_actions.push(action.clone());
878 parsed_messages.push(action.action_type);
879 parsed_messages.push(action.action_goal_type);
880 parsed_messages.push(action.goal_type);
881 parsed_messages.push(action.action_result_type);
882 parsed_messages.push(action.result_type);
883 parsed_messages.push(action.action_feedback_type);
884 parsed_messages.push(action.feedback_type);
885 }
886 _ => {
887 log::error!("File extension not recognized as a ROS file: {path:?}");
888 }
889 }
890 }
891 Ok((parsed_messages, parsed_services, parsed_actions))
892}
893
894pub fn resolve_action_hashes(parsed_actions: Vec<ParsedActionFile>) -> Vec<ActionWithHashes> {
896 parsed_actions
897 .into_iter()
898 .filter_map(|parsed_action| {
899 let json_path = parsed_action.path.with_extension("json");
901
902 ActionWithHashes::from_json_metadata(parsed_action.clone(), &json_path).or_else(|| {
903 log::warn!(
904 "Failed to resolve action hashes for {}/{}",
905 parsed_action.package,
906 parsed_action.name
907 );
908 None
909 })
910 })
911 .collect()
912}
913
914#[cfg(test)]
915mod test {
916 use crate::find_and_generate_ros_messages;
917
918 #[test_log::test]
920 fn generate_ok_on_ros1() {
921 let assets_path = concat!(
922 env!("CARGO_MANIFEST_DIR"),
923 "/../assets/ros1_common_interfaces"
924 );
925
926 let (source, paths) = find_and_generate_ros_messages(vec![assets_path.into()]).unwrap();
927 assert!(!source.is_empty());
929 assert!(!paths.is_empty());
931 }
932
933 #[test_log::test]
935 fn generate_ok_on_ros2() {
936 let assets_path = concat!(
937 env!("CARGO_MANIFEST_DIR"),
938 "/../assets/ros2_common_interfaces"
939 );
940
941 let required_path = concat!(
942 env!("CARGO_MANIFEST_DIR"),
943 "/../assets/ros2_required_msgs/rcl_interfaces/builtin_interfaces"
944 );
945
946 let (source, paths) =
947 find_and_generate_ros_messages(vec![assets_path.into(), required_path.into()]).unwrap();
948 assert!(!source.is_empty());
950 assert!(!paths.is_empty());
952 }
953
954 #[test_log::test]
956 fn generate_ok_on_ros1_test_msgs() {
957 let assets_path = concat!(env!("CARGO_MANIFEST_DIR"), "/../assets/ros1_test_msgs");
960 let std_msgs = concat!(
961 env!("CARGO_MANIFEST_DIR"),
962 "/../assets/ros1_common_interfaces/std_msgs"
963 );
964 let (source, paths) =
965 find_and_generate_ros_messages(vec![assets_path.into(), std_msgs.into()]).unwrap();
966 assert!(!source.is_empty());
967 assert!(!paths.is_empty());
969 }
970
971 #[test_log::test]
973 fn generate_ok_on_ros2_test_msgs() {
974 let assets_path = concat!(env!("CARGO_MANIFEST_DIR"), "/../assets/ros2_test_msgs");
975 let required_path = concat!(
976 env!("CARGO_MANIFEST_DIR"),
977 "/../assets/ros2_required_msgs/rcl_interfaces/builtin_interfaces"
978 );
979
980 let (source, paths) =
981 find_and_generate_ros_messages(vec![assets_path.into(), required_path.into()]).unwrap();
982 assert!(!source.is_empty());
983 assert!(!paths.is_empty());
984 }
985}