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 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 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 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 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 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 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 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 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 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}