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` und `SubscriptionBuiltinTopicDataExt`
7//! erweitern die Standard-DCPS-Discovery-Daten um RPC-Service-
8//! Identitaet (Service-Name, Mapping-Profil, Topic-Aliases fuer
9//! Inheritance).
10//!
11//! # Spec-Mapping
12//!
13//! * **§7.6.2.1.1** [`PublicationBuiltinTopicDataExt`] — extended
14//!   Publication-Data mit RPC-Felder.
15//! * **§7.6.2.1.2** [`SubscriptionBuiltinTopicDataExt`] — analog.
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 der Standard-PublicationBuiltinTopicData.
25#[derive(Debug, Clone, PartialEq, Eq, Default)]
26pub struct PublicationBuiltinTopicDataExt {
27    /// Service-Name aus IDL-`@service`-Annotation.
28    pub service_name: String,
29    /// Mapping-Profil ("Basic" oder "Enhanced").
30    pub mapping_profile: ServiceMappingProfile,
31    /// Topic-Aliases fuer Interface-Inheritance (Spec §7.5.1.2.6).
32    pub topic_aliases: Vec<String>,
33}
34
35/// Spec §7.6.2.1.2 Extension der Standard-SubscriptionBuiltinTopicData.
36#[derive(Debug, Clone, PartialEq, Eq, Default)]
37pub struct SubscriptionBuiltinTopicDataExt {
38    /// Service-Name aus IDL-`@service`-Annotation.
39    pub service_name: String,
40    /// Mapping-Profil.
41    pub mapping_profile: ServiceMappingProfile,
42    /// Topic-Aliases fuer Interface-Inheritance.
43    pub topic_aliases: Vec<String>,
44}
45
46/// Service-Mapping-Profil (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 mit X-Types-Aliases.
53    Enhanced,
54}
55
56/// Spec §7.6.2.2.1: Client-Side-Matching via extended publication data.
57///
58/// Client matched einen Service wenn:
59/// 1. Service-Name uebereinstimmt.
60/// 2. Mapping-Profil kompatibel (Enhanced akzeptiert 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 analog.
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-Kompatibilitaet: gleicher Profile-Typ matched immer; Basic
91/// und Enhanced sind nicht direkt cross-kompatibel (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 muessen dasselbe Mapping nutzen.
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}