Skip to main content

zerodds_rpc/
discovery_ext.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! RPC discovery extensions (Spec §7.6.2.x).
5//!
6//! `PublicationBuiltinTopicDataExt` and `SubscriptionBuiltinTopicDataExt`
7//! extend the standard DCPS discovery data with RPC service
8//! identity (service name, mapping profile, topic aliases for
9//! inheritance).
10//!
11//! # Spec mapping
12//!
13//! * **§7.6.2.1.1** [`PublicationBuiltinTopicDataExt`] — extended
14//!   publication data with RPC fields.
15//! * **§7.6.2.1.2** [`SubscriptionBuiltinTopicDataExt`] — analogous.
16//! * **§7.6.2.2.1** [`client_matches_service`] — client-matching helper.
17//! * **§7.6.2.2.2** [`service_matches_client`] — service-matching helper.
18
19extern crate alloc;
20
21use alloc::string::String;
22use alloc::vec::Vec;
23
24/// Spec §7.6.2.1.1 extension of the standard PublicationBuiltinTopicData.
25#[derive(Debug, Clone, PartialEq, Eq, Default)]
26pub struct PublicationBuiltinTopicDataExt {
27    /// Service name from the IDL `@service` annotation.
28    pub service_name: String,
29    /// Mapping profile ("Basic" or "Enhanced").
30    pub mapping_profile: ServiceMappingProfile,
31    /// Topic aliases for interface inheritance (Spec §7.5.1.2.6).
32    pub topic_aliases: Vec<String>,
33}
34
35/// Spec §7.6.2.1.2 extension of the standard SubscriptionBuiltinTopicData.
36#[derive(Debug, Clone, PartialEq, Eq, Default)]
37pub struct SubscriptionBuiltinTopicDataExt {
38    /// Service name from the IDL `@service` annotation.
39    pub service_name: String,
40    /// Mapping profile.
41    pub mapping_profile: ServiceMappingProfile,
42    /// Topic aliases for interface inheritance.
43    pub topic_aliases: Vec<String>,
44}
45
46/// Service mapping profile (Spec §2.1 + §2.2).
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
48pub enum ServiceMappingProfile {
49    /// Basic mapping (default).
50    #[default]
51    Basic,
52    /// Enhanced mapping with X-Types aliases.
53    Enhanced,
54}
55
56/// Spec §7.6.2.2.1: client-side matching via extended publication data.
57///
58/// A client matches a service if:
59/// 1. The service name matches.
60/// 2. The mapping profile is compatible (Enhanced accepts the Basic subset).
61#[must_use]
62pub fn client_matches_service(
63    client_pub_data: &PublicationBuiltinTopicDataExt,
64    service_sub_data: &SubscriptionBuiltinTopicDataExt,
65) -> bool {
66    if client_pub_data.service_name != service_sub_data.service_name {
67        return false;
68    }
69    profile_compatible(
70        client_pub_data.mapping_profile,
71        service_sub_data.mapping_profile,
72    )
73}
74
75/// Spec §7.6.2.2.2: service-side matching analogously.
76#[must_use]
77pub fn service_matches_client(
78    service_pub_data: &PublicationBuiltinTopicDataExt,
79    client_sub_data: &SubscriptionBuiltinTopicDataExt,
80) -> bool {
81    if service_pub_data.service_name != client_sub_data.service_name {
82        return false;
83    }
84    profile_compatible(
85        service_pub_data.mapping_profile,
86        client_sub_data.mapping_profile,
87    )
88}
89
90/// Profile compatibility: the same profile type always matches; Basic
91/// and Enhanced are not directly cross-compatible (Spec §2.1: "must
92/// use the same Service Mapping").
93fn profile_compatible(a: ServiceMappingProfile, b: ServiceMappingProfile) -> bool {
94    a == b
95}
96
97#[cfg(test)]
98#[allow(clippy::expect_used)]
99mod tests {
100    use super::*;
101
102    fn pub_data(name: &str, profile: ServiceMappingProfile) -> PublicationBuiltinTopicDataExt {
103        PublicationBuiltinTopicDataExt {
104            service_name: name.into(),
105            mapping_profile: profile,
106            topic_aliases: Vec::new(),
107        }
108    }
109
110    fn sub_data(name: &str, profile: ServiceMappingProfile) -> SubscriptionBuiltinTopicDataExt {
111        SubscriptionBuiltinTopicDataExt {
112            service_name: name.into(),
113            mapping_profile: profile,
114            topic_aliases: Vec::new(),
115        }
116    }
117
118    #[test]
119    fn client_matches_service_with_same_name_and_profile() {
120        let p = pub_data("Calc", ServiceMappingProfile::Basic);
121        let s = sub_data("Calc", ServiceMappingProfile::Basic);
122        assert!(client_matches_service(&p, &s));
123    }
124
125    #[test]
126    fn client_does_not_match_service_with_different_name() {
127        let p = pub_data("Calc", ServiceMappingProfile::Basic);
128        let s = sub_data("Other", ServiceMappingProfile::Basic);
129        assert!(!client_matches_service(&p, &s));
130    }
131
132    #[test]
133    fn client_does_not_match_service_with_different_profile() {
134        // Spec §2.1: client + service must use the same mapping.
135        let p = pub_data("Calc", ServiceMappingProfile::Basic);
136        let s = sub_data("Calc", ServiceMappingProfile::Enhanced);
137        assert!(!client_matches_service(&p, &s));
138    }
139
140    #[test]
141    fn service_matches_client_symmetric() {
142        let p = pub_data("Calc", ServiceMappingProfile::Enhanced);
143        let s = sub_data("Calc", ServiceMappingProfile::Enhanced);
144        assert!(service_matches_client(&p, &s));
145    }
146
147    #[test]
148    fn topic_aliases_propagated_in_extended_data() {
149        let p = PublicationBuiltinTopicDataExt {
150            service_name: "Inherited".into(),
151            mapping_profile: ServiceMappingProfile::Enhanced,
152            topic_aliases: alloc::vec!["BaseInterface_Request".into()],
153        };
154        assert_eq!(p.topic_aliases.len(), 1);
155        assert_eq!(p.topic_aliases[0], "BaseInterface_Request");
156    }
157}