Skip to main content

opcua_types/custom/
xml.rs

1use std::{collections::HashMap, io::Write, sync::Arc};
2
3use crate::{
4    xml::*, Array, ByteString, DataValue, DateTime, DiagnosticInfo, DynEncodable, ExpandedNodeId,
5    ExtensionObject, Guid, LocalizedText, NodeId, QualifiedName, StatusCode, StructureType,
6    UAString, Variant, VariantScalarTypeId, XmlElement,
7};
8
9use super::{DynamicStructure, DynamicTypeLoader, ParsedStructureField, StructTypeInfo};
10
11impl XmlType for DynamicStructure {
12    // Should never be used, kept as a fallback.
13    const TAG: &'static str = "Unknown";
14    fn tag(&self) -> &str {
15        &self.type_def.name
16    }
17}
18
19impl DynamicStructure {
20    fn xml_encode_field(
21        &self,
22        stream: &mut XmlStreamWriter<&mut dyn Write>,
23        f: &Variant,
24        field: &ParsedStructureField,
25        ctx: &Context<'_>,
26    ) -> EncodingResult<()> {
27        stream.write_start(&field.name)?;
28        match f {
29            Variant::ExtensionObject(o) => {
30                let Some(field_ty) = self.type_tree.get_struct_type(&field.type_id) else {
31                    return Err(Error::encoding(format!(
32                        "Dynamic type field missing from type tree: {}",
33                        field.type_id
34                    )));
35                };
36                if field_ty.is_abstract {
37                    o.encode(stream, ctx)
38                } else {
39                    let Some(body) = &o.body else {
40                        return Err(Error::encoding(
41                            "Dynamic type field is missing extension object body",
42                        ));
43                    };
44                    body.encode_xml(stream, ctx)
45                }
46            }
47            Variant::Array(a) => {
48                if field.value_rank > 1 {
49                    let Some(dims) = &a.dimensions else {
50                        return Err(Error::encoding(
51                            "ArrayDimensions are required for fields with value rank > 1",
52                        ));
53                    };
54                    if dims.len() as i32 != field.value_rank {
55                        return Err(Error::encoding(
56                            "ArrayDimensions must have length equal to field valuerank",
57                        ));
58                    }
59                    // For some incredibly annoying reason, OPC-UA insists that dimensions be
60                    // encoded as _signed_ integers. For other encoders it's irrelevant,
61                    // but it matters for XML.
62                    let dims: Vec<_> = dims.iter().map(|d| *d as i32).collect();
63                    stream.encode_child("Dimensions", &dims, ctx)?;
64
65                    stream.write_start("Elements")?;
66                    for item in &a.values {
67                        self.xml_encode_field(stream, item, field, ctx)?;
68                    }
69                    stream.write_end("Elements")?;
70                } else {
71                    for item in &a.values {
72                        self.xml_encode_field(stream, item, field, ctx)?;
73                    }
74                }
75                Ok(())
76            }
77            Variant::Empty => Ok(()),
78            Variant::Boolean(v) => v.encode(stream, ctx),
79            Variant::SByte(v) => v.encode(stream, ctx),
80            Variant::Byte(v) => v.encode(stream, ctx),
81            Variant::Int16(v) => v.encode(stream, ctx),
82            Variant::UInt16(v) => v.encode(stream, ctx),
83            Variant::Int32(v) => v.encode(stream, ctx),
84            Variant::UInt32(v) => v.encode(stream, ctx),
85            Variant::Int64(v) => v.encode(stream, ctx),
86            Variant::UInt64(v) => v.encode(stream, ctx),
87            Variant::Float(v) => v.encode(stream, ctx),
88            Variant::Double(v) => v.encode(stream, ctx),
89            Variant::String(v) => v.encode(stream, ctx),
90            Variant::DateTime(v) => v.encode(stream, ctx),
91            Variant::Guid(v) => v.encode(stream, ctx),
92            Variant::StatusCode(v) => v.encode(stream, ctx),
93            Variant::ByteString(v) => v.encode(stream, ctx),
94            Variant::XmlElement(v) => v.encode(stream, ctx),
95            Variant::QualifiedName(v) => v.encode(stream, ctx),
96            Variant::LocalizedText(v) => v.encode(stream, ctx),
97            Variant::NodeId(v) => v.encode(stream, ctx),
98            Variant::ExpandedNodeId(v) => v.encode(stream, ctx),
99            Variant::Variant(v) => v.encode(stream, ctx),
100            Variant::DataValue(v) => v.encode(stream, ctx),
101            Variant::DiagnosticInfo(v) => v.encode(stream, ctx),
102        }?;
103        stream.write_end(&field.name)?;
104        Ok(())
105    }
106}
107
108impl DynamicTypeLoader {
109    fn xml_decode_field_value(
110        &self,
111        field: &ParsedStructureField,
112        stream: &mut crate::xml::XmlStreamReader<&mut dyn std::io::Read>,
113        ctx: &Context<'_>,
114    ) -> EncodingResult<Variant> {
115        match field.scalar_type {
116            VariantScalarTypeId::Boolean => {
117                Ok(Variant::from(<bool as XmlDecodable>::decode(stream, ctx)?))
118            }
119            VariantScalarTypeId::SByte => {
120                Ok(Variant::from(<i8 as XmlDecodable>::decode(stream, ctx)?))
121            }
122            VariantScalarTypeId::Byte => {
123                Ok(Variant::from(<u8 as XmlDecodable>::decode(stream, ctx)?))
124            }
125            VariantScalarTypeId::Int16 => {
126                Ok(Variant::from(<i16 as XmlDecodable>::decode(stream, ctx)?))
127            }
128            VariantScalarTypeId::UInt16 => {
129                Ok(Variant::from(<u16 as XmlDecodable>::decode(stream, ctx)?))
130            }
131            VariantScalarTypeId::Int32 => {
132                Ok(Variant::from(<i32 as XmlDecodable>::decode(stream, ctx)?))
133            }
134            VariantScalarTypeId::UInt32 => {
135                Ok(Variant::from(<u32 as XmlDecodable>::decode(stream, ctx)?))
136            }
137            VariantScalarTypeId::Int64 => {
138                Ok(Variant::from(<i64 as XmlDecodable>::decode(stream, ctx)?))
139            }
140            VariantScalarTypeId::UInt64 => {
141                Ok(Variant::from(<u64 as XmlDecodable>::decode(stream, ctx)?))
142            }
143            VariantScalarTypeId::Float => {
144                Ok(Variant::from(<f32 as XmlDecodable>::decode(stream, ctx)?))
145            }
146            VariantScalarTypeId::Double => {
147                Ok(Variant::from(<f64 as XmlDecodable>::decode(stream, ctx)?))
148            }
149            VariantScalarTypeId::String => Ok(Variant::from(<UAString as XmlDecodable>::decode(
150                stream, ctx,
151            )?)),
152            VariantScalarTypeId::DateTime => Ok(Variant::from(<DateTime as XmlDecodable>::decode(
153                stream, ctx,
154            )?)),
155            VariantScalarTypeId::Guid => {
156                Ok(Variant::from(<Guid as XmlDecodable>::decode(stream, ctx)?))
157            }
158            VariantScalarTypeId::ByteString => Ok(Variant::from(
159                <ByteString as XmlDecodable>::decode(stream, ctx)?,
160            )),
161            VariantScalarTypeId::XmlElement => Ok(Variant::from(
162                <XmlElement as XmlDecodable>::decode(stream, ctx)?,
163            )),
164            VariantScalarTypeId::NodeId => Ok(Variant::from(<NodeId as XmlDecodable>::decode(
165                stream, ctx,
166            )?)),
167            VariantScalarTypeId::ExpandedNodeId => Ok(Variant::from(
168                <ExpandedNodeId as XmlDecodable>::decode(stream, ctx)?,
169            )),
170            VariantScalarTypeId::StatusCode => Ok(Variant::from(
171                <StatusCode as XmlDecodable>::decode(stream, ctx)?,
172            )),
173            VariantScalarTypeId::QualifiedName => Ok(Variant::from(
174                <QualifiedName as XmlDecodable>::decode(stream, ctx)?,
175            )),
176            VariantScalarTypeId::LocalizedText => Ok(Variant::from(
177                <LocalizedText as XmlDecodable>::decode(stream, ctx)?,
178            )),
179            VariantScalarTypeId::ExtensionObject => {
180                let Some(field_ty) = self.type_tree.get_struct_type(&field.type_id) else {
181                    return Err(Error::decoding(format!(
182                        "Dynamic type field missing from type tree: {}",
183                        field.type_id
184                    )));
185                };
186
187                if field_ty.is_abstract {
188                    Ok(Variant::from(<ExtensionObject as XmlDecodable>::decode(
189                        stream, ctx,
190                    )?))
191                } else {
192                    Ok(Variant::from(ctx.load_from_xml(
193                        &field_ty.node_id,
194                        stream,
195                        &field_ty.name,
196                    )?))
197                }
198            }
199            VariantScalarTypeId::DataValue => Ok(Variant::from(
200                <DataValue as XmlDecodable>::decode(stream, ctx)?,
201            )),
202            VariantScalarTypeId::Variant => Ok(Variant::Variant(Box::new(
203                <Variant as XmlDecodable>::decode(stream, ctx)?,
204            ))),
205            VariantScalarTypeId::DiagnosticInfo => Ok(Variant::from(
206                <DiagnosticInfo as XmlDecodable>::decode(stream, ctx)?,
207            )),
208        }
209    }
210
211    fn xml_decode_field(
212        &self,
213        field: &ParsedStructureField,
214        stream: &mut crate::xml::XmlStreamReader<&mut dyn std::io::Read>,
215        ctx: &Context<'_>,
216    ) -> EncodingResult<Variant> {
217        if field.value_rank > 1 {
218            let mut values = Vec::new();
219            let mut dims = Vec::new();
220            stream.iter_children(
221                |key, stream, ctx| {
222                    match key.as_str() {
223                        "Dimensions" => {
224                            dims = Vec::<i32>::decode(stream, ctx)?;
225                        }
226                        "Elements" => stream.iter_children_include_empty(
227                            |_, stream, ctx| {
228                                let Some(stream) = stream else {
229                                    values.push(Variant::get_variant_default(field.scalar_type));
230                                    return Ok(());
231                                };
232                                let r = self.xml_decode_field_value(field, stream, ctx)?;
233                                values.push(r);
234                                Ok(())
235                            },
236                            ctx,
237                        )?,
238                        r => {
239                            return Err(Error::decoding(format!(
240                                "Invalid field in Matrix content: {r}"
241                            )))
242                        }
243                    }
244                    Ok(())
245                },
246                ctx,
247            )?;
248            Ok(Variant::Array(Box::new(
249                Array::new_multi(
250                    field.scalar_type,
251                    values,
252                    dims.into_iter()
253                        .map(|d| d.try_into())
254                        .collect::<Result<Vec<_>, _>>()
255                        .map_err(|_| {
256                            Error::decoding("Invalid array dimensions, must all be non-negative")
257                        })?,
258                )
259                .map_err(Error::decoding)?,
260            )))
261        } else if field.value_rank > 0 {
262            let mut values = Vec::new();
263            stream.iter_children_include_empty(
264                |_, stream, ctx| {
265                    let Some(stream) = stream else {
266                        values.push(Variant::get_variant_default(field.scalar_type));
267                        return Ok(());
268                    };
269                    let r = self.xml_decode_field_value(field, stream, ctx)?;
270                    values.push(r);
271                    Ok(())
272                },
273                ctx,
274            )?;
275            Ok(Variant::Array(Box::new(
276                Array::new(field.scalar_type, values).map_err(Error::decoding)?,
277            )))
278        } else {
279            self.xml_decode_field_value(field, stream, ctx)
280        }
281    }
282
283    pub(super) fn xml_decode_type_inner(
284        &self,
285        stream: &mut crate::xml::XmlStreamReader<&mut dyn std::io::Read>,
286        ctx: &Context<'_>,
287        t: &Arc<StructTypeInfo>,
288    ) -> EncodingResult<Box<dyn DynEncodable>> {
289        match t.structure_type {
290            StructureType::Structure | StructureType::StructureWithOptionalFields => {
291                let mut by_name = HashMap::new();
292                stream.iter_children(
293                    |key, stream, ctx| {
294                        let Some(field) = t.get_field_by_name(&key) else {
295                            stream.skip_value()?;
296                            return Ok(());
297                        };
298                        by_name.insert(
299                            field.name.as_str(),
300                            self.xml_decode_field(field, stream, ctx)?,
301                        );
302                        Ok(())
303                    },
304                    ctx,
305                )?;
306
307                let mut data = Vec::with_capacity(by_name.len());
308                for field in &t.fields {
309                    let Some(f) = by_name.remove(field.name.as_str()) else {
310                        if field.is_optional {
311                            data.push(Variant::Empty);
312                            continue;
313                        }
314                        return Err(Error::decoding(format!(
315                            "Missing required field {}",
316                            field.name
317                        )));
318                    };
319                    data.push(f);
320                }
321
322                Ok(Box::new(DynamicStructure {
323                    type_def: t.clone(),
324                    discriminant: 0,
325                    type_tree: self.type_tree.clone(),
326                    data,
327                }))
328            }
329            StructureType::Union => {
330                let mut value: Option<Variant> = None;
331                let mut discriminant: Option<u32> = None;
332
333                stream.iter_children(
334                    |key, stream, ctx| {
335                        match key.as_str() {
336                            "SwitchField" => {
337                                discriminant = Some(u32::decode(stream, ctx)?);
338                            }
339                            r => {
340                                let Some((idx, value_field)) =
341                                    t.fields.iter().enumerate().find(|(_, f)| f.name == r)
342                                else {
343                                    stream.skip_value()?;
344                                    return Ok(());
345                                };
346
347                                // If we've read the discriminant, double check that it matches the field name.
348                                // OPC-UA unions are really only allowed to have two fields, but we can try to handle
349                                // weird payloads anyway.
350                                // Technically doesn't handle cases where there are multiple options _and_ the discriminant
351                                // is late, but that violates the standard so it's probably fine.
352                                if discriminant.is_some_and(|d| d != (idx + 1) as u32) {
353                                    stream.skip_value()?;
354                                } else {
355                                    value =
356                                        Some(self.xml_decode_field(value_field, stream, ctx)?);
357                                    discriminant = Some((idx + 1) as u32);
358                                }
359                            }
360                        }
361                        Ok(())
362                    },
363                    ctx,
364                )?;
365
366                let (Some(value), Some(discriminant)) = (value, discriminant) else {
367                    return Ok(Box::new(DynamicStructure::new_null_union(
368                        t.clone(),
369                        self.type_tree.clone(),
370                    )));
371                };
372
373                if discriminant == 0 {
374                    return Ok(Box::new(DynamicStructure::new_null_union(
375                        t.clone(),
376                        self.type_tree.clone(),
377                    )));
378                }
379
380                Ok(Box::new(DynamicStructure {
381                    type_def: t.clone(),
382                    discriminant,
383                    type_tree: self.type_tree.clone(),
384                    data: vec![value],
385                }))
386            }
387            StructureType::StructureWithSubtypedValues | StructureType::UnionWithSubtypedValues => {
388                Err(Error::decoding(
389                    "Structures and unions with subtyped values are unsupported",
390                ))
391            }
392        }
393    }
394}
395
396impl XmlEncodable for DynamicStructure {
397    fn encode(
398        &self,
399        stream: &mut XmlStreamWriter<&mut dyn std::io::Write>,
400        ctx: &Context<'_>,
401    ) -> EncodingResult<()> {
402        let s = &self.type_def;
403        match s.structure_type {
404            StructureType::Structure => {
405                for (value, field) in self.data.iter().zip(s.fields.iter()) {
406                    self.xml_encode_field(stream, value, field, ctx)?;
407                }
408            }
409            StructureType::StructureWithOptionalFields => {
410                let mut encoding_mask = 0u32;
411                let mut optional_idx = 0;
412                for (value, field) in self.data.iter().zip(s.fields.iter()) {
413                    if field.is_optional {
414                        if !matches!(value, Variant::Empty) {
415                            encoding_mask |= 1 << optional_idx;
416                        }
417                        optional_idx += 1;
418                    }
419                }
420                stream.encode_child("EncodingMask", &encoding_mask, ctx)?;
421                for (value, field) in self.data.iter().zip(s.fields.iter()) {
422                    if !field.is_optional || !matches!(value, Variant::Empty) {
423                        self.xml_encode_field(stream, value, field, ctx)?;
424                    }
425                }
426            }
427            StructureType::Union => {
428                stream.encode_child("SwitchField", &self.discriminant, ctx)?;
429                let (Some(value), Some(field)) = (
430                    self.data.first(),
431                    s.fields.get(self.discriminant as usize - 1),
432                ) else {
433                    return Err(Error::encoding(
434                        "Discriminant was out of range of known fields",
435                    ));
436                };
437                self.xml_encode_field(stream, value, field, ctx)?;
438            }
439            StructureType::StructureWithSubtypedValues | StructureType::UnionWithSubtypedValues => {
440                return Err(Error::encoding(
441                    "Structures and unions with subtyped values are unsupported",
442                ));
443            }
444        }
445
446        Ok(())
447    }
448}
449
450#[cfg(test)]
451mod tests {
452    use std::{
453        io::{Cursor, Read, Seek, Write},
454        sync::Arc,
455    };
456
457    use crate::{
458        custom::custom_struct::tests::{
459            get_custom_union, get_namespaces, MyUnion, MyUnionTypeLoader,
460        },
461        xml::*,
462        Array, ContextOwned, DataTypeDefinition, DataTypeId, DecodingOptions, EUInformation,
463        ExtensionObject, LocalizedText, NamespaceMap, NodeId, StructureDefinition, StructureField,
464        TypeLoaderCollection, Variant, VariantScalarTypeId,
465    };
466
467    use crate::custom::{
468        custom_struct::tests::{add_eu_information, make_type_tree},
469        type_tree::TypeInfo,
470        DynamicStructure, DynamicTypeLoader, EncodingIds,
471    };
472
473    #[test]
474    fn xml_dynamic_struct_round_trip() {
475        let mut type_tree = make_type_tree();
476        add_eu_information(&mut type_tree);
477
478        let loader = DynamicTypeLoader::new(Arc::new(type_tree));
479        let mut loaders = TypeLoaderCollection::new_empty();
480        loaders.add_type_loader(loader);
481        let ctx = ContextOwned::new(NamespaceMap::new(), loaders, DecodingOptions::test());
482
483        let mut write_buf = Vec::<u8>::new();
484        let mut cursor = Cursor::new(&mut write_buf);
485        let mut writer = XmlStreamWriter::new(&mut cursor as &mut dyn Write);
486
487        let obj = ExtensionObject::from_message(EUInformation {
488            namespace_uri: "my.namespace.uri".into(),
489            unit_id: 5,
490            display_name: "Degrees Celsius".into(),
491            description: "Description".into(),
492        });
493
494        XmlEncodable::encode(&obj, &mut writer, &ctx.context()).unwrap();
495        cursor.seek(std::io::SeekFrom::Start(0)).unwrap();
496
497        let mut reader = XmlStreamReader::new(&mut cursor as &mut dyn Read);
498
499        let obj2: ExtensionObject = XmlDecodable::decode(&mut reader, &ctx.context()).unwrap();
500
501        // Decode it back, resulting in a dynamic structure.
502        let value = obj2.inner_as::<DynamicStructure>().unwrap();
503        assert_eq!(value.data.len(), 4);
504        assert_eq!(value.data[0], Variant::from("my.namespace.uri"));
505        assert_eq!(value.data[1], Variant::from(5i32));
506        assert_eq!(
507            value.data[2],
508            Variant::from(LocalizedText::from("Degrees Celsius"))
509        );
510        assert_eq!(
511            value.data[3],
512            Variant::from(LocalizedText::from("Description"))
513        );
514
515        // Re-encode it
516        cursor.seek(std::io::SeekFrom::Start(0)).unwrap();
517        let mut writer = XmlStreamWriter::new(&mut cursor as &mut dyn Write);
518        XmlEncodable::encode(&obj2, &mut writer, &ctx.context()).unwrap();
519
520        // Make a new context, this time with the regular decoder for EUInformation
521        let ctx = ContextOwned::new_default(NamespaceMap::new(), DecodingOptions::test());
522        cursor.seek(std::io::SeekFrom::Start(0)).unwrap();
523        let mut reader = XmlStreamReader::new(&mut cursor as &mut dyn Read);
524        let obj3: ExtensionObject = XmlDecodable::decode(&mut reader, &ctx.context()).unwrap();
525
526        assert_eq!(obj, obj3);
527    }
528
529    #[test]
530    fn xml_dynamic_nested_struct_round_trip() {
531        let mut type_tree = make_type_tree();
532        add_eu_information(&mut type_tree);
533        let type_node_id = NodeId::new(1, 5);
534        type_tree
535            .parent_ids_mut()
536            .add_type(type_node_id.clone(), DataTypeId::Structure.into());
537        type_tree.add_type(
538            type_node_id.clone(),
539            TypeInfo::from_type_definition(
540                DataTypeDefinition::Structure(StructureDefinition {
541                    default_encoding_id: NodeId::null(),
542                    base_data_type: DataTypeId::Structure.into(),
543                    structure_type: crate::StructureType::Structure,
544                    fields: Some(vec![
545                        StructureField {
546                            name: "Info".into(),
547                            data_type: DataTypeId::EUInformation.into(),
548                            value_rank: -1,
549                            ..Default::default()
550                        },
551                        StructureField {
552                            name: "InfoArray".into(),
553                            data_type: DataTypeId::EUInformation.into(),
554                            value_rank: 1,
555                            ..Default::default()
556                        },
557                        StructureField {
558                            name: "AbstractField".into(),
559                            data_type: DataTypeId::BaseDataType.into(),
560                            value_rank: -1,
561                            ..Default::default()
562                        },
563                        StructureField {
564                            name: "PrimitiveArray".into(),
565                            data_type: DataTypeId::Int32.into(),
566                            value_rank: 2,
567                            ..Default::default()
568                        },
569                    ]),
570                }),
571                "EUInformation".to_owned(),
572                Some(EncodingIds {
573                    binary_id: NodeId::new(1, 6),
574                    json_id: NodeId::new(1, 7),
575                    xml_id: NodeId::new(1, 8),
576                }),
577                false,
578                &type_node_id,
579                type_tree.parent_ids(),
580            )
581            .unwrap(),
582        );
583        let type_tree = Arc::new(type_tree);
584        let loader = DynamicTypeLoader::new(type_tree.clone());
585        let mut loaders = TypeLoaderCollection::new();
586        loaders.add_type_loader(loader);
587        let ctx = ContextOwned::new(NamespaceMap::new(), loaders, DecodingOptions::test());
588
589        let obj = DynamicStructure::new_struct(
590            type_tree.get_struct_type(&type_node_id).unwrap().clone(),
591            type_tree,
592            vec![
593                Variant::from(ExtensionObject::from_message(EUInformation {
594                    namespace_uri: "my.namespace.uri".into(),
595                    unit_id: 5,
596                    display_name: "Degrees Celsius".into(),
597                    description: "Description".into(),
598                })),
599                Variant::from(vec![
600                    ExtensionObject::from_message(EUInformation {
601                        namespace_uri: "my.namespace.uri".into(),
602                        unit_id: 5,
603                        display_name: "Degrees Celsius".into(),
604                        description: "Description".into(),
605                    }),
606                    ExtensionObject::from_message(EUInformation {
607                        namespace_uri: "my.namespace.uri.2".into(),
608                        unit_id: 6,
609                        display_name: "Degrees Celsius 2".into(),
610                        description: "Description 2".into(),
611                    }),
612                ]),
613                Variant::Variant(Box::new(Variant::from(123))),
614                Variant::from(
615                    Array::new_multi(
616                        VariantScalarTypeId::Int32,
617                        [1i32, 2, 3, 4, 5, 6]
618                            .into_iter()
619                            .map(Variant::from)
620                            .collect::<Vec<_>>(),
621                        vec![2, 3],
622                    )
623                    .unwrap(),
624                ),
625            ],
626        )
627        .unwrap();
628        let obj = ExtensionObject::from_message(obj);
629
630        let mut write_buf = Vec::<u8>::new();
631        let mut cursor = Cursor::new(&mut write_buf);
632        let mut writer = XmlStreamWriter::new(&mut cursor as &mut dyn Write);
633
634        XmlEncodable::encode(&obj, &mut writer, &ctx.context()).unwrap();
635
636        cursor.seek(std::io::SeekFrom::Start(0)).unwrap();
637
638        println!("{}", std::str::from_utf8(cursor.get_ref()).unwrap());
639
640        let mut reader = XmlStreamReader::new(&mut cursor as &mut dyn Read);
641        let obj2: ExtensionObject = XmlDecodable::decode(&mut reader, &ctx.context()).unwrap();
642
643        assert_eq!(obj, obj2);
644    }
645
646    #[test]
647    fn union_round_trip() {
648        let ctx = get_custom_union();
649
650        let mut write_buf = Vec::<u8>::new();
651        let mut cursor = Cursor::new(&mut write_buf);
652
653        let obj = ExtensionObject::from_message(MyUnion::Integer(123));
654
655        let mut writer = XmlStreamWriter::new(&mut cursor as &mut dyn Write);
656
657        // Encode the object, using the regular XmlEncodable implementation
658        XmlEncodable::encode(&obj, &mut writer, &ctx.context()).unwrap();
659        cursor.seek(std::io::SeekFrom::Start(0)).unwrap();
660
661        let mut reader = XmlStreamReader::new(&mut cursor as &mut dyn Read);
662
663        let obj2: ExtensionObject = XmlDecodable::decode(&mut reader, &ctx.context()).unwrap();
664
665        // Decode it back, resulting in a dynamic structure.
666        let value = obj2.inner_as::<DynamicStructure>().unwrap();
667        assert_eq!(value.data.len(), 1);
668
669        assert_eq!(value.data[0], Variant::from(123i32));
670        assert_eq!(value.discriminant, 1);
671
672        cursor.seek(std::io::SeekFrom::Start(0)).unwrap();
673        let mut writer = XmlStreamWriter::new(&mut cursor as &mut dyn Write);
674        XmlEncodable::encode(&obj2, &mut writer, &ctx.context()).unwrap();
675
676        // Make a new context, this time with the regular decoder for MyUnion
677        let mut ctx = ContextOwned::new_default(get_namespaces(), DecodingOptions::test());
678        ctx.loaders_mut().add_type_loader(MyUnionTypeLoader);
679        cursor.seek(std::io::SeekFrom::Start(0)).unwrap();
680        let mut reader = XmlStreamReader::new(&mut cursor as &mut dyn Read);
681        let obj3: ExtensionObject = XmlDecodable::decode(&mut reader, &ctx.context()).unwrap();
682
683        assert_eq!(obj, obj3);
684    }
685}