Skip to main content

zerodds_dcps/
interop.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! Interop test types for cross-vendor verification.
4//!
5//! Contains application types that count as a de-facto interop benchmark
6//! in the DDS world — above all `ShapeType` from the RTI/Cyclone/Fast-DDS
7//! ShapesDemo. These types are not meant for production use, but to prove
8//! against other DDS stacks that our wire and type semantics are
9//! byte-compatible.
10//!
11//! # `ShapeType`
12//!
13//! Spec basis (IDL, as used by RTI/Cyclone/Fast-DDS):
14//!
15//! ```idl
16//! struct ShapeType {
17//!     @key string<128> color;
18//!     int32 x;
19//!     int32 y;
20//!     int32 shapesize;
21//! };
22//! ```
23//!
24//! Encoding: XCDR2 little-endian (the default setting of all ShapesDemo
25//! implementations, and matching our user-payload encapsulation header
26//! `0x00 0x07 0x00 0x00`).
27//!
28//! CDR layout:
29//! ```text
30//! offset 0  : uint32   color.length (incl. null-terminator)
31//! offset 4  : bytes    color.utf8_bytes
32//! offset 4+n: uint8    0x00          (null-terminator)
33//! padding            (to the next 4-byte boundary)
34//! offset *  : int32    x
35//! offset *+4: int32    y
36//! offset *+8: int32    shapesize
37//! ```
38//!
39//! The `@key` on `color` is not handled specially at the wire level —
40//! it controls instance keying, not serialization. For our sample
41//! matching in v1.2 (no instance map in the reader yet), every
42//! (color, x, y, shapesize) combination is effectively its own sample.
43
44extern crate alloc;
45
46use alloc::string::String;
47use alloc::vec::Vec;
48
49use zerodds_cdr::buffer::{BufferReader, BufferWriter};
50use zerodds_cdr::endianness::Endianness;
51
52use crate::dds_type::{DdsType, DecodeError, EncodeError};
53
54/// RTI / Cyclone / Fast-DDS ShapesDemo-compatible application type.
55///
56/// See the module docs for spec and layout. Color is the instance key,
57/// x/y/shapesize are the typical shape coordinates.
58#[derive(Debug, Clone, PartialEq, Eq)]
59pub struct ShapeType {
60    /// Color / instance key. In ShapesDemo implementations typically
61    /// `"BLUE"`, `"RED"`, `"GREEN"`, `"YELLOW"`, `"MAGENTA"`, `"CYAN"`,
62    /// `"ORANGE"`, `"PURPLE"`. No content validation here — any UTF-8
63    /// string is allowed.
64    pub color: String,
65    /// X coordinate in pixels (the ShapesDemo canvas is ~240×270).
66    pub x: i32,
67    /// Y coordinate.
68    pub y: i32,
69    /// Shape size in pixels. Typically 30.
70    pub shapesize: i32,
71}
72
73impl ShapeType {
74    /// Constructor.
75    #[must_use]
76    pub fn new(color: impl Into<String>, x: i32, y: i32, shapesize: i32) -> Self {
77        Self {
78            color: color.into(),
79            x,
80            y,
81            shapesize,
82        }
83    }
84}
85
86impl DdsType for ShapeType {
87    /// Type name **exactly** as in RTI/Cyclone/Fast-DDS ShapesDemo.
88    /// Changing it would break matching with any other ShapesDemo client.
89    const TYPE_NAME: &'static str = "ShapeType";
90    /// ShapesDemo IDL: `@key string color`. ShapeType is keyed —
91    /// per-instance QoS (TimeBasedFilter, Ownership, Lifecycle) depends
92    /// on it.
93    const HAS_KEY: bool = true;
94
95    fn encode_key_holder_be(&self, holder: &mut crate::dds_type::PlainCdr2BeKeyHolder) {
96        holder.write_string(&self.color);
97    }
98
99    fn encode(&self, out: &mut Vec<u8>) -> core::result::Result<(), EncodeError> {
100        let mut w = BufferWriter::new(Endianness::Little);
101        w.write_string(&self.color)
102            .map_err(|_| EncodeError::Invalid {
103                what: "ShapeType.color encoding",
104            })?;
105        w.write_u32(self.x as u32)
106            .map_err(|_| EncodeError::Invalid {
107                what: "ShapeType.x encoding",
108            })?;
109        w.write_u32(self.y as u32)
110            .map_err(|_| EncodeError::Invalid {
111                what: "ShapeType.y encoding",
112            })?;
113        w.write_u32(self.shapesize as u32)
114            .map_err(|_| EncodeError::Invalid {
115                what: "ShapeType.shapesize encoding",
116            })?;
117        out.extend_from_slice(w.as_bytes());
118        Ok(())
119    }
120
121    fn decode(bytes: &[u8]) -> core::result::Result<Self, DecodeError> {
122        let mut r = BufferReader::new(bytes, Endianness::Little);
123        let color = r.read_string().map_err(|_| DecodeError::Invalid {
124            what: "ShapeType.color decoding",
125        })?;
126        let x = r.read_u32().map_err(|_| DecodeError::Invalid {
127            what: "ShapeType.x decoding",
128        })? as i32;
129        let y = r.read_u32().map_err(|_| DecodeError::Invalid {
130            what: "ShapeType.y decoding",
131        })? as i32;
132        let shapesize = r.read_u32().map_err(|_| DecodeError::Invalid {
133            what: "ShapeType.shapesize decoding",
134        })? as i32;
135        Ok(Self {
136            color,
137            x,
138            y,
139            shapesize,
140        })
141    }
142}
143
144/// ShapesDemo `ShapeFillKind` enum (RTI 7.x / Fast-DDS ShapeExtended IDL).
145///
146/// ```idl
147/// @final
148/// enum ShapeFillKind { SOLID_FILL, TRANSPARENT_FILL, HORIZONTAL_HATCH, VERTICAL_HATCH };
149/// ```
150///
151/// Wire form: a 4-byte little-endian `int32` discriminant (0..=3), per the
152/// XTypes enum default underlying type.
153#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
154#[repr(i32)]
155pub enum ShapeFillKind {
156    /// `0` — solid fill (the ShapesDemo default).
157    #[default]
158    SolidFill = 0,
159    /// `1` — transparent (outline only).
160    TransparentFill = 1,
161    /// `2` — horizontal hatch.
162    HorizontalHatch = 2,
163    /// `3` — vertical hatch.
164    VerticalHatch = 3,
165}
166
167impl ShapeFillKind {
168    /// The wire discriminant.
169    #[must_use]
170    pub const fn to_i32(self) -> i32 {
171        self as i32
172    }
173
174    /// Maps a wire discriminant to a fill kind, falling back to `SolidFill`
175    /// for an out-of-range value (forward-compatible with extended enums).
176    #[must_use]
177    pub const fn from_i32(v: i32) -> Self {
178        match v {
179            1 => Self::TransparentFill,
180            2 => Self::HorizontalHatch,
181            3 => Self::VerticalHatch,
182            _ => Self::SolidFill,
183        }
184    }
185}
186
187/// RTI 7.x / Fast-DDS **default** ShapesDemo type — `ShapeExtendedType`.
188///
189/// ```idl
190/// @final
191/// struct ShapeExtendedType {
192///     @key string color;
193///     long x;
194///     long y;
195///     long shapesize;
196///     ShapeFillKind fillKind;
197///     float angle;
198/// };
199/// ```
200///
201/// This is the modern vendor default: RTI Connext 7.x ShapesDemo publishes
202/// `ShapeExtendedType` unless started with `-dataType Shape`. Carrying it
203/// natively lets ZeroDDS interop with an unmodified RTI ShapesDemo (no flag).
204/// It is a **distinct topic type** from [`ShapeType`] (different `TYPE_NAME`),
205/// so SEDP matches only against other `ShapeExtendedType` endpoints.
206#[derive(Debug, Clone, PartialEq)]
207pub struct ShapeExtendedType {
208    /// Color / instance key (same semantics as [`ShapeType::color`]).
209    pub color: String,
210    /// X coordinate in pixels.
211    pub x: i32,
212    /// Y coordinate.
213    pub y: i32,
214    /// Shape size in pixels.
215    pub shapesize: i32,
216    /// Fill style (new in the extended type).
217    pub fill_kind: ShapeFillKind,
218    /// Rotation angle in degrees (new in the extended type).
219    pub angle: f32,
220}
221
222impl ShapeExtendedType {
223    /// Constructor.
224    #[must_use]
225    pub fn new(
226        color: impl Into<String>,
227        x: i32,
228        y: i32,
229        shapesize: i32,
230        fill_kind: ShapeFillKind,
231        angle: f32,
232    ) -> Self {
233        Self {
234            color: color.into(),
235            x,
236            y,
237            shapesize,
238            fill_kind,
239            angle,
240        }
241    }
242}
243
244impl DdsType for ShapeExtendedType {
245    /// Type name **exactly** as in RTI/Fast-DDS ShapesDemo. Must not change.
246    const TYPE_NAME: &'static str = "ShapeExtendedType";
247    /// `@key string color` — keyed, like [`ShapeType`].
248    const HAS_KEY: bool = true;
249
250    fn encode_key_holder_be(&self, holder: &mut crate::dds_type::PlainCdr2BeKeyHolder) {
251        holder.write_string(&self.color);
252    }
253
254    fn encode(&self, out: &mut Vec<u8>) -> core::result::Result<(), EncodeError> {
255        let mut w = BufferWriter::new(Endianness::Little);
256        w.write_string(&self.color)
257            .map_err(|_| EncodeError::Invalid {
258                what: "ShapeExtendedType.color encoding",
259            })?;
260        w.write_u32(self.x as u32)
261            .map_err(|_| EncodeError::Invalid {
262                what: "ShapeExtendedType.x encoding",
263            })?;
264        w.write_u32(self.y as u32)
265            .map_err(|_| EncodeError::Invalid {
266                what: "ShapeExtendedType.y encoding",
267            })?;
268        w.write_u32(self.shapesize as u32)
269            .map_err(|_| EncodeError::Invalid {
270                what: "ShapeExtendedType.shapesize encoding",
271            })?;
272        w.write_u32(self.fill_kind.to_i32() as u32)
273            .map_err(|_| EncodeError::Invalid {
274                what: "ShapeExtendedType.fillKind encoding",
275            })?;
276        // `float angle` — IEEE-754 32-bit, written as its little-endian bit
277        // pattern (the CDR float32 wire form).
278        w.write_u32(self.angle.to_bits())
279            .map_err(|_| EncodeError::Invalid {
280                what: "ShapeExtendedType.angle encoding",
281            })?;
282        out.extend_from_slice(w.as_bytes());
283        Ok(())
284    }
285
286    fn decode(bytes: &[u8]) -> core::result::Result<Self, DecodeError> {
287        let mut r = BufferReader::new(bytes, Endianness::Little);
288        let color = r.read_string().map_err(|_| DecodeError::Invalid {
289            what: "ShapeExtendedType.color decoding",
290        })?;
291        let x = r.read_u32().map_err(|_| DecodeError::Invalid {
292            what: "ShapeExtendedType.x decoding",
293        })? as i32;
294        let y = r.read_u32().map_err(|_| DecodeError::Invalid {
295            what: "ShapeExtendedType.y decoding",
296        })? as i32;
297        let shapesize = r.read_u32().map_err(|_| DecodeError::Invalid {
298            what: "ShapeExtendedType.shapesize decoding",
299        })? as i32;
300        let fill_kind = ShapeFillKind::from_i32(r.read_u32().map_err(|_| DecodeError::Invalid {
301            what: "ShapeExtendedType.fillKind decoding",
302        })? as i32);
303        let angle = f32::from_bits(r.read_u32().map_err(|_| DecodeError::Invalid {
304            what: "ShapeExtendedType.angle decoding",
305        })?);
306        Ok(Self {
307            color,
308            x,
309            y,
310            shapesize,
311            fill_kind,
312            angle,
313        })
314    }
315}
316
317#[cfg(test)]
318#[allow(clippy::unwrap_used, clippy::float_cmp)]
319mod tests {
320    use super::*;
321
322    #[test]
323    fn shape_extended_round_trip() {
324        let s = ShapeExtendedType::new("BLUE", 100, 150, 30, ShapeFillKind::HorizontalHatch, 45.5);
325        let mut bytes = Vec::new();
326        s.encode(&mut bytes).unwrap();
327        let back = ShapeExtendedType::decode(&bytes).unwrap();
328        assert_eq!(back, s);
329    }
330
331    #[test]
332    fn shape_extended_type_name_distinct_from_shape() {
333        assert_eq!(ShapeExtendedType::TYPE_NAME, "ShapeExtendedType");
334        assert_ne!(ShapeExtendedType::TYPE_NAME, ShapeType::TYPE_NAME);
335        // Regression guard that the codegen sets `HAS_KEY` for a keyed type.
336        // It is a `const bool`, so clippy folds it and warns it would be
337        // optimised out — that is exactly the intent here (a compile-time fact).
338        #[allow(clippy::assertions_on_constants)]
339        {
340            assert!(ShapeExtendedType::HAS_KEY);
341        }
342    }
343
344    #[test]
345    fn shape_extended_wire_layout() {
346        // @final XCDR2/CDR LE: string(len+bytes) + x + y + shapesize + fillKind
347        // (int32) + angle (float32 bits). "RED\0" = 4-byte len + 4 bytes.
348        let s = ShapeExtendedType::new("RED", 1, 2, 30, ShapeFillKind::SolidFill, 0.0);
349        let mut bytes = Vec::new();
350        s.encode(&mut bytes).unwrap();
351        // 4 (strlen) + 4 ("RED\0") + 4*4 (x,y,shapesize,fillKind) + 4 (angle).
352        assert_eq!(bytes.len(), 4 + 4 + 16 + 4);
353        // String length prefix = 4 ("RED" + NUL), little-endian.
354        assert_eq!(&bytes[0..4], &[4, 0, 0, 0]);
355        assert_eq!(&bytes[4..8], b"RED\0");
356        // fillKind SolidFill = 0, angle 0.0 = 0x00000000.
357        assert_eq!(&bytes[20..24], &[0, 0, 0, 0]); // fillKind
358        assert_eq!(&bytes[24..28], &[0, 0, 0, 0]); // angle
359    }
360
361    #[test]
362    fn fill_kind_round_trips_and_clamps() {
363        for k in [
364            ShapeFillKind::SolidFill,
365            ShapeFillKind::TransparentFill,
366            ShapeFillKind::HorizontalHatch,
367            ShapeFillKind::VerticalHatch,
368        ] {
369            assert_eq!(ShapeFillKind::from_i32(k.to_i32()), k);
370        }
371        // Unknown discriminant → SolidFill (forward-compatible).
372        assert_eq!(ShapeFillKind::from_i32(99), ShapeFillKind::SolidFill);
373    }
374}