Skip to main content

zerodds_dcps/
dds_type.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! `DdsType` — the trait that user types must implement to be sent
4//! over DDS.
5//!
6//! # Usage
7//!
8//! User types implement the trait either by hand or via the codegen
9//! pipeline `zerodds-idl-rust` (IDL → Rust with a derived `DdsType`
10//! impl). The encoder/decoder pairs follow the XCDR2 convention (see
11//! `zerodds-cdr`); the trait stays transport- and QoS-agnostic.
12//!
13//! # Interop note
14//!
15//! For interop with Cyclone/Fast-DDS, the `TYPE_NAME` MUST match the
16//! remote topic type name exactly (strict equality). IDL type
17//! namespacing (e.g. `std_msgs::msg::String`) must be taken into
18//! account.
19
20extern crate alloc;
21use alloc::vec::Vec;
22
23pub use zerodds_cdr::{KEY_HASH_LEN, PlainCdr2BeKeyHolder, compute_key_hash};
24
25/// XTypes 1.3 §7.4.5 struct extensibility kind. Wire-relevant
26/// information for the sample encoder; mirrors the IDL annotations
27/// `@final` / `@appendable` / `@mutable`.
28///
29/// Spec: `zerodds-xcdr2-rust` §2 references this as
30/// `ExtensibilityKind`; the implementation name `Extensibility` and
31/// the spec-aligned alias [`ExtensibilityKind`] are identical.
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
33#[repr(u8)]
34pub enum Extensibility {
35    /// `@final`: tight-packed body, no header.
36    Final = 0,
37    /// `@appendable`: 4-byte DHEADER + body, forward-compatible.
38    Appendable = 1,
39    /// `@mutable`: one EMHEADER + body per member.
40    Mutable = 2,
41}
42
43/// Spec-aligned alias: `zerodds-xcdr2-rust` §2 references the
44/// Extensibility enum under the name `ExtensibilityKind`. We keep
45/// `Extensibility` as the implementation name; both are identical via
46/// the alias.
47pub type ExtensibilityKind = Extensibility;
48
49/// A type that can be published/subscribed via DDS.
50pub trait DdsType: Sized {
51    /// Fully-qualified topic type name (e.g. `"std_msgs::String"`).
52    /// Must match the peer type name exactly (strict matching).
53    const TYPE_NAME: &'static str;
54
55    /// XTypes 1.3 §7.4.5 struct extensibility kind. Default `Final`
56    /// for backwards compat with pre-`EXTENSIBILITY` codegen outputs.
57    /// Spec: zerodds-xcdr2-rust §2.3.
58    const EXTENSIBILITY: Extensibility = Extensibility::Final;
59
60    /// `true` if the topic type is **keyed** (at least one member with
61    /// a `@key` annotation). Default `false` — the caller (proc-macro)
62    /// overrides this for keyed types and also implements
63    /// [`Self::encode_key_holder_be`].
64    ///
65    /// Spec: XTypes 1.3 §7.6.8 (KeyHash requirement for keyed topics).
66    ///
67    /// Note (`zerodds-xcdr2-rust` §11 errata): the spec references this
68    /// field as `IS_KEYED`. We keep `HAS_KEY` for source compat with
69    /// pre-1.0 code; the spec-aligned alias [`Self::IS_KEYED`] always
70    /// returns the same value.
71    const HAS_KEY: bool = false;
72
73    /// Spec-aligned alias for [`Self::HAS_KEY`].
74    /// `zerodds-xcdr2-rust` §2 references this as `IS_KEYED`.
75    const IS_KEYED: bool = Self::HAS_KEY;
76
77    /// Maximum size of the PLAIN_CDR2-BE KeyHolder stream in bytes
78    /// (XTypes 1.3 §7.6.8.4 step 5). `None` = not keyed or unbounded
79    /// (MD5 path). `Some(n)` with `n <= 16` = zero-pad path.
80    const KEY_HOLDER_MAX_SIZE: Option<usize> = None;
81
82    /// `true` if the type is annotated with `@nested` (XTypes 1.3
83    /// §7.4.6.3.5). Nested types are only intended as members of other
84    /// types and MUST NOT be registered as a DDS topic type.
85    /// `DomainParticipant::create_topic` rejects registration of
86    /// nested types with `PreconditionNotMet`.
87    const IS_NESTED: bool = false;
88
89    /// XTypes 1.3 §7.3.4.2 — TypeIdentifier of the type for
90    /// XTypes-aware discovery + compatibility matching. Default
91    /// `TypeIdentifier::None` signals "type-id not provided;
92    /// reader-writer matching falls back to plain `type_name`
93    /// comparison (DDS 1.4 §2.2.3 default path)".
94    ///
95    /// idl-rust codegen emits the appropriate TypeIdentifier here:
96    /// - Primitive `int32` → `TypeIdentifier::Primitive(PrimitiveKind::Int32)`,
97    /// - String `string<N>` → `TypeIdentifier::String8Small{ bound }`,
98    /// - Composite struct → `TypeIdentifier::EquivalenceHash` (once the
99    ///   TypeRegistry lookup is live).
100    ///
101    /// Once both sides (writer + reader) provide a TypeIdentifier, the
102    /// subscriber match path calls
103    /// [`zerodds_types::type_matcher::TypeMatcher::match_types`]
104    /// (XTypes §7.6.3.7 + DDS 1.4 §2.2.3 TypeConsistencyEnforcement).
105    const TYPE_IDENTIFIER: zerodds_types::TypeIdentifier = zerodds_types::TypeIdentifier::None;
106
107    /// Serializes `self` into the XCDR2 payload sent as the
108    /// `serialized_payload` of a DATA submessage. Default endianness:
109    /// little-endian (RTPS 2.5 §10.5
110    /// `RepresentationIdentifier = CDR2_LE = 0x0010`).
111    ///
112    /// # Errors
113    /// CDR encoder error (buffer overflow, etc.).
114    fn encode(&self, out: &mut Vec<u8>) -> core::result::Result<(), EncodeError>;
115
116    /// Big-endian variant of [`Self::encode`]. The default
117    /// implementation delegates to [`Self::encode`] (no byte swap),
118    /// since a generic BE re-encode is not possible without type
119    /// reflection. Codegen overrides this for structures that should
120    /// genuinely go on the wire as BE. Spec: zerodds-xcdr2-rust §2.4.
121    ///
122    /// # Errors
123    /// CDR encoder error.
124    fn encode_be(&self, out: &mut Vec<u8>) -> core::result::Result<(), EncodeError> {
125        self.encode(out)
126    }
127
128    /// Deserializes a little-endian XCDR2 payload. The caller ensures that
129    /// `bytes` contains the full sample payload (encapsulation header already
130    /// stripped).
131    ///
132    /// # Errors
133    /// CDR decoder error (truncation, unexpected bytes, etc.).
134    fn decode(bytes: &[u8]) -> core::result::Result<Self, DecodeError>;
135
136    /// Big-endian variant of [`Self::decode`] — for a payload whose
137    /// encapsulation header declared a big-endian representation identifier
138    /// (`CDR_BE = 0x0000`, `PL_CDR_BE = 0x0002`, `CDR2_BE = 0x0006`,
139    /// `D_CDR2_BE = 0x0008`, `PL_CDR2_BE = 0x000a`). The default implementation
140    /// delegates to [`Self::decode`] (little-endian) so pre-`decode_be` codegen
141    /// keeps working on the canonical wire; idl-rust codegen overrides this to
142    /// build a big-endian reader. Symmetric to [`Self::encode_be`]. Spec:
143    /// zerodds-xcdr2-rust §2.4; RTPS 2.5 §10.5.
144    ///
145    /// # Errors
146    /// CDR decoder error.
147    fn decode_be(bytes: &[u8]) -> core::result::Result<Self, DecodeError> {
148        Self::decode(bytes)
149    }
150
151    /// Serializes as **classic CDR / XCDR1** little-endian (representation
152    /// `CDR_LE = 0x0001`, max-alignment 8, no DHEADER on @final/@appendable,
153    /// PL_CDR1 for @mutable). This is the encoding Cyclone DDS carries in an
154    /// iceoryx PSMX chunk for a serialized sample, so the iceoryx-cyclone
155    /// bridge uses it for cross-vendor same-host interop. The default delegates
156    /// to [`Self::encode`] (XCDR2) — correct only for types whose XCDR1 and
157    /// XCDR2 byte streams coincide (no 8-byte members, @final, no @mutable);
158    /// idl-rust codegen overrides it with a true XCDR1 writer.
159    ///
160    /// # Errors
161    /// CDR encoder error.
162    fn encode_xcdr1(&self, out: &mut Vec<u8>) -> core::result::Result<(), EncodeError> {
163        self.encode(out)
164    }
165
166    /// Deserializes a classic-CDR / XCDR1 little-endian payload. Symmetric to
167    /// [`Self::encode_xcdr1`]; the default delegates to [`Self::decode`] and
168    /// idl-rust codegen overrides it with an XCDR1 reader.
169    ///
170    /// # Errors
171    /// CDR decoder error.
172    fn decode_xcdr1(bytes: &[u8]) -> core::result::Result<Self, DecodeError> {
173        Self::decode(bytes)
174    }
175
176    /// Deserializes a classic-CDR / XCDR1 **big-endian** payload (encapsulation
177    /// `CDR_BE = 0x0000` / `PL_CDR_BE = 0x0002`). The default delegates to
178    /// [`Self::decode_xcdr1`] (little-endian) — correct for byte-order-agnostic
179    /// types and the common case (Cyclone/FastDDS/RTI emit XCDR1 little-endian);
180    /// idl-rust codegen overrides it with a true big-endian XCDR1 reader.
181    /// Symmetric to [`Self::decode_be`] for XCDR2.
182    ///
183    /// # Errors
184    /// CDR decoder error.
185    fn decode_xcdr1_be(bytes: &[u8]) -> core::result::Result<Self, DecodeError> {
186        Self::decode_xcdr1(bytes)
187    }
188
189    /// Serializes the `@key` member values in **PLAIN_CDR2-BE** format
190    /// into the given [`PlainCdr2BeKeyHolder`]. Order: ascending by
191    /// `member_id` (XTypes 1.3 §7.6.8.3.1.b).
192    ///
193    /// **Default implementation**: empty write. Keyed types MUST
194    /// override this.
195    ///
196    /// Called by the DcpsRuntime in the sample-encode path to write
197    /// PID_KEY_HASH into the inline QoS.
198    fn encode_key_holder_be(&self, _holder: &mut PlainCdr2BeKeyHolder) {
199        // Default: no key. Keyed types override.
200    }
201
202    /// Returns the value of a field path (dotted, e.g. `"a.b"`) as a
203    /// `zerodds_sql_filter::Value` for SQL filter evaluation in
204    /// QueryCondition / ContentFilteredTopic. Default: `None` (no field
205    /// reachable — the filter then denies every sample that contains a
206    /// field access).
207    ///
208    /// Spec: DDS 1.4 §B.2.1 (Filter Expressions) together with
209    /// §2.2.2.5.9 (QueryCondition) and §2.2.2.3.5
210    /// (ContentFilteredTopic). Generated IDL stubs override this per
211    /// field.
212    #[must_use]
213    fn field_value(&self, _path: &str) -> Option<zerodds_sql_filter::Value> {
214        None
215    }
216
217    /// Computes the 16-byte KeyHash of this instance per XTypes 1.3
218    /// §7.6.8.4. `None` if `HAS_KEY = false`.
219    ///
220    /// The default implementation uses [`Self::encode_key_holder_be`] +
221    /// [`Self::KEY_HOLDER_MAX_SIZE`] and delegates to
222    /// [`compute_key_hash`].
223    #[must_use]
224    fn compute_key_hash(&self) -> Option<[u8; KEY_HASH_LEN]> {
225        if !Self::HAS_KEY {
226            return None;
227        }
228        let mut holder = PlainCdr2BeKeyHolder::new();
229        self.encode_key_holder_be(&mut holder);
230        let max = Self::KEY_HOLDER_MAX_SIZE.unwrap_or(usize::MAX);
231        Some(compute_key_hash(holder.as_bytes(), max))
232    }
233
234    /// Spec-aligned alias for [`Self::compute_key_hash`].
235    /// `zerodds-xcdr2-rust` §2.5 uses the name `key_hash`; the
236    /// implementation name keeps `compute_key_hash` for historical
237    /// compat. Both return the same value.
238    #[must_use]
239    fn key_hash(&self) -> Option<[u8; KEY_HASH_LEN]> {
240        self.compute_key_hash()
241    }
242}
243
244/// `RowAccess` adapter for a `DdsType` sample value. Used by the
245/// DataReader in `read_w_condition`/`take_w_condition` and by the
246/// `ContentFilteredTopic` filter.
247pub struct DdsTypeRow<'a, T: DdsType> {
248    /// Inner sample whose fields are queried via
249    /// [`DdsType::field_value`].
250    pub sample: &'a T,
251}
252
253impl<'a, T: DdsType> DdsTypeRow<'a, T> {
254    /// Constructor.
255    #[must_use]
256    pub fn new(sample: &'a T) -> Self {
257        Self { sample }
258    }
259}
260
261impl<T: DdsType> zerodds_sql_filter::RowAccess for DdsTypeRow<'_, T> {
262    fn get(&self, path: &str) -> Option<zerodds_sql_filter::Value> {
263        self.sample.field_value(path)
264    }
265}
266
267/// Placeholder error for DdsType::encode. In v1.3 this will be
268/// re-exported as `zerodds_cdr::EncodeError` once the CDR layer is
269/// stabilized from the DCPS perspective.
270#[derive(Debug, Clone, PartialEq, Eq)]
271#[non_exhaustive]
272pub enum EncodeError {
273    /// Buffer overflow or field-specific value-range error.
274    Invalid {
275        /// Static description.
276        what: &'static str,
277    },
278}
279
280impl core::fmt::Display for EncodeError {
281    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
282        match self {
283            Self::Invalid { what } => write!(f, "encode error: {what}"),
284        }
285    }
286}
287
288#[cfg(feature = "std")]
289impl std::error::Error for EncodeError {}
290
291impl From<zerodds_cdr::EncodeError> for EncodeError {
292    fn from(e: zerodds_cdr::EncodeError) -> Self {
293        // zerodds-cdr errors are passed through as an opaque `Invalid`
294        // wrap. That is sufficient for DdsType callers, who only need
295        // the "encoding failed" information — the detailed error
296        // structure lives in the cdr layer and is serialized via
297        // Display when a caller logs the error message.
298        let _ = e;
299        Self::Invalid {
300            what: "zerodds_cdr encode error",
301        }
302    }
303}
304
305/// Placeholder error for DdsType::decode.
306#[derive(Debug, Clone, PartialEq, Eq)]
307#[non_exhaustive]
308pub enum DecodeError {
309    /// Truncation or value out-of-range.
310    Invalid {
311        /// Static description.
312        what: &'static str,
313    },
314}
315
316impl core::fmt::Display for DecodeError {
317    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
318        match self {
319            Self::Invalid { what } => write!(f, "decode error: {what}"),
320        }
321    }
322}
323
324#[cfg(feature = "std")]
325impl std::error::Error for DecodeError {}
326
327impl From<zerodds_cdr::DecodeError> for DecodeError {
328    fn from(e: zerodds_cdr::DecodeError) -> Self {
329        let _ = e;
330        Self::Invalid {
331            what: "zerodds_cdr decode error",
332        }
333    }
334}
335
336// ---------------------------------------------------------------------
337// DdsAny — IDL `any` type erasure (XCDR2 §7.4.4.7)
338//
339// Wire format: TypeIdentifier header (CDR string) + payload bytes.
340// Pure Rust with no external crate dep; full type erasure via a
341// string tag.
342
343/// IDL `any` as a type-erasure wrapper. Carries a type-identifier
344/// string (e.g. `"std_msgs::Header"`) plus the payload bytes.
345///
346/// Consumer pattern: check `type_name`, then deserialize the `payload`
347/// with the concrete DdsType.
348#[derive(Debug, Clone, PartialEq, Eq, Default)]
349pub struct DdsAny {
350    /// Fully-qualified type name (matches `DdsType::TYPE_NAME`).
351    pub type_name: alloc::string::String,
352    /// XCDR2 payload bytes of the wrapped value.
353    pub payload: Vec<u8>,
354}
355
356impl DdsAny {
357    /// Constructs a `DdsAny` from a `DdsType` value.
358    ///
359    /// # Errors
360    /// `EncodeError` on encode failure.
361    pub fn pack<T: DdsType>(value: &T) -> Result<Self, EncodeError> {
362        let mut payload = Vec::new();
363        value.encode(&mut payload)?;
364        Ok(Self {
365            type_name: alloc::string::String::from(T::TYPE_NAME),
366            payload,
367        })
368    }
369
370    /// Attempts to unpack the wrapped value as `T`.
371    ///
372    /// # Errors
373    /// `DecodeError::Invalid` if `T::TYPE_NAME != self.type_name` or on
374    /// a decode error.
375    pub fn unpack<T: DdsType>(&self) -> Result<T, DecodeError> {
376        if self.type_name != T::TYPE_NAME {
377            return Err(DecodeError::Invalid {
378                what: "DdsAny: type-name mismatch",
379            });
380        }
381        T::decode(&self.payload)
382    }
383}
384
385impl zerodds_cdr::CdrEncode for DdsAny {
386    fn encode(
387        &self,
388        w: &mut zerodds_cdr::BufferWriter,
389    ) -> core::result::Result<(), zerodds_cdr::EncodeError> {
390        // Type name as a CDR string + payload bytes with a u32 length prefix.
391        w.write_string(&self.type_name)?;
392        let payload_len = u32::try_from(self.payload.len()).map_err(|_| {
393            zerodds_cdr::EncodeError::ValueOutOfRange {
394                message: "DdsAny: payload > u32::MAX",
395            }
396        })?;
397        w.write_u32(payload_len)?;
398        w.write_bytes(&self.payload)?;
399        Ok(())
400    }
401}
402
403impl zerodds_cdr::CdrDecode for DdsAny {
404    fn decode(
405        r: &mut zerodds_cdr::BufferReader<'_>,
406    ) -> core::result::Result<Self, zerodds_cdr::DecodeError> {
407        let type_name = r.read_string()?;
408        let payload_len = r.read_u32()? as usize;
409        let payload = r.read_bytes(payload_len)?.to_vec();
410        Ok(Self { type_name, payload })
411    }
412}
413
414// ---------------------------------------------------------------------
415// Built-in `DdsType` for &[u8]/Vec<u8> payloads
416//
417// Many ROS use cases and interop tests need to "pass through raw". A
418// `BytesPayload` newtype with a fixed type name allows that.
419// ---------------------------------------------------------------------
420
421/// An opaque raw byte payload with a configurable type name (via an
422/// `impl` of `BytesPayload<T>` or a newtype).
423#[derive(Debug, Clone, PartialEq, Eq)]
424pub struct RawBytes {
425    /// Payload bytes (placed on the wire as-is, no CDR framing).
426    pub data: Vec<u8>,
427}
428
429impl RawBytes {
430    /// Constructor.
431    #[must_use]
432    pub fn new(data: Vec<u8>) -> Self {
433        Self { data }
434    }
435}
436
437impl DdsType for RawBytes {
438    const TYPE_NAME: &'static str = "zerodds::RawBytes";
439
440    fn encode(&self, out: &mut Vec<u8>) -> core::result::Result<(), EncodeError> {
441        out.extend_from_slice(&self.data);
442        Ok(())
443    }
444
445    fn decode(bytes: &[u8]) -> core::result::Result<Self, DecodeError> {
446        Ok(Self {
447            data: bytes.to_vec(),
448        })
449    }
450}
451
452#[cfg(test)]
453#[allow(clippy::expect_used, clippy::unwrap_used)]
454mod tests {
455    use super::*;
456
457    #[test]
458    fn raw_bytes_roundtrip() {
459        let orig = RawBytes::new(vec![1, 2, 3, 4, 5]);
460        let mut buf = Vec::new();
461        orig.encode(&mut buf).unwrap();
462        let back = RawBytes::decode(&buf).unwrap();
463        assert_eq!(back, orig);
464    }
465
466    #[test]
467    fn raw_bytes_type_name_is_namespaced() {
468        assert_eq!(RawBytes::TYPE_NAME, "zerodds::RawBytes");
469    }
470
471    // ---- .B: keyed types + KeyHash ----
472
473    /// Test fixture: keyed topic with @key u32 id (max 4 byte → zero-pad).
474    struct SmallKeyed {
475        id: u32,
476    }
477
478    impl DdsType for SmallKeyed {
479        const TYPE_NAME: &'static str = "test::SmallKeyed";
480        const HAS_KEY: bool = true;
481        const KEY_HOLDER_MAX_SIZE: Option<usize> = Some(4);
482
483        fn encode(&self, out: &mut Vec<u8>) -> core::result::Result<(), EncodeError> {
484            out.extend_from_slice(&self.id.to_le_bytes());
485            Ok(())
486        }
487        fn decode(bytes: &[u8]) -> core::result::Result<Self, DecodeError> {
488            if bytes.len() < 4 {
489                return Err(DecodeError::Invalid {
490                    what: "truncated SmallKeyed",
491                });
492            }
493            let id = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
494            Ok(Self { id })
495        }
496        fn encode_key_holder_be(&self, holder: &mut PlainCdr2BeKeyHolder) {
497            holder.write_u32(self.id);
498        }
499    }
500
501    #[test]
502    fn small_keyed_produces_zero_padded_keyhash() {
503        let s = SmallKeyed { id: 0x1122_3344 };
504        let key = s.compute_key_hash().expect("keyed");
505        assert_eq!(&key[0..4], &[0x11, 0x22, 0x33, 0x44]);
506        assert_eq!(&key[4..16], &[0u8; 12]);
507    }
508
509    #[test]
510    fn non_keyed_returns_none_for_keyhash() {
511        let r = RawBytes::new(vec![1, 2, 3]);
512        assert_eq!(r.compute_key_hash(), None);
513    }
514
515    #[test]
516    fn keyed_two_instances_have_distinct_hashes() {
517        let a = SmallKeyed { id: 1 };
518        let b = SmallKeyed { id: 2 };
519        assert_ne!(a.compute_key_hash(), b.compute_key_hash());
520    }
521
522    /// Test fixture: keyed topic with an unbounded @key string (MD5 path).
523    struct LargeKeyed {
524        topic: alloc::string::String,
525    }
526
527    impl DdsType for LargeKeyed {
528        const TYPE_NAME: &'static str = "test::LargeKeyed";
529        const HAS_KEY: bool = true;
530        const KEY_HOLDER_MAX_SIZE: Option<usize> = None; // unbounded → MD5
531
532        fn encode(&self, out: &mut Vec<u8>) -> core::result::Result<(), EncodeError> {
533            out.extend_from_slice(self.topic.as_bytes());
534            Ok(())
535        }
536        fn decode(_bytes: &[u8]) -> core::result::Result<Self, DecodeError> {
537            Err(DecodeError::Invalid {
538                what: "test fixture",
539            })
540        }
541        fn encode_key_holder_be(&self, holder: &mut PlainCdr2BeKeyHolder) {
542            holder.write_string(&self.topic);
543        }
544    }
545
546    #[test]
547    fn large_keyed_produces_md5_hashed_keyhash() {
548        let s = LargeKeyed {
549            topic: alloc::string::String::from("hello"),
550        };
551        let key = s.compute_key_hash().expect("keyed");
552        // 16-byte deterministic hash, non-zero
553        assert_ne!(key, [0u8; 16]);
554        // Idempotent
555        let key2 = s.compute_key_hash().expect("keyed");
556        assert_eq!(key, key2);
557    }
558
559    #[test]
560    fn spec_aligned_aliases_match_implementation_names() {
561        // zerodds-xcdr2-rust §11 Errata.
562        assert_eq!(
563            <RawBytes as DdsType>::IS_KEYED,
564            <RawBytes as DdsType>::HAS_KEY
565        );
566        fn is_keyed<T: DdsType>() -> bool {
567            T::IS_KEYED
568        }
569        assert!(is_keyed::<SmallKeyed>());
570        let s = SmallKeyed { id: 0xABCD };
571        assert_eq!(s.key_hash(), s.compute_key_hash());
572    }
573
574    #[test]
575    fn extensibility_default_is_final() {
576        assert_eq!(<RawBytes as DdsType>::EXTENSIBILITY, Extensibility::Final);
577        // ExtensibilityKind alias is the same type.
578        let _: ExtensibilityKind = Extensibility::Mutable;
579    }
580
581    #[test]
582    fn encode_be_default_delegates_to_encode() {
583        let r = RawBytes::new(vec![1, 2, 3]);
584        let mut le = Vec::new();
585        let mut be = Vec::new();
586        r.encode(&mut le).unwrap();
587        r.encode_be(&mut be).unwrap();
588        assert_eq!(le, be);
589    }
590
591    #[test]
592    fn keyed_member_order_matters() {
593        // Hypothetically: two members in a different order would yield
594        // different hashes. We verify this with a mock type that writes
595        // two fields in reverse order.
596        struct A {
597            x: u32,
598            y: u32,
599        }
600        impl DdsType for A {
601            const TYPE_NAME: &'static str = "test::A";
602            const HAS_KEY: bool = true;
603            const KEY_HOLDER_MAX_SIZE: Option<usize> = Some(8);
604            fn encode(&self, _out: &mut Vec<u8>) -> Result<(), EncodeError> {
605                Ok(())
606            }
607            fn decode(_b: &[u8]) -> Result<Self, DecodeError> {
608                Err(DecodeError::Invalid { what: "stub" })
609            }
610            fn encode_key_holder_be(&self, holder: &mut PlainCdr2BeKeyHolder) {
611                holder.write_u32(self.x);
612                holder.write_u32(self.y);
613            }
614        }
615        struct B {
616            x: u32,
617            y: u32,
618        }
619        impl DdsType for B {
620            const TYPE_NAME: &'static str = "test::B";
621            const HAS_KEY: bool = true;
622            const KEY_HOLDER_MAX_SIZE: Option<usize> = Some(8);
623            fn encode(&self, _out: &mut Vec<u8>) -> Result<(), EncodeError> {
624                Ok(())
625            }
626            fn decode(_b: &[u8]) -> Result<Self, DecodeError> {
627                Err(DecodeError::Invalid { what: "stub" })
628            }
629            fn encode_key_holder_be(&self, holder: &mut PlainCdr2BeKeyHolder) {
630                holder.write_u32(self.y);
631                holder.write_u32(self.x);
632            }
633        }
634        let a = A { x: 1, y: 2 };
635        let b = B { x: 1, y: 2 };
636        assert_ne!(a.compute_key_hash(), b.compute_key_hash());
637    }
638}