Skip to main content

zerodds_xml/
sample.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! DDS-XML 1.0 §7.3.7 Building Block "Data Samples" — XML-Codec.
4//!
5//! Repraesentiert konkrete Sample-Werte einzelner registrierter Types als
6//! XML. Ein Sample ist Member-Wert-Map: Element-Namen entsprechen
7//! Member-Namen, Children rekursiv kodierte Werte. Sequenzen/Arrays
8//! verwenden `<item>` als Element-Name (Spec §7.3.7.4.4).
9//!
10//! # XML → Rust-Type Mapping
11//!
12//! ```text
13//! <sample type_ref="Mod::Type"> … </sample>  | SampleValue::Struct
14//! <member-name>123</member-name>             | SampleValue::Primitive
15//! <seq-name><item>…</item>…</seq-name>       | SampleValue::Sequence
16//! <arr-name><item>…</item>…</arr-name>       | SampleValue::Array
17//! <union-name><discriminator>…</discriminator>
18//!             <case-name>…</case-name></union-name>
19//!                                            | SampleValue::Union
20//! ```
21
22use alloc::collections::BTreeMap;
23use alloc::format;
24use alloc::string::{String, ToString};
25use alloc::vec::Vec;
26
27use crate::errors::XmlError;
28use crate::parser::{XmlElement, parse_xml_tree};
29use crate::xtypes_def::{
30    PrimitiveType, StructMember, StructType, TypeDef, TypeLibrary, TypeRef, UnionDiscriminator,
31};
32
33/// Wert eines konkreten Sample-Members (rekursiv).
34#[derive(Debug, Clone, PartialEq)]
35pub enum SampleValue {
36    /// Primitiv-Wert.
37    Primitive(PrimitiveValue),
38    /// Struct-Member-Map (Member-Name -> Wert).
39    Struct(BTreeMap<String, SampleValue>),
40    /// Sequence-Werte.
41    Sequence(Vec<SampleValue>),
42    /// Array-Werte.
43    Array(Vec<SampleValue>),
44    /// Union mit aktivem Discriminator und ausgewaehltem Case-Wert.
45    Union {
46        /// String-Form des Discriminator-Wertes.
47        discriminator: String,
48        /// Aktiver Case-Wert.
49        value: alloc::boxed::Box<SampleValue>,
50    },
51    /// Enum-Literal als Symbol.
52    EnumLiteral(String),
53}
54
55/// Konkreter Primitiv-Wert (typed, mit Range-Check beim Parse).
56#[derive(Debug, Clone, PartialEq)]
57pub enum PrimitiveValue {
58    /// `boolean`.
59    Bool(bool),
60    /// `char` als 8-bit signed (IDL-konform).
61    I8(i8),
62    /// `octet`.
63    U8(u8),
64    /// `short`.
65    I16(i16),
66    /// `ushort` / `wchar`.
67    U16(u16),
68    /// `long`.
69    I32(i32),
70    /// `ulong`.
71    U32(u32),
72    /// `longlong`.
73    I64(i64),
74    /// `ulonglong`.
75    U64(u64),
76    /// `float`.
77    F32(f32),
78    /// `double` / `longdouble` (longdouble fallt auf f64 zurueck).
79    F64(f64),
80    /// `string` / `wstring`.
81    Str(String),
82    /// 8-bit `char` als Unicode-Codepoint.
83    Char(char),
84}
85
86/// Parst ein konkretes `<sample>`-Element gegen eine Type-Definition.
87///
88/// `xml` darf entweder ein `<sample>`-Wurzel-Element oder das Wurzel-
89/// Element des Datentyps direkt sein (Spec §7.3.7.4 erlaubt beide
90/// Repraesentationen).
91///
92/// # Errors
93/// * [`XmlError::InvalidXml`] — XML nicht wohlgeformt.
94/// * [`XmlError::UnresolvedReference`] — referenzierter Type nicht im
95///   `type_lib` auffindbar.
96/// * [`XmlError::ValueOutOfRange`] — Primitiv-Wert ausserhalb Range.
97/// * [`XmlError::MissingRequiredElement`] — Pflicht-Member fehlt.
98pub fn parse_sample(
99    xml: &str,
100    type_def: &TypeDef,
101    type_lib: &[TypeLibrary],
102) -> Result<SampleValue, XmlError> {
103    let doc = parse_xml_tree(xml)?;
104    parse_sample_element(&doc.root, type_def, type_lib)
105}
106
107/// Variante von [`parse_sample`], die bereits ein geparstes
108/// [`XmlElement`] entgegen nimmt.
109///
110/// # Errors
111/// Wie [`parse_sample`].
112pub fn parse_sample_element(
113    el: &XmlElement,
114    type_def: &TypeDef,
115    type_lib: &[TypeLibrary],
116) -> Result<SampleValue, XmlError> {
117    parse_value_for_typedef(el, type_def, type_lib)
118}
119
120fn parse_value_for_typedef(
121    el: &XmlElement,
122    type_def: &TypeDef,
123    type_lib: &[TypeLibrary],
124) -> Result<SampleValue, XmlError> {
125    match type_def {
126        TypeDef::Struct(s) => parse_struct_value(el, s, type_lib),
127        TypeDef::Enum(_) => Ok(SampleValue::EnumLiteral(el.text.clone())),
128        TypeDef::Union(u) => parse_union_value(el, u, type_lib),
129        TypeDef::Typedef(td) => {
130            let synthetic = StructMember {
131                name: td.name.clone(),
132                type_ref: td.type_ref.clone(),
133                array_dimensions: td.array_dimensions.clone(),
134                sequence_max_length: td.sequence_max_length,
135                string_max_length: td.string_max_length,
136                ..Default::default()
137            };
138            parse_member_value(el, &synthetic, type_lib)
139        }
140        TypeDef::Bitmask(_) => Ok(SampleValue::Primitive(PrimitiveValue::Str(el.text.clone()))),
141        TypeDef::Bitset(_) => Ok(SampleValue::Primitive(PrimitiveValue::Str(el.text.clone()))),
142        TypeDef::Module(m) => Err(XmlError::UnresolvedReference(format!(
143            "module `{}` is not directly serializable",
144            m.name
145        ))),
146        TypeDef::Include(i) => Err(XmlError::UnresolvedReference(format!(
147            "include `{}` cannot be serialized as sample value",
148            i.file
149        ))),
150        TypeDef::ForwardDcl(f) => Err(XmlError::UnresolvedReference(format!(
151            "forward declaration `{}` cannot be serialized — body missing",
152            f.name
153        ))),
154        TypeDef::Const(c) => Ok(SampleValue::Primitive(PrimitiveValue::Str(c.value.clone()))),
155    }
156}
157
158fn parse_struct_value(
159    el: &XmlElement,
160    s: &StructType,
161    type_lib: &[TypeLibrary],
162) -> Result<SampleValue, XmlError> {
163    let mut map = BTreeMap::new();
164    for member in &s.members {
165        if let Some(child) = el.child(&member.name) {
166            let v = parse_member_value(child, member, type_lib)?;
167            map.insert(member.name.clone(), v);
168        } else if !member.optional {
169            return Err(XmlError::MissingRequiredElement(member.name.clone()));
170        }
171    }
172    Ok(SampleValue::Struct(map))
173}
174
175fn parse_union_value(
176    el: &XmlElement,
177    u: &crate::xtypes_def::UnionType,
178    type_lib: &[TypeLibrary],
179) -> Result<SampleValue, XmlError> {
180    let disc_el = el
181        .child("discriminator")
182        .ok_or_else(|| XmlError::MissingRequiredElement("discriminator".into()))?;
183    let disc = disc_el.text.clone();
184
185    // Active case lookup
186    let case = u
187        .cases
188        .iter()
189        .find(|c| {
190            c.discriminators.iter().any(|d| match d {
191                UnionDiscriminator::Value(v) => v == &disc,
192                UnionDiscriminator::Default => false,
193            })
194        })
195        .or_else(|| {
196            u.cases.iter().find(|c| {
197                c.discriminators
198                    .iter()
199                    .any(|d| matches!(d, UnionDiscriminator::Default))
200            })
201        })
202        .ok_or_else(|| {
203            XmlError::UnresolvedReference(format!("union case for discriminator `{disc}`"))
204        })?;
205    let val_el = el.child(&case.member.name).ok_or_else(|| {
206        XmlError::MissingRequiredElement(format!("union member `{}`", case.member.name))
207    })?;
208    let value = parse_member_value(val_el, &case.member, type_lib)?;
209    Ok(SampleValue::Union {
210        discriminator: disc,
211        value: alloc::boxed::Box::new(value),
212    })
213}
214
215fn parse_member_value(
216    el: &XmlElement,
217    member: &StructMember,
218    type_lib: &[TypeLibrary],
219) -> Result<SampleValue, XmlError> {
220    // Array (multi-dim collapsed to flat sequence of items at top level).
221    if !member.array_dimensions.is_empty() {
222        let mut items = Vec::new();
223        for child in el.children_named("item") {
224            let inner = parse_inner_value(child, &member.type_ref, type_lib)?;
225            items.push(inner);
226        }
227        return Ok(SampleValue::Array(items));
228    }
229    // Sequence — wenn Member als sequence deklariert ist (max-length
230    // gesetzt) ODER der Wrapper-Knoten ausschliesslich `<item>`-Children
231    // enthaelt. Spec §7.3.7.3.2: leere Sequence (`<seq></seq>`) ist
232    // valide → bei `sequence_max_length.is_some()` auch empty zulassen.
233    let is_seq = member.sequence_max_length.is_some() || has_only_item_children(el);
234    if is_seq
235        && (has_item_children(el)
236            || (member.sequence_max_length.is_some() && el.children.is_empty()))
237    {
238        let mut items = Vec::new();
239        for child in el.children_named("item") {
240            let inner = parse_inner_value(child, &member.type_ref, type_lib)?;
241            items.push(inner);
242        }
243        return Ok(SampleValue::Sequence(items));
244    }
245    parse_inner_value(el, &member.type_ref, type_lib)
246}
247
248fn has_item_children(el: &XmlElement) -> bool {
249    el.children.iter().any(|c| c.name == "item")
250}
251
252fn has_only_item_children(el: &XmlElement) -> bool {
253    !el.children.is_empty() && el.children.iter().all(|c| c.name == "item")
254}
255
256fn parse_inner_value(
257    el: &XmlElement,
258    type_ref: &TypeRef,
259    type_lib: &[TypeLibrary],
260) -> Result<SampleValue, XmlError> {
261    match type_ref {
262        TypeRef::Primitive(p) => parse_primitive_value(&el.text, *p).map(SampleValue::Primitive),
263        TypeRef::Named(name) => {
264            let td = resolve_named_type(name, type_lib).ok_or_else(|| {
265                XmlError::UnresolvedReference(format!("type `{name}` not in libraries"))
266            })?;
267            parse_value_for_typedef(el, &td, type_lib)
268        }
269    }
270}
271
272fn resolve_named_type(name: &str, type_lib: &[TypeLibrary]) -> Option<TypeDef> {
273    let parts: Vec<&str> = name.split("::").collect();
274    for lib in type_lib {
275        if let Some(td) = walk(&lib.types, &parts) {
276            return Some(td);
277        }
278    }
279    None
280}
281
282/// zerodds-lint: recursion-depth = anzahl `::`-Segmente + Modul-Schachtelungstiefe.
283/// Effektiv ≤ 16 durch DoS-Cap der XML-Foundation.
284fn walk(types: &[TypeDef], parts: &[&str]) -> Option<TypeDef> {
285    if parts.is_empty() {
286        return None;
287    }
288    let head = parts[0];
289    for t in types {
290        if t.name() == head {
291            if parts.len() == 1 {
292                return Some(t.clone());
293            }
294            if let TypeDef::Module(m) = t {
295                return walk(&m.types, &parts[1..]);
296            }
297        }
298    }
299    // Fallback: recursive search through modules at any depth (bare name).
300    if parts.len() == 1 {
301        for t in types {
302            if let TypeDef::Module(m) = t {
303                if let Some(found) = walk(&m.types, parts) {
304                    return Some(found);
305                }
306            }
307        }
308    }
309    None
310}
311
312fn parse_primitive_value(s: &str, p: PrimitiveType) -> Result<PrimitiveValue, XmlError> {
313    let t = s.trim();
314    match p {
315        PrimitiveType::Boolean => Ok(PrimitiveValue::Bool(crate::types::parse_bool(t)?)),
316        PrimitiveType::Octet => parse_uint::<u8>(t).map(PrimitiveValue::U8),
317        PrimitiveType::Char => {
318            let mut chars = t.chars();
319            let c = chars
320                .next()
321                .ok_or_else(|| XmlError::ValueOutOfRange("empty char".into()))?;
322            if chars.next().is_some() {
323                return Err(XmlError::ValueOutOfRange(format!(
324                    "char `{t}` is multi-char"
325                )));
326            }
327            Ok(PrimitiveValue::Char(c))
328        }
329        PrimitiveType::WChar => {
330            let mut chars = t.chars();
331            let c = chars
332                .next()
333                .ok_or_else(|| XmlError::ValueOutOfRange("empty wchar".into()))?;
334            Ok(PrimitiveValue::Char(c))
335        }
336        PrimitiveType::Short => parse_int::<i16>(t).map(PrimitiveValue::I16),
337        PrimitiveType::UShort => parse_uint::<u16>(t).map(PrimitiveValue::U16),
338        PrimitiveType::Long => parse_int::<i32>(t).map(PrimitiveValue::I32),
339        PrimitiveType::ULong => parse_uint::<u32>(t).map(PrimitiveValue::U32),
340        PrimitiveType::LongLong => parse_int::<i64>(t).map(PrimitiveValue::I64),
341        PrimitiveType::ULongLong => parse_uint::<u64>(t).map(PrimitiveValue::U64),
342        PrimitiveType::Float => t
343            .parse::<f32>()
344            .map(PrimitiveValue::F32)
345            .map_err(|e| XmlError::ValueOutOfRange(format!("float `{t}`: {e}"))),
346        PrimitiveType::Double | PrimitiveType::LongDouble => t
347            .parse::<f64>()
348            .map(PrimitiveValue::F64)
349            .map_err(|e| XmlError::ValueOutOfRange(format!("double `{t}`: {e}"))),
350        PrimitiveType::String | PrimitiveType::WString => Ok(PrimitiveValue::Str(s.to_string())),
351    }
352}
353
354fn parse_int<T>(s: &str) -> Result<T, XmlError>
355where
356    T: core::str::FromStr,
357    T::Err: core::fmt::Display,
358{
359    s.parse::<T>()
360        .map_err(|e| XmlError::ValueOutOfRange(format!("int `{s}`: {e}")))
361}
362
363fn parse_uint<T>(s: &str) -> Result<T, XmlError>
364where
365    T: core::str::FromStr,
366    T::Err: core::fmt::Display,
367{
368    if let Some(hex) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
369        // Parse as u64, then narrow.
370        let v = u64::from_str_radix(hex, 16)
371            .map_err(|e| XmlError::ValueOutOfRange(format!("hex `{s}`: {e}")))?;
372        let s2 = alloc::format!("{v}");
373        return s2
374            .parse::<T>()
375            .map_err(|e| XmlError::ValueOutOfRange(format!("uint `{s}`: {e}")));
376    }
377    s.parse::<T>()
378        .map_err(|e| XmlError::ValueOutOfRange(format!("uint `{s}`: {e}")))
379}
380
381/// Serialisiert einen Sample-Wert als XML.
382///
383/// Wrapped das Ergebnis in `<sample type_ref="…">`. Member-Namen
384/// entsprechen den Schluesseln in `value`, Sequenzen/Arrays nutzen
385/// `<item>`.
386#[must_use]
387pub fn serialize_sample(
388    value: &SampleValue,
389    type_def: &TypeDef,
390    _type_lib: &[TypeLibrary],
391) -> String {
392    let mut out = String::new();
393    out.push_str("<sample type_ref=\"");
394    out.push_str(type_def.name());
395    out.push_str("\">");
396    serialize_value_body(value, &mut out);
397    out.push_str("</sample>");
398    out
399}
400
401/// zerodds-lint: recursion-depth = sample-tree-depth (struct/sequence/array
402/// nesting). DoS-Cap der XML-Foundation begrenzt Tiefe ≤ 16.
403fn serialize_value_body(value: &SampleValue, out: &mut String) {
404    match value {
405        SampleValue::Primitive(p) => out.push_str(&serialize_primitive(p)),
406        SampleValue::EnumLiteral(s) => out.push_str(&xml_escape(s)),
407        SampleValue::Struct(map) => {
408            for (k, v) in map {
409                out.push('<');
410                out.push_str(k);
411                out.push('>');
412                serialize_value_body(v, out);
413                out.push_str("</");
414                out.push_str(k);
415                out.push('>');
416            }
417        }
418        SampleValue::Sequence(items) | SampleValue::Array(items) => {
419            for it in items {
420                out.push_str("<item>");
421                serialize_value_body(it, out);
422                out.push_str("</item>");
423            }
424        }
425        SampleValue::Union {
426            discriminator,
427            value,
428        } => {
429            out.push_str("<discriminator>");
430            out.push_str(&xml_escape(discriminator));
431            out.push_str("</discriminator>");
432            // The active case member name is unknown here; we emit
433            // a generic <value> wrapper.
434            out.push_str("<value>");
435            serialize_value_body(value, out);
436            out.push_str("</value>");
437        }
438    }
439}
440
441fn serialize_primitive(p: &PrimitiveValue) -> String {
442    match p {
443        PrimitiveValue::Bool(b) => if *b { "true" } else { "false" }.to_string(),
444        PrimitiveValue::I8(v) => alloc::format!("{v}"),
445        PrimitiveValue::U8(v) => alloc::format!("{v}"),
446        PrimitiveValue::I16(v) => alloc::format!("{v}"),
447        PrimitiveValue::U16(v) => alloc::format!("{v}"),
448        PrimitiveValue::I32(v) => alloc::format!("{v}"),
449        PrimitiveValue::U32(v) => alloc::format!("{v}"),
450        PrimitiveValue::I64(v) => alloc::format!("{v}"),
451        PrimitiveValue::U64(v) => alloc::format!("{v}"),
452        PrimitiveValue::F32(v) => alloc::format!("{v}"),
453        PrimitiveValue::F64(v) => alloc::format!("{v}"),
454        PrimitiveValue::Str(s) => xml_escape(s),
455        PrimitiveValue::Char(c) => xml_escape(&alloc::format!("{c}")),
456    }
457}
458
459fn xml_escape(s: &str) -> String {
460    let mut out = String::with_capacity(s.len());
461    for c in s.chars() {
462        match c {
463            '<' => out.push_str("&lt;"),
464            '>' => out.push_str("&gt;"),
465            '&' => out.push_str("&amp;"),
466            '"' => out.push_str("&quot;"),
467            '\'' => out.push_str("&apos;"),
468            // Spec §7.3.7.4.5: Non-ASCII via numeric entity.
469            c if (c as u32) > 0x7F => {
470                let code = c as u32;
471                out.push_str(&alloc::format!("&#x{code:x};"));
472            }
473            c => out.push(c),
474        }
475    }
476    out
477}
478
479#[cfg(test)]
480#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
481mod tests {
482    use super::*;
483    use crate::xtypes_def::{StructMember, StructType, TypeRef};
484
485    #[test]
486    fn parse_simple_struct_sample() {
487        let td = TypeDef::Struct(StructType {
488            name: "X".into(),
489            members: alloc::vec![StructMember {
490                name: "id".into(),
491                type_ref: TypeRef::Primitive(PrimitiveType::Long),
492                ..Default::default()
493            }],
494            ..Default::default()
495        });
496        let xml = r#"<sample type_ref="X"><id>42</id></sample>"#;
497        let v = parse_sample(xml, &td, &[]).expect("parse");
498        let SampleValue::Struct(m) = v else { panic!() };
499        assert!(matches!(
500            m.get("id"),
501            Some(SampleValue::Primitive(PrimitiveValue::I32(42)))
502        ));
503    }
504
505    #[test]
506    fn missing_member_rejected() {
507        let td = TypeDef::Struct(StructType {
508            name: "X".into(),
509            members: alloc::vec![StructMember {
510                name: "id".into(),
511                type_ref: TypeRef::Primitive(PrimitiveType::Long),
512                ..Default::default()
513            }],
514            ..Default::default()
515        });
516        let xml = r#"<sample type_ref="X"></sample>"#;
517        let err = parse_sample(xml, &td, &[]).expect_err("missing");
518        assert!(matches!(err, XmlError::MissingRequiredElement(_)));
519    }
520
521    // ---- §7.3.7.3.2 Sequence/Array mit <item>-Tag ---------------------
522
523    #[test]
524    fn parse_sample_with_sequence_using_item_tag() {
525        // Spec §7.3.7.3.2: "complexType contains zero or more elements
526        // named **item**". Hier explizit Sequence<long> mit max=10.
527        let td = TypeDef::Struct(StructType {
528            name: "WithSeq".into(),
529            members: alloc::vec![StructMember {
530                name: "ids".into(),
531                type_ref: TypeRef::Primitive(PrimitiveType::Long),
532                sequence_max_length: Some(10),
533                ..Default::default()
534            }],
535            ..Default::default()
536        });
537        let xml = r#"<sample type_ref="WithSeq"><ids><item>1</item><item>2</item><item>3</item></ids></sample>"#;
538        let v = parse_sample(xml, &td, &[]).expect("parse");
539        let SampleValue::Struct(m) = v else { panic!() };
540        let ids = m.get("ids").expect("ids");
541        let SampleValue::Sequence(items) = ids else {
542            panic!("expected Sequence, got {ids:?}")
543        };
544        assert_eq!(items.len(), 3);
545        assert!(matches!(
546            items[0],
547            SampleValue::Primitive(PrimitiveValue::I32(1))
548        ));
549        assert!(matches!(
550            items[2],
551            SampleValue::Primitive(PrimitiveValue::I32(3))
552        ));
553    }
554
555    #[test]
556    fn parse_sample_with_array_using_item_tag() {
557        // Spec §7.3.7.3.2: Arrays nutzen denselben <item>-Tag.
558        // array_dimensions[3] mit element_type long.
559        let td = TypeDef::Struct(StructType {
560            name: "WithArr".into(),
561            members: alloc::vec![StructMember {
562                name: "coords".into(),
563                type_ref: TypeRef::Primitive(PrimitiveType::Long),
564                array_dimensions: alloc::vec![3],
565                ..Default::default()
566            }],
567            ..Default::default()
568        });
569        let xml = r#"<sample type_ref="WithArr"><coords><item>10</item><item>20</item><item>30</item></coords></sample>"#;
570        let v = parse_sample(xml, &td, &[]).expect("parse");
571        let SampleValue::Struct(m) = v else { panic!() };
572        let coords = m.get("coords").expect("coords");
573        let SampleValue::Array(items) = coords else {
574            panic!("expected Array, got {coords:?}")
575        };
576        assert_eq!(items.len(), 3);
577    }
578
579    #[test]
580    fn parse_sample_with_empty_sequence() {
581        let td = TypeDef::Struct(StructType {
582            name: "X".into(),
583            members: alloc::vec![StructMember {
584                name: "ids".into(),
585                type_ref: TypeRef::Primitive(PrimitiveType::Long),
586                sequence_max_length: Some(10),
587                ..Default::default()
588            }],
589            ..Default::default()
590        });
591        let xml = r#"<sample type_ref="X"><ids></ids></sample>"#;
592        let v = parse_sample(xml, &td, &[]).expect("parse empty");
593        let SampleValue::Struct(m) = v else { panic!() };
594        let ids = m.get("ids").expect("ids");
595        let SampleValue::Sequence(items) = ids else {
596            panic!("expected Sequence")
597        };
598        assert!(items.is_empty());
599    }
600
601    #[test]
602    fn serialize_sample_uses_item_tag_for_sequence() {
603        // serialize_sample emittiert <item> fuer Sequence — Spec §7.3.7.3.2.
604        // Wir kapseln die Sequence in einen Top-Level-Struct, damit
605        // serialize_sample einen Type-Wrapper bekommt.
606        let value = SampleValue::Struct(BTreeMap::from([(
607            "ids".to_string(),
608            SampleValue::Sequence(alloc::vec![
609                SampleValue::Primitive(PrimitiveValue::I32(7)),
610                SampleValue::Primitive(PrimitiveValue::I32(8)),
611            ]),
612        )]));
613        let td = TypeDef::Struct(StructType {
614            name: "Wrap".into(),
615            members: alloc::vec![StructMember {
616                name: "ids".into(),
617                type_ref: TypeRef::Primitive(PrimitiveType::Long),
618                sequence_max_length: Some(10),
619                ..Default::default()
620            }],
621            ..Default::default()
622        });
623        let out = serialize_sample(&value, &td, &[]);
624        assert!(out.contains("<item>7</item>"));
625        assert!(out.contains("<item>8</item>"));
626    }
627
628    #[test]
629    fn parse_sample_with_union() {
630        // §7.3.7.1 Sample-Format fuer Union — discriminator + active case.
631        use crate::xtypes_def::{UnionCase, UnionType};
632        let td = TypeDef::Union(UnionType {
633            name: "U".into(),
634            discriminator: TypeRef::Primitive(PrimitiveType::Long),
635            cases: alloc::vec![UnionCase {
636                discriminators: alloc::vec![UnionDiscriminator::Value("1".into())],
637                member: StructMember {
638                    name: "a".into(),
639                    type_ref: TypeRef::Primitive(PrimitiveType::Long),
640                    ..Default::default()
641                },
642            }],
643        });
644        let xml = r#"<sample type_ref="U"><discriminator>1</discriminator><a>42</a></sample>"#;
645        let v = parse_sample(xml, &td, &[]).expect("parse union");
646        let SampleValue::Union {
647            discriminator,
648            value,
649        } = v
650        else {
651            panic!("expected Union")
652        };
653        assert_eq!(discriminator, "1");
654        assert!(matches!(
655            *value,
656            SampleValue::Primitive(PrimitiveValue::I32(42))
657        ));
658    }
659}