1use crate::{
4 ParameterType, QoSProfile,
5 heapless::{String, Vec},
6};
7
8#[cfg(feature = "alloc")]
9use crate::{QoSDurabilityPolicy, QoSHistoryPolicy, QoSLivelinessPolicy, QoSReliabilityPolicy};
10#[cfg(feature = "alloc")]
11use alloc::{format, string::String as StdString, vec::Vec as StdVec};
16
17pub const DEFAULT_MAX_METADATA_NODES: usize = 8;
19pub const DEFAULT_MAX_METADATA_ENTITIES: usize = 32;
21pub const DEFAULT_MAX_METADATA_CALLBACKS: usize = 32;
23pub const METADATA_STRING_CAPACITY: usize = 128;
25
26pub type MetadataString = String<METADATA_STRING_CAPACITY>;
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
31pub struct NodeSlot(pub usize);
32
33impl NodeSlot {
34 pub const fn new(index: usize) -> Self {
36 Self(index)
37 }
38
39 pub const fn index(self) -> usize {
41 self.0
42 }
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
47pub struct EntitySlot(pub usize);
48
49impl EntitySlot {
50 pub const fn new(index: usize) -> Self {
52 Self(index)
53 }
54
55 pub const fn index(self) -> usize {
57 self.0
58 }
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
63pub struct CallbackSlot(pub usize);
64
65impl CallbackSlot {
66 pub const fn new(index: usize) -> Self {
68 Self(index)
69 }
70
71 pub const fn index(self) -> usize {
73 self.0
74 }
75}
76
77#[derive(Debug, Clone, PartialEq, Eq)]
79pub struct SourceLocationMetadata {
80 pub artifact: MetadataString,
81 pub line: Option<u32>,
82 pub column: Option<u32>,
83}
84
85impl SourceLocationMetadata {
86 pub const fn empty() -> Self {
88 Self {
89 artifact: MetadataString::new(),
90 line: None,
91 column: None,
92 }
93 }
94
95 #[track_caller]
97 pub fn caller() -> Result<Self, NodeMetadataError> {
98 let location = core::panic::Location::caller();
99 Ok(Self {
100 artifact: copy_str_keep_tail(location.file())?,
117 line: Some(location.line()),
118 column: Some(location.column()),
119 })
120 }
121}
122
123#[derive(Debug, Clone, PartialEq, Eq)]
125pub enum ParameterDefault {
126 Bool(bool),
127 Integer(i64),
128 Double(MetadataString),
129 String(MetadataString),
130 BoolArray,
131 IntegerArray,
132 DoubleArray,
133 StringArray,
134}
135
136impl ParameterDefault {
137 pub const fn parameter_type(&self) -> ParameterType {
139 match self {
140 Self::Bool(_) => ParameterType::Bool,
141 Self::Integer(_) => ParameterType::Integer,
142 Self::Double(_) => ParameterType::Double,
143 Self::String(_) => ParameterType::String,
144 Self::BoolArray => ParameterType::BoolArray,
145 Self::IntegerArray => ParameterType::IntegerArray,
146 Self::DoubleArray => ParameterType::DoubleArray,
147 Self::StringArray => ParameterType::StringArray,
148 }
149 }
150
151 pub fn for_type(param_type: ParameterType) -> Result<Self, NodeMetadataError> {
153 Ok(match param_type {
154 ParameterType::Bool => Self::Bool(false),
155 ParameterType::Integer => Self::Integer(0),
156 ParameterType::Double => Self::Double(copy_str("0.0")?),
157 ParameterType::String => Self::String(copy_str("")?),
158 ParameterType::BoolArray => Self::BoolArray,
159 ParameterType::IntegerArray => Self::IntegerArray,
160 ParameterType::DoubleArray => Self::DoubleArray,
161 ParameterType::StringArray => Self::StringArray,
162 ParameterType::ByteArray | ParameterType::NotSet => Self::Integer(0),
163 })
164 }
165}
166
167#[derive(Debug, Clone, Copy, PartialEq, Eq)]
169pub enum SourceNameKind {
170 Absolute,
172 Private,
174 Relative,
176}
177
178impl SourceNameKind {
179 pub const fn from_source_name(name: &str) -> Self {
181 let bytes = name.as_bytes();
182 if bytes.is_empty() {
183 Self::Relative
184 } else if bytes[0] == b'/' {
185 Self::Absolute
186 } else if bytes[0] == b'~' {
187 Self::Private
188 } else {
189 Self::Relative
190 }
191 }
192}
193
194pub use nros_node::names::{MAX_RESOLVED_NAME_LEN, ResolvedName, expand_name, resolve_name};
201
202#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
204pub struct EntityId<'a>(pub &'a str);
205
206impl<'a> EntityId<'a> {
207 pub const fn new(id: &'a str) -> Self {
209 Self(id)
210 }
211
212 pub const fn as_str(self) -> &'a str {
214 self.0
215 }
216}
217
218#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
220pub struct NodeId<'a>(pub &'a str);
221
222impl<'a> NodeId<'a> {
223 pub const fn new(id: &'a str) -> Self {
225 Self(id)
226 }
227
228 pub const fn as_str(self) -> &'a str {
230 self.0
231 }
232}
233
234#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
236pub struct CallbackId<'a>(pub &'a str);
237
238impl<'a> CallbackId<'a> {
239 pub const fn new(id: &'a str) -> Self {
241 Self(id)
242 }
243
244 pub const fn as_str(self) -> &'a str {
246 self.0
247 }
248}
249
250#[derive(Debug, Clone, Copy, PartialEq, Eq)]
252pub enum EntityKind {
253 Publisher,
254 Subscription,
255 Timer,
256 ServiceServer,
257 ServiceClient,
258 ActionServer,
259 ActionClient,
260 Parameter,
261}
262
263#[derive(Debug, Clone, Copy, PartialEq, Eq)]
265pub enum CallbackEffectKind {
266 Reads,
267 Publishes,
268 Writes,
269}
270
271#[derive(Debug, Clone, Copy, PartialEq, Eq)]
273pub enum NodeMetadataError {
274 Capacity,
276 NameTooLong,
278 UnknownNode,
280 UnknownEntity,
282 DuplicateId,
284}
285
286#[derive(Debug, Clone, PartialEq, Eq)]
288pub struct NodeMetadata {
289 pub slot: NodeSlot,
290 pub id: MetadataString,
291 pub source_default_name: MetadataString,
292 pub name: MetadataString,
293 pub namespace: MetadataString,
294 pub domain_id: u32,
295}
296
297#[derive(Debug, Clone, PartialEq, Eq)]
299pub struct EntityMetadata {
300 pub slot: Option<EntitySlot>,
301 pub id: MetadataString,
302 pub node_slot: Option<NodeSlot>,
303 pub node_id: MetadataString,
304 pub kind: EntityKind,
305 pub source_name: MetadataString,
306 pub source_name_kind: SourceNameKind,
307 pub type_name: &'static str,
308 pub type_hash: &'static str,
309 pub qos: QoSProfile,
310 pub callback_slot: Option<CallbackSlot>,
311 pub callback_id: Option<MetadataString>,
312 pub callback_source: SourceLocationMetadata,
313 pub callback_group: Option<MetadataString>,
314 pub action_cancel_callback_slot: Option<CallbackSlot>,
315 pub action_cancel_callback_id: Option<MetadataString>,
316 pub action_cancel_source: SourceLocationMetadata,
317 pub action_accepted_callback_slot: Option<CallbackSlot>,
318 pub action_accepted_callback_id: Option<MetadataString>,
319 pub action_accepted_source: SourceLocationMetadata,
320 pub period_ms: Option<u64>,
321 pub period_us: Option<u64>,
327 pub parameter_type: Option<ParameterType>,
328 pub parameter_default: Option<ParameterDefault>,
329 pub parameter_read_only: bool,
330 pub safety: bool,
338 pub source: SourceLocationMetadata,
339}
340
341#[derive(Debug, Clone, PartialEq, Eq)]
343pub struct CallbackEffectMetadata {
344 pub callback_id: MetadataString,
345 pub callback_slot: Option<CallbackSlot>,
346 pub kind: CallbackEffectKind,
347 pub entity_id: MetadataString,
348 pub entity_slot: Option<EntitySlot>,
349}
350
351#[cfg(feature = "alloc")]
353#[derive(Debug, Clone)]
354pub struct SourceMetadataExport<'a> {
355 pub package: &'a str,
356 pub component: &'a str,
357 pub executable: Option<&'a str>,
358 pub exported_symbol: Option<&'a str>,
359 pub package_manifest: &'a str,
360 pub source_artifacts: &'a [&'a str],
361 pub language: &'a str,
368}
369
370#[cfg(feature = "alloc")]
371impl<'a> SourceMetadataExport<'a> {
372 pub const fn new(package: &'a str, component: &'a str) -> Self {
374 Self {
375 package,
376 component,
377 executable: None,
378 exported_symbol: None,
379 package_manifest: "package.xml",
380 source_artifacts: &[],
381 language: "rust",
382 }
383 }
384
385 pub const fn language(mut self, language: &'a str) -> Self {
387 self.language = language;
388 self
389 }
390
391 pub const fn executable(mut self, executable: &'a str) -> Self {
393 self.executable = Some(executable);
394 self
395 }
396
397 pub const fn exported_symbol(mut self, exported_symbol: &'a str) -> Self {
399 self.exported_symbol = Some(exported_symbol);
400 self
401 }
402
403 pub const fn package_manifest(mut self, package_manifest: &'a str) -> Self {
405 self.package_manifest = package_manifest;
406 self
407 }
408
409 pub const fn source_artifacts(mut self, source_artifacts: &'a [&'a str]) -> Self {
411 self.source_artifacts = source_artifacts;
412 self
413 }
414}
415
416pub fn metadata_string(value: &str) -> Result<MetadataString, NodeMetadataError> {
419 copy_str(value)
420}
421
422pub(crate) fn copy_str_keep_tail(value: &str) -> Result<MetadataString, NodeMetadataError> {
431 if value.len() <= METADATA_STRING_CAPACITY {
432 return copy_str(value);
433 }
434 const MARK: &str = "…/";
436 let budget = METADATA_STRING_CAPACITY - MARK.len();
437 let tail = value
438 .char_indices()
439 .find(|(i, _)| value.len() - i <= budget)
440 .map(|(i, _)| &value[i..])
441 .unwrap_or("");
442 let tail = match tail.find('/') {
444 Some(cut) => &tail[cut + 1..],
445 None => tail,
446 };
447 let mut out = MetadataString::new();
448 out.push_str(MARK)
449 .map_err(|_| NodeMetadataError::NameTooLong)?;
450 out.push_str(tail)
451 .map_err(|_| NodeMetadataError::NameTooLong)?;
452 Ok(out)
453}
454
455pub(crate) fn copy_str(value: &str) -> Result<MetadataString, NodeMetadataError> {
456 let mut out = MetadataString::new();
457 out.push_str(value)
458 .map_err(|_| NodeMetadataError::NameTooLong)?;
459 Ok(out)
460}
461
462#[derive(Debug)]
464pub struct MetadataRecorder<
465 const MAX_NODES: usize = DEFAULT_MAX_METADATA_NODES,
466 const MAX_ENTITIES: usize = DEFAULT_MAX_METADATA_ENTITIES,
467 const MAX_CALLBACKS: usize = DEFAULT_MAX_METADATA_CALLBACKS,
468> {
469 nodes: Vec<NodeMetadata, MAX_NODES>,
470 entities: Vec<EntityMetadata, MAX_ENTITIES>,
471 callback_effects: Vec<CallbackEffectMetadata, MAX_CALLBACKS>,
472}
473
474impl<const MAX_NODES: usize, const MAX_ENTITIES: usize, const MAX_CALLBACKS: usize> Default
475 for MetadataRecorder<MAX_NODES, MAX_ENTITIES, MAX_CALLBACKS>
476{
477 fn default() -> Self {
478 Self::new()
479 }
480}
481
482impl<const MAX_NODES: usize, const MAX_ENTITIES: usize, const MAX_CALLBACKS: usize>
483 MetadataRecorder<MAX_NODES, MAX_ENTITIES, MAX_CALLBACKS>
484{
485 pub const fn new() -> Self {
487 Self {
488 nodes: Vec::new(),
489 entities: Vec::new(),
490 callback_effects: Vec::new(),
491 }
492 }
493
494 pub fn nodes(&self) -> &[NodeMetadata] {
496 &self.nodes
497 }
498
499 pub fn entities(&self) -> &[EntityMetadata] {
501 &self.entities
502 }
503
504 pub fn callback_effects(&self) -> &[CallbackEffectMetadata] {
506 &self.callback_effects
507 }
508
509 #[cfg(feature = "alloc")]
511 pub fn to_source_metadata_json(
512 &self,
513 export: &SourceMetadataExport<'_>,
514 ) -> Result<StdString, core::fmt::Error> {
515 let mut out = StdString::new();
516 self.write_source_metadata_json(export, &mut out)?;
517 Ok(out)
518 }
519
520 #[cfg(feature = "alloc")]
522 pub fn write_source_metadata_json(
523 &self,
524 export: &SourceMetadataExport<'_>,
525 out: &mut impl core::fmt::Write,
526 ) -> core::fmt::Result {
527 write!(out, "{{")?;
528 write!(out, "\"version\":1,")?;
529 write_json_field(out, "package", export.package)?;
530 out.write_char(',')?;
531 write_json_field(out, "component", export.component)?;
532 out.write_char(',')?;
533 write_json_field(out, "language", export.language)?;
534 out.write_char(',')?;
535 write_json_opt_field(out, "executable", export.executable)?;
536 out.write_char(',')?;
537 write_json_opt_field(out, "exported_symbol", export.exported_symbol)?;
538 out.write_char(',')?;
539 self.write_nodes_json(out)?;
540 out.write_char(',')?;
541 self.write_callbacks_json(out)?;
542 out.write_char(',')?;
543 self.write_parameters_json(out)?;
544 out.write_char(',')?;
545 self.write_trace_json(export, out)?;
546 write!(out, "}}")
547 }
548
549 pub fn push_node(
559 &mut self,
560 id: NodeId<'_>,
561 name: &str,
562 namespace: &str,
563 domain_id: u32,
564 ) -> Result<(), NodeMetadataError> {
565 if self.has_node(id.as_str()) {
566 return Err(NodeMetadataError::DuplicateId);
567 }
568
569 self.nodes
570 .push(NodeMetadata {
571 slot: NodeSlot::new(self.nodes.len()),
572 id: copy_str(id.as_str())?,
573 source_default_name: copy_str(name)?,
574 name: copy_str(name)?,
575 namespace: copy_str(namespace)?,
576 domain_id,
577 })
578 .map_err(|_| NodeMetadataError::Capacity)
579 }
580
581 pub fn push_entity(&mut self, mut entity: EntityMetadata) -> Result<(), NodeMetadataError> {
586 if !self.has_node(&entity.node_id) {
587 return Err(NodeMetadataError::UnknownNode);
588 }
589 if self.has_entity(&entity.id) {
590 return Err(NodeMetadataError::DuplicateId);
591 }
592
593 entity.slot = Some(EntitySlot::new(self.entities.len()));
594 entity.node_slot = self.node_slot_for_id(&entity.node_id);
595 let mut current_callbacks = Vec::<MetadataString, 3>::new();
596 let mut next_callback_slot = self.callback_slot_count();
597 entity.callback_slot = entity.callback_id.as_ref().map(|callback_id| {
598 self.callback_slot_for_current_entity(
599 callback_id.as_str(),
600 &mut current_callbacks,
601 &mut next_callback_slot,
602 )
603 });
604 entity.action_cancel_callback_slot =
605 entity
606 .action_cancel_callback_id
607 .as_ref()
608 .map(|callback_id| {
609 self.callback_slot_for_current_entity(
610 callback_id.as_str(),
611 &mut current_callbacks,
612 &mut next_callback_slot,
613 )
614 });
615 entity.action_accepted_callback_slot =
616 entity
617 .action_accepted_callback_id
618 .as_ref()
619 .map(|callback_id| {
620 self.callback_slot_for_current_entity(
621 callback_id.as_str(),
622 &mut current_callbacks,
623 &mut next_callback_slot,
624 )
625 });
626
627 self.entities
628 .push(entity)
629 .map_err(|_| NodeMetadataError::Capacity)
630 }
631
632 pub(crate) fn push_callback_effect(
633 &mut self,
634 callback_id: CallbackId<'_>,
635 kind: CallbackEffectKind,
636 entity_id: EntityId<'_>,
637 ) -> Result<(), NodeMetadataError> {
638 if !self.has_entity(entity_id.as_str()) {
639 return Err(NodeMetadataError::UnknownEntity);
640 }
641
642 self.callback_effects
643 .push(CallbackEffectMetadata {
644 callback_id: copy_str(callback_id.as_str())?,
645 callback_slot: self.callback_slot_for_id(callback_id.as_str()),
646 kind,
647 entity_id: copy_str(entity_id.as_str())?,
648 entity_slot: self.entity_slot_for_id(entity_id.as_str()),
649 })
650 .map_err(|_| NodeMetadataError::Capacity)
651 }
652
653 pub(crate) fn has_node(&self, id: &str) -> bool {
654 self.nodes.iter().any(|node| node.id.as_str() == id)
655 }
656
657 pub(crate) fn has_entity(&self, id: &str) -> bool {
658 self.entities.iter().any(|entity| entity.id.as_str() == id)
659 }
660
661 fn node_slot_for_id(&self, id: &str) -> Option<NodeSlot> {
662 self.nodes
663 .iter()
664 .find(|node| node.id.as_str() == id)
665 .map(|node| node.slot)
666 }
667
668 fn entity_slot_for_id(&self, id: &str) -> Option<EntitySlot> {
669 self.entities
670 .iter()
671 .find(|entity| entity.id.as_str() == id)
672 .and_then(|entity| entity.slot)
673 }
674
675 fn callback_slot_for_current_entity(
676 &self,
677 id: &str,
678 current_callbacks: &mut Vec<MetadataString, 3>,
679 next_callback_slot: &mut usize,
680 ) -> CallbackSlot {
681 if let Some(slot) = self.callback_slot_for_id(id) {
682 return slot;
683 }
684 if let Some((index, _)) = current_callbacks
685 .iter()
686 .enumerate()
687 .find(|(_, callback_id)| callback_id.as_str() == id)
688 {
689 return CallbackSlot::new(self.callback_slot_count() + index);
690 }
691 let slot = CallbackSlot::new(*next_callback_slot);
692 let _ = current_callbacks
693 .push(copy_str(id).expect("callback ID already fits metadata string capacity"));
694 *next_callback_slot += 1;
695 slot
696 }
697
698 fn callback_slot_for_id(&self, id: &str) -> Option<CallbackSlot> {
699 let mut seen = Vec::<&str, MAX_CALLBACKS>::new();
700 for entity in &self.entities {
701 for callback_id in entity_callback_ids(entity) {
702 let Some(callback_id) = callback_id else {
703 continue;
704 };
705 let callback_id = callback_id.as_str();
706 if seen.contains(&callback_id) {
707 continue;
708 }
709 if callback_id == id {
710 return Some(CallbackSlot::new(seen.len()));
711 }
712 let _ = seen.push(callback_id);
713 }
714 }
715 None
716 }
717
718 fn callback_slot_count(&self) -> usize {
719 let mut seen = Vec::<&str, MAX_CALLBACKS>::new();
720 for entity in &self.entities {
721 for callback_id in entity_callback_ids(entity) {
722 let Some(callback_id) = callback_id else {
723 continue;
724 };
725 let callback_id = callback_id.as_str();
726 if !seen.contains(&callback_id) {
727 let _ = seen.push(callback_id);
728 }
729 }
730 }
731 seen.len()
732 }
733
734 #[cfg(feature = "alloc")]
735 fn write_nodes_json(&self, out: &mut impl core::fmt::Write) -> core::fmt::Result {
736 write!(out, "\"nodes\":[")?;
737 for (index, node) in self.nodes.iter().enumerate() {
738 if index > 0 {
739 out.write_char(',')?;
740 }
741 write!(out, "{{")?;
742 write_json_field(out, "id", node.id.as_str())?;
743 out.write_char(',')?;
744 write!(out, "\"declaration_slot\":{},", node.slot.index())?;
745 write_json_field(
746 out,
747 "source_default_name",
748 node.source_default_name.as_str(),
749 )?;
750 out.write_char(',')?;
751 write!(out, "\"unresolved_name\":")?;
752 write_source_name(
753 out,
754 node.name.as_str(),
755 SourceNameKind::from_source_name(&node.name),
756 )?;
757 out.write_char(',')?;
758 if node.namespace.as_str() == "/" {
759 write!(out, "\"namespace\":null,")?;
760 } else {
761 write_json_field(out, "namespace", node.namespace.as_str())?;
762 out.write_char(',')?;
763 }
764 self.write_node_entities(out, node.id.as_str())?;
765 write!(out, "}}")?;
766 }
767 write!(out, "]")
768 }
769
770 #[cfg(feature = "alloc")]
771 fn write_node_entities(
772 &self,
773 out: &mut impl core::fmt::Write,
774 node_id: &str,
775 ) -> core::fmt::Result {
776 self.write_entity_array(out, "publishers", node_id, EntityKind::Publisher)?;
777 out.write_char(',')?;
778 self.write_entity_array(out, "subscribers", node_id, EntityKind::Subscription)?;
779 out.write_char(',')?;
780 self.write_entity_array(out, "timers", node_id, EntityKind::Timer)?;
781 out.write_char(',')?;
782 self.write_entity_array(out, "services", node_id, EntityKind::ServiceServer)?;
783 out.write_char(',')?;
784 self.write_entity_array(out, "actions", node_id, EntityKind::ActionServer)?;
785 out.write_char(',')?;
792 self.write_entity_array(out, "action_clients", node_id, EntityKind::ActionClient)?;
793 out.write_char(',')?;
794 self.write_entity_array(out, "service_clients", node_id, EntityKind::ServiceClient)
795 }
796
797 #[cfg(feature = "alloc")]
798 fn write_entity_array(
799 &self,
800 out: &mut impl core::fmt::Write,
801 field: &str,
802 node_id: &str,
803 kind: EntityKind,
804 ) -> core::fmt::Result {
805 write!(out, "\"{}\":[", field)?;
806 for (index, entity) in self
807 .entities
808 .iter()
809 .filter(|entity| entity.node_id.as_str() == node_id && entity.kind == kind)
810 .enumerate()
811 {
812 if index > 0 {
813 out.write_char(',')?;
814 }
815 match kind {
816 EntityKind::Publisher => write_publisher_json(out, entity)?,
817 EntityKind::Subscription => write_subscriber_json(out, entity)?,
818 EntityKind::Timer => write_timer_json(out, entity)?,
819 EntityKind::ServiceServer => write_service_json(out, entity)?,
820 EntityKind::ActionServer => write_action_json(out, entity)?,
821 EntityKind::ActionClient | EntityKind::ServiceClient => {
825 write_client_json(out, entity)?
826 }
827 _ => {}
828 }
829 }
830 write!(out, "]")
831 }
832
833 #[cfg(feature = "alloc")]
834 fn write_callbacks_json(&self, out: &mut impl core::fmt::Write) -> core::fmt::Result {
835 let callbacks = self.source_callbacks();
836 write!(out, "\"callbacks\":[")?;
837 for (index, callback) in callbacks.iter().enumerate() {
838 if index > 0 {
839 out.write_char(',')?;
840 }
841 write!(out, "{{")?;
842 write_json_field(out, "id", callback.id.as_str())?;
843 out.write_char(',')?;
844 if let Some(slot) = callback.slot {
845 write!(out, "\"declaration_slot\":{},", slot.index())?;
846 }
847 write_json_field(out, "kind", callback.kind)?;
848 out.write_char(',')?;
849 if let Some(group) = callback.group.as_ref() {
850 write_json_field(out, "group", group)?;
851 out.write_char(',')?;
852 } else {
853 write!(out, "\"group\":null,")?;
854 }
855 write!(out, "\"effects\":[")?;
856 for (effect_index, effect) in self
857 .callback_effects
858 .iter()
859 .filter(|effect| effect.callback_id.as_str() == callback.id)
860 .enumerate()
861 {
862 if effect_index > 0 {
863 out.write_char(',')?;
864 }
865 write!(out, "{{")?;
866 write_json_field(out, "kind", effect_json_kind(effect.kind))?;
867 out.write_char(',')?;
868 write_json_field(out, "entity", effect.entity_id.as_str())?;
869 if let Some(entity_slot) = effect.entity_slot {
870 write!(out, ",\"entity_slot\":{}", entity_slot.index())?;
871 }
872 write!(out, "}}")?;
873 }
874 write!(out, "],")?;
875 write_source_location(out, &callback.source)?;
876 write!(out, "}}")?;
877 }
878 write!(out, "]")
879 }
880
881 #[cfg(feature = "alloc")]
882 fn write_parameters_json(&self, out: &mut impl core::fmt::Write) -> core::fmt::Result {
883 write!(out, "\"parameters\":[")?;
884 for (index, entity) in self
885 .entities
886 .iter()
887 .filter(|entity| entity.kind == EntityKind::Parameter)
888 .enumerate()
889 {
890 if index > 0 {
891 out.write_char(',')?;
892 }
893 write!(out, "{{")?;
894 write_json_field(out, "node", entity.node_id.as_str())?;
895 out.write_char(',')?;
896 if let Some(slot) = entity.slot {
897 write!(out, "\"declaration_slot\":{},", slot.index())?;
898 }
899 write_json_field(out, "name", entity.source_name.as_str())?;
900 out.write_char(',')?;
901 write!(out, "\"default\":")?;
902 write_parameter_default(out, entity.parameter_default.as_ref())?;
903 out.write_char(',')?;
904 write!(out, "\"read_only\":{},", entity.parameter_read_only)?;
905 write_source_location(out, &entity.source)?;
906 write!(out, "}}")?;
907 }
908 write!(out, "]")
909 }
910
911 #[cfg(feature = "alloc")]
912 fn write_trace_json(
913 &self,
914 export: &SourceMetadataExport<'_>,
915 out: &mut impl core::fmt::Write,
916 ) -> core::fmt::Result {
917 write!(out, "\"trace\":{{")?;
918 write_json_field(out, "generator", "nros-metadata-rust")?;
919 out.write_char(',')?;
920 write_json_field(out, "package_manifest", export.package_manifest)?;
921 out.write_char(',')?;
922 write!(out, "\"source_artifacts\":[")?;
923 for (index, artifact) in export.source_artifacts.iter().enumerate() {
924 if index > 0 {
925 out.write_char(',')?;
926 }
927 write_json_string(out, artifact)?;
928 }
929 write!(out, "]}}")
930 }
931
932 #[cfg(feature = "alloc")]
933 fn source_callbacks(&self) -> StdVec<SourceCallbackRef> {
934 let mut callbacks = StdVec::new();
935 for entity in &self.entities {
936 let Some(callback_id) = entity.callback_id.as_ref() else {
937 continue;
938 };
939 let kind = match entity.kind {
940 EntityKind::Subscription => "subscription",
941 EntityKind::Timer => "timer",
942 EntityKind::ServiceServer => "service",
943 EntityKind::ActionServer => "action_goal",
944 _ => continue,
945 };
946 if !callbacks
947 .iter()
948 .any(|callback: &SourceCallbackRef| callback.id == callback_id.as_str())
949 {
950 callbacks.push(SourceCallbackRef {
951 id: callback_id.as_str().into(),
952 slot: entity.callback_slot,
953 kind,
954 source: entity.callback_source.clone(),
955 group: entity
956 .callback_group
957 .as_ref()
958 .map(|group| group.as_str().into()),
959 });
960 }
961 if entity.kind == EntityKind::ActionServer {
962 if let Some(cancel_id) = entity.action_cancel_callback_id.as_ref()
963 && !callbacks
964 .iter()
965 .any(|callback: &SourceCallbackRef| callback.id == cancel_id.as_str())
966 {
967 callbacks.push(SourceCallbackRef {
968 id: cancel_id.as_str().into(),
969 slot: entity.action_cancel_callback_slot,
970 kind: "action_cancel",
971 source: entity.action_cancel_source.clone(),
972 group: entity
973 .callback_group
974 .as_ref()
975 .map(|group| group.as_str().into()),
976 });
977 }
978 if let Some(accepted_id) = entity.action_accepted_callback_id.as_ref()
979 && !callbacks
980 .iter()
981 .any(|callback: &SourceCallbackRef| callback.id == accepted_id.as_str())
982 {
983 callbacks.push(SourceCallbackRef {
984 id: accepted_id.as_str().into(),
985 slot: entity.action_accepted_callback_slot,
986 kind: "action_accepted",
987 source: entity.action_accepted_source.clone(),
988 group: entity
989 .callback_group
990 .as_ref()
991 .map(|group| group.as_str().into()),
992 });
993 }
994 }
995 }
996 callbacks
997 }
998}
999
1000#[cfg(feature = "alloc")]
1001struct SourceCallbackRef {
1002 id: StdString,
1003 slot: Option<CallbackSlot>,
1004 kind: &'static str,
1005 source: SourceLocationMetadata,
1006 group: Option<StdString>,
1007}
1008
1009pub(crate) fn entity_callback_ids(entity: &EntityMetadata) -> [Option<&MetadataString>; 3] {
1010 [
1011 entity.callback_id.as_ref(),
1012 entity.action_cancel_callback_id.as_ref(),
1013 entity.action_accepted_callback_id.as_ref(),
1014 ]
1015}
1016
1017pub struct EntityMetadataSpec<'a> {
1025 pub id: EntityId<'a>,
1026 pub node_id: NodeId<'a>,
1027 pub kind: EntityKind,
1028 pub source_name: &'a str,
1029 pub type_name: &'static str,
1030 pub type_hash: &'static str,
1031 pub qos: QoSProfile,
1032}
1033
1034pub fn entity_metadata(spec: EntityMetadataSpec<'_>) -> Result<EntityMetadata, NodeMetadataError> {
1037 let EntityMetadataSpec {
1038 id,
1039 node_id,
1040 kind,
1041 source_name,
1042 type_name,
1043 type_hash,
1044 qos,
1045 } = spec;
1046 Ok(EntityMetadata {
1047 slot: None,
1048 id: copy_str(id.as_str())?,
1049 node_slot: None,
1050 node_id: copy_str(node_id.as_str())?,
1051 kind,
1052 source_name: copy_str(source_name)?,
1053 source_name_kind: SourceNameKind::from_source_name(source_name),
1054 type_name,
1055 type_hash,
1056 qos,
1057 callback_slot: None,
1058 callback_id: None,
1059 callback_source: SourceLocationMetadata::empty(),
1060 callback_group: None,
1061 action_cancel_callback_slot: None,
1062 action_cancel_callback_id: None,
1063 action_cancel_source: SourceLocationMetadata::empty(),
1064 action_accepted_callback_slot: None,
1065 action_accepted_callback_id: None,
1066 action_accepted_source: SourceLocationMetadata::empty(),
1067 period_ms: None,
1068 period_us: None,
1069 parameter_type: None,
1070 parameter_default: None,
1071 parameter_read_only: false,
1072 safety: false,
1073 source: SourceLocationMetadata::empty(),
1074 })
1075}
1076
1077#[cfg(feature = "alloc")]
1078fn write_publisher_json(
1079 out: &mut impl core::fmt::Write,
1080 entity: &EntityMetadata,
1081) -> core::fmt::Result {
1082 write!(out, "{{")?;
1083 write_json_field(out, "id", entity.id.as_str())?;
1084 out.write_char(',')?;
1085 if let Some(slot) = entity.slot {
1086 write!(out, "\"declaration_slot\":{},", slot.index())?;
1087 }
1088 write!(out, "\"unresolved_topic\":")?;
1089 write_source_name(out, entity.source_name.as_str(), entity.source_name_kind)?;
1090 out.write_char(',')?;
1091 write_interface(out, entity.type_name, "message")?;
1092 out.write_char(',')?;
1093 write_qos(out, entity.qos)?;
1094 write!(out, "}}")
1095}
1096
1097#[cfg(feature = "alloc")]
1098fn write_subscriber_json(
1099 out: &mut impl core::fmt::Write,
1100 entity: &EntityMetadata,
1101) -> core::fmt::Result {
1102 write!(out, "{{")?;
1103 write_json_field(out, "id", entity.id.as_str())?;
1104 out.write_char(',')?;
1105 if let Some(slot) = entity.slot {
1106 write!(out, "\"declaration_slot\":{},", slot.index())?;
1107 }
1108 write!(out, "\"unresolved_topic\":")?;
1109 write_source_name(out, entity.source_name.as_str(), entity.source_name_kind)?;
1110 out.write_char(',')?;
1111 write_interface(out, entity.type_name, "message")?;
1112 out.write_char(',')?;
1113 write_qos(out, entity.qos)?;
1114 out.write_char(',')?;
1115 write_json_field(
1116 out,
1117 "callback",
1118 entity
1119 .callback_id
1120 .as_ref()
1121 .map(|id| id.as_str())
1122 .unwrap_or(""),
1123 )?;
1124 if let Some(callback_slot) = entity.callback_slot {
1125 write!(out, ",\"callback_slot\":{}", callback_slot.index())?;
1126 }
1127 write!(out, "}}")
1128}
1129
1130#[cfg(feature = "alloc")]
1131fn write_timer_json(out: &mut impl core::fmt::Write, entity: &EntityMetadata) -> core::fmt::Result {
1132 write!(out, "{{")?;
1133 write_json_field(out, "id", entity.id.as_str())?;
1134 out.write_char(',')?;
1135 if let Some(slot) = entity.slot {
1136 write!(out, "\"declaration_slot\":{},", slot.index())?;
1137 }
1138 write!(out, "\"period_ms\":{},", entity.period_ms.unwrap_or(0))?;
1139 write!(out, "\"period_us\":{},", entity.period_us.unwrap_or(0))?;
1140 write_json_field(
1141 out,
1142 "callback",
1143 entity
1144 .callback_id
1145 .as_ref()
1146 .map(|id| id.as_str())
1147 .unwrap_or(""),
1148 )?;
1149 if let Some(callback_slot) = entity.callback_slot {
1150 write!(out, ",\"callback_slot\":{}", callback_slot.index())?;
1151 }
1152 write!(out, "}}")
1153}
1154
1155#[cfg(feature = "alloc")]
1156fn write_service_json(
1157 out: &mut impl core::fmt::Write,
1158 entity: &EntityMetadata,
1159) -> core::fmt::Result {
1160 write!(out, "{{")?;
1161 write_json_field(out, "id", entity.id.as_str())?;
1162 out.write_char(',')?;
1163 if let Some(slot) = entity.slot {
1164 write!(out, "\"declaration_slot\":{},", slot.index())?;
1165 }
1166 write!(out, "\"unresolved_name\":")?;
1167 write_source_name(out, entity.source_name.as_str(), entity.source_name_kind)?;
1168 out.write_char(',')?;
1169 write_interface(out, entity.type_name, "service")?;
1170 out.write_char(',')?;
1171 write_json_field(
1172 out,
1173 "callback",
1174 entity
1175 .callback_id
1176 .as_ref()
1177 .map(|id| id.as_str())
1178 .unwrap_or(""),
1179 )?;
1180 if let Some(callback_slot) = entity.callback_slot {
1181 write!(out, ",\"callback_slot\":{}", callback_slot.index())?;
1182 }
1183 write!(out, "}}")
1184}
1185
1186#[cfg(feature = "alloc")]
1193fn write_client_json(
1194 out: &mut impl core::fmt::Write,
1195 entity: &EntityMetadata,
1196) -> core::fmt::Result {
1197 let interface_kind = if entity.kind == EntityKind::ActionClient {
1198 "action"
1199 } else {
1200 "service"
1201 };
1202 write!(out, "{{")?;
1203 write_json_field(out, "id", entity.id.as_str())?;
1204 out.write_char(',')?;
1205 if let Some(slot) = entity.slot {
1206 write!(out, "\"declaration_slot\":{},", slot.index())?;
1207 }
1208 write!(out, "\"unresolved_name\":")?;
1209 write_source_name(out, entity.source_name.as_str(), entity.source_name_kind)?;
1210 out.write_char(',')?;
1211 write_interface(out, entity.type_name, interface_kind)?;
1212 write!(out, "}}")
1213}
1214
1215#[cfg(feature = "alloc")]
1216fn write_action_json(
1217 out: &mut impl core::fmt::Write,
1218 entity: &EntityMetadata,
1219) -> core::fmt::Result {
1220 let goal_callback = entity
1221 .callback_id
1222 .as_ref()
1223 .map(|id| id.as_str())
1224 .unwrap_or("");
1225 let cancel_callback = entity
1226 .action_cancel_callback_id
1227 .as_ref()
1228 .map(|id| id.as_str())
1229 .unwrap_or(goal_callback);
1230 let accepted_callback = entity
1231 .action_accepted_callback_id
1232 .as_ref()
1233 .map(|id| id.as_str())
1234 .unwrap_or(goal_callback);
1235 write!(out, "{{")?;
1236 write_json_field(out, "id", entity.id.as_str())?;
1237 out.write_char(',')?;
1238 if let Some(slot) = entity.slot {
1239 write!(out, "\"declaration_slot\":{},", slot.index())?;
1240 }
1241 write!(out, "\"unresolved_name\":")?;
1242 write_source_name(out, entity.source_name.as_str(), entity.source_name_kind)?;
1243 out.write_char(',')?;
1244 write_interface(out, entity.type_name, "action")?;
1245 out.write_char(',')?;
1246 write_json_field(out, "goal_callback", goal_callback)?;
1247 if let Some(callback_slot) = entity.callback_slot {
1248 write!(out, ",\"goal_callback_slot\":{}", callback_slot.index())?;
1249 }
1250 out.write_char(',')?;
1251 write_json_field(out, "cancel_callback", cancel_callback)?;
1252 if let Some(callback_slot) = entity.action_cancel_callback_slot {
1253 write!(out, ",\"cancel_callback_slot\":{}", callback_slot.index())?;
1254 }
1255 out.write_char(',')?;
1256 write_json_field(out, "accepted_callback", accepted_callback)?;
1257 if let Some(callback_slot) = entity.action_accepted_callback_slot {
1258 write!(out, ",\"accepted_callback_slot\":{}", callback_slot.index())?;
1259 }
1260 write!(out, "}}")
1261}
1262
1263#[cfg(feature = "alloc")]
1264fn write_source_name(
1265 out: &mut impl core::fmt::Write,
1266 value: &str,
1267 kind: SourceNameKind,
1268) -> core::fmt::Result {
1269 write!(out, "{{")?;
1270 write_json_field(out, "value", value)?;
1271 out.write_char(',')?;
1272 write_json_field(out, "kind", source_name_kind_json(kind))?;
1273 write!(out, "}}")
1274}
1275
1276#[cfg(feature = "alloc")]
1277fn write_interface(
1278 out: &mut impl core::fmt::Write,
1279 type_name: &str,
1280 fallback_kind: &'static str,
1281) -> core::fmt::Result {
1282 let interface = parse_interface(type_name, fallback_kind);
1283 write!(out, "\"interface\":{{")?;
1284 write_json_field(out, "package", &interface.package)?;
1285 out.write_char(',')?;
1286 write_json_field(out, "name", &interface.name)?;
1287 out.write_char(',')?;
1288 write_json_field(out, "kind", interface.kind)?;
1289 write!(out, "}}")
1290}
1291
1292#[cfg(feature = "alloc")]
1293fn write_qos(out: &mut impl core::fmt::Write, qos: QoSProfile) -> core::fmt::Result {
1294 write!(out, "\"qos\":{{")?;
1295 write_json_field(out, "reliability", reliability_json(qos.reliability))?;
1296 out.write_char(',')?;
1297 write_json_field(out, "durability", durability_json(qos.durability))?;
1298 out.write_char(',')?;
1299 write_json_field(out, "history", history_json(qos.history))?;
1300 out.write_char(',')?;
1301 write!(out, "\"depth\":{},", qos.depth)?;
1302 write_optional_ms(out, "deadline_ms", qos.deadline_ms)?;
1303 out.write_char(',')?;
1304 write_optional_ms(out, "lifespan_ms", qos.lifespan_ms)?;
1305 out.write_char(',')?;
1306 write_json_field(out, "liveliness", liveliness_json(qos.liveliness_kind))?;
1307 out.write_char(',')?;
1308 write_optional_ms(out, "liveliness_lease_duration_ms", qos.liveliness_lease_ms)?;
1309 write!(out, ",\"extensions\":{{}}}}")
1310}
1311
1312#[cfg(feature = "alloc")]
1313fn write_source_location(
1314 out: &mut impl core::fmt::Write,
1315 source: &SourceLocationMetadata,
1316) -> core::fmt::Result {
1317 write!(out, "\"source\":{{")?;
1318 write_json_field(out, "artifact", source.artifact.as_str())?;
1319 out.write_char(',')?;
1320 write!(out, "\"line\":")?;
1321 write_optional_u32(out, source.line)?;
1322 out.write_char(',')?;
1323 write!(out, "\"column\":")?;
1324 write_optional_u32(out, source.column)?;
1325 write!(out, "}}")
1326}
1327
1328#[cfg(feature = "alloc")]
1329fn write_parameter_default(
1330 out: &mut impl core::fmt::Write,
1331 default: Option<&ParameterDefault>,
1332) -> core::fmt::Result {
1333 match default {
1334 Some(ParameterDefault::Bool(value)) => write!(out, "{}", value),
1335 Some(ParameterDefault::Integer(value)) => write!(out, "{}", value),
1336 Some(ParameterDefault::Double(value)) => write!(out, "{}", value.as_str()),
1337 Some(ParameterDefault::String(value)) => write_json_string(out, value.as_str()),
1338 Some(ParameterDefault::BoolArray)
1339 | Some(ParameterDefault::IntegerArray)
1340 | Some(ParameterDefault::DoubleArray)
1341 | Some(ParameterDefault::StringArray)
1342 | None => write!(out, "[]"),
1343 }
1344}
1345
1346#[cfg(feature = "alloc")]
1347fn write_json_field(out: &mut impl core::fmt::Write, name: &str, value: &str) -> core::fmt::Result {
1348 write_json_string(out, name)?;
1349 out.write_char(':')?;
1350 write_json_string(out, value)
1351}
1352
1353#[cfg(feature = "alloc")]
1354fn write_json_opt_field(
1355 out: &mut impl core::fmt::Write,
1356 name: &str,
1357 value: Option<&str>,
1358) -> core::fmt::Result {
1359 write_json_string(out, name)?;
1360 out.write_char(':')?;
1361 if let Some(value) = value {
1362 write_json_string(out, value)
1363 } else {
1364 write!(out, "null")
1365 }
1366}
1367
1368#[cfg(feature = "alloc")]
1369fn write_json_string(out: &mut impl core::fmt::Write, value: &str) -> core::fmt::Result {
1370 out.write_char('"')?;
1371 for ch in value.chars() {
1372 match ch {
1373 '"' => write!(out, "\\\"")?,
1374 '\\' => write!(out, "\\\\")?,
1375 '\n' => write!(out, "\\n")?,
1376 '\r' => write!(out, "\\r")?,
1377 '\t' => write!(out, "\\t")?,
1378 ch if ch.is_control() => write!(out, "\\u{:04x}", ch as u32)?,
1379 ch => out.write_char(ch)?,
1380 }
1381 }
1382 out.write_char('"')
1383}
1384
1385#[cfg(feature = "alloc")]
1386fn write_optional_ms(out: &mut impl core::fmt::Write, name: &str, value: u32) -> core::fmt::Result {
1387 write_json_string(out, name)?;
1388 out.write_char(':')?;
1389 if value == 0 {
1390 write!(out, "null")
1391 } else {
1392 write!(out, "{}", value)
1393 }
1394}
1395
1396#[cfg(feature = "alloc")]
1397fn write_optional_u32(out: &mut impl core::fmt::Write, value: Option<u32>) -> core::fmt::Result {
1398 if let Some(value) = value {
1399 write!(out, "{}", value)
1400 } else {
1401 write!(out, "null")
1402 }
1403}
1404
1405#[cfg(feature = "alloc")]
1406fn source_name_kind_json(kind: SourceNameKind) -> &'static str {
1407 match kind {
1408 SourceNameKind::Absolute => "absolute",
1409 SourceNameKind::Relative => "relative",
1410 SourceNameKind::Private => "private",
1411 }
1412}
1413
1414#[cfg(feature = "alloc")]
1415fn effect_json_kind(kind: CallbackEffectKind) -> &'static str {
1416 match kind {
1417 CallbackEffectKind::Publishes => "publishes",
1418 CallbackEffectKind::Reads => "reads_parameter",
1419 CallbackEffectKind::Writes => "writes_parameter",
1420 }
1421}
1422
1423#[cfg(feature = "alloc")]
1430fn reliability_json(value: QoSReliabilityPolicy) -> &'static str {
1431 match value {
1432 QoSReliabilityPolicy::SystemDefault => "system_default",
1433 QoSReliabilityPolicy::Reliable => "reliable",
1434 QoSReliabilityPolicy::BestEffort => "best_effort",
1435 }
1436}
1437
1438#[cfg(feature = "alloc")]
1439fn durability_json(value: QoSDurabilityPolicy) -> &'static str {
1440 match value {
1441 QoSDurabilityPolicy::SystemDefault => "system_default",
1442 QoSDurabilityPolicy::Volatile => "volatile",
1443 QoSDurabilityPolicy::TransientLocal => "transient_local",
1444 }
1445}
1446
1447#[cfg(feature = "alloc")]
1448fn history_json(value: QoSHistoryPolicy) -> &'static str {
1449 match value {
1450 QoSHistoryPolicy::SystemDefault => "system_default",
1451 QoSHistoryPolicy::KeepLast => "keep_last",
1452 QoSHistoryPolicy::KeepAll => "keep_all",
1453 }
1454}
1455
1456#[cfg(feature = "alloc")]
1457fn liveliness_json(value: QoSLivelinessPolicy) -> &'static str {
1458 match value {
1459 QoSLivelinessPolicy::None => "system_default",
1460 QoSLivelinessPolicy::Automatic => "automatic",
1461 QoSLivelinessPolicy::ManualByTopic => "manual_by_topic",
1462 QoSLivelinessPolicy::ManualByNode => "manual_by_node",
1463 }
1464}
1465
1466#[cfg(feature = "alloc")]
1467struct ParsedInterface {
1468 package: StdString,
1469 name: StdString,
1470 kind: &'static str,
1471}
1472
1473#[cfg(feature = "alloc")]
1474fn parse_interface(type_name: &str, fallback_kind: &'static str) -> ParsedInterface {
1475 let parts: StdVec<&str> = type_name.split("::").collect();
1476 if parts.len() >= 4 {
1477 let package = parts[0].into();
1478 let kind = match parts[1] {
1479 "msg" => "message",
1480 "srv" => "service",
1481 "action" => "action",
1482 _ => fallback_kind,
1483 };
1484 let mut type_leaf = parts[3].trim_end_matches('_');
1485 if type_leaf.is_empty() {
1486 type_leaf = parts.last().copied().unwrap_or("");
1487 }
1488 return ParsedInterface {
1489 package,
1490 name: format!("{}/{}", parts[1], type_leaf),
1491 kind,
1492 };
1493 }
1494
1495 ParsedInterface {
1496 package: StdString::new(),
1497 name: type_name.into(),
1498 kind: fallback_kind,
1499 }
1500}
1501
1502#[cfg(test)]
1503mod tests {
1504 #[test]
1511 fn a_path_longer_than_the_buffer_keeps_its_tail() {
1512 let deep = concat!(
1515 "/deep/nested/nested/nested/nested/nested/nested/nested/nested/",
1516 "nested/nested/nested/nested/nested/nested/nested/nested/nested/",
1517 "ws/build/nros-metadata/metadata-probe/listener/src/lib.rs"
1518 );
1519 assert!(deep.len() > super::METADATA_STRING_CAPACITY);
1520
1521 let got = super::copy_str_keep_tail(deep).expect("must not error on depth");
1522 assert!(got.len() <= super::METADATA_STRING_CAPACITY);
1523 assert!(
1524 got.ends_with("src/lib.rs"),
1525 "the informative tail must survive, got {got:?}"
1526 );
1527 assert!(got.starts_with('…'), "a cut must be visible, got {got:?}");
1528
1529 let short = "src/lib.rs";
1531 assert_eq!(super::copy_str_keep_tail(short).unwrap().as_str(), short);
1532 }
1533
1534 use super::*;
1535 use crate::qos;
1536
1537 #[test]
1538 fn source_name_kind_preserves_unresolved_names() {
1539 assert_eq!(
1540 SourceNameKind::from_source_name("/scan"),
1541 SourceNameKind::Absolute
1542 );
1543 assert_eq!(
1544 SourceNameKind::from_source_name("~/scan"),
1545 SourceNameKind::Private
1546 );
1547 assert_eq!(
1548 SourceNameKind::from_source_name("scan"),
1549 SourceNameKind::Relative
1550 );
1551 }
1552
1553 #[test]
1557 fn expand_name_covers_each_source_name_kind() {
1558 assert_eq!(
1560 expand_name("/scan", "lidar", "/sensing").unwrap().as_str(),
1561 "/scan"
1562 );
1563 assert_eq!(
1565 expand_name("~/scan", "lidar", "/sensing").unwrap().as_str(),
1566 "/sensing/lidar/scan"
1567 );
1568 assert_eq!(
1569 expand_name("~/scan", "lidar", "/").unwrap().as_str(),
1570 "/lidar/scan"
1571 );
1572 assert_eq!(
1574 expand_name("scan", "lidar", "/sensing").unwrap().as_str(),
1575 "/sensing/scan"
1576 );
1577 assert_eq!(expand_name("scan", "lidar", "/").unwrap().as_str(), "/scan");
1578 }
1579
1580 #[test]
1581 fn resolve_name_substitutes_first_matching_rule() {
1582 let remaps = [
1583 ("~/scan", "/points_raw"),
1584 ("/sensing/lidar/scan", "/ignored"),
1585 ];
1586 assert_eq!(
1587 resolve_name("~/scan", "lidar", "/sensing", remaps)
1588 .unwrap()
1589 .as_str(),
1590 "/points_raw"
1591 );
1592 assert_eq!(
1594 resolve_name("other", "lidar", "/sensing", [("/x", "/y")])
1595 .unwrap()
1596 .as_str(),
1597 "/sensing/other"
1598 );
1599 }
1600
1601 #[test]
1602 fn recorder_rejects_duplicate_stable_ids() {
1603 let mut recorder = MetadataRecorder::<1, 2, 1>::new();
1604 recorder
1605 .push_node(NodeId::new("node"), "talker", "/", 0)
1606 .unwrap();
1607
1608 let first = entity_metadata(EntityMetadataSpec {
1609 id: EntityId::new("pub"),
1610 node_id: NodeId::new("node"),
1611 kind: EntityKind::Publisher,
1612 source_name: "chatter",
1613 type_name: "std_msgs::msg::dds_::String_",
1614 type_hash: "hash",
1615 qos: qos::DEFAULT,
1616 })
1617 .unwrap();
1618 recorder.push_entity(first.clone()).unwrap();
1619
1620 assert_eq!(
1621 recorder.push_entity(first),
1622 Err(NodeMetadataError::DuplicateId)
1623 );
1624 }
1625
1626 #[test]
1627 fn recorder_rejects_duplicate_nodes_and_unknown_node_entities() {
1628 let mut recorder = MetadataRecorder::<1, 1, 1>::new();
1629 recorder
1630 .push_node(NodeId::new("node"), "talker", "/", 0)
1631 .unwrap();
1632
1633 assert_eq!(
1634 recorder.push_node(NodeId::new("node"), "other", "/", 0),
1635 Err(NodeMetadataError::DuplicateId)
1636 );
1637
1638 let entity = entity_metadata(EntityMetadataSpec {
1639 id: EntityId::new("pub"),
1640 node_id: NodeId::new("missing_node"),
1641 kind: EntityKind::Publisher,
1642 source_name: "chatter",
1643 type_name: "std_msgs::msg::dds_::String_",
1644 type_hash: "hash",
1645 qos: qos::DEFAULT,
1646 })
1647 .unwrap();
1648
1649 assert_eq!(
1650 recorder.push_entity(entity),
1651 Err(NodeMetadataError::UnknownNode)
1652 );
1653 }
1654
1655 #[test]
1656 fn recorder_assigns_slots_and_source_default_names_by_declaration_order() {
1657 let mut recorder = MetadataRecorder::<2, 4, 3>::new();
1658 recorder
1659 .push_node(NodeId::new("node_alpha"), "talker", "/", 0)
1660 .unwrap();
1661 recorder
1662 .push_node(NodeId::new("node_beta"), "listener", "/demo", 42)
1663 .unwrap();
1664
1665 assert_eq!(recorder.nodes()[0].slot, NodeSlot::new(0));
1666 assert_eq!(recorder.nodes()[0].source_default_name.as_str(), "talker");
1667 assert_eq!(recorder.nodes()[1].slot, NodeSlot::new(1));
1668 assert_eq!(recorder.nodes()[1].source_default_name.as_str(), "listener");
1669
1670 recorder
1671 .push_entity(
1672 entity_metadata(EntityMetadataSpec {
1673 id: EntityId::new("pub_chatter"),
1674 node_id: NodeId::new("node_alpha"),
1675 kind: EntityKind::Publisher,
1676 source_name: "/chatter",
1677 type_name: "std_msgs::msg::dds_::String_",
1678 type_hash: "hash",
1679 qos: qos::DEFAULT,
1680 })
1681 .unwrap(),
1682 )
1683 .unwrap();
1684 let mut subscription = entity_metadata(EntityMetadataSpec {
1685 id: EntityId::new("sub_chatter"),
1686 node_id: NodeId::new("node_beta"),
1687 kind: EntityKind::Subscription,
1688 source_name: "/chatter",
1689 type_name: "std_msgs::msg::dds_::String_",
1690 type_hash: "hash",
1691 qos: qos::DEFAULT,
1692 })
1693 .unwrap();
1694 subscription.callback_id = Some(copy_str("on_message").unwrap());
1695 recorder.push_entity(subscription).unwrap();
1696 let mut timer = entity_metadata(EntityMetadataSpec {
1697 id: EntityId::new("timer_tick"),
1698 node_id: NodeId::new("node_alpha"),
1699 kind: EntityKind::Timer,
1700 source_name: "",
1701 type_name: "",
1702 type_hash: "",
1703 qos: qos::DEFAULT,
1704 })
1705 .unwrap();
1706 timer.callback_id = Some(copy_str("on_tick").unwrap());
1707 recorder.push_entity(timer).unwrap();
1708
1709 assert_eq!(recorder.entities()[0].slot, Some(EntitySlot::new(0)));
1710 assert_eq!(recorder.entities()[0].node_slot, Some(NodeSlot::new(0)));
1711 assert_eq!(recorder.entities()[0].callback_slot, None);
1712 assert_eq!(recorder.entities()[1].slot, Some(EntitySlot::new(1)));
1713 assert_eq!(recorder.entities()[1].node_slot, Some(NodeSlot::new(1)));
1714 assert_eq!(
1715 recorder.entities()[1].callback_slot,
1716 Some(CallbackSlot::new(0))
1717 );
1718 assert_eq!(
1719 recorder.entities()[2].callback_slot,
1720 Some(CallbackSlot::new(1))
1721 );
1722
1723 recorder
1724 .push_callback_effect(
1725 CallbackId::new("on_tick"),
1726 CallbackEffectKind::Publishes,
1727 EntityId::new("pub_chatter"),
1728 )
1729 .unwrap();
1730 assert_eq!(
1731 recorder.callback_effects()[0].callback_slot,
1732 Some(CallbackSlot::new(1))
1733 );
1734 assert_eq!(
1735 recorder.callback_effects()[0].entity_slot,
1736 Some(EntitySlot::new(0))
1737 );
1738 }
1739
1740 #[test]
1741 fn recorder_assigns_distinct_callback_slots_within_one_action_entity() {
1742 let mut recorder = MetadataRecorder::<1, 1, 3>::new();
1743 recorder
1744 .push_node(NodeId::new("node"), "action_node", "/", 0)
1745 .unwrap();
1746 let mut action = entity_metadata(EntityMetadataSpec {
1747 id: EntityId::new("act_count"),
1748 node_id: NodeId::new("node"),
1749 kind: EntityKind::ActionServer,
1750 source_name: "/count",
1751 type_name: "example_interfaces::action::dds_::Fibonacci_",
1752 type_hash: "hash",
1753 qos: qos::DEFAULT,
1754 })
1755 .unwrap();
1756 action.callback_id = Some(copy_str("on_goal").unwrap());
1757 action.action_cancel_callback_id = Some(copy_str("on_cancel").unwrap());
1758 action.action_accepted_callback_id = Some(copy_str("on_accepted").unwrap());
1759
1760 recorder.push_entity(action).unwrap();
1761
1762 assert_eq!(
1763 recorder.entities()[0].callback_slot,
1764 Some(CallbackSlot::new(0))
1765 );
1766 assert_eq!(
1767 recorder.entities()[0].action_cancel_callback_slot,
1768 Some(CallbackSlot::new(1))
1769 );
1770 assert_eq!(
1771 recorder.entities()[0].action_accepted_callback_slot,
1772 Some(CallbackSlot::new(2))
1773 );
1774 }
1775
1776 #[cfg(feature = "alloc")]
1777 #[test]
1778 fn source_metadata_json_uses_agent_a_schema_shape() {
1779 let mut recorder = MetadataRecorder::<1, 7, 1>::new();
1783 recorder
1784 .push_node(NodeId::new("node_talker"), "talker", "/", 0)
1785 .unwrap();
1786 recorder
1787 .push_entity(
1788 entity_metadata(EntityMetadataSpec {
1789 id: EntityId::new("pub_chatter"),
1790 node_id: NodeId::new("node_talker"),
1791 kind: EntityKind::Publisher,
1792 source_name: "chatter",
1793 type_name: "std_msgs::msg::dds_::String_",
1794 type_hash: "hash",
1795 qos: crate::qos::DEFAULT,
1796 })
1797 .unwrap(),
1798 )
1799 .unwrap();
1800 let mut timer = entity_metadata(EntityMetadataSpec {
1801 id: EntityId::new("timer_publish"),
1802 node_id: NodeId::new("node_talker"),
1803 kind: EntityKind::Timer,
1804 source_name: "",
1805 type_name: "",
1806 type_hash: "",
1807 qos: crate::qos::DEFAULT,
1808 })
1809 .unwrap();
1810 timer.callback_id = Some(copy_str("cb_timer").unwrap());
1811 timer.callback_source = SourceLocationMetadata {
1812 artifact: copy_str("src/talker.rs").unwrap(),
1813 line: Some(42),
1814 column: Some(5),
1815 };
1816 timer.period_ms = Some(100);
1817 recorder.push_entity(timer).unwrap();
1818 let mut param = entity_metadata(EntityMetadataSpec {
1819 id: EntityId::new("param_rate"),
1820 node_id: NodeId::new("node_talker"),
1821 kind: EntityKind::Parameter,
1822 source_name: "rate_hz",
1823 type_name: "",
1824 type_hash: "",
1825 qos: crate::qos::DEFAULT,
1826 })
1827 .unwrap();
1828 param.parameter_type = Some(ParameterType::Integer);
1829 param.parameter_default = Some(ParameterDefault::Integer(10));
1830 param.source = SourceLocationMetadata {
1831 artifact: copy_str("src/talker.rs").unwrap(),
1832 line: Some(25),
1833 column: Some(9),
1834 };
1835 recorder.push_entity(param).unwrap();
1836 let mut action = entity_metadata(EntityMetadataSpec {
1837 id: EntityId::new("act_count"),
1838 node_id: NodeId::new("node_talker"),
1839 kind: EntityKind::ActionServer,
1840 source_name: "~/count",
1841 type_name: "example_interfaces::action::dds_::Fibonacci_",
1842 type_hash: "hash",
1843 qos: crate::qos::DEFAULT,
1844 })
1845 .unwrap();
1846 action.callback_id = Some(copy_str("cb_count_goal").unwrap());
1847 action.callback_source = SourceLocationMetadata {
1848 artifact: copy_str("src/talker.rs").unwrap(),
1849 line: Some(90),
1850 column: Some(5),
1851 };
1852 action.action_cancel_callback_id = Some(copy_str("cb_count_cancel").unwrap());
1853 action.action_cancel_source = SourceLocationMetadata {
1854 artifact: copy_str("src/talker.rs").unwrap(),
1855 line: Some(96),
1856 column: Some(5),
1857 };
1858 action.action_accepted_callback_id = Some(copy_str("cb_count_accepted").unwrap());
1859 action.action_accepted_source = SourceLocationMetadata {
1860 artifact: copy_str("src/talker.rs").unwrap(),
1861 line: Some(104),
1862 column: Some(5),
1863 };
1864 recorder.push_entity(action).unwrap();
1865 recorder
1866 .push_callback_effect(
1867 CallbackId::new("cb_timer"),
1868 CallbackEffectKind::Publishes,
1869 EntityId::new("pub_chatter"),
1870 )
1871 .unwrap();
1872
1873 recorder
1877 .push_entity(
1878 entity_metadata(EntityMetadataSpec {
1879 id: EntityId::new("client_fib"),
1880 node_id: NodeId::new("node_talker"),
1881 kind: EntityKind::ActionClient,
1882 source_name: "/fibonacci",
1883 type_name: "example_interfaces::action::dds_::Fibonacci_",
1884 type_hash: "hash",
1885 qos: crate::qos::DEFAULT,
1886 })
1887 .unwrap(),
1888 )
1889 .unwrap();
1890 recorder
1891 .push_entity(
1892 entity_metadata(EntityMetadataSpec {
1893 id: EntityId::new("client_add"),
1894 node_id: NodeId::new("node_talker"),
1895 kind: EntityKind::ServiceClient,
1896 source_name: "/add_two_ints",
1897 type_name: "example_interfaces::srv::dds_::AddTwoInts_",
1898 type_hash: "hash",
1899 qos: crate::qos::DEFAULT,
1900 })
1901 .unwrap(),
1902 )
1903 .unwrap();
1904
1905 let json = recorder
1906 .to_source_metadata_json(
1907 &SourceMetadataExport::new("demo_nodes_rs", "talker")
1908 .executable("talker")
1909 .exported_symbol("nros_node_talker")
1910 .source_artifacts(&["src/talker.rs"]),
1911 )
1912 .unwrap();
1913
1914 assert!(json.contains("\"version\":1"));
1915 assert!(json.contains("\"language\":\"rust\""));
1916 assert!(json.contains("\"unresolved_name\":{\"value\":\"talker\",\"kind\":\"relative\"}"));
1917 assert!(json.contains(
1918 "\"interface\":{\"package\":\"std_msgs\",\"name\":\"msg/String\",\"kind\":\"message\"}"
1919 ));
1920 assert!(json.contains("\"kind\":\"publishes\",\"entity\":\"pub_chatter\""));
1921 assert!(
1922 json.contains("\"source\":{\"artifact\":\"src/talker.rs\",\"line\":42,\"column\":5}")
1923 );
1924 assert!(json.contains("\"name\":\"rate_hz\",\"default\":10,\"read_only\":false"));
1925 assert!(json.contains("\"goal_callback\":\"cb_count_goal\""));
1926 assert!(json.contains("\"cancel_callback\":\"cb_count_cancel\""));
1927 assert!(json.contains("\"accepted_callback\":\"cb_count_accepted\""));
1928 assert!(json.contains("\"kind\":\"action_cancel\""));
1929 assert!(json.contains("\"kind\":\"action_accepted\""));
1930
1931 assert!(
1942 json.contains("\"action_clients\":[{"),
1943 "an action client must reach the sidecar, got {json}"
1944 );
1945 assert!(
1946 json.contains("\"service_clients\":[{"),
1947 "a service client must reach the sidecar, got {json}"
1948 );
1949 let action_clients = &json[json.find("\"action_clients\":").expect("array present")..];
1956 let action_clients = &action_clients[..action_clients.find(']').unwrap()];
1957 let service_clients = &json[json.find("\"service_clients\":").expect("array present")..];
1958 let service_clients = &service_clients[..service_clients.find(']').unwrap()];
1959
1960 assert!(
1966 action_clients.contains(
1967 "\"interface\":{\"package\":\"example_interfaces\",\"name\":\"action/Fibonacci\",\"kind\":\"action\"}"
1968 ),
1969 "action client interface must be parsed, got {action_clients}"
1970 );
1971 assert!(
1972 service_clients.contains(
1973 "\"interface\":{\"package\":\"example_interfaces\",\"name\":\"srv/AddTwoInts\",\"kind\":\"service\"}"
1974 ),
1975 "service client interface must be parsed, got {service_clients}"
1976 );
1977 assert!(
1980 !action_clients.contains("\"kind\":\"service\"")
1981 && !service_clients.contains("\"kind\":\"action\""),
1982 "the client kind word is what distinguishes the arrays"
1983 );
1984 assert!(
1987 !action_clients.contains("callback") && !service_clients.contains("callback"),
1988 "a client registers no callback; emitting one would misdescribe it"
1989 );
1990 assert!(json.contains("\"generator\":\"nros-metadata-rust\""));
1991 }
1992}