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 => {
388                todo!("StructureWithSubtypedValues is unsupported")
389            }
390            StructureType::UnionWithSubtypedValues => {
391                todo!("UnionWithSubtypedValues is unsupported")
392            }
393        }
394    }
395}
396
397impl XmlEncodable for DynamicStructure {
398    fn encode(
399        &self,
400        stream: &mut XmlStreamWriter<&mut dyn std::io::Write>,
401        ctx: &Context<'_>,
402    ) -> EncodingResult<()> {
403        let s = &self.type_def;
404        match s.structure_type {
405            StructureType::Structure => {
406                for (value, field) in self.data.iter().zip(s.fields.iter()) {
407                    self.xml_encode_field(stream, value, field, ctx)?;
408                }
409            }
410            StructureType::StructureWithOptionalFields => {
411                let mut encoding_mask = 0u32;
412                let mut optional_idx = 0;
413                for (value, field) in self.data.iter().zip(s.fields.iter()) {
414                    if field.is_optional {
415                        if !matches!(value, Variant::Empty) {
416                            encoding_mask |= 1 << optional_idx;
417                        }
418                        optional_idx += 1;
419                    }
420                }
421                stream.encode_child("EncodingMask", &encoding_mask, ctx)?;
422                for (value, field) in self.data.iter().zip(s.fields.iter()) {
423                    if !field.is_optional || !matches!(value, Variant::Empty) {
424                        self.xml_encode_field(stream, value, field, ctx)?;
425                    }
426                }
427            }
428            StructureType::Union => {
429                stream.encode_child("SwitchField", &self.discriminant, ctx)?;
430                let (Some(value), Some(field)) = (
431                    self.data.first(),
432                    s.fields.get(self.discriminant as usize - 1),
433                ) else {
434                    return Err(Error::encoding(
435                        "Discriminant was out of range of known fields",
436                    ));
437                };
438                self.xml_encode_field(stream, value, field, ctx)?;
439            }
440            StructureType::StructureWithSubtypedValues => {
441                todo!("StructureWithSubtypedValues is unsupported")
442            }
443            StructureType::UnionWithSubtypedValues => {
444                todo!("UnionWithSubtypedValues is unsupported")
445            }
446        }
447
448        Ok(())
449    }
450}
451
452#[cfg(test)]
453mod tests {
454    use std::{
455        io::{Cursor, Read, Seek, Write},
456        sync::Arc,
457    };
458
459    use crate::{
460        custom::custom_struct::tests::{
461            get_custom_union, get_namespaces, MyUnion, MyUnionTypeLoader,
462        },
463        xml::*,
464        Array, ContextOwned, DataTypeDefinition, DataTypeId, DecodingOptions, EUInformation,
465        ExtensionObject, LocalizedText, NamespaceMap, NodeId, StructureDefinition, StructureField,
466        TypeLoaderCollection, Variant, VariantScalarTypeId,
467    };
468
469    use crate::custom::{
470        custom_struct::tests::{add_eu_information, make_type_tree},
471        type_tree::TypeInfo,
472        DynamicStructure, DynamicTypeLoader, EncodingIds,
473    };
474
475    #[test]
476    fn xml_dynamic_struct_round_trip() {
477        let mut type_tree = make_type_tree();
478        add_eu_information(&mut type_tree);
479
480        let loader = DynamicTypeLoader::new(Arc::new(type_tree));
481        let mut loaders = TypeLoaderCollection::new_empty();
482        loaders.add_type_loader(loader);
483        let ctx = ContextOwned::new(NamespaceMap::new(), loaders, DecodingOptions::test());
484
485        let mut write_buf = Vec::<u8>::new();
486        let mut cursor = Cursor::new(&mut write_buf);
487        let mut writer = XmlStreamWriter::new(&mut cursor as &mut dyn Write);
488
489        let obj = ExtensionObject::from_message(EUInformation {
490            namespace_uri: "my.namespace.uri".into(),
491            unit_id: 5,
492            display_name: "Degrees Celsius".into(),
493            description: "Description".into(),
494        });
495
496        XmlEncodable::encode(&obj, &mut writer, &ctx.context()).unwrap();
497        cursor.seek(std::io::SeekFrom::Start(0)).unwrap();
498
499        let mut reader = XmlStreamReader::new(&mut cursor as &mut dyn Read);
500
501        let obj2: ExtensionObject = XmlDecodable::decode(&mut reader, &ctx.context()).unwrap();
502
503        // Decode it back, resulting in a dynamic structure.
504        let value = obj2.inner_as::<DynamicStructure>().unwrap();
505        assert_eq!(value.data.len(), 4);
506        assert_eq!(value.data[0], Variant::from("my.namespace.uri"));
507        assert_eq!(value.data[1], Variant::from(5i32));
508        assert_eq!(
509            value.data[2],
510            Variant::from(LocalizedText::from("Degrees Celsius"))
511        );
512        assert_eq!(
513            value.data[3],
514            Variant::from(LocalizedText::from("Description"))
515        );
516
517        // Re-encode it
518        cursor.seek(std::io::SeekFrom::Start(0)).unwrap();
519        let mut writer = XmlStreamWriter::new(&mut cursor as &mut dyn Write);
520        XmlEncodable::encode(&obj2, &mut writer, &ctx.context()).unwrap();
521
522        // Make a new context, this time with the regular decoder for EUInformation
523        let ctx = ContextOwned::new_default(NamespaceMap::new(), DecodingOptions::test());
524        cursor.seek(std::io::SeekFrom::Start(0)).unwrap();
525        let mut reader = XmlStreamReader::new(&mut cursor as &mut dyn Read);
526        let obj3: ExtensionObject = XmlDecodable::decode(&mut reader, &ctx.context()).unwrap();
527
528        assert_eq!(obj, obj3);
529    }
530
531    #[test]
532    fn xml_dynamic_nested_struct_round_trip() {
533        let mut type_tree = make_type_tree();
534        add_eu_information(&mut type_tree);
535        let type_node_id = NodeId::new(1, 5);
536        type_tree
537            .parent_ids_mut()
538            .add_type(type_node_id.clone(), DataTypeId::Structure.into());
539        type_tree.add_type(
540            type_node_id.clone(),
541            TypeInfo::from_type_definition(
542                DataTypeDefinition::Structure(StructureDefinition {
543                    default_encoding_id: NodeId::null(),
544                    base_data_type: DataTypeId::Structure.into(),
545                    structure_type: crate::StructureType::Structure,
546                    fields: Some(vec![
547                        StructureField {
548                            name: "Info".into(),
549                            data_type: DataTypeId::EUInformation.into(),
550                            value_rank: -1,
551                            ..Default::default()
552                        },
553                        StructureField {
554                            name: "InfoArray".into(),
555                            data_type: DataTypeId::EUInformation.into(),
556                            value_rank: 1,
557                            ..Default::default()
558                        },
559                        StructureField {
560                            name: "AbstractField".into(),
561                            data_type: DataTypeId::BaseDataType.into(),
562                            value_rank: -1,
563                            ..Default::default()
564                        },
565                        StructureField {
566                            name: "PrimitiveArray".into(),
567                            data_type: DataTypeId::Int32.into(),
568                            value_rank: 2,
569                            ..Default::default()
570                        },
571                    ]),
572                }),
573                "EUInformation".to_owned(),
574                Some(EncodingIds {
575                    binary_id: NodeId::new(1, 6),
576                    json_id: NodeId::new(1, 7),
577                    xml_id: NodeId::new(1, 8),
578                }),
579                false,
580                &type_node_id,
581                type_tree.parent_ids(),
582            )
583            .unwrap(),
584        );
585        let type_tree = Arc::new(type_tree);
586        let loader = DynamicTypeLoader::new(type_tree.clone());
587        let mut loaders = TypeLoaderCollection::new();
588        loaders.add_type_loader(loader);
589        let ctx = ContextOwned::new(NamespaceMap::new(), loaders, DecodingOptions::test());
590
591        let obj = DynamicStructure::new_struct(
592            type_tree.get_struct_type(&type_node_id).unwrap().clone(),
593            type_tree,
594            vec![
595                Variant::from(ExtensionObject::from_message(EUInformation {
596                    namespace_uri: "my.namespace.uri".into(),
597                    unit_id: 5,
598                    display_name: "Degrees Celsius".into(),
599                    description: "Description".into(),
600                })),
601                Variant::from(vec![
602                    ExtensionObject::from_message(EUInformation {
603                        namespace_uri: "my.namespace.uri".into(),
604                        unit_id: 5,
605                        display_name: "Degrees Celsius".into(),
606                        description: "Description".into(),
607                    }),
608                    ExtensionObject::from_message(EUInformation {
609                        namespace_uri: "my.namespace.uri.2".into(),
610                        unit_id: 6,
611                        display_name: "Degrees Celsius 2".into(),
612                        description: "Description 2".into(),
613                    }),
614                ]),
615                Variant::Variant(Box::new(Variant::from(123))),
616                Variant::from(
617                    Array::new_multi(
618                        VariantScalarTypeId::Int32,
619                        [1i32, 2, 3, 4, 5, 6]
620                            .into_iter()
621                            .map(Variant::from)
622                            .collect::<Vec<_>>(),
623                        vec![2, 3],
624                    )
625                    .unwrap(),
626                ),
627            ],
628        )
629        .unwrap();
630        let obj = ExtensionObject::from_message(obj);
631
632        let mut write_buf = Vec::<u8>::new();
633        let mut cursor = Cursor::new(&mut write_buf);
634        let mut writer = XmlStreamWriter::new(&mut cursor as &mut dyn Write);
635
636        XmlEncodable::encode(&obj, &mut writer, &ctx.context()).unwrap();
637
638        cursor.seek(std::io::SeekFrom::Start(0)).unwrap();
639
640        println!("{}", std::str::from_utf8(cursor.get_ref()).unwrap());
641
642        let mut reader = XmlStreamReader::new(&mut cursor as &mut dyn Read);
643        let obj2: ExtensionObject = XmlDecodable::decode(&mut reader, &ctx.context()).unwrap();
644
645        assert_eq!(obj, obj2);
646    }
647
648    #[test]
649    fn union_round_trip() {
650        let ctx = get_custom_union();
651
652        let mut write_buf = Vec::<u8>::new();
653        let mut cursor = Cursor::new(&mut write_buf);
654
655        let obj = ExtensionObject::from_message(MyUnion::Integer(123));
656
657        let mut writer = XmlStreamWriter::new(&mut cursor as &mut dyn Write);
658
659        // Encode the object, using the regular XmlEncodable implementation
660        XmlEncodable::encode(&obj, &mut writer, &ctx.context()).unwrap();
661        cursor.seek(std::io::SeekFrom::Start(0)).unwrap();
662
663        let mut reader = XmlStreamReader::new(&mut cursor as &mut dyn Read);
664
665        let obj2: ExtensionObject = XmlDecodable::decode(&mut reader, &ctx.context()).unwrap();
666
667        // Decode it back, resulting in a dynamic structure.
668        let value = obj2.inner_as::<DynamicStructure>().unwrap();
669        assert_eq!(value.data.len(), 1);
670
671        assert_eq!(value.data[0], Variant::from(123i32));
672        assert_eq!(value.discriminant, 1);
673
674        cursor.seek(std::io::SeekFrom::Start(0)).unwrap();
675        let mut writer = XmlStreamWriter::new(&mut cursor as &mut dyn Write);
676        XmlEncodable::encode(&obj2, &mut writer, &ctx.context()).unwrap();
677
678        // Make a new context, this time with the regular decoder for MyUnion
679        let mut ctx = ContextOwned::new_default(get_namespaces(), DecodingOptions::test());
680        ctx.loaders_mut().add_type_loader(MyUnionTypeLoader);
681        cursor.seek(std::io::SeekFrom::Start(0)).unwrap();
682        let mut reader = XmlStreamReader::new(&mut cursor as &mut dyn Read);
683        let obj3: ExtensionObject = XmlDecodable::decode(&mut reader, &ctx.context()).unwrap();
684
685        assert_eq!(obj, obj3);
686    }
687}