Skip to main content

zerodds_discovery/type_lookup/
mod.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! TypeLookup-Service — XTypes 1.3 §7.6.3.3.4.
4//!
5//! ## Module
6//!
7//! - [`TypeLookupStack`]: Single-Object-Helper (Request-Builder +
8//!   Reply-Parser + Registry-Auflösung).
9//! - [`server::TypeLookupServer`]: server-side Handler mit Pagination.
10//! - [`client::TypeLookupClient`]: client-side Correlation-Table mit
11//!   pending-Request-Cap.
12//! - [`endpoints::TypeLookupEndpoints`]: 4 Builtin-Endpoint-GUIDs
13//!   (TL_SVC_REQ/REPLY × WRITER/READER) + service-instance-name
14//!   Formatter.
15//!
16//! ## Layer-Boundary an DCPS
17//!
18//! Dieses Modul liefert die **wire-format-vollständigen** Primitives:
19//! Request-/Reply-Bytes, Server-Pagination, Client-Correlation. Die
20//! Instantiierung der vier Reliable-Writer/Reader-Pairs auf den
21//! `TL_SVC_*`-GUIDs liegt im DCPS-Layer
22//! (`crates/dcps/src/runtime.rs` Builtin-Endpoint-Spawn-Pfad), analog
23//! zu SEDP-Builtin-Endpoints.
24
25pub mod client;
26pub mod endpoints;
27pub mod server;
28
29pub use client::{
30    ClientCallback, RequestId, TypeLookupClient, TypeLookupReply, hashes_to_minimal_ids,
31    request_dependencies_payload, request_types_payload,
32};
33pub use endpoints::{
34    TYPELOOKUP_TOPIC_PREFIX, TypeLookupEndpoints, format_service_instance_name,
35    format_service_instance_name_short,
36};
37pub use server::TypeLookupServer;
38
39use alloc::vec::Vec;
40
41use zerodds_cdr::{BufferReader, BufferWriter, EncodeError, Endianness};
42use zerodds_rtps::wire_types::{EntityId, Guid, GuidPrefix};
43use zerodds_types::error::TypeCodecError;
44use zerodds_types::resolve::TypeRegistry;
45use zerodds_types::type_lookup::{
46    ContinuationPoint, GetTypeDependenciesReply, GetTypeDependenciesRequest, GetTypesReply,
47    GetTypesRequest, ReplyTypeObject,
48};
49use zerodds_types::type_object::TypeObject;
50use zerodds_types::{EquivalenceHash, TypeIdentifier};
51
52/// TypeLookup-Stack fuer einen lokalen Participant.
53#[derive(Debug)]
54pub struct TypeLookupStack {
55    /// Eigener Prefix (fuer Request-/Reply-GUIDs).
56    pub local_prefix: GuidPrefix,
57    /// Registry von bereits empfangenen TypeObjects.
58    pub registry: TypeRegistry,
59    /// Zaehler fuer Request-Sequenzen (fuer Sample-Identity).
60    next_request_seq: u64,
61}
62
63impl TypeLookupStack {
64    /// Konstruiert einen leeren Stack.
65    #[must_use]
66    pub fn new(local_prefix: GuidPrefix) -> Self {
67        Self {
68            local_prefix,
69            registry: TypeRegistry::new(),
70            next_request_seq: 1,
71        }
72    }
73
74    /// GUID des Request-Writers (von dem wir senden).
75    #[must_use]
76    pub fn request_writer_guid(&self) -> Guid {
77        Guid::new(self.local_prefix, EntityId::TL_SVC_REQ_WRITER)
78    }
79
80    /// GUID des Reply-Readers (auf dem wir empfangen).
81    #[must_use]
82    pub fn reply_reader_guid(&self) -> Guid {
83        Guid::new(self.local_prefix, EntityId::TL_SVC_REPLY_READER)
84    }
85
86    /// Baut einen `getTypes`-Request fuer die angegebenen Hashes.
87    /// Return: request-Payload-Bytes + neue Sequence-ID.
88    ///
89    /// # Errors
90    /// `EncodeError` bei zu grossen Listen.
91    pub fn make_get_types_request(
92        &mut self,
93        hashes: &[EquivalenceHash],
94        minimal: bool,
95    ) -> Result<(Vec<u8>, u64), EncodeError> {
96        let seq = self.next_request_seq;
97        self.next_request_seq = self.next_request_seq.saturating_add(1);
98
99        let type_ids: Vec<TypeIdentifier> = hashes
100            .iter()
101            .map(|h| {
102                if minimal {
103                    TypeIdentifier::EquivalenceHashMinimal(*h)
104                } else {
105                    TypeIdentifier::EquivalenceHashComplete(*h)
106                }
107            })
108            .collect();
109        let req = GetTypesRequest { type_ids };
110        let mut w = BufferWriter::new(Endianness::Little);
111        req.encode_into(&mut w)?;
112        Ok((w.into_bytes(), seq))
113    }
114
115    /// Verarbeitet ein empfangenes `getTypes`-Reply und tragt alle
116    /// enthaltenen TypeObjects in die Registry ein.
117    ///
118    /// # Errors
119    /// Decode-Fehler oder Hash-Berechnung scheitert.
120    pub fn handle_get_types_reply(&mut self, bytes: &[u8]) -> Result<usize, TypeCodecError> {
121        let mut r = BufferReader::new(bytes, Endianness::Little);
122        let reply = GetTypesReply::decode_from(&mut r)?;
123        let mut count = 0;
124        for item in reply.types {
125            match item {
126                ReplyTypeObject::Minimal(m) => {
127                    let hash = zerodds_types::compute_hash(&TypeObject::Minimal(m.clone()))?;
128                    self.registry.insert_minimal(hash, m);
129                    count += 1;
130                }
131                ReplyTypeObject::Complete(c) => {
132                    let hash = zerodds_types::compute_hash(&TypeObject::Complete(c.clone()))?;
133                    self.registry.insert_complete(hash, c);
134                    count += 1;
135                }
136            }
137        }
138        Ok(count)
139    }
140
141    /// Baut einen `getTypeDependencies`-Request.
142    ///
143    /// # Errors
144    /// Encode.
145    pub fn make_get_dependencies_request(
146        &self,
147        hashes: &[EquivalenceHash],
148        cont: ContinuationPoint,
149    ) -> Result<Vec<u8>, EncodeError> {
150        let req = GetTypeDependenciesRequest {
151            type_ids: hashes
152                .iter()
153                .map(|h| TypeIdentifier::EquivalenceHashMinimal(*h))
154                .collect(),
155            continuation_point: cont,
156        };
157        let mut w = BufferWriter::new(Endianness::Little);
158        req.encode_into(&mut w)?;
159        Ok(w.into_bytes())
160    }
161
162    /// Decoded ein getTypeDependencies-Reply (fuer Tests / Caller).
163    ///
164    /// # Errors
165    /// Decode.
166    pub fn parse_dependencies_reply(
167        &self,
168        bytes: &[u8],
169    ) -> Result<GetTypeDependenciesReply, TypeCodecError> {
170        let mut r = BufferReader::new(bytes, Endianness::Little);
171        GetTypeDependenciesReply::decode_from(&mut r)
172    }
173
174    /// Responder-Helper: baut einen `getTypes`-Reply aus der Registry.
175    /// Unbekannte Hashes werden ausgelassen.
176    ///
177    /// # Errors
178    /// Encode.
179    pub fn build_get_types_reply(
180        &self,
181        hashes: &[EquivalenceHash],
182        minimal: bool,
183    ) -> Result<Vec<u8>, EncodeError> {
184        let mut types = Vec::new();
185        for h in hashes {
186            if minimal {
187                if let Some(m) = self.registry.get_minimal(h) {
188                    types.push(ReplyTypeObject::Minimal(m.clone()));
189                }
190            } else if let Some(c) = self.registry.get_complete(h) {
191                types.push(ReplyTypeObject::Complete(c.clone()));
192            }
193        }
194        let reply = GetTypesReply { types };
195        let mut w = BufferWriter::new(Endianness::Little);
196        reply.encode_into(&mut w)?;
197        Ok(w.into_bytes())
198    }
199}
200
201#[cfg(test)]
202#[allow(clippy::unwrap_used)]
203mod tests {
204    use super::*;
205    use zerodds_types::builder::TypeObjectBuilder;
206    use zerodds_types::{MinimalTypeObject, PrimitiveKind};
207
208    fn sample_struct() -> MinimalTypeObject {
209        MinimalTypeObject::Struct(
210            TypeObjectBuilder::struct_type("::X")
211                .member("a", TypeIdentifier::Primitive(PrimitiveKind::Int64), |m| m)
212                .build_minimal(),
213        )
214    }
215
216    #[test]
217    fn make_and_parse_get_types_roundtrip() {
218        let mut stack = TypeLookupStack::new(GuidPrefix::from_bytes([1; 12]));
219        let hash = zerodds_types::compute_minimal_hash(&sample_struct()).unwrap();
220        let (bytes, seq) = stack.make_get_types_request(&[hash], true).unwrap();
221        assert_eq!(seq, 1);
222        let mut r = BufferReader::new(&bytes, Endianness::Little);
223        let decoded = GetTypesRequest::decode_from(&mut r).unwrap();
224        assert_eq!(decoded.type_ids.len(), 1);
225    }
226
227    #[test]
228    fn responder_round_trip_via_registry() {
229        let mut responder = TypeLookupStack::new(GuidPrefix::from_bytes([2; 12]));
230        let m = sample_struct();
231        let hash = zerodds_types::compute_minimal_hash(&m).unwrap();
232        responder.registry.insert_minimal(hash, m);
233
234        let reply_bytes = responder.build_get_types_reply(&[hash], true).unwrap();
235
236        let mut requester = TypeLookupStack::new(GuidPrefix::from_bytes([3; 12]));
237        let n = requester.handle_get_types_reply(&reply_bytes).unwrap();
238        assert_eq!(n, 1);
239        assert!(requester.registry.get_minimal(&hash).is_some());
240    }
241
242    #[test]
243    fn request_seq_increments() {
244        let mut s = TypeLookupStack::new(GuidPrefix::from_bytes([0; 12]));
245        let (_, s1) = s.make_get_types_request(&[], true).unwrap();
246        let (_, s2) = s.make_get_types_request(&[], true).unwrap();
247        assert_eq!(s1, 1);
248        assert_eq!(s2, 2);
249    }
250}