1use crate::{
4 ParameterType, ParameterValue, QoSProfile,
5 heapless::{String, Vec},
6};
7use nros_node::timer::TimerClockSource;
13
14#[cfg(feature = "alloc")]
15use crate::{QoSDurabilityPolicy, QoSHistoryPolicy, QoSLivelinessPolicy, QoSReliabilityPolicy};
16#[cfg(feature = "alloc")]
17use alloc::{format, string::String as StdString, vec::Vec as StdVec};
22
23pub const SOURCE_METADATA_SCHEMA_VERSION: u32 = 4;
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68pub enum BufferedRow {
69 TypedBound,
71 Unbounded,
73}
74
75impl BufferedRow {
76 pub const fn as_str(self) -> &'static str {
78 match self {
79 Self::TypedBound => "typed_bound",
80 Self::Unbounded => "unbounded",
81 }
82 }
83}
84
85pub const DEFAULT_MAX_METADATA_NODES: usize = 8;
87pub const DEFAULT_MAX_METADATA_ENTITIES: usize = 32;
89pub const DEFAULT_MAX_METADATA_CALLBACKS: usize = 32;
91pub const METADATA_STRING_CAPACITY: usize = 128;
93
94pub type MetadataString = String<METADATA_STRING_CAPACITY>;
96
97#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
99pub struct NodeSlot(pub usize);
100
101impl NodeSlot {
102 pub const fn new(index: usize) -> Self {
104 Self(index)
105 }
106
107 pub const fn index(self) -> usize {
109 self.0
110 }
111}
112
113#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
115pub struct EntitySlot(pub usize);
116
117impl EntitySlot {
118 pub const fn new(index: usize) -> Self {
120 Self(index)
121 }
122
123 pub const fn index(self) -> usize {
125 self.0
126 }
127}
128
129#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
131pub struct CallbackSlot(pub usize);
132
133impl CallbackSlot {
134 pub const fn new(index: usize) -> Self {
136 Self(index)
137 }
138
139 pub const fn index(self) -> usize {
141 self.0
142 }
143}
144
145#[derive(Debug, Clone, PartialEq, Eq)]
147pub struct SourceLocationMetadata {
148 pub artifact: MetadataString,
149 pub line: Option<u32>,
150 pub column: Option<u32>,
151}
152
153impl SourceLocationMetadata {
154 pub const fn empty() -> Self {
156 Self {
157 artifact: MetadataString::new(),
158 line: None,
159 column: None,
160 }
161 }
162
163 #[track_caller]
165 pub fn caller() -> Result<Self, NodeMetadataError> {
166 let location = core::panic::Location::caller();
167 Ok(Self {
168 artifact: copy_str_keep_tail(location.file())?,
185 line: Some(location.line()),
186 column: Some(location.column()),
187 })
188 }
189}
190
191#[derive(Debug, Clone, PartialEq, Eq)]
193pub enum ParameterDefault {
194 Bool(bool),
195 Integer(i64),
196 Double(MetadataString),
197 String(MetadataString),
198 BoolArray,
199 IntegerArray,
200 DoubleArray,
201 StringArray,
202}
203
204impl ParameterDefault {
205 pub const fn parameter_type(&self) -> ParameterType {
207 match self {
208 Self::Bool(_) => ParameterType::Bool,
209 Self::Integer(_) => ParameterType::Integer,
210 Self::Double(_) => ParameterType::Double,
211 Self::String(_) => ParameterType::String,
212 Self::BoolArray => ParameterType::BoolArray,
213 Self::IntegerArray => ParameterType::IntegerArray,
214 Self::DoubleArray => ParameterType::DoubleArray,
215 Self::StringArray => ParameterType::StringArray,
216 }
217 }
218
219 pub fn from_value(value: &ParameterValue) -> Result<Self, NodeMetadataError> {
227 use core::fmt::Write as _;
228 Ok(match value {
229 ParameterValue::Bool(v) => Self::Bool(*v),
230 ParameterValue::Integer(v) => Self::Integer(*v),
231 ParameterValue::Double(v) => {
232 let mut text = MetadataString::new();
233 write!(text, "{v:?}").map_err(|_| NodeMetadataError::NameTooLong)?;
234 Self::Double(text)
235 }
236 ParameterValue::String(v) => Self::String(copy_str(v.as_str())?),
237 ParameterValue::BoolArray(_) => Self::BoolArray,
238 ParameterValue::IntegerArray(_) => Self::IntegerArray,
239 ParameterValue::DoubleArray(_) => Self::DoubleArray,
240 ParameterValue::StringArray(_) => Self::StringArray,
241 ParameterValue::ByteArray(_) | ParameterValue::NotSet => Self::Integer(0),
242 })
243 }
244
245 pub fn for_type(param_type: ParameterType) -> Result<Self, NodeMetadataError> {
247 Ok(match param_type {
248 ParameterType::Bool => Self::Bool(false),
249 ParameterType::Integer => Self::Integer(0),
250 ParameterType::Double => Self::Double(copy_str("0.0")?),
251 ParameterType::String => Self::String(copy_str("")?),
252 ParameterType::BoolArray => Self::BoolArray,
253 ParameterType::IntegerArray => Self::IntegerArray,
254 ParameterType::DoubleArray => Self::DoubleArray,
255 ParameterType::StringArray => Self::StringArray,
256 ParameterType::ByteArray | ParameterType::NotSet => Self::Integer(0),
257 })
258 }
259}
260
261#[derive(Debug, Clone, Copy, PartialEq, Eq)]
263pub enum SourceNameKind {
264 Absolute,
266 Private,
268 Relative,
270}
271
272impl SourceNameKind {
273 pub const fn from_source_name(name: &str) -> Self {
275 let bytes = name.as_bytes();
276 if bytes.is_empty() {
277 Self::Relative
278 } else if bytes[0] == b'/' {
279 Self::Absolute
280 } else if bytes[0] == b'~' {
281 Self::Private
282 } else {
283 Self::Relative
284 }
285 }
286}
287
288pub use nros_node::names::{
295 MAX_RESOLVED_NAME_LEN, ResolvedName, expand_name, fully_qualified_name, resolve_name,
296};
297
298#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
300pub struct EntityId<'a>(pub &'a str);
301
302impl<'a> EntityId<'a> {
303 pub const fn new(id: &'a str) -> Self {
305 Self(id)
306 }
307
308 pub const fn as_str(self) -> &'a str {
310 self.0
311 }
312}
313
314#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
316pub struct NodeId<'a>(pub &'a str);
317
318impl<'a> NodeId<'a> {
319 pub const fn new(id: &'a str) -> Self {
321 Self(id)
322 }
323
324 pub const fn as_str(self) -> &'a str {
326 self.0
327 }
328}
329
330#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
332pub struct CallbackId<'a>(pub &'a str);
333
334impl<'a> CallbackId<'a> {
335 pub const fn new(id: &'a str) -> Self {
337 Self(id)
338 }
339
340 pub const fn as_str(self) -> &'a str {
342 self.0
343 }
344}
345
346#[derive(Debug, Clone, Copy, PartialEq, Eq)]
348pub enum EntityKind {
349 Publisher,
350 Subscription,
351 Timer,
352 ServiceServer,
353 ServiceClient,
354 ActionServer,
355 ActionClient,
356 Parameter,
357}
358
359#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
377pub enum TimerKind {
378 #[default]
380 Wall,
381 Clock,
383 Oneshot,
385 InGroup,
387 GuardCondition,
389}
390
391#[derive(Debug, Clone, Copy, PartialEq, Eq)]
393pub enum CallbackEffectKind {
394 Reads,
395 Publishes,
396 Writes,
397}
398
399#[derive(Debug, Clone, Copy, PartialEq, Eq)]
401pub enum NodeMetadataError {
402 Capacity,
404 NameTooLong,
406 UnknownNode,
408 UnknownEntity,
410 DuplicateId,
412}
413
414#[derive(Debug, Clone, PartialEq, Eq)]
416pub struct NodeMetadata {
417 pub slot: NodeSlot,
418 pub id: MetadataString,
419 pub source_default_name: MetadataString,
420 pub name: MetadataString,
421 pub namespace: MetadataString,
422 pub domain_id: u32,
423}
424
425#[derive(Debug, Clone, PartialEq, Eq)]
427pub struct EntityMetadata {
428 pub slot: Option<EntitySlot>,
429 pub id: MetadataString,
430 pub node_slot: Option<NodeSlot>,
431 pub node_id: MetadataString,
432 pub kind: EntityKind,
433 pub source_name: MetadataString,
434 pub source_name_kind: SourceNameKind,
435 pub type_name: &'static str,
436 pub type_hash: &'static str,
437 pub action_hashes: Option<nros_node::ActionEndpointHashes<'static>>,
442 pub qos: QoSProfile,
443 pub callback_slot: Option<CallbackSlot>,
444 pub callback_id: Option<MetadataString>,
445 pub callback_source: SourceLocationMetadata,
446 pub callback_group: Option<MetadataString>,
447 pub action_cancel_callback_slot: Option<CallbackSlot>,
448 pub action_cancel_callback_id: Option<MetadataString>,
449 pub action_cancel_source: SourceLocationMetadata,
450 pub action_accepted_callback_slot: Option<CallbackSlot>,
451 pub action_accepted_callback_id: Option<MetadataString>,
452 pub action_accepted_source: SourceLocationMetadata,
453 pub period_ms: Option<u64>,
454 pub period_us: Option<u64>,
460 pub timer_clock: TimerClockSource,
471 pub timer_autostart: bool,
479 pub timer_kind: TimerKind,
482 pub parameter_type: Option<ParameterType>,
483 pub parameter_default: Option<ParameterDefault>,
484 pub parameter_read_only: bool,
485 pub safety: bool,
493 pub in_place_capable: Option<bool>,
511 pub buffered_row: Option<BufferedRow>,
515 pub rx_bound: Option<usize>,
522 pub source: SourceLocationMetadata,
523}
524
525impl EntityMetadata {
526 pub fn declared_subscription_shape(
547 &self,
548 ) -> nros_node::executor::declared_shape::DeclaredSubscriptionShape {
549 #[cfg(feature = "safety-e2e")]
550 let safety = self.safety;
551 #[cfg(not(feature = "safety-e2e"))]
552 let safety = false;
553 nros_node::executor::declared_shape::DeclaredSubscriptionShape::of_declaration(safety)
554 }
555}
556
557#[derive(Debug, Clone, PartialEq, Eq)]
559pub struct CallbackEffectMetadata {
560 pub callback_id: MetadataString,
561 pub callback_slot: Option<CallbackSlot>,
562 pub kind: CallbackEffectKind,
563 pub entity_id: MetadataString,
564 pub entity_slot: Option<EntitySlot>,
565}
566
567#[cfg(feature = "alloc")]
569#[derive(Debug, Clone)]
570pub struct SourceMetadataExport<'a> {
571 pub package: &'a str,
572 pub component: &'a str,
573 pub executable: Option<&'a str>,
574 pub exported_symbol: Option<&'a str>,
575 pub package_manifest: &'a str,
576 pub source_artifacts: &'a [&'a str],
577 pub language: &'a str,
584}
585
586#[cfg(feature = "alloc")]
587impl<'a> SourceMetadataExport<'a> {
588 pub const fn new(package: &'a str, component: &'a str) -> Self {
590 Self {
591 package,
592 component,
593 executable: None,
594 exported_symbol: None,
595 package_manifest: "package.xml",
596 source_artifacts: &[],
597 language: "rust",
598 }
599 }
600
601 pub const fn language(mut self, language: &'a str) -> Self {
603 self.language = language;
604 self
605 }
606
607 pub const fn executable(mut self, executable: &'a str) -> Self {
609 self.executable = Some(executable);
610 self
611 }
612
613 pub const fn exported_symbol(mut self, exported_symbol: &'a str) -> Self {
615 self.exported_symbol = Some(exported_symbol);
616 self
617 }
618
619 pub const fn package_manifest(mut self, package_manifest: &'a str) -> Self {
621 self.package_manifest = package_manifest;
622 self
623 }
624
625 pub const fn source_artifacts(mut self, source_artifacts: &'a [&'a str]) -> Self {
627 self.source_artifacts = source_artifacts;
628 self
629 }
630}
631
632pub fn metadata_string(value: &str) -> Result<MetadataString, NodeMetadataError> {
635 copy_str(value)
636}
637
638pub(crate) fn copy_str_keep_tail(value: &str) -> Result<MetadataString, NodeMetadataError> {
647 if value.len() <= METADATA_STRING_CAPACITY {
648 return copy_str(value);
649 }
650 const MARK: &str = "…/";
652 let budget = METADATA_STRING_CAPACITY - MARK.len();
653 let tail = value
654 .char_indices()
655 .find(|(i, _)| value.len() - i <= budget)
656 .map(|(i, _)| &value[i..])
657 .unwrap_or("");
658 let tail = match tail.find('/') {
660 Some(cut) => &tail[cut + 1..],
661 None => tail,
662 };
663 let mut out = MetadataString::new();
664 out.push_str(MARK)
665 .map_err(|_| NodeMetadataError::NameTooLong)?;
666 out.push_str(tail)
667 .map_err(|_| NodeMetadataError::NameTooLong)?;
668 Ok(out)
669}
670
671pub(crate) fn copy_str(value: &str) -> Result<MetadataString, NodeMetadataError> {
672 let mut out = MetadataString::new();
673 out.push_str(value)
674 .map_err(|_| NodeMetadataError::NameTooLong)?;
675 Ok(out)
676}
677
678#[derive(Debug)]
680pub struct MetadataRecorder<
681 const MAX_NODES: usize = DEFAULT_MAX_METADATA_NODES,
682 const MAX_ENTITIES: usize = DEFAULT_MAX_METADATA_ENTITIES,
683 const MAX_CALLBACKS: usize = DEFAULT_MAX_METADATA_CALLBACKS,
684> {
685 nodes: Vec<NodeMetadata, MAX_NODES>,
686 entities: Vec<EntityMetadata, MAX_ENTITIES>,
687 callback_effects: Vec<CallbackEffectMetadata, MAX_CALLBACKS>,
688}
689
690impl<const MAX_NODES: usize, const MAX_ENTITIES: usize, const MAX_CALLBACKS: usize> Default
691 for MetadataRecorder<MAX_NODES, MAX_ENTITIES, MAX_CALLBACKS>
692{
693 fn default() -> Self {
694 Self::new()
695 }
696}
697
698impl<const MAX_NODES: usize, const MAX_ENTITIES: usize, const MAX_CALLBACKS: usize>
699 MetadataRecorder<MAX_NODES, MAX_ENTITIES, MAX_CALLBACKS>
700{
701 pub const fn new() -> Self {
703 Self {
704 nodes: Vec::new(),
705 entities: Vec::new(),
706 callback_effects: Vec::new(),
707 }
708 }
709
710 pub fn nodes(&self) -> &[NodeMetadata] {
712 &self.nodes
713 }
714
715 pub fn entities(&self) -> &[EntityMetadata] {
717 &self.entities
718 }
719
720 pub fn callback_effects(&self) -> &[CallbackEffectMetadata] {
722 &self.callback_effects
723 }
724
725 #[must_use]
745 pub fn observe_subscription_registration(
746 &mut self,
747 source_name: &str,
748 type_name: &str,
749 in_place_capable: bool,
750 buffered_row: BufferedRow,
751 ) -> bool {
752 for entity in self.entities.iter_mut() {
753 if entity.kind == EntityKind::Subscription
754 && entity.in_place_capable.is_none()
755 && entity.source_name.as_str() == source_name
756 && entity.type_name == type_name
757 {
758 entity.in_place_capable = Some(in_place_capable);
759 entity.buffered_row = Some(buffered_row);
760 return true;
761 }
762 }
763 false
764 }
765
766 #[cfg(feature = "alloc")]
769 pub fn to_source_metadata_json(
770 &self,
771 export: &SourceMetadataExport<'_>,
772 ) -> Result<StdString, core::fmt::Error> {
773 let mut out = StdString::new();
774 self.write_source_metadata_json(export, &mut out)?;
775 Ok(out)
776 }
777
778 #[cfg(feature = "alloc")]
781 pub fn write_source_metadata_json(
782 &self,
783 export: &SourceMetadataExport<'_>,
784 out: &mut impl core::fmt::Write,
785 ) -> core::fmt::Result {
786 write!(out, "{{")?;
787 write!(out, "\"version\":{},", SOURCE_METADATA_SCHEMA_VERSION)?;
788 write_json_field(out, "package", export.package)?;
789 out.write_char(',')?;
790 write_json_field(out, "component", export.component)?;
791 out.write_char(',')?;
792 write_json_field(out, "language", export.language)?;
793 out.write_char(',')?;
794 write_json_opt_field(out, "executable", export.executable)?;
795 out.write_char(',')?;
796 write_json_opt_field(out, "exported_symbol", export.exported_symbol)?;
797 out.write_char(',')?;
798 self.write_nodes_json(out)?;
799 out.write_char(',')?;
800 self.write_callbacks_json(out)?;
801 out.write_char(',')?;
802 self.write_parameters_json(out)?;
803 out.write_char(',')?;
804 self.write_trace_json(export, out)?;
805 write!(out, "}}")
806 }
807
808 pub fn push_node(
818 &mut self,
819 id: NodeId<'_>,
820 name: &str,
821 namespace: &str,
822 domain_id: u32,
823 ) -> Result<(), NodeMetadataError> {
824 if self.has_node(id.as_str()) {
825 return Err(NodeMetadataError::DuplicateId);
826 }
827
828 self.nodes
829 .push(NodeMetadata {
830 slot: NodeSlot::new(self.nodes.len()),
831 id: copy_str(id.as_str())?,
832 source_default_name: copy_str(name)?,
833 name: copy_str(name)?,
834 namespace: copy_str(namespace)?,
835 domain_id,
836 })
837 .map_err(|_| NodeMetadataError::Capacity)
838 }
839
840 pub fn push_entity(&mut self, mut entity: EntityMetadata) -> Result<(), NodeMetadataError> {
845 if !self.has_node(&entity.node_id) {
846 return Err(NodeMetadataError::UnknownNode);
847 }
848 if self.has_entity(&entity.id) {
849 return Err(NodeMetadataError::DuplicateId);
850 }
851
852 entity.slot = Some(EntitySlot::new(self.entities.len()));
853 entity.node_slot = self.node_slot_for_id(&entity.node_id);
854 let mut current_callbacks = Vec::<MetadataString, 3>::new();
855 let mut next_callback_slot = self.callback_slot_count();
856 entity.callback_slot = entity.callback_id.as_ref().map(|callback_id| {
857 self.callback_slot_for_current_entity(
858 callback_id.as_str(),
859 &mut current_callbacks,
860 &mut next_callback_slot,
861 )
862 });
863 entity.action_cancel_callback_slot =
864 entity
865 .action_cancel_callback_id
866 .as_ref()
867 .map(|callback_id| {
868 self.callback_slot_for_current_entity(
869 callback_id.as_str(),
870 &mut current_callbacks,
871 &mut next_callback_slot,
872 )
873 });
874 entity.action_accepted_callback_slot =
875 entity
876 .action_accepted_callback_id
877 .as_ref()
878 .map(|callback_id| {
879 self.callback_slot_for_current_entity(
880 callback_id.as_str(),
881 &mut current_callbacks,
882 &mut next_callback_slot,
883 )
884 });
885
886 self.entities
887 .push(entity)
888 .map_err(|_| NodeMetadataError::Capacity)
889 }
890
891 pub(crate) fn push_callback_effect(
892 &mut self,
893 callback_id: CallbackId<'_>,
894 kind: CallbackEffectKind,
895 entity_id: EntityId<'_>,
896 ) -> Result<(), NodeMetadataError> {
897 if !self.has_entity(entity_id.as_str()) {
898 return Err(NodeMetadataError::UnknownEntity);
899 }
900
901 self.callback_effects
902 .push(CallbackEffectMetadata {
903 callback_id: copy_str(callback_id.as_str())?,
904 callback_slot: self.callback_slot_for_id(callback_id.as_str()),
905 kind,
906 entity_id: copy_str(entity_id.as_str())?,
907 entity_slot: self.entity_slot_for_id(entity_id.as_str()),
908 })
909 .map_err(|_| NodeMetadataError::Capacity)
910 }
911
912 pub(crate) fn has_node(&self, id: &str) -> bool {
913 self.nodes.iter().any(|node| node.id.as_str() == id)
914 }
915
916 pub(crate) fn has_entity(&self, id: &str) -> bool {
917 self.entities.iter().any(|entity| entity.id.as_str() == id)
918 }
919
920 fn node_slot_for_id(&self, id: &str) -> Option<NodeSlot> {
921 self.nodes
922 .iter()
923 .find(|node| node.id.as_str() == id)
924 .map(|node| node.slot)
925 }
926
927 fn entity_slot_for_id(&self, id: &str) -> Option<EntitySlot> {
928 self.entities
929 .iter()
930 .find(|entity| entity.id.as_str() == id)
931 .and_then(|entity| entity.slot)
932 }
933
934 fn callback_slot_for_current_entity(
935 &self,
936 id: &str,
937 current_callbacks: &mut Vec<MetadataString, 3>,
938 next_callback_slot: &mut usize,
939 ) -> CallbackSlot {
940 if let Some(slot) = self.callback_slot_for_id(id) {
941 return slot;
942 }
943 if let Some((index, _)) = current_callbacks
944 .iter()
945 .enumerate()
946 .find(|(_, callback_id)| callback_id.as_str() == id)
947 {
948 return CallbackSlot::new(self.callback_slot_count() + index);
949 }
950 let slot = CallbackSlot::new(*next_callback_slot);
951 let _ = current_callbacks
952 .push(copy_str(id).expect("callback ID already fits metadata string capacity"));
953 *next_callback_slot += 1;
954 slot
955 }
956
957 fn callback_slot_for_id(&self, id: &str) -> Option<CallbackSlot> {
958 let mut seen = Vec::<&str, MAX_CALLBACKS>::new();
959 for entity in &self.entities {
960 for callback_id in entity_callback_ids(entity) {
961 let Some(callback_id) = callback_id else {
962 continue;
963 };
964 let callback_id = callback_id.as_str();
965 if seen.contains(&callback_id) {
966 continue;
967 }
968 if callback_id == id {
969 return Some(CallbackSlot::new(seen.len()));
970 }
971 let _ = seen.push(callback_id);
972 }
973 }
974 None
975 }
976
977 fn callback_slot_count(&self) -> usize {
978 let mut seen = Vec::<&str, MAX_CALLBACKS>::new();
979 for entity in &self.entities {
980 for callback_id in entity_callback_ids(entity) {
981 let Some(callback_id) = callback_id else {
982 continue;
983 };
984 let callback_id = callback_id.as_str();
985 if !seen.contains(&callback_id) {
986 let _ = seen.push(callback_id);
987 }
988 }
989 }
990 seen.len()
991 }
992
993 #[cfg(feature = "alloc")]
994 fn write_nodes_json(&self, out: &mut impl core::fmt::Write) -> core::fmt::Result {
995 write!(out, "\"nodes\":[")?;
996 for (index, node) in self.nodes.iter().enumerate() {
997 if index > 0 {
998 out.write_char(',')?;
999 }
1000 write!(out, "{{")?;
1001 write_json_field(out, "id", node.id.as_str())?;
1002 out.write_char(',')?;
1003 write!(out, "\"declaration_slot\":{},", node.slot.index())?;
1004 write_json_field(
1005 out,
1006 "source_default_name",
1007 node.source_default_name.as_str(),
1008 )?;
1009 out.write_char(',')?;
1010 write!(out, "\"unresolved_name\":")?;
1011 write_source_name(
1012 out,
1013 node.name.as_str(),
1014 SourceNameKind::from_source_name(&node.name),
1015 )?;
1016 out.write_char(',')?;
1017 if node.namespace.as_str() == "/" {
1018 write!(out, "\"namespace\":null,")?;
1019 } else {
1020 write_json_field(out, "namespace", node.namespace.as_str())?;
1021 out.write_char(',')?;
1022 }
1023 self.write_node_entities(out, node.id.as_str())?;
1024 write!(out, "}}")?;
1025 }
1026 write!(out, "]")
1027 }
1028
1029 #[cfg(feature = "alloc")]
1030 fn write_node_entities(
1031 &self,
1032 out: &mut impl core::fmt::Write,
1033 node_id: &str,
1034 ) -> core::fmt::Result {
1035 self.write_entity_array(out, "publishers", node_id, EntityKind::Publisher)?;
1036 out.write_char(',')?;
1037 self.write_entity_array(out, "subscribers", node_id, EntityKind::Subscription)?;
1038 out.write_char(',')?;
1039 self.write_entity_array(out, "timers", node_id, EntityKind::Timer)?;
1043 out.write_char(',')?;
1044 self.write_entity_array(out, "services", node_id, EntityKind::ServiceServer)?;
1045 out.write_char(',')?;
1046 self.write_entity_array(out, "actions", node_id, EntityKind::ActionServer)?;
1047 out.write_char(',')?;
1054 self.write_entity_array(out, "action_clients", node_id, EntityKind::ActionClient)?;
1055 out.write_char(',')?;
1056 self.write_entity_array(out, "service_clients", node_id, EntityKind::ServiceClient)
1057 }
1058
1059 #[cfg(feature = "alloc")]
1060 fn write_entity_array(
1061 &self,
1062 out: &mut impl core::fmt::Write,
1063 field: &str,
1064 node_id: &str,
1065 kind: EntityKind,
1066 ) -> core::fmt::Result {
1067 write!(out, "\"{}\":[", field)?;
1068 for (index, entity) in self
1069 .entities
1070 .iter()
1071 .filter(|entity| entity.node_id.as_str() == node_id && entity.kind == kind)
1072 .enumerate()
1073 {
1074 if index > 0 {
1075 out.write_char(',')?;
1076 }
1077 match kind {
1078 EntityKind::Publisher => write_publisher_json(out, entity)?,
1079 EntityKind::Subscription => write_subscriber_json(out, entity)?,
1080 EntityKind::Timer => write_timer_json(out, entity)?,
1081 EntityKind::ServiceServer => write_service_json(out, entity)?,
1082 EntityKind::ActionServer => write_action_json(out, entity)?,
1083 EntityKind::ActionClient | EntityKind::ServiceClient => {
1087 write_client_json(out, entity)?
1088 }
1089 _ => {}
1090 }
1091 }
1092 write!(out, "]")
1093 }
1094
1095 #[cfg(feature = "alloc")]
1096 fn write_callbacks_json(&self, out: &mut impl core::fmt::Write) -> core::fmt::Result {
1097 let callbacks = self.source_callbacks();
1098 write!(out, "\"callbacks\":[")?;
1099 for (index, callback) in callbacks.iter().enumerate() {
1100 if index > 0 {
1101 out.write_char(',')?;
1102 }
1103 write!(out, "{{")?;
1104 write_json_field(out, "id", callback.id.as_str())?;
1105 out.write_char(',')?;
1106 if let Some(slot) = callback.slot {
1107 write!(out, "\"declaration_slot\":{},", slot.index())?;
1108 }
1109 write_json_field(out, "kind", callback.kind)?;
1110 out.write_char(',')?;
1111 if let Some(group) = callback.group.as_ref() {
1112 write_json_field(out, "group", group)?;
1113 out.write_char(',')?;
1114 } else {
1115 write!(out, "\"group\":null,")?;
1116 }
1117 write!(out, "\"effects\":[")?;
1118 for (effect_index, effect) in self
1119 .callback_effects
1120 .iter()
1121 .filter(|effect| effect.callback_id.as_str() == callback.id)
1122 .enumerate()
1123 {
1124 if effect_index > 0 {
1125 out.write_char(',')?;
1126 }
1127 write!(out, "{{")?;
1128 write_json_field(out, "kind", effect_json_kind(effect.kind))?;
1129 out.write_char(',')?;
1130 write_json_field(out, "entity", effect.entity_id.as_str())?;
1131 if let Some(entity_slot) = effect.entity_slot {
1132 write!(out, ",\"entity_slot\":{}", entity_slot.index())?;
1133 }
1134 write!(out, "}}")?;
1135 }
1136 write!(out, "],")?;
1137 write_source_location(out, &callback.source)?;
1138 write!(out, "}}")?;
1139 }
1140 write!(out, "]")
1141 }
1142
1143 #[cfg(feature = "alloc")]
1144 fn write_parameters_json(&self, out: &mut impl core::fmt::Write) -> core::fmt::Result {
1145 write!(out, "\"parameters\":[")?;
1146 for (index, entity) in self
1147 .entities
1148 .iter()
1149 .filter(|entity| entity.kind == EntityKind::Parameter)
1150 .enumerate()
1151 {
1152 if index > 0 {
1153 out.write_char(',')?;
1154 }
1155 write!(out, "{{")?;
1156 write_json_field(out, "node", entity.node_id.as_str())?;
1157 out.write_char(',')?;
1158 if let Some(slot) = entity.slot {
1159 write!(out, "\"declaration_slot\":{},", slot.index())?;
1160 }
1161 write_json_field(out, "name", entity.source_name.as_str())?;
1162 out.write_char(',')?;
1163 let parameter_type = entity.parameter_type.unwrap_or_else(|| {
1168 entity
1169 .parameter_default
1170 .as_ref()
1171 .map(ParameterDefault::parameter_type)
1172 .unwrap_or(ParameterType::NotSet)
1173 });
1174 write_json_field(out, "type", parameter_type_json(parameter_type))?;
1175 out.write_char(',')?;
1176 write!(out, "\"default\":")?;
1177 write_parameter_default(out, entity.parameter_default.as_ref())?;
1178 out.write_char(',')?;
1179 write!(out, "\"read_only\":{},", entity.parameter_read_only)?;
1180 write_source_location(out, &entity.source)?;
1181 write!(out, "}}")?;
1182 }
1183 write!(out, "]")
1184 }
1185
1186 #[cfg(feature = "alloc")]
1187 fn write_trace_json(
1188 &self,
1189 export: &SourceMetadataExport<'_>,
1190 out: &mut impl core::fmt::Write,
1191 ) -> core::fmt::Result {
1192 write!(out, "\"trace\":{{")?;
1193 write_json_field(out, "generator", "nros-metadata-rust")?;
1194 out.write_char(',')?;
1195 write_json_field(out, "package_manifest", export.package_manifest)?;
1196 out.write_char(',')?;
1197 write!(out, "\"source_artifacts\":[")?;
1198 for (index, artifact) in export.source_artifacts.iter().enumerate() {
1199 if index > 0 {
1200 out.write_char(',')?;
1201 }
1202 write_json_string(out, artifact)?;
1203 }
1204 write!(out, "]}}")
1205 }
1206
1207 #[cfg(feature = "alloc")]
1208 fn source_callbacks(&self) -> StdVec<SourceCallbackRef> {
1209 let mut callbacks = StdVec::new();
1210 for entity in &self.entities {
1211 let Some(callback_id) = entity.callback_id.as_ref() else {
1212 continue;
1213 };
1214 let kind = match entity.kind {
1215 EntityKind::Subscription => "subscription",
1216 EntityKind::Timer if entity.timer_kind == TimerKind::GuardCondition => {
1217 "guard_condition"
1218 }
1219 EntityKind::Timer => "timer",
1220 EntityKind::ServiceServer => "service",
1221 EntityKind::ActionServer => "action_goal",
1222 _ => continue,
1223 };
1224 if !callbacks
1225 .iter()
1226 .any(|callback: &SourceCallbackRef| callback.id == callback_id.as_str())
1227 {
1228 callbacks.push(SourceCallbackRef {
1229 id: callback_id.as_str().into(),
1230 slot: entity.callback_slot,
1231 kind,
1232 source: entity.callback_source.clone(),
1233 group: entity
1234 .callback_group
1235 .as_ref()
1236 .map(|group| group.as_str().into()),
1237 });
1238 }
1239 if entity.kind == EntityKind::ActionServer {
1240 if let Some(cancel_id) = entity.action_cancel_callback_id.as_ref()
1241 && !callbacks
1242 .iter()
1243 .any(|callback: &SourceCallbackRef| callback.id == cancel_id.as_str())
1244 {
1245 callbacks.push(SourceCallbackRef {
1246 id: cancel_id.as_str().into(),
1247 slot: entity.action_cancel_callback_slot,
1248 kind: "action_cancel",
1249 source: entity.action_cancel_source.clone(),
1250 group: entity
1251 .callback_group
1252 .as_ref()
1253 .map(|group| group.as_str().into()),
1254 });
1255 }
1256 if let Some(accepted_id) = entity.action_accepted_callback_id.as_ref()
1257 && !callbacks
1258 .iter()
1259 .any(|callback: &SourceCallbackRef| callback.id == accepted_id.as_str())
1260 {
1261 callbacks.push(SourceCallbackRef {
1262 id: accepted_id.as_str().into(),
1263 slot: entity.action_accepted_callback_slot,
1264 kind: "action_accepted",
1265 source: entity.action_accepted_source.clone(),
1266 group: entity
1267 .callback_group
1268 .as_ref()
1269 .map(|group| group.as_str().into()),
1270 });
1271 }
1272 }
1273 }
1274 callbacks
1275 }
1276}
1277
1278#[cfg(feature = "alloc")]
1279struct SourceCallbackRef {
1280 id: StdString,
1281 slot: Option<CallbackSlot>,
1282 kind: &'static str,
1283 source: SourceLocationMetadata,
1284 group: Option<StdString>,
1285}
1286
1287pub(crate) fn entity_callback_ids(entity: &EntityMetadata) -> [Option<&MetadataString>; 3] {
1288 [
1289 entity.callback_id.as_ref(),
1290 entity.action_cancel_callback_id.as_ref(),
1291 entity.action_accepted_callback_id.as_ref(),
1292 ]
1293}
1294
1295pub struct EntityMetadataSpec<'a> {
1303 pub id: EntityId<'a>,
1304 pub node_id: NodeId<'a>,
1305 pub kind: EntityKind,
1306 pub source_name: &'a str,
1307 pub type_name: &'static str,
1308 pub type_hash: &'static str,
1309 pub qos: QoSProfile,
1310}
1311
1312pub fn entity_metadata(spec: EntityMetadataSpec<'_>) -> Result<EntityMetadata, NodeMetadataError> {
1315 let EntityMetadataSpec {
1316 id,
1317 node_id,
1318 kind,
1319 source_name,
1320 type_name,
1321 type_hash,
1322 qos,
1323 } = spec;
1324 Ok(EntityMetadata {
1325 slot: None,
1326 id: copy_str(id.as_str())?,
1327 node_slot: None,
1328 node_id: copy_str(node_id.as_str())?,
1329 kind,
1330 source_name: copy_str(source_name)?,
1331 source_name_kind: SourceNameKind::from_source_name(source_name),
1332 type_name,
1333 type_hash,
1334 action_hashes: None,
1335 qos,
1336 callback_slot: None,
1337 callback_id: None,
1338 callback_source: SourceLocationMetadata::empty(),
1339 callback_group: None,
1340 action_cancel_callback_slot: None,
1341 action_cancel_callback_id: None,
1342 action_cancel_source: SourceLocationMetadata::empty(),
1343 action_accepted_callback_slot: None,
1344 action_accepted_callback_id: None,
1345 action_accepted_source: SourceLocationMetadata::empty(),
1346 period_ms: None,
1347 period_us: None,
1348 timer_clock: TimerClockSource::Steady,
1349 timer_autostart: true,
1350 timer_kind: TimerKind::Wall,
1351 parameter_type: None,
1352 parameter_default: None,
1353 parameter_read_only: false,
1354 safety: false,
1355 in_place_capable: None,
1358 buffered_row: None,
1359 rx_bound: None,
1360 source: SourceLocationMetadata::empty(),
1361 })
1362}
1363
1364#[cfg(feature = "alloc")]
1365fn write_publisher_json(
1366 out: &mut impl core::fmt::Write,
1367 entity: &EntityMetadata,
1368) -> core::fmt::Result {
1369 write!(out, "{{")?;
1370 write_json_field(out, "id", entity.id.as_str())?;
1371 out.write_char(',')?;
1372 if let Some(slot) = entity.slot {
1373 write!(out, "\"declaration_slot\":{},", slot.index())?;
1374 }
1375 write!(out, "\"unresolved_topic\":")?;
1376 write_source_name(out, entity.source_name.as_str(), entity.source_name_kind)?;
1377 out.write_char(',')?;
1378 write_interface(out, entity.type_name, "message")?;
1379 out.write_char(',')?;
1380 write_qos(out, entity.qos)?;
1381 write!(out, "}}")
1382}
1383
1384#[cfg(feature = "alloc")]
1385fn write_subscriber_json(
1386 out: &mut impl core::fmt::Write,
1387 entity: &EntityMetadata,
1388) -> core::fmt::Result {
1389 write!(out, "{{")?;
1390 write_json_field(out, "id", entity.id.as_str())?;
1391 out.write_char(',')?;
1392 if let Some(slot) = entity.slot {
1393 write!(out, "\"declaration_slot\":{},", slot.index())?;
1394 }
1395 write!(out, "\"unresolved_topic\":")?;
1396 write_source_name(out, entity.source_name.as_str(), entity.source_name_kind)?;
1397 out.write_char(',')?;
1398 write_interface(out, entity.type_name, "message")?;
1399 out.write_char(',')?;
1400 write_qos(out, entity.qos)?;
1401 out.write_char(',')?;
1402 write_json_field(
1403 out,
1404 "callback",
1405 entity
1406 .callback_id
1407 .as_ref()
1408 .map(|id| id.as_str())
1409 .unwrap_or(""),
1410 )?;
1411 if let Some(callback_slot) = entity.callback_slot {
1412 write!(out, ",\"callback_slot\":{}", callback_slot.index())?;
1413 }
1414 if let Some(in_place) = entity.in_place_capable {
1419 write!(out, ",\"in_place\":{in_place}")?;
1420 }
1421 if let Some(row) = entity.buffered_row {
1424 write!(out, ",\"buffered\":\"{}\"", row.as_str())?;
1425 }
1426 write!(out, "}}")
1427}
1428
1429#[cfg(feature = "alloc")]
1430fn write_timer_json(out: &mut impl core::fmt::Write, entity: &EntityMetadata) -> core::fmt::Result {
1431 write!(out, "{{")?;
1432 write_json_field(out, "id", entity.id.as_str())?;
1433 out.write_char(',')?;
1434 if let Some(slot) = entity.slot {
1435 write!(out, "\"declaration_slot\":{},", slot.index())?;
1436 }
1437 write_json_field(out, "kind", timer_kind_json(entity.timer_kind))?;
1441 out.write_char(',')?;
1442 write!(out, "\"period_ms\":{},", entity.period_ms.unwrap_or(0))?;
1443 write!(out, "\"period_us\":{},", entity.period_us.unwrap_or(0))?;
1444 write_json_field(
1445 out,
1446 "callback",
1447 entity
1448 .callback_id
1449 .as_ref()
1450 .map(|id| id.as_str())
1451 .unwrap_or(""),
1452 )?;
1453 if let Some(callback_slot) = entity.callback_slot {
1454 write!(out, ",\"callback_slot\":{}", callback_slot.index())?;
1455 }
1456 write!(out, "}}")
1457}
1458
1459#[cfg(feature = "alloc")]
1460fn write_service_json(
1461 out: &mut impl core::fmt::Write,
1462 entity: &EntityMetadata,
1463) -> core::fmt::Result {
1464 write!(out, "{{")?;
1465 write_json_field(out, "id", entity.id.as_str())?;
1466 out.write_char(',')?;
1467 if let Some(slot) = entity.slot {
1468 write!(out, "\"declaration_slot\":{},", slot.index())?;
1469 }
1470 write!(out, "\"unresolved_name\":")?;
1471 write_source_name(out, entity.source_name.as_str(), entity.source_name_kind)?;
1472 out.write_char(',')?;
1473 write_interface(out, entity.type_name, "service")?;
1474 out.write_char(',')?;
1475 write_qos(out, entity.qos)?;
1477 out.write_char(',')?;
1478 write_json_field(
1479 out,
1480 "callback",
1481 entity
1482 .callback_id
1483 .as_ref()
1484 .map(|id| id.as_str())
1485 .unwrap_or(""),
1486 )?;
1487 if let Some(callback_slot) = entity.callback_slot {
1488 write!(out, ",\"callback_slot\":{}", callback_slot.index())?;
1489 }
1490 write!(out, "}}")
1491}
1492
1493#[cfg(feature = "alloc")]
1500fn write_client_json(
1501 out: &mut impl core::fmt::Write,
1502 entity: &EntityMetadata,
1503) -> core::fmt::Result {
1504 let interface_kind = if entity.kind == EntityKind::ActionClient {
1505 "action"
1506 } else {
1507 "service"
1508 };
1509 write!(out, "{{")?;
1510 write_json_field(out, "id", entity.id.as_str())?;
1511 out.write_char(',')?;
1512 if let Some(slot) = entity.slot {
1513 write!(out, "\"declaration_slot\":{},", slot.index())?;
1514 }
1515 write!(out, "\"unresolved_name\":")?;
1516 write_source_name(out, entity.source_name.as_str(), entity.source_name_kind)?;
1517 out.write_char(',')?;
1518 write_interface(out, entity.type_name, interface_kind)?;
1519 out.write_char(',')?;
1520 write_qos(out, entity.qos)?;
1522 write!(out, "}}")
1523}
1524
1525#[cfg(feature = "alloc")]
1526fn write_action_json(
1527 out: &mut impl core::fmt::Write,
1528 entity: &EntityMetadata,
1529) -> core::fmt::Result {
1530 let goal_callback = entity
1531 .callback_id
1532 .as_ref()
1533 .map(|id| id.as_str())
1534 .unwrap_or("");
1535 let cancel_callback = entity
1536 .action_cancel_callback_id
1537 .as_ref()
1538 .map(|id| id.as_str())
1539 .unwrap_or(goal_callback);
1540 let accepted_callback = entity
1541 .action_accepted_callback_id
1542 .as_ref()
1543 .map(|id| id.as_str())
1544 .unwrap_or(goal_callback);
1545 write!(out, "{{")?;
1546 write_json_field(out, "id", entity.id.as_str())?;
1547 out.write_char(',')?;
1548 if let Some(slot) = entity.slot {
1549 write!(out, "\"declaration_slot\":{},", slot.index())?;
1550 }
1551 write!(out, "\"unresolved_name\":")?;
1552 write_source_name(out, entity.source_name.as_str(), entity.source_name_kind)?;
1553 out.write_char(',')?;
1554 write_interface(out, entity.type_name, "action")?;
1555 out.write_char(',')?;
1556 write_qos(out, entity.qos)?;
1558 out.write_char(',')?;
1559 write_json_field(out, "goal_callback", goal_callback)?;
1560 if let Some(callback_slot) = entity.callback_slot {
1561 write!(out, ",\"goal_callback_slot\":{}", callback_slot.index())?;
1562 }
1563 out.write_char(',')?;
1564 write_json_field(out, "cancel_callback", cancel_callback)?;
1565 if let Some(callback_slot) = entity.action_cancel_callback_slot {
1566 write!(out, ",\"cancel_callback_slot\":{}", callback_slot.index())?;
1567 }
1568 out.write_char(',')?;
1569 write_json_field(out, "accepted_callback", accepted_callback)?;
1570 if let Some(callback_slot) = entity.action_accepted_callback_slot {
1571 write!(out, ",\"accepted_callback_slot\":{}", callback_slot.index())?;
1572 }
1573 write!(out, "}}")
1574}
1575
1576#[cfg(feature = "alloc")]
1577fn write_source_name(
1578 out: &mut impl core::fmt::Write,
1579 value: &str,
1580 kind: SourceNameKind,
1581) -> core::fmt::Result {
1582 write!(out, "{{")?;
1583 write_json_field(out, "value", value)?;
1584 out.write_char(',')?;
1585 write_json_field(out, "kind", source_name_kind_json(kind))?;
1586 write!(out, "}}")
1587}
1588
1589#[cfg(feature = "alloc")]
1590fn write_interface(
1591 out: &mut impl core::fmt::Write,
1592 type_name: &str,
1593 fallback_kind: &'static str,
1594) -> core::fmt::Result {
1595 let interface = parse_interface(type_name, fallback_kind);
1596 write!(out, "\"interface\":{{")?;
1597 write_json_field(out, "package", &interface.package)?;
1598 out.write_char(',')?;
1599 write_json_field(out, "name", &interface.name)?;
1600 out.write_char(',')?;
1601 write_json_field(out, "kind", interface.kind)?;
1602 write!(out, "}}")
1603}
1604
1605#[cfg(feature = "alloc")]
1606fn write_qos(out: &mut impl core::fmt::Write, qos: QoSProfile) -> core::fmt::Result {
1607 write!(out, "\"qos\":{{")?;
1608 write_json_field(out, "reliability", reliability_json(qos.reliability))?;
1609 out.write_char(',')?;
1610 write_json_field(out, "durability", durability_json(qos.durability))?;
1611 out.write_char(',')?;
1612 write_json_field(out, "history", history_json(qos.history))?;
1613 out.write_char(',')?;
1614 write!(out, "\"depth\":{},", qos.depth)?;
1615 write_optional_ms(out, "deadline_ms", qos.deadline_ms)?;
1616 out.write_char(',')?;
1617 write_optional_ms(out, "lifespan_ms", qos.lifespan_ms)?;
1618 out.write_char(',')?;
1619 write_json_field(out, "liveliness", liveliness_json(qos.liveliness_kind))?;
1620 out.write_char(',')?;
1621 write_optional_ms(out, "liveliness_lease_duration_ms", qos.liveliness_lease_ms)?;
1622 write!(out, ",\"extensions\":{{}}}}")
1623}
1624
1625#[cfg(feature = "alloc")]
1626fn write_source_location(
1627 out: &mut impl core::fmt::Write,
1628 source: &SourceLocationMetadata,
1629) -> core::fmt::Result {
1630 write!(out, "\"source\":{{")?;
1631 write_json_field(out, "artifact", source.artifact.as_str())?;
1632 out.write_char(',')?;
1633 write!(out, "\"line\":")?;
1634 write_optional_u32(out, source.line)?;
1635 out.write_char(',')?;
1636 write!(out, "\"column\":")?;
1637 write_optional_u32(out, source.column)?;
1638 write!(out, "}}")
1639}
1640
1641#[cfg(feature = "alloc")]
1642fn write_parameter_default(
1643 out: &mut impl core::fmt::Write,
1644 default: Option<&ParameterDefault>,
1645) -> core::fmt::Result {
1646 match default {
1647 Some(ParameterDefault::Bool(value)) => write!(out, "{}", value),
1648 Some(ParameterDefault::Integer(value)) => write!(out, "{}", value),
1649 Some(ParameterDefault::Double(value)) => write!(out, "{}", value.as_str()),
1650 Some(ParameterDefault::String(value)) => write_json_string(out, value.as_str()),
1651 Some(ParameterDefault::BoolArray)
1652 | Some(ParameterDefault::IntegerArray)
1653 | Some(ParameterDefault::DoubleArray)
1654 | Some(ParameterDefault::StringArray)
1655 | None => write!(out, "[]"),
1656 }
1657}
1658
1659#[cfg(feature = "alloc")]
1660fn write_json_field(out: &mut impl core::fmt::Write, name: &str, value: &str) -> core::fmt::Result {
1661 write_json_string(out, name)?;
1662 out.write_char(':')?;
1663 write_json_string(out, value)
1664}
1665
1666#[cfg(feature = "alloc")]
1667fn write_json_opt_field(
1668 out: &mut impl core::fmt::Write,
1669 name: &str,
1670 value: Option<&str>,
1671) -> core::fmt::Result {
1672 write_json_string(out, name)?;
1673 out.write_char(':')?;
1674 if let Some(value) = value {
1675 write_json_string(out, value)
1676 } else {
1677 write!(out, "null")
1678 }
1679}
1680
1681#[cfg(feature = "alloc")]
1682fn write_json_string(out: &mut impl core::fmt::Write, value: &str) -> core::fmt::Result {
1683 out.write_char('"')?;
1684 for ch in value.chars() {
1685 match ch {
1686 '"' => write!(out, "\\\"")?,
1687 '\\' => write!(out, "\\\\")?,
1688 '\n' => write!(out, "\\n")?,
1689 '\r' => write!(out, "\\r")?,
1690 '\t' => write!(out, "\\t")?,
1691 ch if ch.is_control() => write!(out, "\\u{:04x}", ch as u32)?,
1692 ch => out.write_char(ch)?,
1693 }
1694 }
1695 out.write_char('"')
1696}
1697
1698#[cfg(feature = "alloc")]
1699fn write_optional_ms(out: &mut impl core::fmt::Write, name: &str, value: u32) -> core::fmt::Result {
1700 write_json_string(out, name)?;
1701 out.write_char(':')?;
1702 if value == 0 {
1703 write!(out, "null")
1704 } else {
1705 write!(out, "{}", value)
1706 }
1707}
1708
1709#[cfg(feature = "alloc")]
1710fn write_optional_u32(out: &mut impl core::fmt::Write, value: Option<u32>) -> core::fmt::Result {
1711 if let Some(value) = value {
1712 write!(out, "{}", value)
1713 } else {
1714 write!(out, "null")
1715 }
1716}
1717
1718#[cfg(feature = "alloc")]
1719fn source_name_kind_json(kind: SourceNameKind) -> &'static str {
1720 match kind {
1721 SourceNameKind::Absolute => "absolute",
1722 SourceNameKind::Relative => "relative",
1723 SourceNameKind::Private => "private",
1724 }
1725}
1726
1727#[cfg(feature = "alloc")]
1728fn timer_kind_json(kind: TimerKind) -> &'static str {
1729 match kind {
1730 TimerKind::Wall => "wall",
1731 TimerKind::Clock => "clock",
1732 TimerKind::Oneshot => "oneshot",
1733 TimerKind::InGroup => "in_group",
1734 TimerKind::GuardCondition => "guard_condition",
1735 }
1736}
1737
1738#[cfg(feature = "alloc")]
1742fn parameter_type_json(value: ParameterType) -> &'static str {
1743 match value {
1744 ParameterType::NotSet => "not_set",
1745 ParameterType::Bool => "bool",
1746 ParameterType::Integer => "integer",
1747 ParameterType::Double => "double",
1748 ParameterType::String => "string",
1749 ParameterType::ByteArray => "byte_array",
1750 ParameterType::BoolArray => "bool_array",
1751 ParameterType::IntegerArray => "integer_array",
1752 ParameterType::DoubleArray => "double_array",
1753 ParameterType::StringArray => "string_array",
1754 }
1755}
1756
1757#[cfg(feature = "alloc")]
1758fn effect_json_kind(kind: CallbackEffectKind) -> &'static str {
1759 match kind {
1760 CallbackEffectKind::Publishes => "publishes",
1761 CallbackEffectKind::Reads => "reads_parameter",
1762 CallbackEffectKind::Writes => "writes_parameter",
1763 }
1764}
1765
1766#[cfg(feature = "alloc")]
1773fn reliability_json(value: QoSReliabilityPolicy) -> &'static str {
1774 match value {
1775 QoSReliabilityPolicy::SystemDefault => "system_default",
1776 QoSReliabilityPolicy::Reliable => "reliable",
1777 QoSReliabilityPolicy::BestEffort => "best_effort",
1778 QoSReliabilityPolicy::Unknown => "unknown",
1783 QoSReliabilityPolicy::BestAvailable => "best_available",
1787 }
1788}
1789
1790#[cfg(feature = "alloc")]
1791fn durability_json(value: QoSDurabilityPolicy) -> &'static str {
1792 match value {
1793 QoSDurabilityPolicy::SystemDefault => "system_default",
1794 QoSDurabilityPolicy::Volatile => "volatile",
1795 QoSDurabilityPolicy::TransientLocal => "transient_local",
1796 QoSDurabilityPolicy::Unknown => "unknown",
1798 QoSDurabilityPolicy::BestAvailable => "best_available",
1799 }
1800}
1801
1802#[cfg(feature = "alloc")]
1803fn history_json(value: QoSHistoryPolicy) -> &'static str {
1804 match value {
1805 QoSHistoryPolicy::SystemDefault => "system_default",
1806 QoSHistoryPolicy::KeepLast => "keep_last",
1807 QoSHistoryPolicy::KeepAll => "keep_all",
1808 QoSHistoryPolicy::Unknown => "unknown",
1810 }
1811}
1812
1813#[cfg(feature = "alloc")]
1814fn liveliness_json(value: QoSLivelinessPolicy) -> &'static str {
1815 match value {
1816 QoSLivelinessPolicy::None => "system_default",
1817 QoSLivelinessPolicy::Automatic => "automatic",
1818 QoSLivelinessPolicy::ManualByTopic => "manual_by_topic",
1819 QoSLivelinessPolicy::ManualByNode => "manual_by_node",
1820 QoSLivelinessPolicy::Unknown => "unknown",
1822 QoSLivelinessPolicy::BestAvailable => "best_available",
1823 }
1824}
1825
1826#[cfg(feature = "alloc")]
1827struct ParsedInterface {
1828 package: StdString,
1829 name: StdString,
1830 kind: &'static str,
1831}
1832
1833#[cfg(feature = "alloc")]
1834fn parse_interface(type_name: &str, fallback_kind: &'static str) -> ParsedInterface {
1835 let parts: StdVec<&str> = type_name.split("::").collect();
1836 if parts.len() >= 4 {
1837 let package = parts[0].into();
1838 let kind = match parts[1] {
1839 "msg" => "message",
1840 "srv" => "service",
1841 "action" => "action",
1842 _ => fallback_kind,
1843 };
1844 let mut type_leaf = parts[3].trim_end_matches('_');
1845 if type_leaf.is_empty() {
1846 type_leaf = parts.last().copied().unwrap_or("");
1847 }
1848 return ParsedInterface {
1849 package,
1850 name: format!("{}/{}", parts[1], type_leaf),
1851 kind,
1852 };
1853 }
1854
1855 ParsedInterface {
1856 package: StdString::new(),
1857 name: type_name.into(),
1858 kind: fallback_kind,
1859 }
1860}
1861
1862#[cfg(test)]
1863mod tests {
1864 #[test]
1871 fn a_path_longer_than_the_buffer_keeps_its_tail() {
1872 let deep = concat!(
1875 "/deep/nested/nested/nested/nested/nested/nested/nested/nested/",
1876 "nested/nested/nested/nested/nested/nested/nested/nested/nested/",
1877 "ws/build/nros-metadata/metadata-probe/listener/src/lib.rs"
1878 );
1879 assert!(deep.len() > super::METADATA_STRING_CAPACITY);
1880
1881 let got = super::copy_str_keep_tail(deep).expect("must not error on depth");
1882 assert!(got.len() <= super::METADATA_STRING_CAPACITY);
1883 assert!(
1884 got.ends_with("src/lib.rs"),
1885 "the informative tail must survive, got {got:?}"
1886 );
1887 assert!(got.starts_with('…'), "a cut must be visible, got {got:?}");
1888
1889 let short = "src/lib.rs";
1891 assert_eq!(super::copy_str_keep_tail(short).unwrap().as_str(), short);
1892 }
1893
1894 use super::*;
1895 use crate::qos;
1896
1897 #[test]
1898 fn source_name_kind_preserves_unresolved_names() {
1899 assert_eq!(
1900 SourceNameKind::from_source_name("/scan"),
1901 SourceNameKind::Absolute
1902 );
1903 assert_eq!(
1904 SourceNameKind::from_source_name("~/scan"),
1905 SourceNameKind::Private
1906 );
1907 assert_eq!(
1908 SourceNameKind::from_source_name("scan"),
1909 SourceNameKind::Relative
1910 );
1911 }
1912
1913 #[test]
1917 fn expand_name_covers_each_source_name_kind() {
1918 assert_eq!(
1920 expand_name("/scan", "lidar", "/sensing").unwrap().as_str(),
1921 "/scan"
1922 );
1923 assert_eq!(
1925 expand_name("~/scan", "lidar", "/sensing").unwrap().as_str(),
1926 "/sensing/lidar/scan"
1927 );
1928 assert_eq!(
1929 expand_name("~/scan", "lidar", "/").unwrap().as_str(),
1930 "/lidar/scan"
1931 );
1932 assert_eq!(
1934 expand_name("scan", "lidar", "/sensing").unwrap().as_str(),
1935 "/sensing/scan"
1936 );
1937 assert_eq!(expand_name("scan", "lidar", "/").unwrap().as_str(), "/scan");
1938 }
1939
1940 #[test]
1941 fn resolve_name_substitutes_first_matching_rule() {
1942 let remaps = [
1943 ("~/scan", "/points_raw"),
1944 ("/sensing/lidar/scan", "/ignored"),
1945 ];
1946 assert_eq!(
1947 resolve_name("~/scan", "lidar", "/sensing", remaps)
1948 .unwrap()
1949 .as_str(),
1950 "/points_raw"
1951 );
1952 assert_eq!(
1954 resolve_name("other", "lidar", "/sensing", [("/x", "/y")])
1955 .unwrap()
1956 .as_str(),
1957 "/sensing/other"
1958 );
1959 }
1960
1961 #[test]
1962 fn recorder_rejects_duplicate_stable_ids() {
1963 let mut recorder = MetadataRecorder::<1, 2, 1>::new();
1964 recorder
1965 .push_node(NodeId::new("node"), "talker", "/", 0)
1966 .unwrap();
1967
1968 let first = entity_metadata(EntityMetadataSpec {
1969 id: EntityId::new("pub"),
1970 node_id: NodeId::new("node"),
1971 kind: EntityKind::Publisher,
1972 source_name: "chatter",
1973 type_name: "std_msgs::msg::dds_::String_",
1974 type_hash: "hash",
1975 qos: qos::DEFAULT,
1976 })
1977 .unwrap();
1978 recorder.push_entity(first.clone()).unwrap();
1979
1980 assert_eq!(
1981 recorder.push_entity(first),
1982 Err(NodeMetadataError::DuplicateId)
1983 );
1984 }
1985
1986 #[test]
1987 fn recorder_rejects_duplicate_nodes_and_unknown_node_entities() {
1988 let mut recorder = MetadataRecorder::<1, 1, 1>::new();
1989 recorder
1990 .push_node(NodeId::new("node"), "talker", "/", 0)
1991 .unwrap();
1992
1993 assert_eq!(
1994 recorder.push_node(NodeId::new("node"), "other", "/", 0),
1995 Err(NodeMetadataError::DuplicateId)
1996 );
1997
1998 let entity = entity_metadata(EntityMetadataSpec {
1999 id: EntityId::new("pub"),
2000 node_id: NodeId::new("missing_node"),
2001 kind: EntityKind::Publisher,
2002 source_name: "chatter",
2003 type_name: "std_msgs::msg::dds_::String_",
2004 type_hash: "hash",
2005 qos: qos::DEFAULT,
2006 })
2007 .unwrap();
2008
2009 assert_eq!(
2010 recorder.push_entity(entity),
2011 Err(NodeMetadataError::UnknownNode)
2012 );
2013 }
2014
2015 #[test]
2016 fn recorder_assigns_slots_and_source_default_names_by_declaration_order() {
2017 let mut recorder = MetadataRecorder::<2, 4, 3>::new();
2018 recorder
2019 .push_node(NodeId::new("node_alpha"), "talker", "/", 0)
2020 .unwrap();
2021 recorder
2022 .push_node(NodeId::new("node_beta"), "listener", "/demo", 42)
2023 .unwrap();
2024
2025 assert_eq!(recorder.nodes()[0].slot, NodeSlot::new(0));
2026 assert_eq!(recorder.nodes()[0].source_default_name.as_str(), "talker");
2027 assert_eq!(recorder.nodes()[1].slot, NodeSlot::new(1));
2028 assert_eq!(recorder.nodes()[1].source_default_name.as_str(), "listener");
2029
2030 recorder
2031 .push_entity(
2032 entity_metadata(EntityMetadataSpec {
2033 id: EntityId::new("pub_chatter"),
2034 node_id: NodeId::new("node_alpha"),
2035 kind: EntityKind::Publisher,
2036 source_name: "/chatter",
2037 type_name: "std_msgs::msg::dds_::String_",
2038 type_hash: "hash",
2039 qos: qos::DEFAULT,
2040 })
2041 .unwrap(),
2042 )
2043 .unwrap();
2044 let mut subscription = entity_metadata(EntityMetadataSpec {
2045 id: EntityId::new("sub_chatter"),
2046 node_id: NodeId::new("node_beta"),
2047 kind: EntityKind::Subscription,
2048 source_name: "/chatter",
2049 type_name: "std_msgs::msg::dds_::String_",
2050 type_hash: "hash",
2051 qos: qos::DEFAULT,
2052 })
2053 .unwrap();
2054 subscription.callback_id = Some(copy_str("on_message").unwrap());
2055 recorder.push_entity(subscription).unwrap();
2056 let mut timer = entity_metadata(EntityMetadataSpec {
2057 id: EntityId::new("timer_tick"),
2058 node_id: NodeId::new("node_alpha"),
2059 kind: EntityKind::Timer,
2060 source_name: "",
2061 type_name: "",
2062 type_hash: "",
2063 qos: qos::DEFAULT,
2064 })
2065 .unwrap();
2066 timer.callback_id = Some(copy_str("on_tick").unwrap());
2067 recorder.push_entity(timer).unwrap();
2068
2069 assert_eq!(recorder.entities()[0].slot, Some(EntitySlot::new(0)));
2070 assert_eq!(recorder.entities()[0].node_slot, Some(NodeSlot::new(0)));
2071 assert_eq!(recorder.entities()[0].callback_slot, None);
2072 assert_eq!(recorder.entities()[1].slot, Some(EntitySlot::new(1)));
2073 assert_eq!(recorder.entities()[1].node_slot, Some(NodeSlot::new(1)));
2074 assert_eq!(
2075 recorder.entities()[1].callback_slot,
2076 Some(CallbackSlot::new(0))
2077 );
2078 assert_eq!(
2079 recorder.entities()[2].callback_slot,
2080 Some(CallbackSlot::new(1))
2081 );
2082
2083 recorder
2084 .push_callback_effect(
2085 CallbackId::new("on_tick"),
2086 CallbackEffectKind::Publishes,
2087 EntityId::new("pub_chatter"),
2088 )
2089 .unwrap();
2090 assert_eq!(
2091 recorder.callback_effects()[0].callback_slot,
2092 Some(CallbackSlot::new(1))
2093 );
2094 assert_eq!(
2095 recorder.callback_effects()[0].entity_slot,
2096 Some(EntitySlot::new(0))
2097 );
2098 }
2099
2100 #[test]
2101 fn recorder_assigns_distinct_callback_slots_within_one_action_entity() {
2102 let mut recorder = MetadataRecorder::<1, 1, 3>::new();
2103 recorder
2104 .push_node(NodeId::new("node"), "action_node", "/", 0)
2105 .unwrap();
2106 let mut action = entity_metadata(EntityMetadataSpec {
2107 id: EntityId::new("act_count"),
2108 node_id: NodeId::new("node"),
2109 kind: EntityKind::ActionServer,
2110 source_name: "/count",
2111 type_name: "example_interfaces::action::dds_::Fibonacci_",
2112 type_hash: "hash",
2113 qos: qos::DEFAULT,
2114 })
2115 .unwrap();
2116 action.callback_id = Some(copy_str("on_goal").unwrap());
2117 action.action_cancel_callback_id = Some(copy_str("on_cancel").unwrap());
2118 action.action_accepted_callback_id = Some(copy_str("on_accepted").unwrap());
2119
2120 recorder.push_entity(action).unwrap();
2121
2122 assert_eq!(
2123 recorder.entities()[0].callback_slot,
2124 Some(CallbackSlot::new(0))
2125 );
2126 assert_eq!(
2127 recorder.entities()[0].action_cancel_callback_slot,
2128 Some(CallbackSlot::new(1))
2129 );
2130 assert_eq!(
2131 recorder.entities()[0].action_accepted_callback_slot,
2132 Some(CallbackSlot::new(2))
2133 );
2134 }
2135
2136 #[cfg(feature = "alloc")]
2137 #[test]
2138 fn source_metadata_json_uses_agent_a_schema_shape() {
2139 let mut recorder = MetadataRecorder::<1, 7, 1>::new();
2143 recorder
2144 .push_node(NodeId::new("node_talker"), "talker", "/", 0)
2145 .unwrap();
2146 recorder
2147 .push_entity(
2148 entity_metadata(EntityMetadataSpec {
2149 id: EntityId::new("pub_chatter"),
2150 node_id: NodeId::new("node_talker"),
2151 kind: EntityKind::Publisher,
2152 source_name: "chatter",
2153 type_name: "std_msgs::msg::dds_::String_",
2154 type_hash: "hash",
2155 qos: crate::qos::DEFAULT,
2156 })
2157 .unwrap(),
2158 )
2159 .unwrap();
2160 let mut timer = entity_metadata(EntityMetadataSpec {
2161 id: EntityId::new("timer_publish"),
2162 node_id: NodeId::new("node_talker"),
2163 kind: EntityKind::Timer,
2164 source_name: "",
2165 type_name: "",
2166 type_hash: "",
2167 qos: crate::qos::DEFAULT,
2168 })
2169 .unwrap();
2170 timer.callback_id = Some(copy_str("cb_timer").unwrap());
2171 timer.callback_source = SourceLocationMetadata {
2172 artifact: copy_str("src/talker.rs").unwrap(),
2173 line: Some(42),
2174 column: Some(5),
2175 };
2176 timer.period_ms = Some(100);
2177 recorder.push_entity(timer).unwrap();
2178 let mut param = entity_metadata(EntityMetadataSpec {
2179 id: EntityId::new("param_rate"),
2180 node_id: NodeId::new("node_talker"),
2181 kind: EntityKind::Parameter,
2182 source_name: "rate_hz",
2183 type_name: "",
2184 type_hash: "",
2185 qos: crate::qos::DEFAULT,
2186 })
2187 .unwrap();
2188 param.parameter_type = Some(ParameterType::Integer);
2189 param.parameter_default = Some(ParameterDefault::Integer(10));
2190 param.source = SourceLocationMetadata {
2191 artifact: copy_str("src/talker.rs").unwrap(),
2192 line: Some(25),
2193 column: Some(9),
2194 };
2195 recorder.push_entity(param).unwrap();
2196 let mut action = entity_metadata(EntityMetadataSpec {
2197 id: EntityId::new("act_count"),
2198 node_id: NodeId::new("node_talker"),
2199 kind: EntityKind::ActionServer,
2200 source_name: "~/count",
2201 type_name: "example_interfaces::action::dds_::Fibonacci_",
2202 type_hash: "hash",
2203 qos: crate::qos::DEFAULT,
2204 })
2205 .unwrap();
2206 action.callback_id = Some(copy_str("cb_count_goal").unwrap());
2207 action.callback_source = SourceLocationMetadata {
2208 artifact: copy_str("src/talker.rs").unwrap(),
2209 line: Some(90),
2210 column: Some(5),
2211 };
2212 action.action_cancel_callback_id = Some(copy_str("cb_count_cancel").unwrap());
2213 action.action_cancel_source = SourceLocationMetadata {
2214 artifact: copy_str("src/talker.rs").unwrap(),
2215 line: Some(96),
2216 column: Some(5),
2217 };
2218 action.action_accepted_callback_id = Some(copy_str("cb_count_accepted").unwrap());
2219 action.action_accepted_source = SourceLocationMetadata {
2220 artifact: copy_str("src/talker.rs").unwrap(),
2221 line: Some(104),
2222 column: Some(5),
2223 };
2224 recorder.push_entity(action).unwrap();
2225 recorder
2226 .push_callback_effect(
2227 CallbackId::new("cb_timer"),
2228 CallbackEffectKind::Publishes,
2229 EntityId::new("pub_chatter"),
2230 )
2231 .unwrap();
2232
2233 recorder
2237 .push_entity(
2238 entity_metadata(EntityMetadataSpec {
2239 id: EntityId::new("client_fib"),
2240 node_id: NodeId::new("node_talker"),
2241 kind: EntityKind::ActionClient,
2242 source_name: "/fibonacci",
2243 type_name: "example_interfaces::action::dds_::Fibonacci_",
2244 type_hash: "hash",
2245 qos: crate::qos::DEFAULT,
2246 })
2247 .unwrap(),
2248 )
2249 .unwrap();
2250 recorder
2251 .push_entity(
2252 entity_metadata(EntityMetadataSpec {
2253 id: EntityId::new("client_add"),
2254 node_id: NodeId::new("node_talker"),
2255 kind: EntityKind::ServiceClient,
2256 source_name: "/add_two_ints",
2257 type_name: "example_interfaces::srv::dds_::AddTwoInts_",
2258 type_hash: "hash",
2259 qos: crate::qos::DEFAULT,
2260 })
2261 .unwrap(),
2262 )
2263 .unwrap();
2264
2265 let json = recorder
2266 .to_source_metadata_json(
2267 &SourceMetadataExport::new("demo_nodes_rs", "talker")
2268 .executable("talker")
2269 .exported_symbol("nros_node_talker")
2270 .source_artifacts(&["src/talker.rs"]),
2271 )
2272 .unwrap();
2273
2274 assert!(
2278 json.contains(&format!("\"version\":{SOURCE_METADATA_SCHEMA_VERSION}")),
2279 "the current schema version, got {json}"
2280 );
2281 assert!(json.contains("\"language\":\"rust\""));
2282 assert!(json.contains("\"unresolved_name\":{\"value\":\"talker\",\"kind\":\"relative\"}"));
2283 assert!(json.contains(
2284 "\"interface\":{\"package\":\"std_msgs\",\"name\":\"msg/String\",\"kind\":\"message\"}"
2285 ));
2286 assert!(json.contains("\"kind\":\"publishes\",\"entity\":\"pub_chatter\""));
2287 assert!(
2288 json.contains("\"source\":{\"artifact\":\"src/talker.rs\",\"line\":42,\"column\":5}")
2289 );
2290 assert!(
2292 json.contains(
2293 "\"name\":\"rate_hz\",\"type\":\"integer\",\"default\":10,\"read_only\":false"
2294 ),
2295 "got {json}"
2296 );
2297 assert!(json.contains("\"goal_callback\":\"cb_count_goal\""));
2298 assert!(json.contains("\"cancel_callback\":\"cb_count_cancel\""));
2299 assert!(json.contains("\"accepted_callback\":\"cb_count_accepted\""));
2300 assert!(json.contains("\"kind\":\"action_cancel\""));
2301 assert!(json.contains("\"kind\":\"action_accepted\""));
2302
2303 assert!(
2314 json.contains("\"action_clients\":[{"),
2315 "an action client must reach the sidecar, got {json}"
2316 );
2317 assert!(
2318 json.contains("\"service_clients\":[{"),
2319 "a service client must reach the sidecar, got {json}"
2320 );
2321 let action_clients = &json[json.find("\"action_clients\":").expect("array present")..];
2328 let action_clients = &action_clients[..action_clients.find(']').unwrap()];
2329 let service_clients = &json[json.find("\"service_clients\":").expect("array present")..];
2330 let service_clients = &service_clients[..service_clients.find(']').unwrap()];
2331
2332 assert!(
2338 action_clients.contains(
2339 "\"interface\":{\"package\":\"example_interfaces\",\"name\":\"action/Fibonacci\",\"kind\":\"action\"}"
2340 ),
2341 "action client interface must be parsed, got {action_clients}"
2342 );
2343 assert!(
2344 service_clients.contains(
2345 "\"interface\":{\"package\":\"example_interfaces\",\"name\":\"srv/AddTwoInts\",\"kind\":\"service\"}"
2346 ),
2347 "service client interface must be parsed, got {service_clients}"
2348 );
2349 assert!(
2352 !action_clients.contains("\"kind\":\"service\"")
2353 && !service_clients.contains("\"kind\":\"action\""),
2354 "the client kind word is what distinguishes the arrays"
2355 );
2356 assert!(
2359 !action_clients.contains("callback") && !service_clients.contains("callback"),
2360 "a client registers no callback; emitting one would misdescribe it"
2361 );
2362 assert!(json.contains("\"generator\":\"nros-metadata-rust\""));
2363 }
2364}