Skip to main content

zerodds_types/dynamic/
descriptor.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! TypeDescriptor + MemberDescriptor (XTypes 1.3 §7.5.1, §7.5.2).
4//!
5//! A `TypeDescriptor` fully describes a DynamicType: kind +
6//! name + bound + element type etc. It is the **constructive** entry point
7//! to `DynamicTypeBuilderFactory::create_type` (Spec §7.5.5.1).
8//!
9//! A `MemberDescriptor` describes a member within a
10//! composite type (struct/union/annotation). Spec §7.5.2 lists all
11//! fields, which are mapped 1:1 here. The apply logic for
12//! `try_construct` (DISCARD/USE_DEFAULT/TRIM) is added in C4.7.
13
14use alloc::boxed::Box;
15use alloc::string::String;
16use alloc::vec::Vec;
17
18/// XTypes 1.3 TypeKind-Enum (§7.5.1 Table 10).
19///
20/// Covers the 24 kinds named in the spec. `NoType` corresponds to
21/// `TK_NONE`.
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
23#[non_exhaustive]
24pub enum TypeKind {
25    /// No type — sentinel value.
26    NoType,
27    /// `boolean`.
28    Boolean,
29    /// `octet` / `byte` (8-bit unsigned).
30    Byte,
31    /// `int8`.
32    Int8,
33    /// `uint8`.
34    UInt8,
35    /// `int16`.
36    Int16,
37    /// `uint16`.
38    UInt16,
39    /// `int32`.
40    Int32,
41    /// `uint32`.
42    UInt32,
43    /// `int64`.
44    Int64,
45    /// `uint64`.
46    UInt64,
47    /// `float32`.
48    Float32,
49    /// `float64`.
50    Float64,
51    /// `float128` (long double).
52    Float128,
53    /// `char` (8-bit).
54    Char8,
55    /// `wchar` (16-bit).
56    Char16,
57    /// `string<N>`.
58    String8,
59    /// `wstring<N>`.
60    String16,
61    /// Enumeration.
62    Enumeration,
63    /// Bitmask.
64    Bitmask,
65    /// Alias / typedef.
66    Alias,
67    /// Array `T[D1,D2,...]`.
68    Array,
69    /// `sequence<T,N>`.
70    Sequence,
71    /// `map<K,V,N>`.
72    Map,
73    /// `struct`.
74    Structure,
75    /// `union`.
76    Union,
77    /// `bitset`.
78    Bitset,
79    /// `annotation`.
80    Annotation,
81}
82
83impl TypeKind {
84    /// `true` if the kind is a primitive, atomic type (not a
85    /// composite, not a collection). Spec §7.5.1.
86    #[must_use]
87    pub const fn is_primitive(self) -> bool {
88        matches!(
89            self,
90            Self::Boolean
91                | Self::Byte
92                | Self::Int8
93                | Self::UInt8
94                | Self::Int16
95                | Self::UInt16
96                | Self::Int32
97                | Self::UInt32
98                | Self::Int64
99                | Self::UInt64
100                | Self::Float32
101                | Self::Float64
102                | Self::Float128
103                | Self::Char8
104                | Self::Char16
105        )
106    }
107
108    /// `true` if this kind can carry members (Struct/Union/
109    /// Annotation/Bitset/Bitmask/Enum).
110    #[must_use]
111    pub const fn is_aggregable(self) -> bool {
112        matches!(
113            self,
114            Self::Structure
115                | Self::Union
116                | Self::Annotation
117                | Self::Bitset
118                | Self::Bitmask
119                | Self::Enumeration
120        )
121    }
122}
123
124/// Extensibility kind (§7.2.2.4).
125#[derive(Debug, Clone, Copy, PartialEq, Eq)]
126pub enum ExtensibilityKind {
127    /// `@final` — the type is closed.
128    Final,
129    /// `@appendable` — new fields at the end allowed (default).
130    Appendable,
131    /// `@mutable` — arbitrary evolution with `@id` bindings.
132    Mutable,
133}
134
135impl Default for ExtensibilityKind {
136    fn default() -> Self {
137        Self::Appendable
138    }
139}
140
141/// Try-Construct-Strategie (Spec §7.5.2 + §7.6.4).
142///
143/// The apply semantics (what happens on a decoder failure) is implemented
144/// in C4.7 — here only the enum + member field.
145#[derive(Debug, Clone, Copy, PartialEq, Eq)]
146pub enum TryConstructKind {
147    /// Discard the sample.
148    Discard,
149    /// Set to the default value.
150    UseDefault,
151    /// Truncate to the bound (strings/sequences).
152    Trim,
153}
154
155impl Default for TryConstructKind {
156    fn default() -> Self {
157        Self::Discard
158    }
159}
160
161/// `MemberId` — consistent with XTypes 1.3 §7.3.1.1 (32 bits).
162pub type MemberId = u32;
163
164/// XTypes §7.5.1.2 TypeDescriptor.
165///
166/// Describes a DynamicType — for construction via
167/// [`crate::dynamic::DynamicTypeBuilderFactory::create_type`] or as a
168/// read-only view via [`crate::dynamic::DynamicType::descriptor`].
169///
170/// Fields that are irrelevant for a given kind can be left empty
171/// (e.g. `bound` for a struct = `Vec::new()`).
172#[derive(Debug, Clone, PartialEq, Eq)]
173pub struct TypeDescriptor {
174    /// TypeKind.
175    pub kind: TypeKind,
176    /// Fully qualified name, e.g. `"::sensors::Chatter"`.
177    pub name: String,
178    /// Base type for inheritance (struct/union).
179    pub base_type: Option<Box<TypeDescriptor>>,
180    /// Discriminator type for `kind == Union` (mandatory).
181    pub discriminator_type: Option<Box<TypeDescriptor>>,
182    /// Bound — array dimensions, or `[max]` for sequence/string/map.
183    /// Empty for composite/primitive.
184    pub bound: Vec<u32>,
185    /// Element type for array/sequence/map.
186    pub element_type: Option<Box<TypeDescriptor>>,
187    /// Key type for map.
188    pub key_element_type: Option<Box<TypeDescriptor>>,
189    /// Extensibility kind (relevant for struct/union).
190    pub extensibility_kind: ExtensibilityKind,
191    /// `@nested` — the type is not intended as a top-level topic.
192    pub is_nested: bool,
193}
194
195impl TypeDescriptor {
196    /// Constructs a primitive descriptor.
197    #[must_use]
198    pub fn primitive(kind: TypeKind, name: impl Into<String>) -> Self {
199        Self {
200            kind,
201            name: name.into(),
202            base_type: None,
203            discriminator_type: None,
204            bound: Vec::new(),
205            element_type: None,
206            key_element_type: None,
207            extensibility_kind: ExtensibilityKind::default(),
208            is_nested: false,
209        }
210    }
211
212    /// Constructs a struct descriptor.
213    #[must_use]
214    pub fn structure(name: impl Into<String>) -> Self {
215        Self {
216            kind: TypeKind::Structure,
217            name: name.into(),
218            base_type: None,
219            discriminator_type: None,
220            bound: Vec::new(),
221            element_type: None,
222            key_element_type: None,
223            extensibility_kind: ExtensibilityKind::default(),
224            is_nested: false,
225        }
226    }
227
228    /// Constructs a union descriptor.
229    #[must_use]
230    pub fn union(name: impl Into<String>, discriminator: TypeDescriptor) -> Self {
231        Self {
232            kind: TypeKind::Union,
233            name: name.into(),
234            base_type: None,
235            discriminator_type: Some(Box::new(discriminator)),
236            bound: Vec::new(),
237            element_type: None,
238            key_element_type: None,
239            extensibility_kind: ExtensibilityKind::default(),
240            is_nested: false,
241        }
242    }
243
244    /// Constructs a sequence descriptor.
245    #[must_use]
246    pub fn sequence(name: impl Into<String>, element: TypeDescriptor, max: u32) -> Self {
247        Self {
248            kind: TypeKind::Sequence,
249            name: name.into(),
250            base_type: None,
251            discriminator_type: None,
252            bound: alloc::vec![max],
253            element_type: Some(Box::new(element)),
254            key_element_type: None,
255            extensibility_kind: ExtensibilityKind::default(),
256            is_nested: false,
257        }
258    }
259
260    /// Constructs an array descriptor.
261    #[must_use]
262    pub fn array(name: impl Into<String>, element: TypeDescriptor, dims: Vec<u32>) -> Self {
263        Self {
264            kind: TypeKind::Array,
265            name: name.into(),
266            base_type: None,
267            discriminator_type: None,
268            bound: dims,
269            element_type: Some(Box::new(element)),
270            key_element_type: None,
271            extensibility_kind: ExtensibilityKind::default(),
272            is_nested: false,
273        }
274    }
275
276    /// Constructs a map descriptor.
277    #[must_use]
278    pub fn map(
279        name: impl Into<String>,
280        key: TypeDescriptor,
281        element: TypeDescriptor,
282        max: u32,
283    ) -> Self {
284        Self {
285            kind: TypeKind::Map,
286            name: name.into(),
287            base_type: None,
288            discriminator_type: None,
289            bound: alloc::vec![max],
290            element_type: Some(Box::new(element)),
291            key_element_type: Some(Box::new(key)),
292            extensibility_kind: ExtensibilityKind::default(),
293            is_nested: false,
294        }
295    }
296
297    /// Constructs a string descriptor (`string<bound>`).
298    #[must_use]
299    pub fn string8(bound: u32) -> Self {
300        Self {
301            kind: TypeKind::String8,
302            name: alloc::format!("string<{bound}>"),
303            base_type: None,
304            discriminator_type: None,
305            bound: alloc::vec![bound],
306            element_type: None,
307            key_element_type: None,
308            extensibility_kind: ExtensibilityKind::default(),
309            is_nested: false,
310        }
311    }
312
313    /// Constructs a WString descriptor.
314    #[must_use]
315    pub fn string16(bound: u32) -> Self {
316        Self {
317            kind: TypeKind::String16,
318            name: alloc::format!("wstring<{bound}>"),
319            base_type: None,
320            discriminator_type: None,
321            bound: alloc::vec![bound],
322            element_type: None,
323            key_element_type: None,
324            extensibility_kind: ExtensibilityKind::default(),
325            is_nested: false,
326        }
327    }
328
329    /// Constructs an enum descriptor.
330    #[must_use]
331    pub fn enumeration(name: impl Into<String>) -> Self {
332        Self {
333            kind: TypeKind::Enumeration,
334            name: name.into(),
335            base_type: None,
336            discriminator_type: None,
337            bound: Vec::new(),
338            element_type: None,
339            key_element_type: None,
340            extensibility_kind: ExtensibilityKind::default(),
341            is_nested: false,
342        }
343    }
344
345    /// Validierung — Spec §7.5.1.4 `is_consistent()`.
346    ///
347    /// Checks the constraints defined in the spec for a
348    /// descriptor object: a discriminator is mandatory for a union, a bound
349    /// is mandatory for array/sequence/string/map etc.
350    ///
351    /// # Errors
352    /// `String` with a human-readable error description.
353    pub fn is_consistent(&self) -> Result<(), String> {
354        // Cycle check: a descriptor may not reference itself as
355        // base_type or element_type (detected by structural
356        // equality — the builder performs the robust cycle check).
357        if self.name.is_empty() && self.kind != TypeKind::NoType {
358            return Err(String::from("descriptor without name"));
359        }
360        match self.kind {
361            TypeKind::Union => {
362                let Some(d) = &self.discriminator_type else {
363                    return Err(String::from("union without discriminator_type"));
364                };
365                if !is_valid_discriminator(d.kind) {
366                    return Err(alloc::format!(
367                        "union discriminator must be int/enum/bool, got {:?}",
368                        d.kind
369                    ));
370                }
371            }
372            TypeKind::Array => {
373                if self.bound.is_empty() {
374                    return Err(String::from("array without dimensions"));
375                }
376                if self.bound.contains(&0) {
377                    return Err(String::from("array dimension must be > 0"));
378                }
379                if self.element_type.is_none() {
380                    return Err(String::from("array without element_type"));
381                }
382            }
383            TypeKind::Sequence | TypeKind::String8 | TypeKind::String16 => {
384                if self.bound.len() != 1 {
385                    return Err(String::from("sequence/string needs exactly 1 bound"));
386                }
387                if matches!(self.kind, TypeKind::Sequence) && self.element_type.is_none() {
388                    return Err(String::from("sequence without element_type"));
389                }
390            }
391            TypeKind::Map => {
392                if self.bound.len() != 1 {
393                    return Err(String::from("map needs exactly 1 bound"));
394                }
395                if self.element_type.is_none() {
396                    return Err(String::from("map without value element_type"));
397                }
398                if self.key_element_type.is_none() {
399                    return Err(String::from("map without key_element_type"));
400                }
401            }
402            _ => {}
403        }
404        // Inheritance cycle check (1 level; deeper levels are checked in the
405        // builder via `build()` against the final DynamicType).
406        if let Some(b) = &self.base_type {
407            if b.name == self.name && !self.name.is_empty() {
408                return Err(String::from("inheritance cycle: base_type == self"));
409            }
410        }
411        Ok(())
412    }
413}
414
415/// XTypes §7.5.2.2 MemberDescriptor.
416///
417/// Describes a member within a composite type (struct,
418/// union, annotation, bitset, bitmask). For a bitmask,
419/// `member_type` is typically `Boolean` and `id` is the bit position.
420#[derive(Debug, Clone, PartialEq, Eq)]
421pub struct MemberDescriptor {
422    /// Member name (case-sensitive, unique within the composite).
423    pub name: String,
424    /// Member id (unique within the composite, for XCDR2).
425    pub id: MemberId,
426    /// Type of the member.
427    pub member_type: Box<TypeDescriptor>,
428    /// Default value in canonical IDL literal form.
429    pub default_value: Option<String>,
430    /// Order index (0-based) — for `member_by_index`.
431    pub index: u32,
432    /// Union case labels. Spec §7.5.2 — only populated for unions.
433    pub label: Vec<i64>,
434    /// Try-construct strategy (apply in C4.7).
435    pub try_construct: TryConstructKind,
436    /// `@key` — the member is part of the topic key.
437    pub is_key: bool,
438    /// `@optional`.
439    pub is_optional: bool,
440    /// `@must_understand`.
441    pub is_must_understand: bool,
442    /// `@external` — indirect storage (shared_ptr in C++).
443    pub is_shared: bool,
444    /// `default:` branch for a union.
445    pub is_default_label: bool,
446    /// Bitfield-Breite in Bits (1..=64) — nur fuer `Bitset`-Felder belegt;
447    /// `id` traegt dabei die Bit-Startposition. Wird vom DynamicType →
448    /// TypeObject-Bridge (XTypes §7.3.4.4 CompleteBitfield) ausgewertet.
449    pub bit_bound: Option<u8>,
450}
451
452impl MemberDescriptor {
453    /// Creates a MemberDescriptor with the most common defaults
454    /// for struct members.
455    #[must_use]
456    pub fn new(name: impl Into<String>, id: MemberId, ty: TypeDescriptor) -> Self {
457        Self {
458            name: name.into(),
459            id,
460            member_type: Box::new(ty),
461            default_value: None,
462            index: 0,
463            label: Vec::new(),
464            try_construct: TryConstructKind::default(),
465            is_key: false,
466            is_optional: false,
467            is_must_understand: false,
468            is_shared: false,
469            is_default_label: false,
470            bit_bound: None,
471        }
472    }
473
474    /// Validierung — Spec §7.5.2.4 `is_consistent()`.
475    ///
476    /// # Errors
477    /// `String` with the error text.
478    pub fn is_consistent(&self) -> Result<(), String> {
479        if self.name.is_empty() {
480            return Err(String::from("member without name"));
481        }
482        self.member_type.is_consistent()?;
483        if self.is_default_label && !self.label.is_empty() {
484            return Err(String::from(
485                "member with is_default_label must not have explicit labels",
486            ));
487        }
488        Ok(())
489    }
490}
491
492/// True if the TypeKind is a valid union discriminator
493/// (Spec §7.4.1.4.4: integral or enum or boolean or char).
494const fn is_valid_discriminator(kind: TypeKind) -> bool {
495    matches!(
496        kind,
497        TypeKind::Boolean
498            | TypeKind::Byte
499            | TypeKind::Int8
500            | TypeKind::UInt8
501            | TypeKind::Int16
502            | TypeKind::UInt16
503            | TypeKind::Int32
504            | TypeKind::UInt32
505            | TypeKind::Int64
506            | TypeKind::UInt64
507            | TypeKind::Char8
508            | TypeKind::Char16
509            | TypeKind::Enumeration
510    )
511}
512
513#[cfg(test)]
514#[allow(clippy::unwrap_used)]
515mod tests {
516    use super::*;
517
518    #[test]
519    fn type_kind_primitive_set_matches_spec_table_10() {
520        for k in [
521            TypeKind::Boolean,
522            TypeKind::Byte,
523            TypeKind::Int8,
524            TypeKind::UInt8,
525            TypeKind::Int16,
526            TypeKind::UInt16,
527            TypeKind::Int32,
528            TypeKind::UInt32,
529            TypeKind::Int64,
530            TypeKind::UInt64,
531            TypeKind::Float32,
532            TypeKind::Float64,
533            TypeKind::Float128,
534            TypeKind::Char8,
535            TypeKind::Char16,
536        ] {
537            assert!(k.is_primitive(), "{k:?} should be primitive");
538        }
539        for k in [
540            TypeKind::Structure,
541            TypeKind::Union,
542            TypeKind::Sequence,
543            TypeKind::Array,
544            TypeKind::Map,
545            TypeKind::String8,
546            TypeKind::String16,
547            TypeKind::Alias,
548        ] {
549            assert!(!k.is_primitive(), "{k:?} should not be primitive");
550        }
551    }
552
553    #[test]
554    fn type_kind_aggregable_set() {
555        assert!(TypeKind::Structure.is_aggregable());
556        assert!(TypeKind::Union.is_aggregable());
557        assert!(TypeKind::Annotation.is_aggregable());
558        assert!(TypeKind::Bitset.is_aggregable());
559        assert!(TypeKind::Bitmask.is_aggregable());
560        assert!(TypeKind::Enumeration.is_aggregable());
561        assert!(!TypeKind::Int32.is_aggregable());
562        assert!(!TypeKind::Sequence.is_aggregable());
563    }
564
565    #[test]
566    fn descriptor_struct_passes_consistency() {
567        let s = TypeDescriptor::structure("::Foo");
568        assert!(s.is_consistent().is_ok());
569    }
570
571    #[test]
572    fn descriptor_union_without_discriminator_fails() {
573        let mut u = TypeDescriptor::structure("::U");
574        u.kind = TypeKind::Union;
575        let err = u.is_consistent().unwrap_err();
576        assert!(err.contains("discriminator"));
577    }
578
579    #[test]
580    fn descriptor_union_with_invalid_discriminator_fails() {
581        let bad_disc = TypeDescriptor::structure("::S");
582        let u = TypeDescriptor::union("::U", bad_disc);
583        let err = u.is_consistent().unwrap_err();
584        assert!(err.contains("discriminator"));
585    }
586
587    #[test]
588    fn descriptor_array_without_dims_fails() {
589        let mut a = TypeDescriptor::array(
590            "::A",
591            TypeDescriptor::primitive(TypeKind::Int32, "int32"),
592            alloc::vec![3, 3],
593        );
594        a.bound.clear();
595        let err = a.is_consistent().unwrap_err();
596        assert!(err.contains("dimensions"));
597    }
598
599    #[test]
600    fn descriptor_array_with_zero_dim_fails() {
601        let a = TypeDescriptor::array(
602            "::A",
603            TypeDescriptor::primitive(TypeKind::Int32, "int32"),
604            alloc::vec![3, 0, 4],
605        );
606        let err = a.is_consistent().unwrap_err();
607        assert!(err.contains("> 0"));
608    }
609
610    #[test]
611    fn descriptor_sequence_with_element_passes() {
612        let s = TypeDescriptor::sequence(
613            "::S",
614            TypeDescriptor::primitive(TypeKind::Int32, "int32"),
615            100,
616        );
617        assert!(s.is_consistent().is_ok());
618    }
619
620    #[test]
621    fn descriptor_map_requires_both_key_and_value() {
622        let mut m = TypeDescriptor::map(
623            "::M",
624            TypeDescriptor::string8(64),
625            TypeDescriptor::primitive(TypeKind::Int64, "int64"),
626            500,
627        );
628        assert!(m.is_consistent().is_ok());
629        m.key_element_type = None;
630        assert!(m.is_consistent().is_err());
631    }
632
633    #[test]
634    fn descriptor_inheritance_cycle_self_reference_rejected() {
635        let mut s = TypeDescriptor::structure("::Foo");
636        let cycle = TypeDescriptor::structure("::Foo");
637        s.base_type = Some(Box::new(cycle));
638        let err = s.is_consistent().unwrap_err();
639        assert!(err.contains("cycle"));
640    }
641
642    #[test]
643    fn member_descriptor_default_label_with_labels_rejected() {
644        let mut m =
645            MemberDescriptor::new("x", 1, TypeDescriptor::primitive(TypeKind::Int32, "int32"));
646        m.is_default_label = true;
647        m.label = alloc::vec![0];
648        let err = m.is_consistent().unwrap_err();
649        assert!(err.contains("default_label"));
650    }
651
652    #[test]
653    fn member_descriptor_empty_name_rejected() {
654        let m = MemberDescriptor::new("", 1, TypeDescriptor::primitive(TypeKind::Int32, "int32"));
655        assert!(m.is_consistent().is_err());
656    }
657
658    #[test]
659    fn try_construct_default_is_discard() {
660        assert_eq!(TryConstructKind::default(), TryConstructKind::Discard);
661    }
662
663    #[test]
664    fn extensibility_default_is_appendable() {
665        assert_eq!(ExtensibilityKind::default(), ExtensibilityKind::Appendable);
666    }
667}