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_kind: TimerKind,
474 pub parameter_type: Option<ParameterType>,
475 pub parameter_default: Option<ParameterDefault>,
476 pub parameter_read_only: bool,
477 pub safety: bool,
485 pub in_place_capable: Option<bool>,
503 pub buffered_row: Option<BufferedRow>,
507 pub source: SourceLocationMetadata,
508}
509
510impl EntityMetadata {
511 pub fn declared_subscription_shape(
532 &self,
533 ) -> nros_node::executor::declared_shape::DeclaredSubscriptionShape {
534 #[cfg(feature = "safety-e2e")]
535 let safety = self.safety;
536 #[cfg(not(feature = "safety-e2e"))]
537 let safety = false;
538 nros_node::executor::declared_shape::DeclaredSubscriptionShape::of_declaration(safety)
539 }
540}
541
542#[derive(Debug, Clone, PartialEq, Eq)]
544pub struct CallbackEffectMetadata {
545 pub callback_id: MetadataString,
546 pub callback_slot: Option<CallbackSlot>,
547 pub kind: CallbackEffectKind,
548 pub entity_id: MetadataString,
549 pub entity_slot: Option<EntitySlot>,
550}
551
552#[cfg(feature = "alloc")]
554#[derive(Debug, Clone)]
555pub struct SourceMetadataExport<'a> {
556 pub package: &'a str,
557 pub component: &'a str,
558 pub executable: Option<&'a str>,
559 pub exported_symbol: Option<&'a str>,
560 pub package_manifest: &'a str,
561 pub source_artifacts: &'a [&'a str],
562 pub language: &'a str,
569}
570
571#[cfg(feature = "alloc")]
572impl<'a> SourceMetadataExport<'a> {
573 pub const fn new(package: &'a str, component: &'a str) -> Self {
575 Self {
576 package,
577 component,
578 executable: None,
579 exported_symbol: None,
580 package_manifest: "package.xml",
581 source_artifacts: &[],
582 language: "rust",
583 }
584 }
585
586 pub const fn language(mut self, language: &'a str) -> Self {
588 self.language = language;
589 self
590 }
591
592 pub const fn executable(mut self, executable: &'a str) -> Self {
594 self.executable = Some(executable);
595 self
596 }
597
598 pub const fn exported_symbol(mut self, exported_symbol: &'a str) -> Self {
600 self.exported_symbol = Some(exported_symbol);
601 self
602 }
603
604 pub const fn package_manifest(mut self, package_manifest: &'a str) -> Self {
606 self.package_manifest = package_manifest;
607 self
608 }
609
610 pub const fn source_artifacts(mut self, source_artifacts: &'a [&'a str]) -> Self {
612 self.source_artifacts = source_artifacts;
613 self
614 }
615}
616
617pub fn metadata_string(value: &str) -> Result<MetadataString, NodeMetadataError> {
620 copy_str(value)
621}
622
623pub(crate) fn copy_str_keep_tail(value: &str) -> Result<MetadataString, NodeMetadataError> {
632 if value.len() <= METADATA_STRING_CAPACITY {
633 return copy_str(value);
634 }
635 const MARK: &str = "…/";
637 let budget = METADATA_STRING_CAPACITY - MARK.len();
638 let tail = value
639 .char_indices()
640 .find(|(i, _)| value.len() - i <= budget)
641 .map(|(i, _)| &value[i..])
642 .unwrap_or("");
643 let tail = match tail.find('/') {
645 Some(cut) => &tail[cut + 1..],
646 None => tail,
647 };
648 let mut out = MetadataString::new();
649 out.push_str(MARK)
650 .map_err(|_| NodeMetadataError::NameTooLong)?;
651 out.push_str(tail)
652 .map_err(|_| NodeMetadataError::NameTooLong)?;
653 Ok(out)
654}
655
656pub(crate) fn copy_str(value: &str) -> Result<MetadataString, NodeMetadataError> {
657 let mut out = MetadataString::new();
658 out.push_str(value)
659 .map_err(|_| NodeMetadataError::NameTooLong)?;
660 Ok(out)
661}
662
663#[derive(Debug)]
665pub struct MetadataRecorder<
666 const MAX_NODES: usize = DEFAULT_MAX_METADATA_NODES,
667 const MAX_ENTITIES: usize = DEFAULT_MAX_METADATA_ENTITIES,
668 const MAX_CALLBACKS: usize = DEFAULT_MAX_METADATA_CALLBACKS,
669> {
670 nodes: Vec<NodeMetadata, MAX_NODES>,
671 entities: Vec<EntityMetadata, MAX_ENTITIES>,
672 callback_effects: Vec<CallbackEffectMetadata, MAX_CALLBACKS>,
673}
674
675impl<const MAX_NODES: usize, const MAX_ENTITIES: usize, const MAX_CALLBACKS: usize> Default
676 for MetadataRecorder<MAX_NODES, MAX_ENTITIES, MAX_CALLBACKS>
677{
678 fn default() -> Self {
679 Self::new()
680 }
681}
682
683impl<const MAX_NODES: usize, const MAX_ENTITIES: usize, const MAX_CALLBACKS: usize>
684 MetadataRecorder<MAX_NODES, MAX_ENTITIES, MAX_CALLBACKS>
685{
686 pub const fn new() -> Self {
688 Self {
689 nodes: Vec::new(),
690 entities: Vec::new(),
691 callback_effects: Vec::new(),
692 }
693 }
694
695 pub fn nodes(&self) -> &[NodeMetadata] {
697 &self.nodes
698 }
699
700 pub fn entities(&self) -> &[EntityMetadata] {
702 &self.entities
703 }
704
705 pub fn callback_effects(&self) -> &[CallbackEffectMetadata] {
707 &self.callback_effects
708 }
709
710 #[must_use]
730 pub fn observe_subscription_registration(
731 &mut self,
732 source_name: &str,
733 type_name: &str,
734 in_place_capable: bool,
735 buffered_row: BufferedRow,
736 ) -> bool {
737 for entity in self.entities.iter_mut() {
738 if entity.kind == EntityKind::Subscription
739 && entity.in_place_capable.is_none()
740 && entity.source_name.as_str() == source_name
741 && entity.type_name == type_name
742 {
743 entity.in_place_capable = Some(in_place_capable);
744 entity.buffered_row = Some(buffered_row);
745 return true;
746 }
747 }
748 false
749 }
750
751 #[cfg(feature = "alloc")]
754 pub fn to_source_metadata_json(
755 &self,
756 export: &SourceMetadataExport<'_>,
757 ) -> Result<StdString, core::fmt::Error> {
758 let mut out = StdString::new();
759 self.write_source_metadata_json(export, &mut out)?;
760 Ok(out)
761 }
762
763 #[cfg(feature = "alloc")]
766 pub fn write_source_metadata_json(
767 &self,
768 export: &SourceMetadataExport<'_>,
769 out: &mut impl core::fmt::Write,
770 ) -> core::fmt::Result {
771 write!(out, "{{")?;
772 write!(out, "\"version\":{},", SOURCE_METADATA_SCHEMA_VERSION)?;
773 write_json_field(out, "package", export.package)?;
774 out.write_char(',')?;
775 write_json_field(out, "component", export.component)?;
776 out.write_char(',')?;
777 write_json_field(out, "language", export.language)?;
778 out.write_char(',')?;
779 write_json_opt_field(out, "executable", export.executable)?;
780 out.write_char(',')?;
781 write_json_opt_field(out, "exported_symbol", export.exported_symbol)?;
782 out.write_char(',')?;
783 self.write_nodes_json(out)?;
784 out.write_char(',')?;
785 self.write_callbacks_json(out)?;
786 out.write_char(',')?;
787 self.write_parameters_json(out)?;
788 out.write_char(',')?;
789 self.write_trace_json(export, out)?;
790 write!(out, "}}")
791 }
792
793 pub fn push_node(
803 &mut self,
804 id: NodeId<'_>,
805 name: &str,
806 namespace: &str,
807 domain_id: u32,
808 ) -> Result<(), NodeMetadataError> {
809 if self.has_node(id.as_str()) {
810 return Err(NodeMetadataError::DuplicateId);
811 }
812
813 self.nodes
814 .push(NodeMetadata {
815 slot: NodeSlot::new(self.nodes.len()),
816 id: copy_str(id.as_str())?,
817 source_default_name: copy_str(name)?,
818 name: copy_str(name)?,
819 namespace: copy_str(namespace)?,
820 domain_id,
821 })
822 .map_err(|_| NodeMetadataError::Capacity)
823 }
824
825 pub fn push_entity(&mut self, mut entity: EntityMetadata) -> Result<(), NodeMetadataError> {
830 if !self.has_node(&entity.node_id) {
831 return Err(NodeMetadataError::UnknownNode);
832 }
833 if self.has_entity(&entity.id) {
834 return Err(NodeMetadataError::DuplicateId);
835 }
836
837 entity.slot = Some(EntitySlot::new(self.entities.len()));
838 entity.node_slot = self.node_slot_for_id(&entity.node_id);
839 let mut current_callbacks = Vec::<MetadataString, 3>::new();
840 let mut next_callback_slot = self.callback_slot_count();
841 entity.callback_slot = entity.callback_id.as_ref().map(|callback_id| {
842 self.callback_slot_for_current_entity(
843 callback_id.as_str(),
844 &mut current_callbacks,
845 &mut next_callback_slot,
846 )
847 });
848 entity.action_cancel_callback_slot =
849 entity
850 .action_cancel_callback_id
851 .as_ref()
852 .map(|callback_id| {
853 self.callback_slot_for_current_entity(
854 callback_id.as_str(),
855 &mut current_callbacks,
856 &mut next_callback_slot,
857 )
858 });
859 entity.action_accepted_callback_slot =
860 entity
861 .action_accepted_callback_id
862 .as_ref()
863 .map(|callback_id| {
864 self.callback_slot_for_current_entity(
865 callback_id.as_str(),
866 &mut current_callbacks,
867 &mut next_callback_slot,
868 )
869 });
870
871 self.entities
872 .push(entity)
873 .map_err(|_| NodeMetadataError::Capacity)
874 }
875
876 pub(crate) fn push_callback_effect(
877 &mut self,
878 callback_id: CallbackId<'_>,
879 kind: CallbackEffectKind,
880 entity_id: EntityId<'_>,
881 ) -> Result<(), NodeMetadataError> {
882 if !self.has_entity(entity_id.as_str()) {
883 return Err(NodeMetadataError::UnknownEntity);
884 }
885
886 self.callback_effects
887 .push(CallbackEffectMetadata {
888 callback_id: copy_str(callback_id.as_str())?,
889 callback_slot: self.callback_slot_for_id(callback_id.as_str()),
890 kind,
891 entity_id: copy_str(entity_id.as_str())?,
892 entity_slot: self.entity_slot_for_id(entity_id.as_str()),
893 })
894 .map_err(|_| NodeMetadataError::Capacity)
895 }
896
897 pub(crate) fn has_node(&self, id: &str) -> bool {
898 self.nodes.iter().any(|node| node.id.as_str() == id)
899 }
900
901 pub(crate) fn has_entity(&self, id: &str) -> bool {
902 self.entities.iter().any(|entity| entity.id.as_str() == id)
903 }
904
905 fn node_slot_for_id(&self, id: &str) -> Option<NodeSlot> {
906 self.nodes
907 .iter()
908 .find(|node| node.id.as_str() == id)
909 .map(|node| node.slot)
910 }
911
912 fn entity_slot_for_id(&self, id: &str) -> Option<EntitySlot> {
913 self.entities
914 .iter()
915 .find(|entity| entity.id.as_str() == id)
916 .and_then(|entity| entity.slot)
917 }
918
919 fn callback_slot_for_current_entity(
920 &self,
921 id: &str,
922 current_callbacks: &mut Vec<MetadataString, 3>,
923 next_callback_slot: &mut usize,
924 ) -> CallbackSlot {
925 if let Some(slot) = self.callback_slot_for_id(id) {
926 return slot;
927 }
928 if let Some((index, _)) = current_callbacks
929 .iter()
930 .enumerate()
931 .find(|(_, callback_id)| callback_id.as_str() == id)
932 {
933 return CallbackSlot::new(self.callback_slot_count() + index);
934 }
935 let slot = CallbackSlot::new(*next_callback_slot);
936 let _ = current_callbacks
937 .push(copy_str(id).expect("callback ID already fits metadata string capacity"));
938 *next_callback_slot += 1;
939 slot
940 }
941
942 fn callback_slot_for_id(&self, id: &str) -> Option<CallbackSlot> {
943 let mut seen = Vec::<&str, MAX_CALLBACKS>::new();
944 for entity in &self.entities {
945 for callback_id in entity_callback_ids(entity) {
946 let Some(callback_id) = callback_id else {
947 continue;
948 };
949 let callback_id = callback_id.as_str();
950 if seen.contains(&callback_id) {
951 continue;
952 }
953 if callback_id == id {
954 return Some(CallbackSlot::new(seen.len()));
955 }
956 let _ = seen.push(callback_id);
957 }
958 }
959 None
960 }
961
962 fn callback_slot_count(&self) -> usize {
963 let mut seen = Vec::<&str, MAX_CALLBACKS>::new();
964 for entity in &self.entities {
965 for callback_id in entity_callback_ids(entity) {
966 let Some(callback_id) = callback_id else {
967 continue;
968 };
969 let callback_id = callback_id.as_str();
970 if !seen.contains(&callback_id) {
971 let _ = seen.push(callback_id);
972 }
973 }
974 }
975 seen.len()
976 }
977
978 #[cfg(feature = "alloc")]
979 fn write_nodes_json(&self, out: &mut impl core::fmt::Write) -> core::fmt::Result {
980 write!(out, "\"nodes\":[")?;
981 for (index, node) in self.nodes.iter().enumerate() {
982 if index > 0 {
983 out.write_char(',')?;
984 }
985 write!(out, "{{")?;
986 write_json_field(out, "id", node.id.as_str())?;
987 out.write_char(',')?;
988 write!(out, "\"declaration_slot\":{},", node.slot.index())?;
989 write_json_field(
990 out,
991 "source_default_name",
992 node.source_default_name.as_str(),
993 )?;
994 out.write_char(',')?;
995 write!(out, "\"unresolved_name\":")?;
996 write_source_name(
997 out,
998 node.name.as_str(),
999 SourceNameKind::from_source_name(&node.name),
1000 )?;
1001 out.write_char(',')?;
1002 if node.namespace.as_str() == "/" {
1003 write!(out, "\"namespace\":null,")?;
1004 } else {
1005 write_json_field(out, "namespace", node.namespace.as_str())?;
1006 out.write_char(',')?;
1007 }
1008 self.write_node_entities(out, node.id.as_str())?;
1009 write!(out, "}}")?;
1010 }
1011 write!(out, "]")
1012 }
1013
1014 #[cfg(feature = "alloc")]
1015 fn write_node_entities(
1016 &self,
1017 out: &mut impl core::fmt::Write,
1018 node_id: &str,
1019 ) -> core::fmt::Result {
1020 self.write_entity_array(out, "publishers", node_id, EntityKind::Publisher)?;
1021 out.write_char(',')?;
1022 self.write_entity_array(out, "subscribers", node_id, EntityKind::Subscription)?;
1023 out.write_char(',')?;
1024 self.write_entity_array(out, "timers", node_id, EntityKind::Timer)?;
1028 out.write_char(',')?;
1029 self.write_entity_array(out, "services", node_id, EntityKind::ServiceServer)?;
1030 out.write_char(',')?;
1031 self.write_entity_array(out, "actions", node_id, EntityKind::ActionServer)?;
1032 out.write_char(',')?;
1039 self.write_entity_array(out, "action_clients", node_id, EntityKind::ActionClient)?;
1040 out.write_char(',')?;
1041 self.write_entity_array(out, "service_clients", node_id, EntityKind::ServiceClient)
1042 }
1043
1044 #[cfg(feature = "alloc")]
1045 fn write_entity_array(
1046 &self,
1047 out: &mut impl core::fmt::Write,
1048 field: &str,
1049 node_id: &str,
1050 kind: EntityKind,
1051 ) -> core::fmt::Result {
1052 write!(out, "\"{}\":[", field)?;
1053 for (index, entity) in self
1054 .entities
1055 .iter()
1056 .filter(|entity| entity.node_id.as_str() == node_id && entity.kind == kind)
1057 .enumerate()
1058 {
1059 if index > 0 {
1060 out.write_char(',')?;
1061 }
1062 match kind {
1063 EntityKind::Publisher => write_publisher_json(out, entity)?,
1064 EntityKind::Subscription => write_subscriber_json(out, entity)?,
1065 EntityKind::Timer => write_timer_json(out, entity)?,
1066 EntityKind::ServiceServer => write_service_json(out, entity)?,
1067 EntityKind::ActionServer => write_action_json(out, entity)?,
1068 EntityKind::ActionClient | EntityKind::ServiceClient => {
1072 write_client_json(out, entity)?
1073 }
1074 _ => {}
1075 }
1076 }
1077 write!(out, "]")
1078 }
1079
1080 #[cfg(feature = "alloc")]
1081 fn write_callbacks_json(&self, out: &mut impl core::fmt::Write) -> core::fmt::Result {
1082 let callbacks = self.source_callbacks();
1083 write!(out, "\"callbacks\":[")?;
1084 for (index, callback) in callbacks.iter().enumerate() {
1085 if index > 0 {
1086 out.write_char(',')?;
1087 }
1088 write!(out, "{{")?;
1089 write_json_field(out, "id", callback.id.as_str())?;
1090 out.write_char(',')?;
1091 if let Some(slot) = callback.slot {
1092 write!(out, "\"declaration_slot\":{},", slot.index())?;
1093 }
1094 write_json_field(out, "kind", callback.kind)?;
1095 out.write_char(',')?;
1096 if let Some(group) = callback.group.as_ref() {
1097 write_json_field(out, "group", group)?;
1098 out.write_char(',')?;
1099 } else {
1100 write!(out, "\"group\":null,")?;
1101 }
1102 write!(out, "\"effects\":[")?;
1103 for (effect_index, effect) in self
1104 .callback_effects
1105 .iter()
1106 .filter(|effect| effect.callback_id.as_str() == callback.id)
1107 .enumerate()
1108 {
1109 if effect_index > 0 {
1110 out.write_char(',')?;
1111 }
1112 write!(out, "{{")?;
1113 write_json_field(out, "kind", effect_json_kind(effect.kind))?;
1114 out.write_char(',')?;
1115 write_json_field(out, "entity", effect.entity_id.as_str())?;
1116 if let Some(entity_slot) = effect.entity_slot {
1117 write!(out, ",\"entity_slot\":{}", entity_slot.index())?;
1118 }
1119 write!(out, "}}")?;
1120 }
1121 write!(out, "],")?;
1122 write_source_location(out, &callback.source)?;
1123 write!(out, "}}")?;
1124 }
1125 write!(out, "]")
1126 }
1127
1128 #[cfg(feature = "alloc")]
1129 fn write_parameters_json(&self, out: &mut impl core::fmt::Write) -> core::fmt::Result {
1130 write!(out, "\"parameters\":[")?;
1131 for (index, entity) in self
1132 .entities
1133 .iter()
1134 .filter(|entity| entity.kind == EntityKind::Parameter)
1135 .enumerate()
1136 {
1137 if index > 0 {
1138 out.write_char(',')?;
1139 }
1140 write!(out, "{{")?;
1141 write_json_field(out, "node", entity.node_id.as_str())?;
1142 out.write_char(',')?;
1143 if let Some(slot) = entity.slot {
1144 write!(out, "\"declaration_slot\":{},", slot.index())?;
1145 }
1146 write_json_field(out, "name", entity.source_name.as_str())?;
1147 out.write_char(',')?;
1148 let parameter_type = entity.parameter_type.unwrap_or_else(|| {
1153 entity
1154 .parameter_default
1155 .as_ref()
1156 .map(ParameterDefault::parameter_type)
1157 .unwrap_or(ParameterType::NotSet)
1158 });
1159 write_json_field(out, "type", parameter_type_json(parameter_type))?;
1160 out.write_char(',')?;
1161 write!(out, "\"default\":")?;
1162 write_parameter_default(out, entity.parameter_default.as_ref())?;
1163 out.write_char(',')?;
1164 write!(out, "\"read_only\":{},", entity.parameter_read_only)?;
1165 write_source_location(out, &entity.source)?;
1166 write!(out, "}}")?;
1167 }
1168 write!(out, "]")
1169 }
1170
1171 #[cfg(feature = "alloc")]
1172 fn write_trace_json(
1173 &self,
1174 export: &SourceMetadataExport<'_>,
1175 out: &mut impl core::fmt::Write,
1176 ) -> core::fmt::Result {
1177 write!(out, "\"trace\":{{")?;
1178 write_json_field(out, "generator", "nros-metadata-rust")?;
1179 out.write_char(',')?;
1180 write_json_field(out, "package_manifest", export.package_manifest)?;
1181 out.write_char(',')?;
1182 write!(out, "\"source_artifacts\":[")?;
1183 for (index, artifact) in export.source_artifacts.iter().enumerate() {
1184 if index > 0 {
1185 out.write_char(',')?;
1186 }
1187 write_json_string(out, artifact)?;
1188 }
1189 write!(out, "]}}")
1190 }
1191
1192 #[cfg(feature = "alloc")]
1193 fn source_callbacks(&self) -> StdVec<SourceCallbackRef> {
1194 let mut callbacks = StdVec::new();
1195 for entity in &self.entities {
1196 let Some(callback_id) = entity.callback_id.as_ref() else {
1197 continue;
1198 };
1199 let kind = match entity.kind {
1200 EntityKind::Subscription => "subscription",
1201 EntityKind::Timer if entity.timer_kind == TimerKind::GuardCondition => {
1202 "guard_condition"
1203 }
1204 EntityKind::Timer => "timer",
1205 EntityKind::ServiceServer => "service",
1206 EntityKind::ActionServer => "action_goal",
1207 _ => continue,
1208 };
1209 if !callbacks
1210 .iter()
1211 .any(|callback: &SourceCallbackRef| callback.id == callback_id.as_str())
1212 {
1213 callbacks.push(SourceCallbackRef {
1214 id: callback_id.as_str().into(),
1215 slot: entity.callback_slot,
1216 kind,
1217 source: entity.callback_source.clone(),
1218 group: entity
1219 .callback_group
1220 .as_ref()
1221 .map(|group| group.as_str().into()),
1222 });
1223 }
1224 if entity.kind == EntityKind::ActionServer {
1225 if let Some(cancel_id) = entity.action_cancel_callback_id.as_ref()
1226 && !callbacks
1227 .iter()
1228 .any(|callback: &SourceCallbackRef| callback.id == cancel_id.as_str())
1229 {
1230 callbacks.push(SourceCallbackRef {
1231 id: cancel_id.as_str().into(),
1232 slot: entity.action_cancel_callback_slot,
1233 kind: "action_cancel",
1234 source: entity.action_cancel_source.clone(),
1235 group: entity
1236 .callback_group
1237 .as_ref()
1238 .map(|group| group.as_str().into()),
1239 });
1240 }
1241 if let Some(accepted_id) = entity.action_accepted_callback_id.as_ref()
1242 && !callbacks
1243 .iter()
1244 .any(|callback: &SourceCallbackRef| callback.id == accepted_id.as_str())
1245 {
1246 callbacks.push(SourceCallbackRef {
1247 id: accepted_id.as_str().into(),
1248 slot: entity.action_accepted_callback_slot,
1249 kind: "action_accepted",
1250 source: entity.action_accepted_source.clone(),
1251 group: entity
1252 .callback_group
1253 .as_ref()
1254 .map(|group| group.as_str().into()),
1255 });
1256 }
1257 }
1258 }
1259 callbacks
1260 }
1261}
1262
1263#[cfg(feature = "alloc")]
1264struct SourceCallbackRef {
1265 id: StdString,
1266 slot: Option<CallbackSlot>,
1267 kind: &'static str,
1268 source: SourceLocationMetadata,
1269 group: Option<StdString>,
1270}
1271
1272pub(crate) fn entity_callback_ids(entity: &EntityMetadata) -> [Option<&MetadataString>; 3] {
1273 [
1274 entity.callback_id.as_ref(),
1275 entity.action_cancel_callback_id.as_ref(),
1276 entity.action_accepted_callback_id.as_ref(),
1277 ]
1278}
1279
1280pub struct EntityMetadataSpec<'a> {
1288 pub id: EntityId<'a>,
1289 pub node_id: NodeId<'a>,
1290 pub kind: EntityKind,
1291 pub source_name: &'a str,
1292 pub type_name: &'static str,
1293 pub type_hash: &'static str,
1294 pub qos: QoSProfile,
1295}
1296
1297pub fn entity_metadata(spec: EntityMetadataSpec<'_>) -> Result<EntityMetadata, NodeMetadataError> {
1300 let EntityMetadataSpec {
1301 id,
1302 node_id,
1303 kind,
1304 source_name,
1305 type_name,
1306 type_hash,
1307 qos,
1308 } = spec;
1309 Ok(EntityMetadata {
1310 slot: None,
1311 id: copy_str(id.as_str())?,
1312 node_slot: None,
1313 node_id: copy_str(node_id.as_str())?,
1314 kind,
1315 source_name: copy_str(source_name)?,
1316 source_name_kind: SourceNameKind::from_source_name(source_name),
1317 type_name,
1318 type_hash,
1319 action_hashes: None,
1320 qos,
1321 callback_slot: None,
1322 callback_id: None,
1323 callback_source: SourceLocationMetadata::empty(),
1324 callback_group: None,
1325 action_cancel_callback_slot: None,
1326 action_cancel_callback_id: None,
1327 action_cancel_source: SourceLocationMetadata::empty(),
1328 action_accepted_callback_slot: None,
1329 action_accepted_callback_id: None,
1330 action_accepted_source: SourceLocationMetadata::empty(),
1331 period_ms: None,
1332 period_us: None,
1333 timer_clock: TimerClockSource::Steady,
1334 timer_kind: TimerKind::Wall,
1335 parameter_type: None,
1336 parameter_default: None,
1337 parameter_read_only: false,
1338 safety: false,
1339 in_place_capable: None,
1342 buffered_row: None,
1343 source: SourceLocationMetadata::empty(),
1344 })
1345}
1346
1347#[cfg(feature = "alloc")]
1348fn write_publisher_json(
1349 out: &mut impl core::fmt::Write,
1350 entity: &EntityMetadata,
1351) -> core::fmt::Result {
1352 write!(out, "{{")?;
1353 write_json_field(out, "id", entity.id.as_str())?;
1354 out.write_char(',')?;
1355 if let Some(slot) = entity.slot {
1356 write!(out, "\"declaration_slot\":{},", slot.index())?;
1357 }
1358 write!(out, "\"unresolved_topic\":")?;
1359 write_source_name(out, entity.source_name.as_str(), entity.source_name_kind)?;
1360 out.write_char(',')?;
1361 write_interface(out, entity.type_name, "message")?;
1362 out.write_char(',')?;
1363 write_qos(out, entity.qos)?;
1364 write!(out, "}}")
1365}
1366
1367#[cfg(feature = "alloc")]
1368fn write_subscriber_json(
1369 out: &mut impl core::fmt::Write,
1370 entity: &EntityMetadata,
1371) -> core::fmt::Result {
1372 write!(out, "{{")?;
1373 write_json_field(out, "id", entity.id.as_str())?;
1374 out.write_char(',')?;
1375 if let Some(slot) = entity.slot {
1376 write!(out, "\"declaration_slot\":{},", slot.index())?;
1377 }
1378 write!(out, "\"unresolved_topic\":")?;
1379 write_source_name(out, entity.source_name.as_str(), entity.source_name_kind)?;
1380 out.write_char(',')?;
1381 write_interface(out, entity.type_name, "message")?;
1382 out.write_char(',')?;
1383 write_qos(out, entity.qos)?;
1384 out.write_char(',')?;
1385 write_json_field(
1386 out,
1387 "callback",
1388 entity
1389 .callback_id
1390 .as_ref()
1391 .map(|id| id.as_str())
1392 .unwrap_or(""),
1393 )?;
1394 if let Some(callback_slot) = entity.callback_slot {
1395 write!(out, ",\"callback_slot\":{}", callback_slot.index())?;
1396 }
1397 if let Some(in_place) = entity.in_place_capable {
1402 write!(out, ",\"in_place\":{in_place}")?;
1403 }
1404 if let Some(row) = entity.buffered_row {
1407 write!(out, ",\"buffered\":\"{}\"", row.as_str())?;
1408 }
1409 write!(out, "}}")
1410}
1411
1412#[cfg(feature = "alloc")]
1413fn write_timer_json(out: &mut impl core::fmt::Write, entity: &EntityMetadata) -> core::fmt::Result {
1414 write!(out, "{{")?;
1415 write_json_field(out, "id", entity.id.as_str())?;
1416 out.write_char(',')?;
1417 if let Some(slot) = entity.slot {
1418 write!(out, "\"declaration_slot\":{},", slot.index())?;
1419 }
1420 write_json_field(out, "kind", timer_kind_json(entity.timer_kind))?;
1424 out.write_char(',')?;
1425 write!(out, "\"period_ms\":{},", entity.period_ms.unwrap_or(0))?;
1426 write!(out, "\"period_us\":{},", entity.period_us.unwrap_or(0))?;
1427 write_json_field(
1428 out,
1429 "callback",
1430 entity
1431 .callback_id
1432 .as_ref()
1433 .map(|id| id.as_str())
1434 .unwrap_or(""),
1435 )?;
1436 if let Some(callback_slot) = entity.callback_slot {
1437 write!(out, ",\"callback_slot\":{}", callback_slot.index())?;
1438 }
1439 write!(out, "}}")
1440}
1441
1442#[cfg(feature = "alloc")]
1443fn write_service_json(
1444 out: &mut impl core::fmt::Write,
1445 entity: &EntityMetadata,
1446) -> core::fmt::Result {
1447 write!(out, "{{")?;
1448 write_json_field(out, "id", entity.id.as_str())?;
1449 out.write_char(',')?;
1450 if let Some(slot) = entity.slot {
1451 write!(out, "\"declaration_slot\":{},", slot.index())?;
1452 }
1453 write!(out, "\"unresolved_name\":")?;
1454 write_source_name(out, entity.source_name.as_str(), entity.source_name_kind)?;
1455 out.write_char(',')?;
1456 write_interface(out, entity.type_name, "service")?;
1457 out.write_char(',')?;
1458 write_qos(out, entity.qos)?;
1460 out.write_char(',')?;
1461 write_json_field(
1462 out,
1463 "callback",
1464 entity
1465 .callback_id
1466 .as_ref()
1467 .map(|id| id.as_str())
1468 .unwrap_or(""),
1469 )?;
1470 if let Some(callback_slot) = entity.callback_slot {
1471 write!(out, ",\"callback_slot\":{}", callback_slot.index())?;
1472 }
1473 write!(out, "}}")
1474}
1475
1476#[cfg(feature = "alloc")]
1483fn write_client_json(
1484 out: &mut impl core::fmt::Write,
1485 entity: &EntityMetadata,
1486) -> core::fmt::Result {
1487 let interface_kind = if entity.kind == EntityKind::ActionClient {
1488 "action"
1489 } else {
1490 "service"
1491 };
1492 write!(out, "{{")?;
1493 write_json_field(out, "id", entity.id.as_str())?;
1494 out.write_char(',')?;
1495 if let Some(slot) = entity.slot {
1496 write!(out, "\"declaration_slot\":{},", slot.index())?;
1497 }
1498 write!(out, "\"unresolved_name\":")?;
1499 write_source_name(out, entity.source_name.as_str(), entity.source_name_kind)?;
1500 out.write_char(',')?;
1501 write_interface(out, entity.type_name, interface_kind)?;
1502 out.write_char(',')?;
1503 write_qos(out, entity.qos)?;
1505 write!(out, "}}")
1506}
1507
1508#[cfg(feature = "alloc")]
1509fn write_action_json(
1510 out: &mut impl core::fmt::Write,
1511 entity: &EntityMetadata,
1512) -> core::fmt::Result {
1513 let goal_callback = entity
1514 .callback_id
1515 .as_ref()
1516 .map(|id| id.as_str())
1517 .unwrap_or("");
1518 let cancel_callback = entity
1519 .action_cancel_callback_id
1520 .as_ref()
1521 .map(|id| id.as_str())
1522 .unwrap_or(goal_callback);
1523 let accepted_callback = entity
1524 .action_accepted_callback_id
1525 .as_ref()
1526 .map(|id| id.as_str())
1527 .unwrap_or(goal_callback);
1528 write!(out, "{{")?;
1529 write_json_field(out, "id", entity.id.as_str())?;
1530 out.write_char(',')?;
1531 if let Some(slot) = entity.slot {
1532 write!(out, "\"declaration_slot\":{},", slot.index())?;
1533 }
1534 write!(out, "\"unresolved_name\":")?;
1535 write_source_name(out, entity.source_name.as_str(), entity.source_name_kind)?;
1536 out.write_char(',')?;
1537 write_interface(out, entity.type_name, "action")?;
1538 out.write_char(',')?;
1539 write_qos(out, entity.qos)?;
1541 out.write_char(',')?;
1542 write_json_field(out, "goal_callback", goal_callback)?;
1543 if let Some(callback_slot) = entity.callback_slot {
1544 write!(out, ",\"goal_callback_slot\":{}", callback_slot.index())?;
1545 }
1546 out.write_char(',')?;
1547 write_json_field(out, "cancel_callback", cancel_callback)?;
1548 if let Some(callback_slot) = entity.action_cancel_callback_slot {
1549 write!(out, ",\"cancel_callback_slot\":{}", callback_slot.index())?;
1550 }
1551 out.write_char(',')?;
1552 write_json_field(out, "accepted_callback", accepted_callback)?;
1553 if let Some(callback_slot) = entity.action_accepted_callback_slot {
1554 write!(out, ",\"accepted_callback_slot\":{}", callback_slot.index())?;
1555 }
1556 write!(out, "}}")
1557}
1558
1559#[cfg(feature = "alloc")]
1560fn write_source_name(
1561 out: &mut impl core::fmt::Write,
1562 value: &str,
1563 kind: SourceNameKind,
1564) -> core::fmt::Result {
1565 write!(out, "{{")?;
1566 write_json_field(out, "value", value)?;
1567 out.write_char(',')?;
1568 write_json_field(out, "kind", source_name_kind_json(kind))?;
1569 write!(out, "}}")
1570}
1571
1572#[cfg(feature = "alloc")]
1573fn write_interface(
1574 out: &mut impl core::fmt::Write,
1575 type_name: &str,
1576 fallback_kind: &'static str,
1577) -> core::fmt::Result {
1578 let interface = parse_interface(type_name, fallback_kind);
1579 write!(out, "\"interface\":{{")?;
1580 write_json_field(out, "package", &interface.package)?;
1581 out.write_char(',')?;
1582 write_json_field(out, "name", &interface.name)?;
1583 out.write_char(',')?;
1584 write_json_field(out, "kind", interface.kind)?;
1585 write!(out, "}}")
1586}
1587
1588#[cfg(feature = "alloc")]
1589fn write_qos(out: &mut impl core::fmt::Write, qos: QoSProfile) -> core::fmt::Result {
1590 write!(out, "\"qos\":{{")?;
1591 write_json_field(out, "reliability", reliability_json(qos.reliability))?;
1592 out.write_char(',')?;
1593 write_json_field(out, "durability", durability_json(qos.durability))?;
1594 out.write_char(',')?;
1595 write_json_field(out, "history", history_json(qos.history))?;
1596 out.write_char(',')?;
1597 write!(out, "\"depth\":{},", qos.depth)?;
1598 write_optional_ms(out, "deadline_ms", qos.deadline_ms)?;
1599 out.write_char(',')?;
1600 write_optional_ms(out, "lifespan_ms", qos.lifespan_ms)?;
1601 out.write_char(',')?;
1602 write_json_field(out, "liveliness", liveliness_json(qos.liveliness_kind))?;
1603 out.write_char(',')?;
1604 write_optional_ms(out, "liveliness_lease_duration_ms", qos.liveliness_lease_ms)?;
1605 write!(out, ",\"extensions\":{{}}}}")
1606}
1607
1608#[cfg(feature = "alloc")]
1609fn write_source_location(
1610 out: &mut impl core::fmt::Write,
1611 source: &SourceLocationMetadata,
1612) -> core::fmt::Result {
1613 write!(out, "\"source\":{{")?;
1614 write_json_field(out, "artifact", source.artifact.as_str())?;
1615 out.write_char(',')?;
1616 write!(out, "\"line\":")?;
1617 write_optional_u32(out, source.line)?;
1618 out.write_char(',')?;
1619 write!(out, "\"column\":")?;
1620 write_optional_u32(out, source.column)?;
1621 write!(out, "}}")
1622}
1623
1624#[cfg(feature = "alloc")]
1625fn write_parameter_default(
1626 out: &mut impl core::fmt::Write,
1627 default: Option<&ParameterDefault>,
1628) -> core::fmt::Result {
1629 match default {
1630 Some(ParameterDefault::Bool(value)) => write!(out, "{}", value),
1631 Some(ParameterDefault::Integer(value)) => write!(out, "{}", value),
1632 Some(ParameterDefault::Double(value)) => write!(out, "{}", value.as_str()),
1633 Some(ParameterDefault::String(value)) => write_json_string(out, value.as_str()),
1634 Some(ParameterDefault::BoolArray)
1635 | Some(ParameterDefault::IntegerArray)
1636 | Some(ParameterDefault::DoubleArray)
1637 | Some(ParameterDefault::StringArray)
1638 | None => write!(out, "[]"),
1639 }
1640}
1641
1642#[cfg(feature = "alloc")]
1643fn write_json_field(out: &mut impl core::fmt::Write, name: &str, value: &str) -> core::fmt::Result {
1644 write_json_string(out, name)?;
1645 out.write_char(':')?;
1646 write_json_string(out, value)
1647}
1648
1649#[cfg(feature = "alloc")]
1650fn write_json_opt_field(
1651 out: &mut impl core::fmt::Write,
1652 name: &str,
1653 value: Option<&str>,
1654) -> core::fmt::Result {
1655 write_json_string(out, name)?;
1656 out.write_char(':')?;
1657 if let Some(value) = value {
1658 write_json_string(out, value)
1659 } else {
1660 write!(out, "null")
1661 }
1662}
1663
1664#[cfg(feature = "alloc")]
1665fn write_json_string(out: &mut impl core::fmt::Write, value: &str) -> core::fmt::Result {
1666 out.write_char('"')?;
1667 for ch in value.chars() {
1668 match ch {
1669 '"' => write!(out, "\\\"")?,
1670 '\\' => write!(out, "\\\\")?,
1671 '\n' => write!(out, "\\n")?,
1672 '\r' => write!(out, "\\r")?,
1673 '\t' => write!(out, "\\t")?,
1674 ch if ch.is_control() => write!(out, "\\u{:04x}", ch as u32)?,
1675 ch => out.write_char(ch)?,
1676 }
1677 }
1678 out.write_char('"')
1679}
1680
1681#[cfg(feature = "alloc")]
1682fn write_optional_ms(out: &mut impl core::fmt::Write, name: &str, value: u32) -> core::fmt::Result {
1683 write_json_string(out, name)?;
1684 out.write_char(':')?;
1685 if value == 0 {
1686 write!(out, "null")
1687 } else {
1688 write!(out, "{}", value)
1689 }
1690}
1691
1692#[cfg(feature = "alloc")]
1693fn write_optional_u32(out: &mut impl core::fmt::Write, value: Option<u32>) -> core::fmt::Result {
1694 if let Some(value) = value {
1695 write!(out, "{}", value)
1696 } else {
1697 write!(out, "null")
1698 }
1699}
1700
1701#[cfg(feature = "alloc")]
1702fn source_name_kind_json(kind: SourceNameKind) -> &'static str {
1703 match kind {
1704 SourceNameKind::Absolute => "absolute",
1705 SourceNameKind::Relative => "relative",
1706 SourceNameKind::Private => "private",
1707 }
1708}
1709
1710#[cfg(feature = "alloc")]
1711fn timer_kind_json(kind: TimerKind) -> &'static str {
1712 match kind {
1713 TimerKind::Wall => "wall",
1714 TimerKind::Clock => "clock",
1715 TimerKind::Oneshot => "oneshot",
1716 TimerKind::InGroup => "in_group",
1717 TimerKind::GuardCondition => "guard_condition",
1718 }
1719}
1720
1721#[cfg(feature = "alloc")]
1725fn parameter_type_json(value: ParameterType) -> &'static str {
1726 match value {
1727 ParameterType::NotSet => "not_set",
1728 ParameterType::Bool => "bool",
1729 ParameterType::Integer => "integer",
1730 ParameterType::Double => "double",
1731 ParameterType::String => "string",
1732 ParameterType::ByteArray => "byte_array",
1733 ParameterType::BoolArray => "bool_array",
1734 ParameterType::IntegerArray => "integer_array",
1735 ParameterType::DoubleArray => "double_array",
1736 ParameterType::StringArray => "string_array",
1737 }
1738}
1739
1740#[cfg(feature = "alloc")]
1741fn effect_json_kind(kind: CallbackEffectKind) -> &'static str {
1742 match kind {
1743 CallbackEffectKind::Publishes => "publishes",
1744 CallbackEffectKind::Reads => "reads_parameter",
1745 CallbackEffectKind::Writes => "writes_parameter",
1746 }
1747}
1748
1749#[cfg(feature = "alloc")]
1756fn reliability_json(value: QoSReliabilityPolicy) -> &'static str {
1757 match value {
1758 QoSReliabilityPolicy::SystemDefault => "system_default",
1759 QoSReliabilityPolicy::Reliable => "reliable",
1760 QoSReliabilityPolicy::BestEffort => "best_effort",
1761 QoSReliabilityPolicy::Unknown => "unknown",
1766 }
1767}
1768
1769#[cfg(feature = "alloc")]
1770fn durability_json(value: QoSDurabilityPolicy) -> &'static str {
1771 match value {
1772 QoSDurabilityPolicy::SystemDefault => "system_default",
1773 QoSDurabilityPolicy::Volatile => "volatile",
1774 QoSDurabilityPolicy::TransientLocal => "transient_local",
1775 QoSDurabilityPolicy::Unknown => "unknown",
1777 }
1778}
1779
1780#[cfg(feature = "alloc")]
1781fn history_json(value: QoSHistoryPolicy) -> &'static str {
1782 match value {
1783 QoSHistoryPolicy::SystemDefault => "system_default",
1784 QoSHistoryPolicy::KeepLast => "keep_last",
1785 QoSHistoryPolicy::KeepAll => "keep_all",
1786 QoSHistoryPolicy::Unknown => "unknown",
1788 }
1789}
1790
1791#[cfg(feature = "alloc")]
1792fn liveliness_json(value: QoSLivelinessPolicy) -> &'static str {
1793 match value {
1794 QoSLivelinessPolicy::None => "system_default",
1795 QoSLivelinessPolicy::Automatic => "automatic",
1796 QoSLivelinessPolicy::ManualByTopic => "manual_by_topic",
1797 QoSLivelinessPolicy::ManualByNode => "manual_by_node",
1798 QoSLivelinessPolicy::Unknown => "unknown",
1800 }
1801}
1802
1803#[cfg(feature = "alloc")]
1804struct ParsedInterface {
1805 package: StdString,
1806 name: StdString,
1807 kind: &'static str,
1808}
1809
1810#[cfg(feature = "alloc")]
1811fn parse_interface(type_name: &str, fallback_kind: &'static str) -> ParsedInterface {
1812 let parts: StdVec<&str> = type_name.split("::").collect();
1813 if parts.len() >= 4 {
1814 let package = parts[0].into();
1815 let kind = match parts[1] {
1816 "msg" => "message",
1817 "srv" => "service",
1818 "action" => "action",
1819 _ => fallback_kind,
1820 };
1821 let mut type_leaf = parts[3].trim_end_matches('_');
1822 if type_leaf.is_empty() {
1823 type_leaf = parts.last().copied().unwrap_or("");
1824 }
1825 return ParsedInterface {
1826 package,
1827 name: format!("{}/{}", parts[1], type_leaf),
1828 kind,
1829 };
1830 }
1831
1832 ParsedInterface {
1833 package: StdString::new(),
1834 name: type_name.into(),
1835 kind: fallback_kind,
1836 }
1837}
1838
1839#[cfg(test)]
1840mod tests {
1841 #[test]
1848 fn a_path_longer_than_the_buffer_keeps_its_tail() {
1849 let deep = concat!(
1852 "/deep/nested/nested/nested/nested/nested/nested/nested/nested/",
1853 "nested/nested/nested/nested/nested/nested/nested/nested/nested/",
1854 "ws/build/nros-metadata/metadata-probe/listener/src/lib.rs"
1855 );
1856 assert!(deep.len() > super::METADATA_STRING_CAPACITY);
1857
1858 let got = super::copy_str_keep_tail(deep).expect("must not error on depth");
1859 assert!(got.len() <= super::METADATA_STRING_CAPACITY);
1860 assert!(
1861 got.ends_with("src/lib.rs"),
1862 "the informative tail must survive, got {got:?}"
1863 );
1864 assert!(got.starts_with('…'), "a cut must be visible, got {got:?}");
1865
1866 let short = "src/lib.rs";
1868 assert_eq!(super::copy_str_keep_tail(short).unwrap().as_str(), short);
1869 }
1870
1871 use super::*;
1872 use crate::qos;
1873
1874 #[test]
1875 fn source_name_kind_preserves_unresolved_names() {
1876 assert_eq!(
1877 SourceNameKind::from_source_name("/scan"),
1878 SourceNameKind::Absolute
1879 );
1880 assert_eq!(
1881 SourceNameKind::from_source_name("~/scan"),
1882 SourceNameKind::Private
1883 );
1884 assert_eq!(
1885 SourceNameKind::from_source_name("scan"),
1886 SourceNameKind::Relative
1887 );
1888 }
1889
1890 #[test]
1894 fn expand_name_covers_each_source_name_kind() {
1895 assert_eq!(
1897 expand_name("/scan", "lidar", "/sensing").unwrap().as_str(),
1898 "/scan"
1899 );
1900 assert_eq!(
1902 expand_name("~/scan", "lidar", "/sensing").unwrap().as_str(),
1903 "/sensing/lidar/scan"
1904 );
1905 assert_eq!(
1906 expand_name("~/scan", "lidar", "/").unwrap().as_str(),
1907 "/lidar/scan"
1908 );
1909 assert_eq!(
1911 expand_name("scan", "lidar", "/sensing").unwrap().as_str(),
1912 "/sensing/scan"
1913 );
1914 assert_eq!(expand_name("scan", "lidar", "/").unwrap().as_str(), "/scan");
1915 }
1916
1917 #[test]
1918 fn resolve_name_substitutes_first_matching_rule() {
1919 let remaps = [
1920 ("~/scan", "/points_raw"),
1921 ("/sensing/lidar/scan", "/ignored"),
1922 ];
1923 assert_eq!(
1924 resolve_name("~/scan", "lidar", "/sensing", remaps)
1925 .unwrap()
1926 .as_str(),
1927 "/points_raw"
1928 );
1929 assert_eq!(
1931 resolve_name("other", "lidar", "/sensing", [("/x", "/y")])
1932 .unwrap()
1933 .as_str(),
1934 "/sensing/other"
1935 );
1936 }
1937
1938 #[test]
1939 fn recorder_rejects_duplicate_stable_ids() {
1940 let mut recorder = MetadataRecorder::<1, 2, 1>::new();
1941 recorder
1942 .push_node(NodeId::new("node"), "talker", "/", 0)
1943 .unwrap();
1944
1945 let first = entity_metadata(EntityMetadataSpec {
1946 id: EntityId::new("pub"),
1947 node_id: NodeId::new("node"),
1948 kind: EntityKind::Publisher,
1949 source_name: "chatter",
1950 type_name: "std_msgs::msg::dds_::String_",
1951 type_hash: "hash",
1952 qos: qos::DEFAULT,
1953 })
1954 .unwrap();
1955 recorder.push_entity(first.clone()).unwrap();
1956
1957 assert_eq!(
1958 recorder.push_entity(first),
1959 Err(NodeMetadataError::DuplicateId)
1960 );
1961 }
1962
1963 #[test]
1964 fn recorder_rejects_duplicate_nodes_and_unknown_node_entities() {
1965 let mut recorder = MetadataRecorder::<1, 1, 1>::new();
1966 recorder
1967 .push_node(NodeId::new("node"), "talker", "/", 0)
1968 .unwrap();
1969
1970 assert_eq!(
1971 recorder.push_node(NodeId::new("node"), "other", "/", 0),
1972 Err(NodeMetadataError::DuplicateId)
1973 );
1974
1975 let entity = entity_metadata(EntityMetadataSpec {
1976 id: EntityId::new("pub"),
1977 node_id: NodeId::new("missing_node"),
1978 kind: EntityKind::Publisher,
1979 source_name: "chatter",
1980 type_name: "std_msgs::msg::dds_::String_",
1981 type_hash: "hash",
1982 qos: qos::DEFAULT,
1983 })
1984 .unwrap();
1985
1986 assert_eq!(
1987 recorder.push_entity(entity),
1988 Err(NodeMetadataError::UnknownNode)
1989 );
1990 }
1991
1992 #[test]
1993 fn recorder_assigns_slots_and_source_default_names_by_declaration_order() {
1994 let mut recorder = MetadataRecorder::<2, 4, 3>::new();
1995 recorder
1996 .push_node(NodeId::new("node_alpha"), "talker", "/", 0)
1997 .unwrap();
1998 recorder
1999 .push_node(NodeId::new("node_beta"), "listener", "/demo", 42)
2000 .unwrap();
2001
2002 assert_eq!(recorder.nodes()[0].slot, NodeSlot::new(0));
2003 assert_eq!(recorder.nodes()[0].source_default_name.as_str(), "talker");
2004 assert_eq!(recorder.nodes()[1].slot, NodeSlot::new(1));
2005 assert_eq!(recorder.nodes()[1].source_default_name.as_str(), "listener");
2006
2007 recorder
2008 .push_entity(
2009 entity_metadata(EntityMetadataSpec {
2010 id: EntityId::new("pub_chatter"),
2011 node_id: NodeId::new("node_alpha"),
2012 kind: EntityKind::Publisher,
2013 source_name: "/chatter",
2014 type_name: "std_msgs::msg::dds_::String_",
2015 type_hash: "hash",
2016 qos: qos::DEFAULT,
2017 })
2018 .unwrap(),
2019 )
2020 .unwrap();
2021 let mut subscription = entity_metadata(EntityMetadataSpec {
2022 id: EntityId::new("sub_chatter"),
2023 node_id: NodeId::new("node_beta"),
2024 kind: EntityKind::Subscription,
2025 source_name: "/chatter",
2026 type_name: "std_msgs::msg::dds_::String_",
2027 type_hash: "hash",
2028 qos: qos::DEFAULT,
2029 })
2030 .unwrap();
2031 subscription.callback_id = Some(copy_str("on_message").unwrap());
2032 recorder.push_entity(subscription).unwrap();
2033 let mut timer = entity_metadata(EntityMetadataSpec {
2034 id: EntityId::new("timer_tick"),
2035 node_id: NodeId::new("node_alpha"),
2036 kind: EntityKind::Timer,
2037 source_name: "",
2038 type_name: "",
2039 type_hash: "",
2040 qos: qos::DEFAULT,
2041 })
2042 .unwrap();
2043 timer.callback_id = Some(copy_str("on_tick").unwrap());
2044 recorder.push_entity(timer).unwrap();
2045
2046 assert_eq!(recorder.entities()[0].slot, Some(EntitySlot::new(0)));
2047 assert_eq!(recorder.entities()[0].node_slot, Some(NodeSlot::new(0)));
2048 assert_eq!(recorder.entities()[0].callback_slot, None);
2049 assert_eq!(recorder.entities()[1].slot, Some(EntitySlot::new(1)));
2050 assert_eq!(recorder.entities()[1].node_slot, Some(NodeSlot::new(1)));
2051 assert_eq!(
2052 recorder.entities()[1].callback_slot,
2053 Some(CallbackSlot::new(0))
2054 );
2055 assert_eq!(
2056 recorder.entities()[2].callback_slot,
2057 Some(CallbackSlot::new(1))
2058 );
2059
2060 recorder
2061 .push_callback_effect(
2062 CallbackId::new("on_tick"),
2063 CallbackEffectKind::Publishes,
2064 EntityId::new("pub_chatter"),
2065 )
2066 .unwrap();
2067 assert_eq!(
2068 recorder.callback_effects()[0].callback_slot,
2069 Some(CallbackSlot::new(1))
2070 );
2071 assert_eq!(
2072 recorder.callback_effects()[0].entity_slot,
2073 Some(EntitySlot::new(0))
2074 );
2075 }
2076
2077 #[test]
2078 fn recorder_assigns_distinct_callback_slots_within_one_action_entity() {
2079 let mut recorder = MetadataRecorder::<1, 1, 3>::new();
2080 recorder
2081 .push_node(NodeId::new("node"), "action_node", "/", 0)
2082 .unwrap();
2083 let mut action = entity_metadata(EntityMetadataSpec {
2084 id: EntityId::new("act_count"),
2085 node_id: NodeId::new("node"),
2086 kind: EntityKind::ActionServer,
2087 source_name: "/count",
2088 type_name: "example_interfaces::action::dds_::Fibonacci_",
2089 type_hash: "hash",
2090 qos: qos::DEFAULT,
2091 })
2092 .unwrap();
2093 action.callback_id = Some(copy_str("on_goal").unwrap());
2094 action.action_cancel_callback_id = Some(copy_str("on_cancel").unwrap());
2095 action.action_accepted_callback_id = Some(copy_str("on_accepted").unwrap());
2096
2097 recorder.push_entity(action).unwrap();
2098
2099 assert_eq!(
2100 recorder.entities()[0].callback_slot,
2101 Some(CallbackSlot::new(0))
2102 );
2103 assert_eq!(
2104 recorder.entities()[0].action_cancel_callback_slot,
2105 Some(CallbackSlot::new(1))
2106 );
2107 assert_eq!(
2108 recorder.entities()[0].action_accepted_callback_slot,
2109 Some(CallbackSlot::new(2))
2110 );
2111 }
2112
2113 #[cfg(feature = "alloc")]
2114 #[test]
2115 fn source_metadata_json_uses_agent_a_schema_shape() {
2116 let mut recorder = MetadataRecorder::<1, 7, 1>::new();
2120 recorder
2121 .push_node(NodeId::new("node_talker"), "talker", "/", 0)
2122 .unwrap();
2123 recorder
2124 .push_entity(
2125 entity_metadata(EntityMetadataSpec {
2126 id: EntityId::new("pub_chatter"),
2127 node_id: NodeId::new("node_talker"),
2128 kind: EntityKind::Publisher,
2129 source_name: "chatter",
2130 type_name: "std_msgs::msg::dds_::String_",
2131 type_hash: "hash",
2132 qos: crate::qos::DEFAULT,
2133 })
2134 .unwrap(),
2135 )
2136 .unwrap();
2137 let mut timer = entity_metadata(EntityMetadataSpec {
2138 id: EntityId::new("timer_publish"),
2139 node_id: NodeId::new("node_talker"),
2140 kind: EntityKind::Timer,
2141 source_name: "",
2142 type_name: "",
2143 type_hash: "",
2144 qos: crate::qos::DEFAULT,
2145 })
2146 .unwrap();
2147 timer.callback_id = Some(copy_str("cb_timer").unwrap());
2148 timer.callback_source = SourceLocationMetadata {
2149 artifact: copy_str("src/talker.rs").unwrap(),
2150 line: Some(42),
2151 column: Some(5),
2152 };
2153 timer.period_ms = Some(100);
2154 recorder.push_entity(timer).unwrap();
2155 let mut param = entity_metadata(EntityMetadataSpec {
2156 id: EntityId::new("param_rate"),
2157 node_id: NodeId::new("node_talker"),
2158 kind: EntityKind::Parameter,
2159 source_name: "rate_hz",
2160 type_name: "",
2161 type_hash: "",
2162 qos: crate::qos::DEFAULT,
2163 })
2164 .unwrap();
2165 param.parameter_type = Some(ParameterType::Integer);
2166 param.parameter_default = Some(ParameterDefault::Integer(10));
2167 param.source = SourceLocationMetadata {
2168 artifact: copy_str("src/talker.rs").unwrap(),
2169 line: Some(25),
2170 column: Some(9),
2171 };
2172 recorder.push_entity(param).unwrap();
2173 let mut action = entity_metadata(EntityMetadataSpec {
2174 id: EntityId::new("act_count"),
2175 node_id: NodeId::new("node_talker"),
2176 kind: EntityKind::ActionServer,
2177 source_name: "~/count",
2178 type_name: "example_interfaces::action::dds_::Fibonacci_",
2179 type_hash: "hash",
2180 qos: crate::qos::DEFAULT,
2181 })
2182 .unwrap();
2183 action.callback_id = Some(copy_str("cb_count_goal").unwrap());
2184 action.callback_source = SourceLocationMetadata {
2185 artifact: copy_str("src/talker.rs").unwrap(),
2186 line: Some(90),
2187 column: Some(5),
2188 };
2189 action.action_cancel_callback_id = Some(copy_str("cb_count_cancel").unwrap());
2190 action.action_cancel_source = SourceLocationMetadata {
2191 artifact: copy_str("src/talker.rs").unwrap(),
2192 line: Some(96),
2193 column: Some(5),
2194 };
2195 action.action_accepted_callback_id = Some(copy_str("cb_count_accepted").unwrap());
2196 action.action_accepted_source = SourceLocationMetadata {
2197 artifact: copy_str("src/talker.rs").unwrap(),
2198 line: Some(104),
2199 column: Some(5),
2200 };
2201 recorder.push_entity(action).unwrap();
2202 recorder
2203 .push_callback_effect(
2204 CallbackId::new("cb_timer"),
2205 CallbackEffectKind::Publishes,
2206 EntityId::new("pub_chatter"),
2207 )
2208 .unwrap();
2209
2210 recorder
2214 .push_entity(
2215 entity_metadata(EntityMetadataSpec {
2216 id: EntityId::new("client_fib"),
2217 node_id: NodeId::new("node_talker"),
2218 kind: EntityKind::ActionClient,
2219 source_name: "/fibonacci",
2220 type_name: "example_interfaces::action::dds_::Fibonacci_",
2221 type_hash: "hash",
2222 qos: crate::qos::DEFAULT,
2223 })
2224 .unwrap(),
2225 )
2226 .unwrap();
2227 recorder
2228 .push_entity(
2229 entity_metadata(EntityMetadataSpec {
2230 id: EntityId::new("client_add"),
2231 node_id: NodeId::new("node_talker"),
2232 kind: EntityKind::ServiceClient,
2233 source_name: "/add_two_ints",
2234 type_name: "example_interfaces::srv::dds_::AddTwoInts_",
2235 type_hash: "hash",
2236 qos: crate::qos::DEFAULT,
2237 })
2238 .unwrap(),
2239 )
2240 .unwrap();
2241
2242 let json = recorder
2243 .to_source_metadata_json(
2244 &SourceMetadataExport::new("demo_nodes_rs", "talker")
2245 .executable("talker")
2246 .exported_symbol("nros_node_talker")
2247 .source_artifacts(&["src/talker.rs"]),
2248 )
2249 .unwrap();
2250
2251 assert!(
2255 json.contains(&format!("\"version\":{SOURCE_METADATA_SCHEMA_VERSION}")),
2256 "the current schema version, got {json}"
2257 );
2258 assert!(json.contains("\"language\":\"rust\""));
2259 assert!(json.contains("\"unresolved_name\":{\"value\":\"talker\",\"kind\":\"relative\"}"));
2260 assert!(json.contains(
2261 "\"interface\":{\"package\":\"std_msgs\",\"name\":\"msg/String\",\"kind\":\"message\"}"
2262 ));
2263 assert!(json.contains("\"kind\":\"publishes\",\"entity\":\"pub_chatter\""));
2264 assert!(
2265 json.contains("\"source\":{\"artifact\":\"src/talker.rs\",\"line\":42,\"column\":5}")
2266 );
2267 assert!(
2269 json.contains(
2270 "\"name\":\"rate_hz\",\"type\":\"integer\",\"default\":10,\"read_only\":false"
2271 ),
2272 "got {json}"
2273 );
2274 assert!(json.contains("\"goal_callback\":\"cb_count_goal\""));
2275 assert!(json.contains("\"cancel_callback\":\"cb_count_cancel\""));
2276 assert!(json.contains("\"accepted_callback\":\"cb_count_accepted\""));
2277 assert!(json.contains("\"kind\":\"action_cancel\""));
2278 assert!(json.contains("\"kind\":\"action_accepted\""));
2279
2280 assert!(
2291 json.contains("\"action_clients\":[{"),
2292 "an action client must reach the sidecar, got {json}"
2293 );
2294 assert!(
2295 json.contains("\"service_clients\":[{"),
2296 "a service client must reach the sidecar, got {json}"
2297 );
2298 let action_clients = &json[json.find("\"action_clients\":").expect("array present")..];
2305 let action_clients = &action_clients[..action_clients.find(']').unwrap()];
2306 let service_clients = &json[json.find("\"service_clients\":").expect("array present")..];
2307 let service_clients = &service_clients[..service_clients.find(']').unwrap()];
2308
2309 assert!(
2315 action_clients.contains(
2316 "\"interface\":{\"package\":\"example_interfaces\",\"name\":\"action/Fibonacci\",\"kind\":\"action\"}"
2317 ),
2318 "action client interface must be parsed, got {action_clients}"
2319 );
2320 assert!(
2321 service_clients.contains(
2322 "\"interface\":{\"package\":\"example_interfaces\",\"name\":\"srv/AddTwoInts\",\"kind\":\"service\"}"
2323 ),
2324 "service client interface must be parsed, got {service_clients}"
2325 );
2326 assert!(
2329 !action_clients.contains("\"kind\":\"service\"")
2330 && !service_clients.contains("\"kind\":\"action\""),
2331 "the client kind word is what distinguishes the arrays"
2332 );
2333 assert!(
2336 !action_clients.contains("callback") && !service_clients.contains("callback"),
2337 "a client registers no callback; emitting one would misdescribe it"
2338 );
2339 assert!(json.contains("\"generator\":\"nros-metadata-rust\""));
2340 }
2341}