Skip to main content

zerodds_xml/
typeobject_bridge.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! XML → TypeObject Bridge (Cluster C4.5-b).
4//!
5//! Converts the internal XML data model from [`xtypes_def`] into the
6//! XTypes-1.3 TypeObject format from `zerodds-types`. Wire-/hash-compatible
7//! with the IDL lowering from `zerodds-idl::semantics::to_typeobject`.
8//!
9//! # Spec sources
10//!
11//! - OMG XTypes 1.3 §7.3.4.x — TypeObject + Minimal/Complete variants.
12//! - OMG XTypes 1.3 §7.3.4.5 — MinimalStructType / EnumType / UnionType.
13//! - OMG XTypes 1.3 Annex A — XML mapping.
14//!
15//! # Scope C4.5-b (this stage)
16//!
17//! - `MinimalTypeObject` only. `CompleteTypeObject` not implemented.
18//! - Top-level mapping (struct, enum, union, typedef, bitmask, bitset).
19//! - Member types via [`TypeRef::Primitive`] (direct) or
20//!   [`TypeRef::Named`] (lookup in [`TypeLibrary`] with hash precomputation).
21//! - The modifiers `arrayDimensions`/`sequenceMaxLength`/`stringMaxLength`
22//!   wrap the member TypeIdentifier in PlainArray-/PlainSequence-/
23//!   string-bound variants.
24//! - Member IDs: `@id` from XSD if set, otherwise sequential from 1
25//!   (AUTOID_SEQUENTIAL, Spec §7.3.1.2.1.1).
26//! - Extensibility: map `final`/`appendable`/`mutable` from
27//!   [`Extensibility`] onto `StructTypeFlag`/`UnionTypeFlag`.
28//!
29//! # Deliberately not in the crate
30//!
31//! - `CompleteTypeObject` (comes in phase 6).
32//! - Forward declarations / cross-library refs.
33//! - Inheritance via `baseType` — the bridge does set `base_type` as
34//!   `EquivalenceHashMinimal`, but cycle detection remains the task of the
35//!   existing [`crate::inheritance`] module.
36//! - `@autoid(HASH)` — XML schema has no corresponding annotation.
37
38use alloc::collections::BTreeMap;
39use alloc::string::{String, ToString};
40use alloc::vec::Vec;
41use core::fmt;
42
43use zerodds_types::builder::{Extensibility as TypeExt, TypeObjectBuilder};
44use zerodds_types::hash::compute_minimal_hash;
45use zerodds_types::type_object::flags::{
46    CollectionElementFlag, CollectionTypeFlag, EnumLiteralFlag, EnumTypeFlag, StructTypeFlag,
47    UnionDiscriminatorFlag, UnionMemberFlag, UnionTypeFlag,
48};
49use zerodds_types::type_object::minimal::{
50    CommonCollectionElement, CommonDiscriminatorMember, CommonEnumeratedHeader,
51    CommonEnumeratedLiteral, MinimalArrayType, MinimalCollectionElement,
52    MinimalDiscriminatorMember, MinimalEnumeratedHeader, MinimalEnumeratedLiteral,
53    MinimalEnumeratedType, MinimalSequenceType, MinimalUnionMember, MinimalUnionType,
54};
55use zerodds_types::{MinimalTypeObject, PrimitiveKind, TypeIdentifier, TypeObject};
56
57use crate::xtypes_def::{
58    BitField, BitValue, BitmaskType, BitsetType, EnumLiteral, EnumType, Extensibility,
59    PrimitiveType as XmlPrimitive, StructMember, StructType, TypeDef, TypeLibrary, TypeRef,
60    TypedefType, UnionDiscriminator, UnionType,
61};
62
63/// Error during the XML→TypeObject mapping.
64#[derive(Debug, Clone, PartialEq, Eq)]
65pub enum BridgeError {
66    /// An XSD construct is (in C4.5-b) not yet mappable — e.g.
67    /// inline maps or self-recursion.
68    UnsupportedXsdConstruct(String),
69    /// A `TypeRef::Named` reference could not be resolved,
70    /// because it does not appear in the supplied [`TypeLibrary`] (or via
71    /// `bridge_with_resolver`).
72    UnresolvedReference(String),
73    /// A numeric value (e.g. a union discriminator label) is
74    /// not parseable.
75    InvalidLiteral(String),
76    /// The EquivalenceHash computation failed (buffer overflow
77    /// in the internal encoder).
78    HashFailed(String),
79    /// Module entries are not a top-level type — the XML has a
80    /// `<module>` at a position where only a type may appear.
81    ModuleAtTopLevel(String),
82}
83
84impl fmt::Display for BridgeError {
85    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86        match self {
87            Self::UnsupportedXsdConstruct(s) => {
88                write!(f, "unsupported XSD construct: {s}")
89            }
90            Self::UnresolvedReference(s) => write!(f, "unresolved reference: {s}"),
91            Self::InvalidLiteral(s) => write!(f, "invalid literal: {s}"),
92            Self::HashFailed(s) => write!(f, "TypeIdentifier hashing failed: {s}"),
93            Self::ModuleAtTopLevel(s) => {
94                write!(f, "<module> not allowed at this position: {s}")
95            }
96        }
97    }
98}
99
100#[cfg(feature = "std")]
101impl std::error::Error for BridgeError {}
102
103// ============================================================================
104// Public API
105// ============================================================================
106
107/// Converts a single XmlType-[`TypeDef`] into a [`TypeObject`]
108/// (minimal variant).
109///
110/// Named member refs are stored as a null-hash placeholder
111/// (`TypeIdentifier::EquivalenceHashMinimal([0; 14])`) — a
112/// caller wanting to resolve a whole library at once should
113/// use [`bridge_library`] instead.
114///
115/// # Errors
116/// `BridgeError::UnsupportedXsdConstruct` for constructs not mapped in this
117/// stage; `BridgeError::ModuleAtTopLevel` if
118/// a `<module>` was passed directly (modules are not types).
119pub fn xml_type_to_typeobject(xml: &TypeDef) -> Result<TypeObject, BridgeError> {
120    let mto = xml_type_to_minimal_typeobject(xml)?;
121    Ok(TypeObject::Minimal(mto))
122}
123
124/// Like [`xml_type_to_typeobject`], but directly as a
125/// [`MinimalTypeObject`] (without the discriminator wrapper).
126///
127/// # Errors
128/// See [`xml_type_to_typeobject`].
129pub fn xml_type_to_minimal_typeobject(xml: &TypeDef) -> Result<MinimalTypeObject, BridgeError> {
130    let resolver = NullResolver;
131    bridge_typedef(xml, &resolver)
132}
133
134/// Converts a whole [`TypeLibrary`] into a map
135/// `Name → MinimalTypeObject`. Named refs between types within the
136/// library are resolved in a two-stage pass:
137///
138/// 1. Pre-pass: each top-level type is mapped with null-hash placeholders,
139///    then its EquivalenceHashMinimal is computed.
140/// 2. Final pass: all TypeIdentifier refs to named types are replaced with
141///    the pre-pass hashes.
142///
143/// `<module>` entries are flattened recursively. The map key for
144/// nested types is the scoped name (`Module::Inner`).
145///
146/// # Errors
147/// `BridgeError` for all errors from the individual type mappings.
148pub fn bridge_library(
149    lib: &TypeLibrary,
150) -> Result<BTreeMap<String, MinimalTypeObject>, BridgeError> {
151    let flat = flatten(&lib.types, "");
152
153    // Pre-pass: build all types with a null hash, then EquivalenceHashMinimal.
154    let pre_resolver = NullResolver;
155    let mut pre: BTreeMap<String, MinimalTypeObject> = BTreeMap::new();
156    let mut pre_hashes: BTreeMap<String, TypeIdentifier> = BTreeMap::new();
157    for (scoped, td) in &flat {
158        let mto = bridge_typedef(td, &pre_resolver)?;
159        let h = compute_minimal_hash(&mto)
160            .map_err(|e| BridgeError::HashFailed(alloc::format!("{e:?}")))?;
161        pre_hashes.insert(scoped.clone(), TypeIdentifier::EquivalenceHashMinimal(h));
162        pre.insert(scoped.clone(), mto);
163    }
164
165    // Final pass: replace all named refs via pre_hashes.
166    let resolver = MapResolver { named: &pre_hashes };
167    let mut out: BTreeMap<String, MinimalTypeObject> = BTreeMap::new();
168    for (scoped, td) in &flat {
169        let mto = bridge_typedef(td, &resolver)?;
170        out.insert(scoped.clone(), mto);
171    }
172    Ok(out)
173}
174
175// ============================================================================
176// Resolver — strategy for named references
177// ============================================================================
178
179/// Strategy for mapping `TypeRef::Named(...)` onto a [`TypeIdentifier`].
180/// Implementations decide how unresolvable refs
181/// are handled (null hash vs. error).
182trait NameResolver {
183    fn resolve(&self, name: &str) -> TypeIdentifier;
184}
185
186/// Always returns `EquivalenceHashMinimal([0; 14])`. Used in the pre-pass
187/// and for the single-type bridge when no library is known.
188struct NullResolver;
189
190impl NameResolver for NullResolver {
191    fn resolve(&self, _name: &str) -> TypeIdentifier {
192        TypeIdentifier::EquivalenceHashMinimal(zerodds_types::EquivalenceHash([0; 14]))
193    }
194}
195
196/// Returns the hashes registered in `named`; falls back to a null hash.
197struct MapResolver<'a> {
198    named: &'a BTreeMap<String, TypeIdentifier>,
199}
200
201impl NameResolver for MapResolver<'_> {
202    fn resolve(&self, name: &str) -> TypeIdentifier {
203        self.named
204            .get(name)
205            .cloned()
206            .unwrap_or(TypeIdentifier::EquivalenceHashMinimal(
207                zerodds_types::EquivalenceHash([0; 14]),
208            ))
209    }
210}
211
212// ============================================================================
213// Flattening
214// ============================================================================
215
216/// zerodds-lint: recursion-depth 32
217///
218/// XML module hierarchies are rarely deeper than ~8 levels
219/// (`org::omg::dds::core::policy` style). A cap of 32 also covers
220/// pathologically nested test fixtures.
221fn flatten<'a>(types: &'a [TypeDef], prefix: &str) -> Vec<(String, &'a TypeDef)> {
222    let mut out: Vec<(String, &TypeDef)> = Vec::new();
223    for t in types {
224        match t {
225            TypeDef::Module(m) => {
226                let new_prefix = if prefix.is_empty() {
227                    m.name.clone()
228                } else {
229                    alloc::format!("{prefix}::{}", m.name)
230                };
231                out.extend(flatten(&m.types, &new_prefix));
232            }
233            other => {
234                let scoped = if prefix.is_empty() {
235                    other.name().to_string()
236                } else {
237                    alloc::format!("{prefix}::{}", other.name())
238                };
239                out.push((scoped, other));
240            }
241        }
242    }
243    out
244}
245
246// ============================================================================
247// Top-Level Dispatcher
248// ============================================================================
249
250fn bridge_typedef<R: NameResolver>(
251    xml: &TypeDef,
252    res: &R,
253) -> Result<MinimalTypeObject, BridgeError> {
254    match xml {
255        TypeDef::Struct(s) => bridge_struct(s, res),
256        TypeDef::Enum(e) => Ok(MinimalTypeObject::Enumerated(bridge_enum(e))),
257        TypeDef::Union(u) => bridge_union(u, res),
258        TypeDef::Typedef(t) => bridge_typedef_alias(t, res),
259        TypeDef::Bitmask(b) => Ok(MinimalTypeObject::Bitmask(bridge_bitmask(b))),
260        TypeDef::Bitset(b) => bridge_bitset(b),
261        TypeDef::Module(m) => Err(BridgeError::ModuleAtTopLevel(m.name.clone())),
262        TypeDef::Include(_) | TypeDef::ForwardDcl(_) | TypeDef::Const(_) => {
263            Err(BridgeError::UnsupportedXsdConstruct(alloc::format!(
264                "non-bridgeable XML element kind: {}",
265                xml.name()
266            )))
267        }
268    }
269}
270
271// ============================================================================
272// Struct
273// ============================================================================
274
275fn bridge_struct<R: NameResolver>(
276    s: &StructType,
277    res: &R,
278) -> Result<MinimalTypeObject, BridgeError> {
279    let ext = map_extensibility(s.extensibility.unwrap_or_default());
280    let mut builder = TypeObjectBuilder::struct_type(s.name.clone()).extensibility(ext);
281
282    if let Some(base) = &s.base_type {
283        builder = builder.base(res.resolve(base));
284    }
285
286    for m in &s.members {
287        let ti = wrap_member_type(m, res)?;
288        let key = m.key;
289        let optional = m.optional;
290        let must_understand = m.must_understand;
291        let explicit_id = m.id;
292        let name = m.name.clone();
293        builder = builder.member(name, ti, |mut mb| {
294            if key {
295                mb = mb.key();
296            }
297            if optional {
298                mb = mb.optional();
299            }
300            if must_understand {
301                mb = mb.must_understand();
302            }
303            if let Some(id) = explicit_id {
304                mb = mb.id(id);
305            }
306            mb
307        });
308    }
309    Ok(MinimalTypeObject::Struct(builder.build_minimal()))
310}
311
312fn map_extensibility(e: Extensibility) -> TypeExt {
313    match e {
314        Extensibility::Final => TypeExt::Final,
315        Extensibility::Appendable => TypeExt::Appendable,
316        Extensibility::Mutable => TypeExt::Mutable,
317    }
318}
319
320// ============================================================================
321// Enum
322// ============================================================================
323
324fn bridge_enum(e: &EnumType) -> MinimalEnumeratedType {
325    let bit_bound: u16 = e.bit_bound.unwrap_or(32).min(64) as u16;
326    // Auto-numbering: Spec §7.3.1.2.4.1 — if `value` is missing,
327    // uses `prev + 1`, starting at 0.
328    let mut prev: i32 = -1;
329    let literal_seq: Vec<MinimalEnumeratedLiteral> = e
330        .enumerators
331        .iter()
332        .map(|l: &EnumLiteral| {
333            let value = l.value.unwrap_or(prev.saturating_add(1));
334            prev = value;
335            MinimalEnumeratedLiteral {
336                common: CommonEnumeratedLiteral {
337                    value,
338                    flags: EnumLiteralFlag::default(),
339                },
340                detail: zerodds_types::type_object::common::NameHash::from_name(&l.name),
341            }
342        })
343        .collect();
344    MinimalEnumeratedType {
345        enum_flags: EnumTypeFlag::default(),
346        header: MinimalEnumeratedHeader {
347            common: CommonEnumeratedHeader { bit_bound },
348        },
349        literal_seq,
350    }
351}
352
353// ============================================================================
354// Union
355// ============================================================================
356
357fn bridge_union<R: NameResolver>(u: &UnionType, res: &R) -> Result<MinimalTypeObject, BridgeError> {
358    let disc_ti = type_ref_to_identifier(&u.discriminator, res);
359    let mut next_seq: u32 = 1;
360    let mut member_seq: Vec<MinimalUnionMember> = Vec::with_capacity(u.cases.len());
361    for case in &u.cases {
362        let m = &case.member;
363        let mut labels: Vec<i32> = Vec::with_capacity(case.discriminators.len());
364        let mut is_default = false;
365        for d in &case.discriminators {
366            match d {
367                UnionDiscriminator::Default => {
368                    is_default = true;
369                }
370                UnionDiscriminator::Value(s) => {
371                    let v = parse_label(s)?;
372                    labels.push(v);
373                }
374            }
375        }
376        let id = m.id.unwrap_or_else(|| {
377            let v = next_seq;
378            next_seq += 1;
379            v
380        });
381        let ti = wrap_member_type(m, res)?;
382        let mut flags: u16 = 0;
383        if is_default {
384            flags |= UnionMemberFlag::IS_DEFAULT;
385        }
386        member_seq.push(MinimalUnionMember {
387            common: zerodds_types::type_object::common::CommonUnionMember {
388                member_id: id,
389                member_flags: UnionMemberFlag(flags),
390                type_id: ti,
391                label_seq: labels,
392            },
393            detail: zerodds_types::type_object::common::NameHash::from_name(&m.name),
394        });
395    }
396    // The XML union currently has no extensibility marker — default
397    // appendable (Spec §7.2.2.4) → IS_APPENDABLE. Reuse the struct
398    // flag bit positions that the builder mirrors for union flags.
399    let _ = &u.name;
400    let union_flags_bits = StructTypeFlag::IS_APPENDABLE;
401    Ok(MinimalTypeObject::Union(MinimalUnionType {
402        union_flags: UnionTypeFlag(union_flags_bits),
403        discriminator: MinimalDiscriminatorMember {
404            common: CommonDiscriminatorMember {
405                member_flags: UnionDiscriminatorFlag::default(),
406                type_id: disc_ti,
407            },
408        },
409        member_seq,
410    }))
411}
412
413fn parse_label(s: &str) -> Result<i32, BridgeError> {
414    let trimmed = s.trim();
415    if let Some(rest) = trimmed
416        .strip_prefix("0x")
417        .or_else(|| trimmed.strip_prefix("0X"))
418    {
419        return i64::from_str_radix(rest, 16)
420            .ok()
421            .and_then(|v| i32::try_from(v).ok())
422            .ok_or_else(|| BridgeError::InvalidLiteral(s.to_string()));
423    }
424    trimmed
425        .parse::<i32>()
426        .map_err(|_| BridgeError::InvalidLiteral(s.to_string()))
427}
428
429// ============================================================================
430// Typedef → Alias / PlainSequence / PlainArray
431// ============================================================================
432
433fn bridge_typedef_alias<R: NameResolver>(
434    t: &TypedefType,
435    res: &R,
436) -> Result<MinimalTypeObject, BridgeError> {
437    // If arrayDimensions is set, the typedef is a
438    // MinimalArrayType. If sequenceMaxLength is set, it is a
439    // MinimalSequenceType. Otherwise a MinimalAliasType.
440    if !t.array_dimensions.is_empty() {
441        let element =
442            type_ref_to_identifier_with_string_bound(&t.type_ref, t.string_max_length, res);
443        return Ok(MinimalTypeObject::Array(MinimalArrayType {
444            collection_flag: CollectionTypeFlag::default(),
445            bound_seq: t.array_dimensions.clone(),
446            element: MinimalCollectionElement {
447                common: CommonCollectionElement {
448                    element_flags: CollectionElementFlag::default(),
449                    type_id: element,
450                },
451            },
452        }));
453    }
454    if let Some(bound) = t.sequence_max_length {
455        let element =
456            type_ref_to_identifier_with_string_bound(&t.type_ref, t.string_max_length, res);
457        return Ok(MinimalTypeObject::Sequence(MinimalSequenceType {
458            collection_flag: CollectionTypeFlag::default(),
459            bound,
460            element: MinimalCollectionElement {
461                common: CommonCollectionElement {
462                    element_flags: CollectionElementFlag::default(),
463                    type_id: element,
464                },
465            },
466        }));
467    }
468    let related = type_ref_to_identifier_with_string_bound(&t.type_ref, t.string_max_length, res);
469    let alias = TypeObjectBuilder::alias(t.name.clone(), related).build_minimal();
470    Ok(MinimalTypeObject::Alias(alias))
471}
472
473// ============================================================================
474// Bitmask
475// ============================================================================
476
477fn bridge_bitmask(b: &BitmaskType) -> zerodds_types::type_object::minimal::MinimalBitmaskType {
478    let bit_bound: u16 = b.bit_bound.unwrap_or(32).min(64) as u16;
479    let mut prev: i32 = -1;
480    let mut builder = TypeObjectBuilder::bitmask(b.name.clone()).bit_bound(bit_bound);
481    for v in &b.bit_values {
482        let pos = match v.position {
483            Some(p) => {
484                prev = p as i32;
485                p
486            }
487            None => {
488                let p = (prev.saturating_add(1)).max(0) as u32;
489                prev = p as i32;
490                p
491            }
492        };
493        let p_u16 = pos as u16;
494        let _ = v as &BitValue;
495        builder = builder.flag(v.name.clone(), p_u16);
496    }
497    builder.build_minimal()
498}
499
500// ============================================================================
501// Bitset
502// ============================================================================
503
504fn bridge_bitset(b: &BitsetType) -> Result<MinimalTypeObject, BridgeError> {
505    use zerodds_types::type_identifier::kinds::{
506        TK_INT8, TK_INT16, TK_INT32, TK_INT64, TK_UINT8, TK_UINT16, TK_UINT32, TK_UINT64,
507    };
508    // Bit position runs cumulatively: each field sets prev + bitcount.
509    let mut next_pos: u16 = 0;
510    let mut builder = TypeObjectBuilder::bitset(b.name.clone());
511    for f in &b.bit_fields {
512        let bitcount = parse_bitset_mask_bits(&f.mask)?;
513        let holder = match &f.type_ref {
514            TypeRef::Primitive(p) => holder_kind_byte(*p).ok_or_else(|| {
515                BridgeError::UnsupportedXsdConstruct(alloc::format!(
516                    "bitset holder type: {}",
517                    p.as_xml()
518                ))
519            })?,
520            TypeRef::Named(_) => {
521                return Err(BridgeError::UnsupportedXsdConstruct(
522                    "bitset holder must be a primitive integer".to_string(),
523                ));
524            }
525        };
526        // Defensive: that the TK_ constants exist.
527        debug_assert!(matches!(
528            holder,
529            TK_INT8 | TK_INT16 | TK_INT32 | TK_INT64 | TK_UINT8 | TK_UINT16 | TK_UINT32 | TK_UINT64
530        ));
531        let pos = next_pos;
532        next_pos = next_pos.saturating_add(u16::from(bitcount));
533        builder = builder.field(f.name.clone(), pos, bitcount, holder);
534        let _ = f as &BitField;
535    }
536    Ok(MinimalTypeObject::Bitset(builder.build_minimal()))
537}
538
539fn holder_kind_byte(p: XmlPrimitive) -> Option<u8> {
540    use zerodds_types::type_identifier::kinds::*;
541    Some(match p {
542        XmlPrimitive::Octet => TK_BYTE,
543        XmlPrimitive::Short => TK_INT16,
544        XmlPrimitive::UShort => TK_UINT16,
545        XmlPrimitive::Long => TK_INT32,
546        XmlPrimitive::ULong => TK_UINT32,
547        XmlPrimitive::LongLong => TK_INT64,
548        XmlPrimitive::ULongLong => TK_UINT64,
549        XmlPrimitive::Boolean => TK_BOOLEAN,
550        XmlPrimitive::Char => TK_CHAR8,
551        XmlPrimitive::WChar => TK_CHAR16,
552        _ => return None,
553    })
554}
555
556/// Parses a bitmask such as `0x0F` into the number of set bits.
557fn parse_bitset_mask_bits(mask: &str) -> Result<u8, BridgeError> {
558    let trimmed = mask.trim();
559    let (rest, radix) = if let Some(r) = trimmed
560        .strip_prefix("0x")
561        .or_else(|| trimmed.strip_prefix("0X"))
562    {
563        (r, 16u32)
564    } else if let Some(r) = trimmed
565        .strip_prefix("0b")
566        .or_else(|| trimmed.strip_prefix("0B"))
567    {
568        (r, 2u32)
569    } else {
570        (trimmed, 10u32)
571    };
572    let v = u64::from_str_radix(rest, radix)
573        .map_err(|_| BridgeError::InvalidLiteral(alloc::format!("bitset mask {mask}")))?;
574    let bits = u8::try_from(v.count_ones())
575        .map_err(|_| BridgeError::InvalidLiteral(alloc::format!("bitset mask too wide: {mask}")))?;
576    if bits == 0 {
577        return Err(BridgeError::InvalidLiteral(alloc::format!(
578            "bitset mask must have >= 1 bit: {mask}"
579        )));
580    }
581    Ok(bits)
582}
583
584// ============================================================================
585// TypeRef → TypeIdentifier
586// ============================================================================
587
588/// Wraps a struct member into its final TypeIdentifier — incl.
589/// `arrayDimensions`, `sequenceMaxLength` and `stringMaxLength`.
590fn wrap_member_type<R: NameResolver>(
591    m: &StructMember,
592    res: &R,
593) -> Result<TypeIdentifier, BridgeError> {
594    let inner = type_ref_to_identifier_with_string_bound(&m.type_ref, m.string_max_length, res);
595
596    // arrayDimensions takes precedence over sequenceMaxLength.
597    if !m.array_dimensions.is_empty() {
598        return Ok(wrap_array(inner, &m.array_dimensions));
599    }
600    if let Some(bound) = m.sequence_max_length {
601        return Ok(wrap_sequence(inner, bound));
602    }
603    Ok(inner)
604}
605
606fn wrap_sequence(element: TypeIdentifier, bound: u32) -> TypeIdentifier {
607    if bound <= 255 {
608        TypeIdentifier::PlainSequenceSmall {
609            header: zerodds_types::PlainCollectionHeader::default(),
610            bound: bound as u8,
611            element: alloc::boxed::Box::new(element),
612        }
613    } else {
614        TypeIdentifier::PlainSequenceLarge {
615            header: zerodds_types::PlainCollectionHeader::default(),
616            bound,
617            element: alloc::boxed::Box::new(element),
618        }
619    }
620}
621
622fn wrap_array(element: TypeIdentifier, dims: &[u32]) -> TypeIdentifier {
623    let any_large = dims.iter().any(|d| *d > 255);
624    if any_large {
625        TypeIdentifier::PlainArrayLarge {
626            header: zerodds_types::PlainCollectionHeader::default(),
627            array_bounds: dims.to_vec(),
628            element: alloc::boxed::Box::new(element),
629        }
630    } else {
631        TypeIdentifier::PlainArraySmall {
632            header: zerodds_types::PlainCollectionHeader::default(),
633            array_bounds: dims.iter().map(|d| *d as u8).collect(),
634            element: alloc::boxed::Box::new(element),
635        }
636    }
637}
638
639fn type_ref_to_identifier<R: NameResolver>(t: &TypeRef, res: &R) -> TypeIdentifier {
640    type_ref_to_identifier_with_string_bound(t, None, res)
641}
642
643fn type_ref_to_identifier_with_string_bound<R: NameResolver>(
644    t: &TypeRef,
645    string_max_length: Option<u32>,
646    res: &R,
647) -> TypeIdentifier {
648    match t {
649        TypeRef::Primitive(p) => primitive_to_identifier(*p, string_max_length),
650        TypeRef::Named(n) => res.resolve(n),
651    }
652}
653
654fn primitive_to_identifier(p: XmlPrimitive, bound: Option<u32>) -> TypeIdentifier {
655    match p {
656        XmlPrimitive::Boolean => TypeIdentifier::Primitive(PrimitiveKind::Boolean),
657        XmlPrimitive::Octet => TypeIdentifier::Primitive(PrimitiveKind::Byte),
658        XmlPrimitive::Char => TypeIdentifier::Primitive(PrimitiveKind::Char8),
659        XmlPrimitive::WChar => TypeIdentifier::Primitive(PrimitiveKind::Char16),
660        XmlPrimitive::Short => TypeIdentifier::Primitive(PrimitiveKind::Int16),
661        XmlPrimitive::UShort => TypeIdentifier::Primitive(PrimitiveKind::UInt16),
662        XmlPrimitive::Long => TypeIdentifier::Primitive(PrimitiveKind::Int32),
663        XmlPrimitive::ULong => TypeIdentifier::Primitive(PrimitiveKind::UInt32),
664        XmlPrimitive::LongLong => TypeIdentifier::Primitive(PrimitiveKind::Int64),
665        XmlPrimitive::ULongLong => TypeIdentifier::Primitive(PrimitiveKind::UInt64),
666        XmlPrimitive::Float => TypeIdentifier::Primitive(PrimitiveKind::Float32),
667        XmlPrimitive::Double => TypeIdentifier::Primitive(PrimitiveKind::Float64),
668        XmlPrimitive::LongDouble => TypeIdentifier::Primitive(PrimitiveKind::Float128),
669        XmlPrimitive::String => string_id(false, bound.unwrap_or(0)),
670        XmlPrimitive::WString => string_id(true, bound.unwrap_or(0)),
671    }
672}
673
674fn string_id(wide: bool, bound: u32) -> TypeIdentifier {
675    if wide {
676        if bound <= 255 {
677            TypeIdentifier::String16Small { bound: bound as u8 }
678        } else {
679            TypeIdentifier::String16Large { bound }
680        }
681    } else if bound <= 255 {
682        TypeIdentifier::String8Small { bound: bound as u8 }
683    } else {
684        TypeIdentifier::String8Large { bound }
685    }
686}
687
688// ============================================================================
689// Tests
690// ============================================================================
691
692#[cfg(test)]
693#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
694mod tests {
695    use super::*;
696    use crate::xtypes_def::{
697        BitField, BitValue, BitmaskType, BitsetType, EnumLiteral, EnumType, ModuleEntry,
698        StructMember, StructType, TypedefType, UnionCase, UnionType,
699    };
700
701    fn primitive(p: XmlPrimitive) -> TypeRef {
702        TypeRef::Primitive(p)
703    }
704
705    fn make_struct() -> StructType {
706        StructType {
707            name: "Position".into(),
708            extensibility: Some(Extensibility::Final),
709            base_type: None,
710            members: alloc::vec![
711                StructMember {
712                    name: "x".into(),
713                    type_ref: primitive(XmlPrimitive::Float),
714                    key: true,
715                    ..Default::default()
716                },
717                StructMember {
718                    name: "y".into(),
719                    type_ref: primitive(XmlPrimitive::Float),
720                    ..Default::default()
721                },
722            ],
723        }
724    }
725
726    // ---- Struct ----------------------------------------------------------
727
728    #[test]
729    fn struct_two_members_lower_to_minimal_struct() {
730        let s = make_struct();
731        let mto = xml_type_to_minimal_typeobject(&TypeDef::Struct(s)).unwrap();
732        match mto {
733            MinimalTypeObject::Struct(st) => {
734                assert_eq!(st.member_seq.len(), 2);
735                assert!(st.struct_flags.has(StructTypeFlag::IS_FINAL));
736                assert!(matches!(
737                    st.member_seq[0].common.member_type_id,
738                    TypeIdentifier::Primitive(PrimitiveKind::Float32)
739                ));
740            }
741            other => panic!("expected struct, got {other:?}"),
742        }
743    }
744
745    #[test]
746    fn struct_explicit_id_is_preserved() {
747        let s = StructType {
748            name: "X".into(),
749            extensibility: None,
750            base_type: None,
751            members: alloc::vec![StructMember {
752                name: "a".into(),
753                type_ref: primitive(XmlPrimitive::Long),
754                id: Some(42),
755                ..Default::default()
756            }],
757        };
758        let mto = xml_type_to_minimal_typeobject(&TypeDef::Struct(s)).unwrap();
759        if let MinimalTypeObject::Struct(st) = mto {
760            assert_eq!(st.member_seq[0].common.member_id, 42);
761        } else {
762            panic!("not a struct");
763        }
764    }
765
766    #[test]
767    fn struct_autoid_sequential_starts_at_0() {
768        let s = StructType {
769            name: "X".into(),
770            extensibility: None,
771            base_type: None,
772            members: alloc::vec![
773                StructMember {
774                    name: "a".into(),
775                    type_ref: primitive(XmlPrimitive::Long),
776                    ..Default::default()
777                },
778                StructMember {
779                    name: "b".into(),
780                    type_ref: primitive(XmlPrimitive::Long),
781                    ..Default::default()
782                },
783            ],
784        };
785        let mto = xml_type_to_minimal_typeobject(&TypeDef::Struct(s)).unwrap();
786        if let MinimalTypeObject::Struct(st) = mto {
787            // Sequential @autoid is 0-based (XTypes §7.2.2.4.9).
788            assert_eq!(st.member_seq[0].common.member_id, 0);
789            assert_eq!(st.member_seq[1].common.member_id, 1);
790        } else {
791            panic!("not a struct");
792        }
793    }
794
795    #[test]
796    fn struct_member_flags_key_optional_must_understand() {
797        use zerodds_types::type_object::flags::StructMemberFlag;
798        let s = StructType {
799            name: "X".into(),
800            extensibility: None,
801            base_type: None,
802            members: alloc::vec![StructMember {
803                name: "a".into(),
804                type_ref: primitive(XmlPrimitive::Long),
805                key: true,
806                optional: true,
807                must_understand: true,
808                ..Default::default()
809            }],
810        };
811        let mto = xml_type_to_minimal_typeobject(&TypeDef::Struct(s)).unwrap();
812        if let MinimalTypeObject::Struct(st) = mto {
813            let f = st.member_seq[0].common.member_flags;
814            assert!(f.has(StructMemberFlag::IS_KEY));
815            assert!(f.has(StructMemberFlag::IS_OPTIONAL));
816            assert!(f.has(StructMemberFlag::IS_MUST_UNDERSTAND));
817        } else {
818            panic!("not a struct");
819        }
820    }
821
822    #[test]
823    fn struct_string_member_with_bound_uses_string_small() {
824        let s = StructType {
825            name: "X".into(),
826            extensibility: None,
827            base_type: None,
828            members: alloc::vec![StructMember {
829                name: "name".into(),
830                type_ref: primitive(XmlPrimitive::String),
831                string_max_length: Some(64),
832                ..Default::default()
833            }],
834        };
835        let mto = xml_type_to_minimal_typeobject(&TypeDef::Struct(s)).unwrap();
836        if let MinimalTypeObject::Struct(st) = mto {
837            assert_eq!(
838                st.member_seq[0].common.member_type_id,
839                TypeIdentifier::String8Small { bound: 64 }
840            );
841        } else {
842            panic!();
843        }
844    }
845
846    #[test]
847    fn struct_member_with_array_dimensions_wraps_into_plain_array() {
848        let s = StructType {
849            name: "X".into(),
850            extensibility: None,
851            base_type: None,
852            members: alloc::vec![StructMember {
853                name: "a".into(),
854                type_ref: primitive(XmlPrimitive::Long),
855                array_dimensions: alloc::vec![3, 4],
856                ..Default::default()
857            }],
858        };
859        let mto = xml_type_to_minimal_typeobject(&TypeDef::Struct(s)).unwrap();
860        if let MinimalTypeObject::Struct(st) = mto {
861            assert!(matches!(
862                st.member_seq[0].common.member_type_id,
863                TypeIdentifier::PlainArraySmall { .. }
864            ));
865        } else {
866            panic!();
867        }
868    }
869
870    #[test]
871    fn struct_member_with_sequence_bound_wraps_into_plain_sequence() {
872        let s = StructType {
873            name: "X".into(),
874            extensibility: None,
875            base_type: None,
876            members: alloc::vec![StructMember {
877                name: "a".into(),
878                type_ref: primitive(XmlPrimitive::Long),
879                sequence_max_length: Some(100),
880                ..Default::default()
881            }],
882        };
883        let mto = xml_type_to_minimal_typeobject(&TypeDef::Struct(s)).unwrap();
884        if let MinimalTypeObject::Struct(st) = mto {
885            assert!(matches!(
886                st.member_seq[0].common.member_type_id,
887                TypeIdentifier::PlainSequenceSmall { bound: 100, .. }
888            ));
889        } else {
890            panic!();
891        }
892    }
893
894    #[test]
895    fn struct_member_with_large_array_uses_plain_array_large() {
896        let s = StructType {
897            name: "X".into(),
898            extensibility: None,
899            base_type: None,
900            members: alloc::vec![StructMember {
901                name: "a".into(),
902                type_ref: primitive(XmlPrimitive::Long),
903                array_dimensions: alloc::vec![300, 4],
904                ..Default::default()
905            }],
906        };
907        let mto = xml_type_to_minimal_typeobject(&TypeDef::Struct(s)).unwrap();
908        if let MinimalTypeObject::Struct(st) = mto {
909            assert!(matches!(
910                st.member_seq[0].common.member_type_id,
911                TypeIdentifier::PlainArrayLarge { .. }
912            ));
913        } else {
914            panic!();
915        }
916    }
917
918    #[test]
919    fn struct_extensibility_mutable_sets_flag() {
920        let mut s = make_struct();
921        s.extensibility = Some(Extensibility::Mutable);
922        let mto = xml_type_to_minimal_typeobject(&TypeDef::Struct(s)).unwrap();
923        if let MinimalTypeObject::Struct(st) = mto {
924            assert!(st.struct_flags.has(StructTypeFlag::IS_MUTABLE));
925        } else {
926            panic!();
927        }
928    }
929
930    // ---- Enum ------------------------------------------------------------
931
932    #[test]
933    fn enum_with_explicit_values() {
934        let e = EnumType {
935            name: "Color".into(),
936            bit_bound: Some(16),
937            enumerators: alloc::vec![
938                EnumLiteral {
939                    name: "RED".into(),
940                    value: Some(0),
941                },
942                EnumLiteral {
943                    name: "GREEN".into(),
944                    value: Some(1),
945                },
946                EnumLiteral {
947                    name: "BLUE".into(),
948                    value: Some(2),
949                },
950            ],
951        };
952        let mto = xml_type_to_minimal_typeobject(&TypeDef::Enum(e)).unwrap();
953        if let MinimalTypeObject::Enumerated(en) = mto {
954            assert_eq!(en.header.common.bit_bound, 16);
955            assert_eq!(en.literal_seq.len(), 3);
956            assert_eq!(en.literal_seq[2].common.value, 2);
957        } else {
958            panic!();
959        }
960    }
961
962    #[test]
963    fn enum_auto_numbering_starts_at_zero() {
964        let e = EnumType {
965            name: "X".into(),
966            bit_bound: None,
967            enumerators: alloc::vec![
968                EnumLiteral {
969                    name: "A".into(),
970                    value: None,
971                },
972                EnumLiteral {
973                    name: "B".into(),
974                    value: None,
975                },
976                EnumLiteral {
977                    name: "C".into(),
978                    value: Some(10),
979                },
980                EnumLiteral {
981                    name: "D".into(),
982                    value: None,
983                },
984            ],
985        };
986        let mto = xml_type_to_minimal_typeobject(&TypeDef::Enum(e)).unwrap();
987        if let MinimalTypeObject::Enumerated(en) = mto {
988            assert_eq!(en.literal_seq[0].common.value, 0);
989            assert_eq!(en.literal_seq[1].common.value, 1);
990            assert_eq!(en.literal_seq[2].common.value, 10);
991            assert_eq!(en.literal_seq[3].common.value, 11);
992            // Default bit_bound = 32.
993            assert_eq!(en.header.common.bit_bound, 32);
994        } else {
995            panic!();
996        }
997    }
998
999    // ---- Union -----------------------------------------------------------
1000
1001    #[test]
1002    fn union_with_int_disc_and_default_branch() {
1003        let u = UnionType {
1004            name: "U".into(),
1005            discriminator: primitive(XmlPrimitive::Long),
1006            cases: alloc::vec![
1007                UnionCase {
1008                    discriminators: alloc::vec![
1009                        UnionDiscriminator::Value("1".into()),
1010                        UnionDiscriminator::Value("2".into()),
1011                    ],
1012                    member: StructMember {
1013                        name: "a".into(),
1014                        type_ref: primitive(XmlPrimitive::LongLong),
1015                        ..Default::default()
1016                    },
1017                },
1018                UnionCase {
1019                    discriminators: alloc::vec![UnionDiscriminator::Default],
1020                    member: StructMember {
1021                        name: "b".into(),
1022                        type_ref: primitive(XmlPrimitive::String),
1023                        string_max_length: Some(64),
1024                        ..Default::default()
1025                    },
1026                },
1027            ],
1028        };
1029        let mto = xml_type_to_minimal_typeobject(&TypeDef::Union(u)).unwrap();
1030        if let MinimalTypeObject::Union(un) = mto {
1031            assert_eq!(un.member_seq.len(), 2);
1032            assert_eq!(un.member_seq[0].common.label_seq, alloc::vec![1_i32, 2]);
1033            assert!(un.member_seq[1].common.member_flags.0 & UnionMemberFlag::IS_DEFAULT != 0);
1034        } else {
1035            panic!();
1036        }
1037    }
1038
1039    #[test]
1040    fn union_label_hex_parsed() {
1041        let u = UnionType {
1042            name: "U".into(),
1043            discriminator: primitive(XmlPrimitive::Long),
1044            cases: alloc::vec![UnionCase {
1045                discriminators: alloc::vec![UnionDiscriminator::Value("0x10".into())],
1046                member: StructMember {
1047                    name: "a".into(),
1048                    type_ref: primitive(XmlPrimitive::Long),
1049                    ..Default::default()
1050                },
1051            }],
1052        };
1053        let mto = xml_type_to_minimal_typeobject(&TypeDef::Union(u)).unwrap();
1054        if let MinimalTypeObject::Union(un) = mto {
1055            assert_eq!(un.member_seq[0].common.label_seq, alloc::vec![16_i32]);
1056        } else {
1057            panic!();
1058        }
1059    }
1060
1061    #[test]
1062    fn union_label_invalid_returns_invalid_literal() {
1063        let u = UnionType {
1064            name: "U".into(),
1065            discriminator: primitive(XmlPrimitive::Long),
1066            cases: alloc::vec![UnionCase {
1067                discriminators: alloc::vec![UnionDiscriminator::Value("not-an-int".into())],
1068                member: StructMember {
1069                    name: "a".into(),
1070                    type_ref: primitive(XmlPrimitive::Long),
1071                    ..Default::default()
1072                },
1073            }],
1074        };
1075        let err = xml_type_to_minimal_typeobject(&TypeDef::Union(u)).unwrap_err();
1076        assert!(matches!(err, BridgeError::InvalidLiteral(_)));
1077    }
1078
1079    // ---- Typedef ---------------------------------------------------------
1080
1081    #[test]
1082    fn typedef_to_alias() {
1083        let t = TypedefType {
1084            name: "Count".into(),
1085            type_ref: primitive(XmlPrimitive::ULongLong),
1086            ..Default::default()
1087        };
1088        let mto = xml_type_to_minimal_typeobject(&TypeDef::Typedef(t)).unwrap();
1089        if let MinimalTypeObject::Alias(a) = mto {
1090            assert_eq!(
1091                a.body.common.related_type,
1092                TypeIdentifier::Primitive(PrimitiveKind::UInt64)
1093            );
1094        } else {
1095            panic!("not an alias");
1096        }
1097    }
1098
1099    #[test]
1100    fn typedef_with_array_dims_lowers_to_array_type() {
1101        let t = TypedefType {
1102            name: "Vec3".into(),
1103            type_ref: primitive(XmlPrimitive::Float),
1104            array_dimensions: alloc::vec![3],
1105            ..Default::default()
1106        };
1107        let mto = xml_type_to_minimal_typeobject(&TypeDef::Typedef(t)).unwrap();
1108        match mto {
1109            MinimalTypeObject::Array(a) => {
1110                assert_eq!(a.bound_seq, alloc::vec![3]);
1111                assert_eq!(
1112                    a.element.common.type_id,
1113                    TypeIdentifier::Primitive(PrimitiveKind::Float32)
1114                );
1115            }
1116            other => panic!("expected array, got {other:?}"),
1117        }
1118    }
1119
1120    #[test]
1121    fn typedef_with_sequence_bound_lowers_to_sequence_type() {
1122        let t = TypedefType {
1123            name: "Bytes".into(),
1124            type_ref: primitive(XmlPrimitive::Octet),
1125            sequence_max_length: Some(1024),
1126            ..Default::default()
1127        };
1128        let mto = xml_type_to_minimal_typeobject(&TypeDef::Typedef(t)).unwrap();
1129        match mto {
1130            MinimalTypeObject::Sequence(s) => {
1131                assert_eq!(s.bound, 1024);
1132            }
1133            _ => panic!(),
1134        }
1135    }
1136
1137    // ---- Bitmask ---------------------------------------------------------
1138
1139    #[test]
1140    fn bitmask_three_flags() {
1141        let b = BitmaskType {
1142            name: "Perms".into(),
1143            bit_bound: Some(8),
1144            bit_values: alloc::vec![
1145                BitValue {
1146                    name: "READ".into(),
1147                    position: Some(0),
1148                },
1149                BitValue {
1150                    name: "WRITE".into(),
1151                    position: Some(1),
1152                },
1153                BitValue {
1154                    name: "EXEC".into(),
1155                    position: Some(2),
1156                },
1157            ],
1158        };
1159        let mto = xml_type_to_minimal_typeobject(&TypeDef::Bitmask(b)).unwrap();
1160        if let MinimalTypeObject::Bitmask(bm) = mto {
1161            assert_eq!(bm.bit_bound, 8);
1162            assert_eq!(bm.flag_seq.len(), 3);
1163            assert_eq!(bm.flag_seq[0].common.position, 0);
1164            assert_eq!(bm.flag_seq[2].common.position, 2);
1165        } else {
1166            panic!();
1167        }
1168    }
1169
1170    #[test]
1171    fn bitmask_auto_position() {
1172        let b = BitmaskType {
1173            name: "X".into(),
1174            bit_bound: None,
1175            bit_values: alloc::vec![
1176                BitValue {
1177                    name: "A".into(),
1178                    position: None,
1179                },
1180                BitValue {
1181                    name: "B".into(),
1182                    position: None,
1183                },
1184                BitValue {
1185                    name: "C".into(),
1186                    position: Some(10),
1187                },
1188                BitValue {
1189                    name: "D".into(),
1190                    position: None,
1191                },
1192            ],
1193        };
1194        let mto = xml_type_to_minimal_typeobject(&TypeDef::Bitmask(b)).unwrap();
1195        if let MinimalTypeObject::Bitmask(bm) = mto {
1196            assert_eq!(bm.flag_seq[0].common.position, 0);
1197            assert_eq!(bm.flag_seq[1].common.position, 1);
1198            assert_eq!(bm.flag_seq[2].common.position, 10);
1199            assert_eq!(bm.flag_seq[3].common.position, 11);
1200            assert_eq!(bm.bit_bound, 32);
1201        } else {
1202            panic!();
1203        }
1204    }
1205
1206    // ---- Bitset ----------------------------------------------------------
1207
1208    #[test]
1209    fn bitset_with_two_fields() {
1210        let b = BitsetType {
1211            name: "Packed".into(),
1212            bit_fields: alloc::vec![
1213                BitField {
1214                    name: "header".into(),
1215                    type_ref: primitive(XmlPrimitive::ULong),
1216                    mask: "0x0F".into(),
1217                },
1218                BitField {
1219                    name: "body".into(),
1220                    type_ref: primitive(XmlPrimitive::ULong),
1221                    mask: "0xFFFFFFF0".into(),
1222                },
1223            ],
1224        };
1225        let mto = xml_type_to_minimal_typeobject(&TypeDef::Bitset(b)).unwrap();
1226        if let MinimalTypeObject::Bitset(bs) = mto {
1227            assert_eq!(bs.field_seq.len(), 2);
1228            assert_eq!(bs.field_seq[0].common.bitcount, 4);
1229            assert_eq!(bs.field_seq[0].common.position, 0);
1230            assert_eq!(bs.field_seq[1].common.bitcount, 28);
1231            assert_eq!(bs.field_seq[1].common.position, 4);
1232        } else {
1233            panic!();
1234        }
1235    }
1236
1237    #[test]
1238    fn bitset_invalid_mask_rejected() {
1239        let b = BitsetType {
1240            name: "X".into(),
1241            bit_fields: alloc::vec![BitField {
1242                name: "f".into(),
1243                type_ref: primitive(XmlPrimitive::ULong),
1244                mask: "garbage".into(),
1245            }],
1246        };
1247        let err = xml_type_to_minimal_typeobject(&TypeDef::Bitset(b)).unwrap_err();
1248        assert!(matches!(err, BridgeError::InvalidLiteral(_)));
1249    }
1250
1251    #[test]
1252    fn bitset_zero_mask_rejected() {
1253        let b = BitsetType {
1254            name: "X".into(),
1255            bit_fields: alloc::vec![BitField {
1256                name: "f".into(),
1257                type_ref: primitive(XmlPrimitive::ULong),
1258                mask: "0x00".into(),
1259            }],
1260        };
1261        let err = xml_type_to_minimal_typeobject(&TypeDef::Bitset(b)).unwrap_err();
1262        assert!(matches!(err, BridgeError::InvalidLiteral(_)));
1263    }
1264
1265    // ---- Module / Library -----------------------------------------------
1266
1267    #[test]
1268    fn module_at_top_level_is_error() {
1269        let m = TypeDef::Module(ModuleEntry {
1270            name: "M".into(),
1271            types: alloc::vec![],
1272        });
1273        let err = xml_type_to_minimal_typeobject(&m).unwrap_err();
1274        assert!(matches!(err, BridgeError::ModuleAtTopLevel(_)));
1275    }
1276
1277    #[test]
1278    fn library_resolves_named_refs_between_types() {
1279        // struct Point { float x; float y; };
1280        // struct Line { Point a; Point b; };
1281        let lib = TypeLibrary {
1282            name: "Lib".into(),
1283            types: alloc::vec![
1284                TypeDef::Struct(StructType {
1285                    name: "Point".into(),
1286                    extensibility: None,
1287                    base_type: None,
1288                    members: alloc::vec![
1289                        StructMember {
1290                            name: "x".into(),
1291                            type_ref: primitive(XmlPrimitive::Float),
1292                            ..Default::default()
1293                        },
1294                        StructMember {
1295                            name: "y".into(),
1296                            type_ref: primitive(XmlPrimitive::Float),
1297                            ..Default::default()
1298                        },
1299                    ],
1300                }),
1301                TypeDef::Struct(StructType {
1302                    name: "Line".into(),
1303                    extensibility: None,
1304                    base_type: None,
1305                    members: alloc::vec![
1306                        StructMember {
1307                            name: "a".into(),
1308                            type_ref: TypeRef::Named("Point".into()),
1309                            ..Default::default()
1310                        },
1311                        StructMember {
1312                            name: "b".into(),
1313                            type_ref: TypeRef::Named("Point".into()),
1314                            ..Default::default()
1315                        },
1316                    ],
1317                }),
1318            ],
1319        };
1320        let map = bridge_library(&lib).unwrap();
1321        let line = map.get("Line").expect("Line registered");
1322        if let MinimalTypeObject::Struct(st) = line {
1323            // Both members should point to the same hash.
1324            assert_eq!(
1325                st.member_seq[0].common.member_type_id,
1326                st.member_seq[1].common.member_type_id
1327            );
1328            assert!(matches!(
1329                st.member_seq[0].common.member_type_id,
1330                TypeIdentifier::EquivalenceHashMinimal(_)
1331            ));
1332        } else {
1333            panic!("Line not a struct");
1334        }
1335        // Both types in the map.
1336        assert!(map.contains_key("Point"));
1337    }
1338
1339    #[test]
1340    fn library_module_flattens_to_scoped_name() {
1341        let lib = TypeLibrary {
1342            name: "L".into(),
1343            types: alloc::vec![TypeDef::Module(ModuleEntry {
1344                name: "Inner".into(),
1345                types: alloc::vec![TypeDef::Struct(StructType {
1346                    name: "S".into(),
1347                    extensibility: None,
1348                    base_type: None,
1349                    members: alloc::vec![],
1350                })],
1351            })],
1352        };
1353        let map = bridge_library(&lib).unwrap();
1354        assert!(map.contains_key("Inner::S"));
1355    }
1356
1357    #[test]
1358    fn library_unknown_named_ref_falls_back_to_zero_hash() {
1359        let lib = TypeLibrary {
1360            name: "L".into(),
1361            types: alloc::vec![TypeDef::Struct(StructType {
1362                name: "S".into(),
1363                extensibility: None,
1364                base_type: None,
1365                members: alloc::vec![StructMember {
1366                    name: "missing".into(),
1367                    type_ref: TypeRef::Named("DoesNotExist".into()),
1368                    ..Default::default()
1369                }],
1370            })],
1371        };
1372        let map = bridge_library(&lib).unwrap();
1373        let s = map.get("S").unwrap();
1374        if let MinimalTypeObject::Struct(st) = s {
1375            // Null hash as fallback.
1376            assert_eq!(
1377                st.member_seq[0].common.member_type_id,
1378                TypeIdentifier::EquivalenceHashMinimal(zerodds_types::EquivalenceHash([0; 14]))
1379            );
1380        } else {
1381            panic!();
1382        }
1383    }
1384
1385    // ---- TypeIdentifier-Hash --------------------------------------------
1386
1387    #[test]
1388    fn typeobject_can_be_hashed() {
1389        let s = make_struct();
1390        let to = xml_type_to_typeobject(&TypeDef::Struct(s)).unwrap();
1391        let h = zerodds_types::compute_hash(&to).expect("hash");
1392        // Deterministic: the same result twice.
1393        let h2 = zerodds_types::compute_hash(&to).unwrap();
1394        assert_eq!(h, h2);
1395    }
1396
1397    #[test]
1398    fn xml_struct_matches_idl_struct_hash() {
1399        // Cross-check: an XML struct and an equivalent definition built
1400        // directly in the builder should have the same hash
1401        // (provided member IDs/flags/types are identical).
1402        let xml_struct = StructType {
1403            name: "Sensor".into(),
1404            extensibility: Some(Extensibility::Appendable),
1405            base_type: None,
1406            members: alloc::vec![
1407                StructMember {
1408                    name: "id".into(),
1409                    type_ref: primitive(XmlPrimitive::LongLong),
1410                    key: true,
1411                    ..Default::default()
1412                },
1413                StructMember {
1414                    name: "temp".into(),
1415                    type_ref: primitive(XmlPrimitive::Float),
1416                    ..Default::default()
1417                },
1418            ],
1419        };
1420        let xml_mto = xml_type_to_minimal_typeobject(&TypeDef::Struct(xml_struct)).unwrap();
1421        let xml_hash = compute_minimal_hash(&xml_mto).unwrap();
1422
1423        // Aequivalentes via Builder direkt:
1424        let builder_struct = TypeObjectBuilder::struct_type("Sensor")
1425            .extensibility(TypeExt::Appendable)
1426            .member("id", TypeIdentifier::Primitive(PrimitiveKind::Int64), |m| {
1427                m.key()
1428            })
1429            .member(
1430                "temp",
1431                TypeIdentifier::Primitive(PrimitiveKind::Float32),
1432                |m| m,
1433            )
1434            .build_minimal();
1435        let builder_mto = MinimalTypeObject::Struct(builder_struct);
1436        let builder_hash = compute_minimal_hash(&builder_mto).unwrap();
1437        assert_eq!(
1438            xml_hash, builder_hash,
1439            "the XML bridge and builder must produce byte-identical TypeObjects"
1440        );
1441    }
1442
1443    // ---- Display impl of BridgeError -------------------------------------
1444
1445    #[test]
1446    fn bridge_error_display() {
1447        let e = BridgeError::UnsupportedXsdConstruct("foo".into());
1448        assert!(e.to_string().contains("foo"));
1449        let e = BridgeError::UnresolvedReference("Bar".into());
1450        assert!(e.to_string().contains("Bar"));
1451        let e = BridgeError::InvalidLiteral("x".into());
1452        assert!(e.to_string().contains("x"));
1453        let e = BridgeError::HashFailed("oom".into());
1454        assert!(e.to_string().contains("oom"));
1455        let e = BridgeError::ModuleAtTopLevel("M".into());
1456        assert!(e.to_string().contains("M"));
1457    }
1458}