1use crate::{
4 ParameterType, QosSettings,
5 heapless::{String, Vec},
6};
7
8#[cfg(feature = "std")]
9use crate::{QosDurabilityPolicy, QosHistoryPolicy, QosLivelinessPolicy, QosReliabilityPolicy};
10#[cfg(feature = "std")]
11use std::{format, string::String as StdString, vec::Vec as StdVec};
12
13pub const DEFAULT_MAX_METADATA_NODES: usize = 8;
15pub const DEFAULT_MAX_METADATA_ENTITIES: usize = 32;
17pub const DEFAULT_MAX_METADATA_CALLBACKS: usize = 32;
19pub const METADATA_STRING_CAPACITY: usize = 128;
21
22pub type MetadataString = String<METADATA_STRING_CAPACITY>;
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
27pub struct NodeSlot(pub usize);
28
29impl NodeSlot {
30 pub const fn new(index: usize) -> Self {
32 Self(index)
33 }
34
35 pub const fn index(self) -> usize {
37 self.0
38 }
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
43pub struct EntitySlot(pub usize);
44
45impl EntitySlot {
46 pub const fn new(index: usize) -> Self {
48 Self(index)
49 }
50
51 pub const fn index(self) -> usize {
53 self.0
54 }
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
59pub struct CallbackSlot(pub usize);
60
61impl CallbackSlot {
62 pub const fn new(index: usize) -> Self {
64 Self(index)
65 }
66
67 pub const fn index(self) -> usize {
69 self.0
70 }
71}
72
73#[derive(Debug, Clone, PartialEq, Eq)]
75pub struct SourceLocationMetadata {
76 pub artifact: MetadataString,
77 pub line: Option<u32>,
78 pub column: Option<u32>,
79}
80
81impl SourceLocationMetadata {
82 pub const fn empty() -> Self {
84 Self {
85 artifact: MetadataString::new(),
86 line: None,
87 column: None,
88 }
89 }
90
91 #[track_caller]
93 pub fn caller() -> Result<Self, NodeMetadataError> {
94 let location = core::panic::Location::caller();
95 Ok(Self {
96 artifact: copy_str(location.file())?,
97 line: Some(location.line()),
98 column: Some(location.column()),
99 })
100 }
101}
102
103#[derive(Debug, Clone, PartialEq, Eq)]
105pub enum ParameterDefault {
106 Bool(bool),
107 Integer(i64),
108 Double(MetadataString),
109 String(MetadataString),
110 BoolArray,
111 IntegerArray,
112 DoubleArray,
113 StringArray,
114}
115
116impl ParameterDefault {
117 pub const fn parameter_type(&self) -> ParameterType {
119 match self {
120 Self::Bool(_) => ParameterType::Bool,
121 Self::Integer(_) => ParameterType::Integer,
122 Self::Double(_) => ParameterType::Double,
123 Self::String(_) => ParameterType::String,
124 Self::BoolArray => ParameterType::BoolArray,
125 Self::IntegerArray => ParameterType::IntegerArray,
126 Self::DoubleArray => ParameterType::DoubleArray,
127 Self::StringArray => ParameterType::StringArray,
128 }
129 }
130
131 pub fn for_type(param_type: ParameterType) -> Result<Self, NodeMetadataError> {
133 Ok(match param_type {
134 ParameterType::Bool => Self::Bool(false),
135 ParameterType::Integer => Self::Integer(0),
136 ParameterType::Double => Self::Double(copy_str("0.0")?),
137 ParameterType::String => Self::String(copy_str("")?),
138 ParameterType::BoolArray => Self::BoolArray,
139 ParameterType::IntegerArray => Self::IntegerArray,
140 ParameterType::DoubleArray => Self::DoubleArray,
141 ParameterType::StringArray => Self::StringArray,
142 ParameterType::ByteArray | ParameterType::NotSet => Self::Integer(0),
143 })
144 }
145}
146
147#[derive(Debug, Clone, Copy, PartialEq, Eq)]
149pub enum SourceNameKind {
150 Absolute,
152 Private,
154 Relative,
156}
157
158impl SourceNameKind {
159 pub const fn from_source_name(name: &str) -> Self {
161 let bytes = name.as_bytes();
162 if bytes.is_empty() {
163 Self::Relative
164 } else if bytes[0] == b'/' {
165 Self::Absolute
166 } else if bytes[0] == b'~' {
167 Self::Private
168 } else {
169 Self::Relative
170 }
171 }
172}
173
174#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
176pub struct EntityId<'a>(pub &'a str);
177
178impl<'a> EntityId<'a> {
179 pub const fn new(id: &'a str) -> Self {
181 Self(id)
182 }
183
184 pub const fn as_str(self) -> &'a str {
186 self.0
187 }
188}
189
190#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
192pub struct NodeId<'a>(pub &'a str);
193
194impl<'a> NodeId<'a> {
195 pub const fn new(id: &'a str) -> Self {
197 Self(id)
198 }
199
200 pub const fn as_str(self) -> &'a str {
202 self.0
203 }
204}
205
206#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
208pub struct CallbackId<'a>(pub &'a str);
209
210impl<'a> CallbackId<'a> {
211 pub const fn new(id: &'a str) -> Self {
213 Self(id)
214 }
215
216 pub const fn as_str(self) -> &'a str {
218 self.0
219 }
220}
221
222#[derive(Debug, Clone, Copy, PartialEq, Eq)]
224pub enum EntityKind {
225 Publisher,
226 Subscription,
227 Timer,
228 ServiceServer,
229 ServiceClient,
230 ActionServer,
231 ActionClient,
232 Parameter,
233}
234
235#[derive(Debug, Clone, Copy, PartialEq, Eq)]
237pub enum CallbackEffectKind {
238 Reads,
239 Publishes,
240 Writes,
241}
242
243#[derive(Debug, Clone, Copy, PartialEq, Eq)]
245pub enum NodeMetadataError {
246 Capacity,
248 NameTooLong,
250 UnknownNode,
252 UnknownEntity,
254 DuplicateId,
256}
257
258#[derive(Debug, Clone, PartialEq, Eq)]
260pub struct NodeMetadata {
261 pub slot: NodeSlot,
262 pub id: MetadataString,
263 pub source_default_name: MetadataString,
264 pub name: MetadataString,
265 pub namespace: MetadataString,
266 pub domain_id: u32,
267}
268
269#[derive(Debug, Clone, PartialEq, Eq)]
271pub struct EntityMetadata {
272 pub slot: Option<EntitySlot>,
273 pub id: MetadataString,
274 pub node_slot: Option<NodeSlot>,
275 pub node_id: MetadataString,
276 pub kind: EntityKind,
277 pub source_name: MetadataString,
278 pub source_name_kind: SourceNameKind,
279 pub type_name: &'static str,
280 pub type_hash: &'static str,
281 pub qos: QosSettings,
282 pub callback_slot: Option<CallbackSlot>,
283 pub callback_id: Option<MetadataString>,
284 pub callback_source: SourceLocationMetadata,
285 pub callback_group: Option<MetadataString>,
286 pub action_cancel_callback_slot: Option<CallbackSlot>,
287 pub action_cancel_callback_id: Option<MetadataString>,
288 pub action_cancel_source: SourceLocationMetadata,
289 pub action_accepted_callback_slot: Option<CallbackSlot>,
290 pub action_accepted_callback_id: Option<MetadataString>,
291 pub action_accepted_source: SourceLocationMetadata,
292 pub period_ms: Option<u64>,
293 pub parameter_type: Option<ParameterType>,
294 pub parameter_default: Option<ParameterDefault>,
295 pub parameter_read_only: bool,
296 pub safety: bool,
304 pub source: SourceLocationMetadata,
305}
306
307#[derive(Debug, Clone, PartialEq, Eq)]
309pub struct CallbackEffectMetadata {
310 pub callback_id: MetadataString,
311 pub callback_slot: Option<CallbackSlot>,
312 pub kind: CallbackEffectKind,
313 pub entity_id: MetadataString,
314 pub entity_slot: Option<EntitySlot>,
315}
316
317#[cfg(feature = "std")]
319#[derive(Debug, Clone)]
320pub struct SourceMetadataExport<'a> {
321 pub package: &'a str,
322 pub component: &'a str,
323 pub executable: Option<&'a str>,
324 pub exported_symbol: Option<&'a str>,
325 pub package_manifest: &'a str,
326 pub source_artifacts: &'a [&'a str],
327}
328
329#[cfg(feature = "std")]
330impl<'a> SourceMetadataExport<'a> {
331 pub const fn new(package: &'a str, component: &'a str) -> Self {
333 Self {
334 package,
335 component,
336 executable: None,
337 exported_symbol: None,
338 package_manifest: "package.xml",
339 source_artifacts: &[],
340 }
341 }
342
343 pub const fn executable(mut self, executable: &'a str) -> Self {
345 self.executable = Some(executable);
346 self
347 }
348
349 pub const fn exported_symbol(mut self, exported_symbol: &'a str) -> Self {
351 self.exported_symbol = Some(exported_symbol);
352 self
353 }
354
355 pub const fn package_manifest(mut self, package_manifest: &'a str) -> Self {
357 self.package_manifest = package_manifest;
358 self
359 }
360
361 pub const fn source_artifacts(mut self, source_artifacts: &'a [&'a str]) -> Self {
363 self.source_artifacts = source_artifacts;
364 self
365 }
366}
367
368pub(crate) fn copy_str(value: &str) -> Result<MetadataString, NodeMetadataError> {
369 let mut out = MetadataString::new();
370 out.push_str(value)
371 .map_err(|_| NodeMetadataError::NameTooLong)?;
372 Ok(out)
373}
374
375#[derive(Debug)]
377pub struct MetadataRecorder<
378 const MAX_NODES: usize = DEFAULT_MAX_METADATA_NODES,
379 const MAX_ENTITIES: usize = DEFAULT_MAX_METADATA_ENTITIES,
380 const MAX_CALLBACKS: usize = DEFAULT_MAX_METADATA_CALLBACKS,
381> {
382 nodes: Vec<NodeMetadata, MAX_NODES>,
383 entities: Vec<EntityMetadata, MAX_ENTITIES>,
384 callback_effects: Vec<CallbackEffectMetadata, MAX_CALLBACKS>,
385}
386
387impl<const MAX_NODES: usize, const MAX_ENTITIES: usize, const MAX_CALLBACKS: usize> Default
388 for MetadataRecorder<MAX_NODES, MAX_ENTITIES, MAX_CALLBACKS>
389{
390 fn default() -> Self {
391 Self::new()
392 }
393}
394
395impl<const MAX_NODES: usize, const MAX_ENTITIES: usize, const MAX_CALLBACKS: usize>
396 MetadataRecorder<MAX_NODES, MAX_ENTITIES, MAX_CALLBACKS>
397{
398 pub const fn new() -> Self {
400 Self {
401 nodes: Vec::new(),
402 entities: Vec::new(),
403 callback_effects: Vec::new(),
404 }
405 }
406
407 pub fn nodes(&self) -> &[NodeMetadata] {
409 &self.nodes
410 }
411
412 pub fn entities(&self) -> &[EntityMetadata] {
414 &self.entities
415 }
416
417 pub fn callback_effects(&self) -> &[CallbackEffectMetadata] {
419 &self.callback_effects
420 }
421
422 #[cfg(feature = "std")]
424 pub fn to_source_metadata_json(
425 &self,
426 export: &SourceMetadataExport<'_>,
427 ) -> Result<StdString, core::fmt::Error> {
428 let mut out = StdString::new();
429 self.write_source_metadata_json(export, &mut out)?;
430 Ok(out)
431 }
432
433 #[cfg(feature = "std")]
435 pub fn write_source_metadata_json(
436 &self,
437 export: &SourceMetadataExport<'_>,
438 out: &mut impl core::fmt::Write,
439 ) -> core::fmt::Result {
440 write!(out, "{{")?;
441 write!(out, "\"version\":1,")?;
442 write_json_field(out, "package", export.package)?;
443 out.write_char(',')?;
444 write_json_field(out, "component", export.component)?;
445 out.write_char(',')?;
446 write!(out, "\"language\":\"rust\",")?;
447 write_json_opt_field(out, "executable", export.executable)?;
448 out.write_char(',')?;
449 write_json_opt_field(out, "exported_symbol", export.exported_symbol)?;
450 out.write_char(',')?;
451 self.write_nodes_json(out)?;
452 out.write_char(',')?;
453 self.write_callbacks_json(out)?;
454 out.write_char(',')?;
455 self.write_parameters_json(out)?;
456 out.write_char(',')?;
457 self.write_trace_json(export, out)?;
458 write!(out, "}}")
459 }
460
461 pub(crate) fn push_node(
462 &mut self,
463 id: NodeId<'_>,
464 name: &str,
465 namespace: &str,
466 domain_id: u32,
467 ) -> Result<(), NodeMetadataError> {
468 if self.has_node(id.as_str()) {
469 return Err(NodeMetadataError::DuplicateId);
470 }
471
472 self.nodes
473 .push(NodeMetadata {
474 slot: NodeSlot::new(self.nodes.len()),
475 id: copy_str(id.as_str())?,
476 source_default_name: copy_str(name)?,
477 name: copy_str(name)?,
478 namespace: copy_str(namespace)?,
479 domain_id,
480 })
481 .map_err(|_| NodeMetadataError::Capacity)
482 }
483
484 pub(crate) fn push_entity(
485 &mut self,
486 mut entity: EntityMetadata,
487 ) -> Result<(), NodeMetadataError> {
488 if !self.has_node(&entity.node_id) {
489 return Err(NodeMetadataError::UnknownNode);
490 }
491 if self.has_entity(&entity.id) {
492 return Err(NodeMetadataError::DuplicateId);
493 }
494
495 entity.slot = Some(EntitySlot::new(self.entities.len()));
496 entity.node_slot = self.node_slot_for_id(&entity.node_id);
497 let mut current_callbacks = Vec::<MetadataString, 3>::new();
498 let mut next_callback_slot = self.callback_slot_count();
499 entity.callback_slot = entity.callback_id.as_ref().map(|callback_id| {
500 self.callback_slot_for_current_entity(
501 callback_id.as_str(),
502 &mut current_callbacks,
503 &mut next_callback_slot,
504 )
505 });
506 entity.action_cancel_callback_slot =
507 entity
508 .action_cancel_callback_id
509 .as_ref()
510 .map(|callback_id| {
511 self.callback_slot_for_current_entity(
512 callback_id.as_str(),
513 &mut current_callbacks,
514 &mut next_callback_slot,
515 )
516 });
517 entity.action_accepted_callback_slot =
518 entity
519 .action_accepted_callback_id
520 .as_ref()
521 .map(|callback_id| {
522 self.callback_slot_for_current_entity(
523 callback_id.as_str(),
524 &mut current_callbacks,
525 &mut next_callback_slot,
526 )
527 });
528
529 self.entities
530 .push(entity)
531 .map_err(|_| NodeMetadataError::Capacity)
532 }
533
534 pub(crate) fn push_callback_effect(
535 &mut self,
536 callback_id: CallbackId<'_>,
537 kind: CallbackEffectKind,
538 entity_id: EntityId<'_>,
539 ) -> Result<(), NodeMetadataError> {
540 if !self.has_entity(entity_id.as_str()) {
541 return Err(NodeMetadataError::UnknownEntity);
542 }
543
544 self.callback_effects
545 .push(CallbackEffectMetadata {
546 callback_id: copy_str(callback_id.as_str())?,
547 callback_slot: self.callback_slot_for_id(callback_id.as_str()),
548 kind,
549 entity_id: copy_str(entity_id.as_str())?,
550 entity_slot: self.entity_slot_for_id(entity_id.as_str()),
551 })
552 .map_err(|_| NodeMetadataError::Capacity)
553 }
554
555 pub(crate) fn has_node(&self, id: &str) -> bool {
556 self.nodes.iter().any(|node| node.id.as_str() == id)
557 }
558
559 pub(crate) fn has_entity(&self, id: &str) -> bool {
560 self.entities.iter().any(|entity| entity.id.as_str() == id)
561 }
562
563 fn node_slot_for_id(&self, id: &str) -> Option<NodeSlot> {
564 self.nodes
565 .iter()
566 .find(|node| node.id.as_str() == id)
567 .map(|node| node.slot)
568 }
569
570 fn entity_slot_for_id(&self, id: &str) -> Option<EntitySlot> {
571 self.entities
572 .iter()
573 .find(|entity| entity.id.as_str() == id)
574 .and_then(|entity| entity.slot)
575 }
576
577 fn callback_slot_for_current_entity(
578 &self,
579 id: &str,
580 current_callbacks: &mut Vec<MetadataString, 3>,
581 next_callback_slot: &mut usize,
582 ) -> CallbackSlot {
583 if let Some(slot) = self.callback_slot_for_id(id) {
584 return slot;
585 }
586 if let Some((index, _)) = current_callbacks
587 .iter()
588 .enumerate()
589 .find(|(_, callback_id)| callback_id.as_str() == id)
590 {
591 return CallbackSlot::new(self.callback_slot_count() + index);
592 }
593 let slot = CallbackSlot::new(*next_callback_slot);
594 let _ = current_callbacks
595 .push(copy_str(id).expect("callback ID already fits metadata string capacity"));
596 *next_callback_slot += 1;
597 slot
598 }
599
600 fn callback_slot_for_id(&self, id: &str) -> Option<CallbackSlot> {
601 let mut seen = Vec::<&str, MAX_CALLBACKS>::new();
602 for entity in &self.entities {
603 for callback_id in entity_callback_ids(entity) {
604 let Some(callback_id) = callback_id else {
605 continue;
606 };
607 let callback_id = callback_id.as_str();
608 if seen.contains(&callback_id) {
609 continue;
610 }
611 if callback_id == id {
612 return Some(CallbackSlot::new(seen.len()));
613 }
614 let _ = seen.push(callback_id);
615 }
616 }
617 None
618 }
619
620 fn callback_slot_count(&self) -> usize {
621 let mut seen = Vec::<&str, MAX_CALLBACKS>::new();
622 for entity in &self.entities {
623 for callback_id in entity_callback_ids(entity) {
624 let Some(callback_id) = callback_id else {
625 continue;
626 };
627 let callback_id = callback_id.as_str();
628 if !seen.contains(&callback_id) {
629 let _ = seen.push(callback_id);
630 }
631 }
632 }
633 seen.len()
634 }
635
636 #[cfg(feature = "std")]
637 fn write_nodes_json(&self, out: &mut impl core::fmt::Write) -> core::fmt::Result {
638 write!(out, "\"nodes\":[")?;
639 for (index, node) in self.nodes.iter().enumerate() {
640 if index > 0 {
641 out.write_char(',')?;
642 }
643 write!(out, "{{")?;
644 write_json_field(out, "id", node.id.as_str())?;
645 out.write_char(',')?;
646 write!(out, "\"declaration_slot\":{},", node.slot.index())?;
647 write_json_field(
648 out,
649 "source_default_name",
650 node.source_default_name.as_str(),
651 )?;
652 out.write_char(',')?;
653 write!(out, "\"unresolved_name\":")?;
654 write_source_name(
655 out,
656 node.name.as_str(),
657 SourceNameKind::from_source_name(&node.name),
658 )?;
659 out.write_char(',')?;
660 if node.namespace.as_str() == "/" {
661 write!(out, "\"namespace\":null,")?;
662 } else {
663 write_json_field(out, "namespace", node.namespace.as_str())?;
664 out.write_char(',')?;
665 }
666 self.write_node_entities(out, node.id.as_str())?;
667 write!(out, "}}")?;
668 }
669 write!(out, "]")
670 }
671
672 #[cfg(feature = "std")]
673 fn write_node_entities(
674 &self,
675 out: &mut impl core::fmt::Write,
676 node_id: &str,
677 ) -> core::fmt::Result {
678 self.write_entity_array(out, "publishers", node_id, EntityKind::Publisher)?;
679 out.write_char(',')?;
680 self.write_entity_array(out, "subscribers", node_id, EntityKind::Subscription)?;
681 out.write_char(',')?;
682 self.write_entity_array(out, "timers", node_id, EntityKind::Timer)?;
683 out.write_char(',')?;
684 self.write_entity_array(out, "services", node_id, EntityKind::ServiceServer)?;
685 out.write_char(',')?;
686 self.write_entity_array(out, "actions", node_id, EntityKind::ActionServer)
687 }
688
689 #[cfg(feature = "std")]
690 fn write_entity_array(
691 &self,
692 out: &mut impl core::fmt::Write,
693 field: &str,
694 node_id: &str,
695 kind: EntityKind,
696 ) -> core::fmt::Result {
697 write!(out, "\"{}\":[", field)?;
698 for (index, entity) in self
699 .entities
700 .iter()
701 .filter(|entity| entity.node_id.as_str() == node_id && entity.kind == kind)
702 .enumerate()
703 {
704 if index > 0 {
705 out.write_char(',')?;
706 }
707 match kind {
708 EntityKind::Publisher => write_publisher_json(out, entity)?,
709 EntityKind::Subscription => write_subscriber_json(out, entity)?,
710 EntityKind::Timer => write_timer_json(out, entity)?,
711 EntityKind::ServiceServer => write_service_json(out, entity)?,
712 EntityKind::ActionServer => write_action_json(out, entity)?,
713 _ => {}
714 }
715 }
716 write!(out, "]")
717 }
718
719 #[cfg(feature = "std")]
720 fn write_callbacks_json(&self, out: &mut impl core::fmt::Write) -> core::fmt::Result {
721 let callbacks = self.source_callbacks();
722 write!(out, "\"callbacks\":[")?;
723 for (index, callback) in callbacks.iter().enumerate() {
724 if index > 0 {
725 out.write_char(',')?;
726 }
727 write!(out, "{{")?;
728 write_json_field(out, "id", callback.id.as_str())?;
729 out.write_char(',')?;
730 if let Some(slot) = callback.slot {
731 write!(out, "\"declaration_slot\":{},", slot.index())?;
732 }
733 write_json_field(out, "kind", callback.kind)?;
734 out.write_char(',')?;
735 if let Some(group) = callback.group.as_ref() {
736 write_json_field(out, "group", group)?;
737 out.write_char(',')?;
738 } else {
739 write!(out, "\"group\":null,")?;
740 }
741 write!(out, "\"effects\":[")?;
742 for (effect_index, effect) in self
743 .callback_effects
744 .iter()
745 .filter(|effect| effect.callback_id.as_str() == callback.id)
746 .enumerate()
747 {
748 if effect_index > 0 {
749 out.write_char(',')?;
750 }
751 write!(out, "{{")?;
752 write_json_field(out, "kind", effect_json_kind(effect.kind))?;
753 out.write_char(',')?;
754 write_json_field(out, "entity", effect.entity_id.as_str())?;
755 if let Some(entity_slot) = effect.entity_slot {
756 write!(out, ",\"entity_slot\":{}", entity_slot.index())?;
757 }
758 write!(out, "}}")?;
759 }
760 write!(out, "],")?;
761 write_source_location(out, &callback.source)?;
762 write!(out, "}}")?;
763 }
764 write!(out, "]")
765 }
766
767 #[cfg(feature = "std")]
768 fn write_parameters_json(&self, out: &mut impl core::fmt::Write) -> core::fmt::Result {
769 write!(out, "\"parameters\":[")?;
770 for (index, entity) in self
771 .entities
772 .iter()
773 .filter(|entity| entity.kind == EntityKind::Parameter)
774 .enumerate()
775 {
776 if index > 0 {
777 out.write_char(',')?;
778 }
779 write!(out, "{{")?;
780 write_json_field(out, "node", entity.node_id.as_str())?;
781 out.write_char(',')?;
782 if let Some(slot) = entity.slot {
783 write!(out, "\"declaration_slot\":{},", slot.index())?;
784 }
785 write_json_field(out, "name", entity.source_name.as_str())?;
786 out.write_char(',')?;
787 write!(out, "\"default\":")?;
788 write_parameter_default(out, entity.parameter_default.as_ref())?;
789 out.write_char(',')?;
790 write!(out, "\"read_only\":{},", entity.parameter_read_only)?;
791 write_source_location(out, &entity.source)?;
792 write!(out, "}}")?;
793 }
794 write!(out, "]")
795 }
796
797 #[cfg(feature = "std")]
798 fn write_trace_json(
799 &self,
800 export: &SourceMetadataExport<'_>,
801 out: &mut impl core::fmt::Write,
802 ) -> core::fmt::Result {
803 write!(out, "\"trace\":{{")?;
804 write_json_field(out, "generator", "nros-metadata-rust")?;
805 out.write_char(',')?;
806 write_json_field(out, "package_manifest", export.package_manifest)?;
807 out.write_char(',')?;
808 write!(out, "\"source_artifacts\":[")?;
809 for (index, artifact) in export.source_artifacts.iter().enumerate() {
810 if index > 0 {
811 out.write_char(',')?;
812 }
813 write_json_string(out, artifact)?;
814 }
815 write!(out, "]}}")
816 }
817
818 #[cfg(feature = "std")]
819 fn source_callbacks(&self) -> StdVec<SourceCallbackRef> {
820 let mut callbacks = StdVec::new();
821 for entity in &self.entities {
822 let Some(callback_id) = entity.callback_id.as_ref() else {
823 continue;
824 };
825 let kind = match entity.kind {
826 EntityKind::Subscription => "subscription",
827 EntityKind::Timer => "timer",
828 EntityKind::ServiceServer => "service",
829 EntityKind::ActionServer => "action_goal",
830 _ => continue,
831 };
832 if !callbacks
833 .iter()
834 .any(|callback: &SourceCallbackRef| callback.id == callback_id.as_str())
835 {
836 callbacks.push(SourceCallbackRef {
837 id: callback_id.as_str().into(),
838 slot: entity.callback_slot,
839 kind,
840 source: entity.callback_source.clone(),
841 group: entity
842 .callback_group
843 .as_ref()
844 .map(|group| group.as_str().into()),
845 });
846 }
847 if entity.kind == EntityKind::ActionServer {
848 if let Some(cancel_id) = entity.action_cancel_callback_id.as_ref()
849 && !callbacks
850 .iter()
851 .any(|callback: &SourceCallbackRef| callback.id == cancel_id.as_str())
852 {
853 callbacks.push(SourceCallbackRef {
854 id: cancel_id.as_str().into(),
855 slot: entity.action_cancel_callback_slot,
856 kind: "action_cancel",
857 source: entity.action_cancel_source.clone(),
858 group: entity
859 .callback_group
860 .as_ref()
861 .map(|group| group.as_str().into()),
862 });
863 }
864 if let Some(accepted_id) = entity.action_accepted_callback_id.as_ref()
865 && !callbacks
866 .iter()
867 .any(|callback: &SourceCallbackRef| callback.id == accepted_id.as_str())
868 {
869 callbacks.push(SourceCallbackRef {
870 id: accepted_id.as_str().into(),
871 slot: entity.action_accepted_callback_slot,
872 kind: "action_accepted",
873 source: entity.action_accepted_source.clone(),
874 group: entity
875 .callback_group
876 .as_ref()
877 .map(|group| group.as_str().into()),
878 });
879 }
880 }
881 }
882 callbacks
883 }
884}
885
886#[cfg(feature = "std")]
887struct SourceCallbackRef {
888 id: StdString,
889 slot: Option<CallbackSlot>,
890 kind: &'static str,
891 source: SourceLocationMetadata,
892 group: Option<StdString>,
893}
894
895pub(crate) fn entity_callback_ids(entity: &EntityMetadata) -> [Option<&MetadataString>; 3] {
896 [
897 entity.callback_id.as_ref(),
898 entity.action_cancel_callback_id.as_ref(),
899 entity.action_accepted_callback_id.as_ref(),
900 ]
901}
902
903pub(crate) struct EntityMetadataSpec<'a> {
908 pub id: EntityId<'a>,
909 pub node_id: NodeId<'a>,
910 pub kind: EntityKind,
911 pub source_name: &'a str,
912 pub type_name: &'static str,
913 pub type_hash: &'static str,
914 pub qos: QosSettings,
915}
916
917pub(crate) fn entity_metadata(
918 spec: EntityMetadataSpec<'_>,
919) -> Result<EntityMetadata, NodeMetadataError> {
920 let EntityMetadataSpec {
921 id,
922 node_id,
923 kind,
924 source_name,
925 type_name,
926 type_hash,
927 qos,
928 } = spec;
929 Ok(EntityMetadata {
930 slot: None,
931 id: copy_str(id.as_str())?,
932 node_slot: None,
933 node_id: copy_str(node_id.as_str())?,
934 kind,
935 source_name: copy_str(source_name)?,
936 source_name_kind: SourceNameKind::from_source_name(source_name),
937 type_name,
938 type_hash,
939 qos,
940 callback_slot: None,
941 callback_id: None,
942 callback_source: SourceLocationMetadata::empty(),
943 callback_group: None,
944 action_cancel_callback_slot: None,
945 action_cancel_callback_id: None,
946 action_cancel_source: SourceLocationMetadata::empty(),
947 action_accepted_callback_slot: None,
948 action_accepted_callback_id: None,
949 action_accepted_source: SourceLocationMetadata::empty(),
950 period_ms: None,
951 parameter_type: None,
952 parameter_default: None,
953 parameter_read_only: false,
954 safety: false,
955 source: SourceLocationMetadata::empty(),
956 })
957}
958
959#[cfg(feature = "std")]
960fn write_publisher_json(
961 out: &mut impl core::fmt::Write,
962 entity: &EntityMetadata,
963) -> core::fmt::Result {
964 write!(out, "{{")?;
965 write_json_field(out, "id", entity.id.as_str())?;
966 out.write_char(',')?;
967 if let Some(slot) = entity.slot {
968 write!(out, "\"declaration_slot\":{},", slot.index())?;
969 }
970 write!(out, "\"unresolved_topic\":")?;
971 write_source_name(out, entity.source_name.as_str(), entity.source_name_kind)?;
972 out.write_char(',')?;
973 write_interface(out, entity.type_name, "message")?;
974 out.write_char(',')?;
975 write_qos(out, entity.qos)?;
976 write!(out, "}}")
977}
978
979#[cfg(feature = "std")]
980fn write_subscriber_json(
981 out: &mut impl core::fmt::Write,
982 entity: &EntityMetadata,
983) -> core::fmt::Result {
984 write!(out, "{{")?;
985 write_json_field(out, "id", entity.id.as_str())?;
986 out.write_char(',')?;
987 if let Some(slot) = entity.slot {
988 write!(out, "\"declaration_slot\":{},", slot.index())?;
989 }
990 write!(out, "\"unresolved_topic\":")?;
991 write_source_name(out, entity.source_name.as_str(), entity.source_name_kind)?;
992 out.write_char(',')?;
993 write_interface(out, entity.type_name, "message")?;
994 out.write_char(',')?;
995 write_qos(out, entity.qos)?;
996 out.write_char(',')?;
997 write_json_field(
998 out,
999 "callback",
1000 entity
1001 .callback_id
1002 .as_ref()
1003 .map(|id| id.as_str())
1004 .unwrap_or(""),
1005 )?;
1006 if let Some(callback_slot) = entity.callback_slot {
1007 write!(out, ",\"callback_slot\":{}", callback_slot.index())?;
1008 }
1009 write!(out, "}}")
1010}
1011
1012#[cfg(feature = "std")]
1013fn write_timer_json(out: &mut impl core::fmt::Write, entity: &EntityMetadata) -> core::fmt::Result {
1014 write!(out, "{{")?;
1015 write_json_field(out, "id", entity.id.as_str())?;
1016 out.write_char(',')?;
1017 if let Some(slot) = entity.slot {
1018 write!(out, "\"declaration_slot\":{},", slot.index())?;
1019 }
1020 write!(out, "\"period_ms\":{},", entity.period_ms.unwrap_or(0))?;
1021 write_json_field(
1022 out,
1023 "callback",
1024 entity
1025 .callback_id
1026 .as_ref()
1027 .map(|id| id.as_str())
1028 .unwrap_or(""),
1029 )?;
1030 if let Some(callback_slot) = entity.callback_slot {
1031 write!(out, ",\"callback_slot\":{}", callback_slot.index())?;
1032 }
1033 write!(out, "}}")
1034}
1035
1036#[cfg(feature = "std")]
1037fn write_service_json(
1038 out: &mut impl core::fmt::Write,
1039 entity: &EntityMetadata,
1040) -> core::fmt::Result {
1041 write!(out, "{{")?;
1042 write_json_field(out, "id", entity.id.as_str())?;
1043 out.write_char(',')?;
1044 if let Some(slot) = entity.slot {
1045 write!(out, "\"declaration_slot\":{},", slot.index())?;
1046 }
1047 write!(out, "\"unresolved_name\":")?;
1048 write_source_name(out, entity.source_name.as_str(), entity.source_name_kind)?;
1049 out.write_char(',')?;
1050 write_interface(out, entity.type_name, "service")?;
1051 out.write_char(',')?;
1052 write_json_field(
1053 out,
1054 "callback",
1055 entity
1056 .callback_id
1057 .as_ref()
1058 .map(|id| id.as_str())
1059 .unwrap_or(""),
1060 )?;
1061 if let Some(callback_slot) = entity.callback_slot {
1062 write!(out, ",\"callback_slot\":{}", callback_slot.index())?;
1063 }
1064 write!(out, "}}")
1065}
1066
1067#[cfg(feature = "std")]
1068fn write_action_json(
1069 out: &mut impl core::fmt::Write,
1070 entity: &EntityMetadata,
1071) -> core::fmt::Result {
1072 let goal_callback = entity
1073 .callback_id
1074 .as_ref()
1075 .map(|id| id.as_str())
1076 .unwrap_or("");
1077 let cancel_callback = entity
1078 .action_cancel_callback_id
1079 .as_ref()
1080 .map(|id| id.as_str())
1081 .unwrap_or(goal_callback);
1082 let accepted_callback = entity
1083 .action_accepted_callback_id
1084 .as_ref()
1085 .map(|id| id.as_str())
1086 .unwrap_or(goal_callback);
1087 write!(out, "{{")?;
1088 write_json_field(out, "id", entity.id.as_str())?;
1089 out.write_char(',')?;
1090 if let Some(slot) = entity.slot {
1091 write!(out, "\"declaration_slot\":{},", slot.index())?;
1092 }
1093 write!(out, "\"unresolved_name\":")?;
1094 write_source_name(out, entity.source_name.as_str(), entity.source_name_kind)?;
1095 out.write_char(',')?;
1096 write_interface(out, entity.type_name, "action")?;
1097 out.write_char(',')?;
1098 write_json_field(out, "goal_callback", goal_callback)?;
1099 if let Some(callback_slot) = entity.callback_slot {
1100 write!(out, ",\"goal_callback_slot\":{}", callback_slot.index())?;
1101 }
1102 out.write_char(',')?;
1103 write_json_field(out, "cancel_callback", cancel_callback)?;
1104 if let Some(callback_slot) = entity.action_cancel_callback_slot {
1105 write!(out, ",\"cancel_callback_slot\":{}", callback_slot.index())?;
1106 }
1107 out.write_char(',')?;
1108 write_json_field(out, "accepted_callback", accepted_callback)?;
1109 if let Some(callback_slot) = entity.action_accepted_callback_slot {
1110 write!(out, ",\"accepted_callback_slot\":{}", callback_slot.index())?;
1111 }
1112 write!(out, "}}")
1113}
1114
1115#[cfg(feature = "std")]
1116fn write_source_name(
1117 out: &mut impl core::fmt::Write,
1118 value: &str,
1119 kind: SourceNameKind,
1120) -> core::fmt::Result {
1121 write!(out, "{{")?;
1122 write_json_field(out, "value", value)?;
1123 out.write_char(',')?;
1124 write_json_field(out, "kind", source_name_kind_json(kind))?;
1125 write!(out, "}}")
1126}
1127
1128#[cfg(feature = "std")]
1129fn write_interface(
1130 out: &mut impl core::fmt::Write,
1131 type_name: &str,
1132 fallback_kind: &'static str,
1133) -> core::fmt::Result {
1134 let interface = parse_interface(type_name, fallback_kind);
1135 write!(out, "\"interface\":{{")?;
1136 write_json_field(out, "package", &interface.package)?;
1137 out.write_char(',')?;
1138 write_json_field(out, "name", &interface.name)?;
1139 out.write_char(',')?;
1140 write_json_field(out, "kind", interface.kind)?;
1141 write!(out, "}}")
1142}
1143
1144#[cfg(feature = "std")]
1145fn write_qos(out: &mut impl core::fmt::Write, qos: QosSettings) -> core::fmt::Result {
1146 write!(out, "\"qos\":{{")?;
1147 write_json_field(out, "reliability", reliability_json(qos.reliability))?;
1148 out.write_char(',')?;
1149 write_json_field(out, "durability", durability_json(qos.durability))?;
1150 out.write_char(',')?;
1151 write_json_field(out, "history", history_json(qos.history))?;
1152 out.write_char(',')?;
1153 write!(out, "\"depth\":{},", qos.depth)?;
1154 write_optional_ms(out, "deadline_ms", qos.deadline_ms)?;
1155 out.write_char(',')?;
1156 write_optional_ms(out, "lifespan_ms", qos.lifespan_ms)?;
1157 out.write_char(',')?;
1158 write_json_field(out, "liveliness", liveliness_json(qos.liveliness_kind))?;
1159 out.write_char(',')?;
1160 write_optional_ms(out, "liveliness_lease_duration_ms", qos.liveliness_lease_ms)?;
1161 write!(out, ",\"extensions\":{{}}}}")
1162}
1163
1164#[cfg(feature = "std")]
1165fn write_source_location(
1166 out: &mut impl core::fmt::Write,
1167 source: &SourceLocationMetadata,
1168) -> core::fmt::Result {
1169 write!(out, "\"source\":{{")?;
1170 write_json_field(out, "artifact", source.artifact.as_str())?;
1171 out.write_char(',')?;
1172 write!(out, "\"line\":")?;
1173 write_optional_u32(out, source.line)?;
1174 out.write_char(',')?;
1175 write!(out, "\"column\":")?;
1176 write_optional_u32(out, source.column)?;
1177 write!(out, "}}")
1178}
1179
1180#[cfg(feature = "std")]
1181fn write_parameter_default(
1182 out: &mut impl core::fmt::Write,
1183 default: Option<&ParameterDefault>,
1184) -> core::fmt::Result {
1185 match default {
1186 Some(ParameterDefault::Bool(value)) => write!(out, "{}", value),
1187 Some(ParameterDefault::Integer(value)) => write!(out, "{}", value),
1188 Some(ParameterDefault::Double(value)) => write!(out, "{}", value.as_str()),
1189 Some(ParameterDefault::String(value)) => write_json_string(out, value.as_str()),
1190 Some(ParameterDefault::BoolArray)
1191 | Some(ParameterDefault::IntegerArray)
1192 | Some(ParameterDefault::DoubleArray)
1193 | Some(ParameterDefault::StringArray)
1194 | None => write!(out, "[]"),
1195 }
1196}
1197
1198#[cfg(feature = "std")]
1199fn write_json_field(out: &mut impl core::fmt::Write, name: &str, value: &str) -> core::fmt::Result {
1200 write_json_string(out, name)?;
1201 out.write_char(':')?;
1202 write_json_string(out, value)
1203}
1204
1205#[cfg(feature = "std")]
1206fn write_json_opt_field(
1207 out: &mut impl core::fmt::Write,
1208 name: &str,
1209 value: Option<&str>,
1210) -> core::fmt::Result {
1211 write_json_string(out, name)?;
1212 out.write_char(':')?;
1213 if let Some(value) = value {
1214 write_json_string(out, value)
1215 } else {
1216 write!(out, "null")
1217 }
1218}
1219
1220#[cfg(feature = "std")]
1221fn write_json_string(out: &mut impl core::fmt::Write, value: &str) -> core::fmt::Result {
1222 out.write_char('"')?;
1223 for ch in value.chars() {
1224 match ch {
1225 '"' => write!(out, "\\\"")?,
1226 '\\' => write!(out, "\\\\")?,
1227 '\n' => write!(out, "\\n")?,
1228 '\r' => write!(out, "\\r")?,
1229 '\t' => write!(out, "\\t")?,
1230 ch if ch.is_control() => write!(out, "\\u{:04x}", ch as u32)?,
1231 ch => out.write_char(ch)?,
1232 }
1233 }
1234 out.write_char('"')
1235}
1236
1237#[cfg(feature = "std")]
1238fn write_optional_ms(out: &mut impl core::fmt::Write, name: &str, value: u32) -> core::fmt::Result {
1239 write_json_string(out, name)?;
1240 out.write_char(':')?;
1241 if value == 0 {
1242 write!(out, "null")
1243 } else {
1244 write!(out, "{}", value)
1245 }
1246}
1247
1248#[cfg(feature = "std")]
1249fn write_optional_u32(out: &mut impl core::fmt::Write, value: Option<u32>) -> core::fmt::Result {
1250 if let Some(value) = value {
1251 write!(out, "{}", value)
1252 } else {
1253 write!(out, "null")
1254 }
1255}
1256
1257#[cfg(feature = "std")]
1258fn source_name_kind_json(kind: SourceNameKind) -> &'static str {
1259 match kind {
1260 SourceNameKind::Absolute => "absolute",
1261 SourceNameKind::Relative => "relative",
1262 SourceNameKind::Private => "private",
1263 }
1264}
1265
1266#[cfg(feature = "std")]
1267fn effect_json_kind(kind: CallbackEffectKind) -> &'static str {
1268 match kind {
1269 CallbackEffectKind::Publishes => "publishes",
1270 CallbackEffectKind::Reads => "reads_parameter",
1271 CallbackEffectKind::Writes => "writes_parameter",
1272 }
1273}
1274
1275#[cfg(feature = "std")]
1276fn reliability_json(value: QosReliabilityPolicy) -> &'static str {
1277 match value {
1278 QosReliabilityPolicy::Reliable => "reliable",
1279 QosReliabilityPolicy::BestEffort => "best_effort",
1280 }
1281}
1282
1283#[cfg(feature = "std")]
1284fn durability_json(value: QosDurabilityPolicy) -> &'static str {
1285 match value {
1286 QosDurabilityPolicy::Volatile => "volatile",
1287 QosDurabilityPolicy::TransientLocal => "transient_local",
1288 }
1289}
1290
1291#[cfg(feature = "std")]
1292fn history_json(value: QosHistoryPolicy) -> &'static str {
1293 match value {
1294 QosHistoryPolicy::KeepLast => "keep_last",
1295 QosHistoryPolicy::KeepAll => "keep_all",
1296 }
1297}
1298
1299#[cfg(feature = "std")]
1300fn liveliness_json(value: QosLivelinessPolicy) -> &'static str {
1301 match value {
1302 QosLivelinessPolicy::None => "system_default",
1303 QosLivelinessPolicy::Automatic => "automatic",
1304 QosLivelinessPolicy::ManualByTopic => "manual_by_topic",
1305 QosLivelinessPolicy::ManualByNode => "manual_by_topic",
1306 }
1307}
1308
1309#[cfg(feature = "std")]
1310struct ParsedInterface {
1311 package: StdString,
1312 name: StdString,
1313 kind: &'static str,
1314}
1315
1316#[cfg(feature = "std")]
1317fn parse_interface(type_name: &str, fallback_kind: &'static str) -> ParsedInterface {
1318 let parts: StdVec<&str> = type_name.split("::").collect();
1319 if parts.len() >= 4 {
1320 let package = parts[0].into();
1321 let kind = match parts[1] {
1322 "msg" => "message",
1323 "srv" => "service",
1324 "action" => "action",
1325 _ => fallback_kind,
1326 };
1327 let mut type_leaf = parts[3].trim_end_matches('_');
1328 if type_leaf.is_empty() {
1329 type_leaf = parts.last().copied().unwrap_or("");
1330 }
1331 return ParsedInterface {
1332 package,
1333 name: format!("{}/{}", parts[1], type_leaf),
1334 kind,
1335 };
1336 }
1337
1338 ParsedInterface {
1339 package: StdString::new(),
1340 name: type_name.into(),
1341 kind: fallback_kind,
1342 }
1343}
1344
1345#[cfg(test)]
1346mod tests {
1347 use super::*;
1348 use crate::qos;
1349
1350 #[test]
1351 fn source_name_kind_preserves_unresolved_names() {
1352 assert_eq!(
1353 SourceNameKind::from_source_name("/scan"),
1354 SourceNameKind::Absolute
1355 );
1356 assert_eq!(
1357 SourceNameKind::from_source_name("~/scan"),
1358 SourceNameKind::Private
1359 );
1360 assert_eq!(
1361 SourceNameKind::from_source_name("scan"),
1362 SourceNameKind::Relative
1363 );
1364 }
1365
1366 #[test]
1367 fn recorder_rejects_duplicate_stable_ids() {
1368 let mut recorder = MetadataRecorder::<1, 2, 1>::new();
1369 recorder
1370 .push_node(NodeId::new("node"), "talker", "/", 0)
1371 .unwrap();
1372
1373 let first = entity_metadata(EntityMetadataSpec {
1374 id: EntityId::new("pub"),
1375 node_id: NodeId::new("node"),
1376 kind: EntityKind::Publisher,
1377 source_name: "chatter",
1378 type_name: "std_msgs::msg::dds_::String_",
1379 type_hash: "hash",
1380 qos: qos::DEFAULT,
1381 })
1382 .unwrap();
1383 recorder.push_entity(first.clone()).unwrap();
1384
1385 assert_eq!(
1386 recorder.push_entity(first),
1387 Err(NodeMetadataError::DuplicateId)
1388 );
1389 }
1390
1391 #[test]
1392 fn recorder_rejects_duplicate_nodes_and_unknown_node_entities() {
1393 let mut recorder = MetadataRecorder::<1, 1, 1>::new();
1394 recorder
1395 .push_node(NodeId::new("node"), "talker", "/", 0)
1396 .unwrap();
1397
1398 assert_eq!(
1399 recorder.push_node(NodeId::new("node"), "other", "/", 0),
1400 Err(NodeMetadataError::DuplicateId)
1401 );
1402
1403 let entity = entity_metadata(EntityMetadataSpec {
1404 id: EntityId::new("pub"),
1405 node_id: NodeId::new("missing_node"),
1406 kind: EntityKind::Publisher,
1407 source_name: "chatter",
1408 type_name: "std_msgs::msg::dds_::String_",
1409 type_hash: "hash",
1410 qos: qos::DEFAULT,
1411 })
1412 .unwrap();
1413
1414 assert_eq!(
1415 recorder.push_entity(entity),
1416 Err(NodeMetadataError::UnknownNode)
1417 );
1418 }
1419
1420 #[test]
1421 fn recorder_assigns_slots_and_source_default_names_by_declaration_order() {
1422 let mut recorder = MetadataRecorder::<2, 4, 3>::new();
1423 recorder
1424 .push_node(NodeId::new("node_alpha"), "talker", "/", 0)
1425 .unwrap();
1426 recorder
1427 .push_node(NodeId::new("node_beta"), "listener", "/demo", 42)
1428 .unwrap();
1429
1430 assert_eq!(recorder.nodes()[0].slot, NodeSlot::new(0));
1431 assert_eq!(recorder.nodes()[0].source_default_name.as_str(), "talker");
1432 assert_eq!(recorder.nodes()[1].slot, NodeSlot::new(1));
1433 assert_eq!(recorder.nodes()[1].source_default_name.as_str(), "listener");
1434
1435 recorder
1436 .push_entity(
1437 entity_metadata(EntityMetadataSpec {
1438 id: EntityId::new("pub_chatter"),
1439 node_id: NodeId::new("node_alpha"),
1440 kind: EntityKind::Publisher,
1441 source_name: "/chatter",
1442 type_name: "std_msgs::msg::dds_::String_",
1443 type_hash: "hash",
1444 qos: qos::DEFAULT,
1445 })
1446 .unwrap(),
1447 )
1448 .unwrap();
1449 let mut subscription = entity_metadata(EntityMetadataSpec {
1450 id: EntityId::new("sub_chatter"),
1451 node_id: NodeId::new("node_beta"),
1452 kind: EntityKind::Subscription,
1453 source_name: "/chatter",
1454 type_name: "std_msgs::msg::dds_::String_",
1455 type_hash: "hash",
1456 qos: qos::DEFAULT,
1457 })
1458 .unwrap();
1459 subscription.callback_id = Some(copy_str("on_message").unwrap());
1460 recorder.push_entity(subscription).unwrap();
1461 let mut timer = entity_metadata(EntityMetadataSpec {
1462 id: EntityId::new("timer_tick"),
1463 node_id: NodeId::new("node_alpha"),
1464 kind: EntityKind::Timer,
1465 source_name: "",
1466 type_name: "",
1467 type_hash: "",
1468 qos: qos::DEFAULT,
1469 })
1470 .unwrap();
1471 timer.callback_id = Some(copy_str("on_tick").unwrap());
1472 recorder.push_entity(timer).unwrap();
1473
1474 assert_eq!(recorder.entities()[0].slot, Some(EntitySlot::new(0)));
1475 assert_eq!(recorder.entities()[0].node_slot, Some(NodeSlot::new(0)));
1476 assert_eq!(recorder.entities()[0].callback_slot, None);
1477 assert_eq!(recorder.entities()[1].slot, Some(EntitySlot::new(1)));
1478 assert_eq!(recorder.entities()[1].node_slot, Some(NodeSlot::new(1)));
1479 assert_eq!(
1480 recorder.entities()[1].callback_slot,
1481 Some(CallbackSlot::new(0))
1482 );
1483 assert_eq!(
1484 recorder.entities()[2].callback_slot,
1485 Some(CallbackSlot::new(1))
1486 );
1487
1488 recorder
1489 .push_callback_effect(
1490 CallbackId::new("on_tick"),
1491 CallbackEffectKind::Publishes,
1492 EntityId::new("pub_chatter"),
1493 )
1494 .unwrap();
1495 assert_eq!(
1496 recorder.callback_effects()[0].callback_slot,
1497 Some(CallbackSlot::new(1))
1498 );
1499 assert_eq!(
1500 recorder.callback_effects()[0].entity_slot,
1501 Some(EntitySlot::new(0))
1502 );
1503 }
1504
1505 #[test]
1506 fn recorder_assigns_distinct_callback_slots_within_one_action_entity() {
1507 let mut recorder = MetadataRecorder::<1, 1, 3>::new();
1508 recorder
1509 .push_node(NodeId::new("node"), "action_node", "/", 0)
1510 .unwrap();
1511 let mut action = entity_metadata(EntityMetadataSpec {
1512 id: EntityId::new("act_count"),
1513 node_id: NodeId::new("node"),
1514 kind: EntityKind::ActionServer,
1515 source_name: "/count",
1516 type_name: "example_interfaces::action::dds_::Fibonacci_",
1517 type_hash: "hash",
1518 qos: qos::DEFAULT,
1519 })
1520 .unwrap();
1521 action.callback_id = Some(copy_str("on_goal").unwrap());
1522 action.action_cancel_callback_id = Some(copy_str("on_cancel").unwrap());
1523 action.action_accepted_callback_id = Some(copy_str("on_accepted").unwrap());
1524
1525 recorder.push_entity(action).unwrap();
1526
1527 assert_eq!(
1528 recorder.entities()[0].callback_slot,
1529 Some(CallbackSlot::new(0))
1530 );
1531 assert_eq!(
1532 recorder.entities()[0].action_cancel_callback_slot,
1533 Some(CallbackSlot::new(1))
1534 );
1535 assert_eq!(
1536 recorder.entities()[0].action_accepted_callback_slot,
1537 Some(CallbackSlot::new(2))
1538 );
1539 }
1540
1541 #[cfg(feature = "std")]
1542 #[test]
1543 fn source_metadata_json_uses_agent_a_schema_shape() {
1544 let mut recorder = MetadataRecorder::<1, 5, 1>::new();
1545 recorder
1546 .push_node(NodeId::new("node_talker"), "talker", "/", 0)
1547 .unwrap();
1548 recorder
1549 .push_entity(
1550 entity_metadata(EntityMetadataSpec {
1551 id: EntityId::new("pub_chatter"),
1552 node_id: NodeId::new("node_talker"),
1553 kind: EntityKind::Publisher,
1554 source_name: "chatter",
1555 type_name: "std_msgs::msg::dds_::String_",
1556 type_hash: "hash",
1557 qos: crate::qos::DEFAULT,
1558 })
1559 .unwrap(),
1560 )
1561 .unwrap();
1562 let mut timer = entity_metadata(EntityMetadataSpec {
1563 id: EntityId::new("timer_publish"),
1564 node_id: NodeId::new("node_talker"),
1565 kind: EntityKind::Timer,
1566 source_name: "",
1567 type_name: "",
1568 type_hash: "",
1569 qos: crate::qos::DEFAULT,
1570 })
1571 .unwrap();
1572 timer.callback_id = Some(copy_str("cb_timer").unwrap());
1573 timer.callback_source = SourceLocationMetadata {
1574 artifact: copy_str("src/talker.rs").unwrap(),
1575 line: Some(42),
1576 column: Some(5),
1577 };
1578 timer.period_ms = Some(100);
1579 recorder.push_entity(timer).unwrap();
1580 let mut param = entity_metadata(EntityMetadataSpec {
1581 id: EntityId::new("param_rate"),
1582 node_id: NodeId::new("node_talker"),
1583 kind: EntityKind::Parameter,
1584 source_name: "rate_hz",
1585 type_name: "",
1586 type_hash: "",
1587 qos: crate::qos::DEFAULT,
1588 })
1589 .unwrap();
1590 param.parameter_type = Some(ParameterType::Integer);
1591 param.parameter_default = Some(ParameterDefault::Integer(10));
1592 param.source = SourceLocationMetadata {
1593 artifact: copy_str("src/talker.rs").unwrap(),
1594 line: Some(25),
1595 column: Some(9),
1596 };
1597 recorder.push_entity(param).unwrap();
1598 let mut action = entity_metadata(EntityMetadataSpec {
1599 id: EntityId::new("act_count"),
1600 node_id: NodeId::new("node_talker"),
1601 kind: EntityKind::ActionServer,
1602 source_name: "~/count",
1603 type_name: "example_interfaces::action::dds_::Fibonacci_",
1604 type_hash: "hash",
1605 qos: crate::qos::DEFAULT,
1606 })
1607 .unwrap();
1608 action.callback_id = Some(copy_str("cb_count_goal").unwrap());
1609 action.callback_source = SourceLocationMetadata {
1610 artifact: copy_str("src/talker.rs").unwrap(),
1611 line: Some(90),
1612 column: Some(5),
1613 };
1614 action.action_cancel_callback_id = Some(copy_str("cb_count_cancel").unwrap());
1615 action.action_cancel_source = SourceLocationMetadata {
1616 artifact: copy_str("src/talker.rs").unwrap(),
1617 line: Some(96),
1618 column: Some(5),
1619 };
1620 action.action_accepted_callback_id = Some(copy_str("cb_count_accepted").unwrap());
1621 action.action_accepted_source = SourceLocationMetadata {
1622 artifact: copy_str("src/talker.rs").unwrap(),
1623 line: Some(104),
1624 column: Some(5),
1625 };
1626 recorder.push_entity(action).unwrap();
1627 recorder
1628 .push_callback_effect(
1629 CallbackId::new("cb_timer"),
1630 CallbackEffectKind::Publishes,
1631 EntityId::new("pub_chatter"),
1632 )
1633 .unwrap();
1634
1635 let json = recorder
1636 .to_source_metadata_json(
1637 &SourceMetadataExport::new("demo_nodes_rs", "talker")
1638 .executable("talker")
1639 .exported_symbol("nros_node_talker")
1640 .source_artifacts(&["src/talker.rs"]),
1641 )
1642 .unwrap();
1643
1644 assert!(json.contains("\"version\":1"));
1645 assert!(json.contains("\"language\":\"rust\""));
1646 assert!(json.contains("\"unresolved_name\":{\"value\":\"talker\",\"kind\":\"relative\"}"));
1647 assert!(json.contains(
1648 "\"interface\":{\"package\":\"std_msgs\",\"name\":\"msg/String\",\"kind\":\"message\"}"
1649 ));
1650 assert!(json.contains("\"kind\":\"publishes\",\"entity\":\"pub_chatter\""));
1651 assert!(
1652 json.contains("\"source\":{\"artifact\":\"src/talker.rs\",\"line\":42,\"column\":5}")
1653 );
1654 assert!(json.contains("\"name\":\"rate_hz\",\"default\":10,\"read_only\":false"));
1655 assert!(json.contains("\"goal_callback\":\"cb_count_goal\""));
1656 assert!(json.contains("\"cancel_callback\":\"cb_count_cancel\""));
1657 assert!(json.contains("\"accepted_callback\":\"cb_count_accepted\""));
1658 assert!(json.contains("\"kind\":\"action_cancel\""));
1659 assert!(json.contains("\"kind\":\"action_accepted\""));
1660 assert!(json.contains("\"generator\":\"nros-metadata-rust\""));
1661 }
1662}