opcua_types/custom/
custom_struct.rs

1use std::{io::Write, sync::Arc};
2
3use crate::{
4    write_i32, write_u32, Array, BinaryDecodable, BinaryEncodable, ByteString, Context, DataValue,
5    DateTime, DiagnosticInfo, EncodingResult, Error, ExpandedMessageInfo, ExpandedNodeId,
6    ExtensionObject, Guid, LocalizedText, NodeId, QualifiedName, StatusCode, StructureType,
7    TypeLoader, UAString, UaNullable, Variant, XmlElement,
8};
9
10use super::type_tree::{DataTypeTree, ParsedStructureField, StructTypeInfo};
11
12#[derive(Debug, Clone)]
13/// A type representing an OPC-UA structure decoded dynamically.
14/// This can use runtime information to properly encode and decode
15/// binary information. In order to make the type encodable, it
16/// contains references to a [StructTypeInfo] describing the type it contains,
17/// as well as the [DataTypeTree] containing any types it references.
18///
19/// This type can be used to reason about data on a server without explicitly
20/// defining the server types in code, which is useful if you want to create
21/// a generic client that can work with data from any server.
22///
23/// Internally it is simply an array of [Variant].
24///
25/// Note that this type is intended to support all encoding/decoding types
26/// in the OPC-UA standard, which is more than what the encoding macros
27/// currently do. This includes structurs with optional fields, unions,
28/// and multi-dimensional arrays, none of which are used in the core types.
29pub struct DynamicStructure {
30    pub(super) type_def: Arc<StructTypeInfo>,
31    pub(super) discriminant: u32,
32    pub(super) type_tree: Arc<DataTypeTree>,
33    pub(super) data: Vec<Variant>,
34}
35
36impl PartialEq for DynamicStructure {
37    fn eq(&self, other: &Self) -> bool {
38        self.type_def.node_id == other.type_def.node_id
39            && self.discriminant == other.discriminant
40            && self.data == other.data
41    }
42}
43
44impl ExpandedMessageInfo for DynamicStructure {
45    fn full_json_type_id(&self) -> ExpandedNodeId {
46        ExpandedNodeId::new(self.type_def.encoding_ids.json_id.clone())
47    }
48
49    fn full_type_id(&self) -> ExpandedNodeId {
50        ExpandedNodeId::new(self.type_def.encoding_ids.binary_id.clone())
51    }
52
53    fn full_xml_type_id(&self) -> ExpandedNodeId {
54        ExpandedNodeId::new(self.type_def.encoding_ids.xml_id.clone())
55    }
56
57    fn full_data_type_id(&self) -> ExpandedNodeId {
58        ExpandedNodeId::new(self.type_def.node_id.clone())
59    }
60}
61
62impl DynamicStructure {
63    /// Create a new struct, validating that it matches the provided type definition.
64    pub fn new_struct(
65        type_def: Arc<StructTypeInfo>,
66        type_tree: Arc<DataTypeTree>,
67        data: Vec<Variant>,
68    ) -> Result<Self, Error> {
69        if data.len() != type_def.fields.len() {
70            return Err(Error::new(
71                StatusCode::BadInvalidArgument,
72                format!(
73                    "Invalid number of fields, got {}, expected {}",
74                    data.len(),
75                    type_def.fields.len()
76                ),
77            ));
78        }
79        if matches!(type_def.structure_type, StructureType::Union) {
80            return Err(Error::new(
81                StatusCode::BadInvalidArgument,
82                "Cannot construct a union using new_struct, call new_union instead",
83            ));
84        }
85
86        for (value, field) in data.iter().zip(type_def.fields.iter()) {
87            field.validate(value)?;
88        }
89        Ok(Self {
90            type_def,
91            discriminant: 0,
92            type_tree,
93            data,
94        })
95    }
96
97    /// Create a new union, validating that it matches the provided type definition.
98    pub fn new_union(
99        type_def: Arc<StructTypeInfo>,
100        type_tree: Arc<DataTypeTree>,
101        data: Variant,
102        discriminant: u32,
103    ) -> Result<Self, Error> {
104        if discriminant == 0 {
105            return Err(Error::new(
106                StatusCode::BadInvalidArgument,
107                "Discriminant must be non-zero.",
108            ));
109        }
110
111        if !matches!(type_def.structure_type, StructureType::Union) {
112            return Err(Error::new(
113                StatusCode::BadInvalidArgument,
114                "Cannot construct a struct using new_union, call new_struct instead",
115            ));
116        }
117        let Some(field) = type_def.fields.get(discriminant as usize - 1) else {
118            return Err(Error::new(
119                StatusCode::BadInvalidArgument,
120                format!("Invalid discriminant {discriminant}"),
121            ));
122        };
123        field.validate(&data)?;
124        Ok(Self {
125            type_def,
126            discriminant,
127            type_tree,
128            data: vec![data],
129        })
130    }
131
132    /// Create a new union, with value null.
133    pub fn new_null_union(type_def: Arc<StructTypeInfo>, type_tree: Arc<DataTypeTree>) -> Self {
134        Self {
135            type_def,
136            type_tree,
137            discriminant: 0,
138            data: Vec::new(),
139        }
140    }
141
142    /// Get a reference to the fields in order.
143    pub fn values(&self) -> &[Variant] {
144        &self.data
145    }
146
147    /// Get a reference to the field at `index`
148    pub fn get_field(&self, index: usize) -> Option<&Variant> {
149        self.data.get(index)
150    }
151
152    /// Get a reference to the field with the given name.
153    pub fn get_field_by_name(&self, name: &str) -> Option<&Variant> {
154        self.type_def
155            .index_by_name
156            .get(name)
157            .and_then(|v| self.data.get(*v))
158    }
159
160    fn field_variant_len(
161        &self,
162        f: &Variant,
163        field: &ParsedStructureField,
164        ctx: &Context<'_>,
165    ) -> usize {
166        match f {
167            Variant::ExtensionObject(o) => {
168                let Some(field_ty) = self.type_tree.get_struct_type(&field.type_id) else {
169                    // The field is missing, we'll fail later, probably, but for now just assume that we're encoding
170                    // an extension object.
171                    return o.byte_len(ctx);
172                };
173
174                // If the field is abstract, encode it as an extension object
175                if field_ty.is_abstract {
176                    return o.byte_len(ctx);
177                }
178                match &o.body {
179                    Some(b) => b.byte_len_dyn(ctx),
180                    None => 0,
181                }
182            }
183            r => r.value_byte_len(ctx),
184        }
185    }
186
187    fn encode_field<S: Write + ?Sized>(
188        &self,
189        mut stream: &mut S,
190        f: &Variant,
191        field: &ParsedStructureField,
192        ctx: &Context<'_>,
193    ) -> EncodingResult<()> {
194        match f {
195            Variant::ExtensionObject(o) => {
196                let Some(field_ty) = self.type_tree.get_struct_type(&field.type_id) else {
197                    return Err(Error::encoding(format!(
198                        "Dynamic type field missing from type tree: {}",
199                        field.type_id
200                    )));
201                };
202
203                if field_ty.is_abstract {
204                    o.encode(stream, ctx)
205                } else {
206                    let Some(body) = &o.body else {
207                        return Err(Error::encoding(
208                            "Dynamic type field is missing extension object body",
209                        ));
210                    };
211                    body.encode_binary(&mut stream, ctx)
212                }
213            }
214            Variant::Array(a) => {
215                if field.value_rank > 1 {
216                    let Some(dims) = &a.dimensions else {
217                        return Err(Error::encoding(
218                            "ArrayDimensions are required for fields with value rank > 1",
219                        ));
220                    };
221                    // Note array dimensions are encoded as Int32 even though they are presented
222                    // as UInt32 through attribute.
223
224                    // Encode dimensions
225                    write_i32(stream, dims.len() as i32)?;
226                    for dimension in dims {
227                        write_i32(stream, *dimension as i32)?;
228                    }
229                } else {
230                    write_i32(stream, a.values.len() as i32)?;
231                }
232
233                for value in a.values.iter() {
234                    self.encode_field(stream, value, field, ctx)?;
235                }
236                Ok(())
237            }
238            Variant::Empty => Err(Error::encoding("Empty variant value in structure")),
239            r => r.encode_value(stream, ctx),
240        }
241    }
242}
243
244impl UaNullable for DynamicStructure {
245    fn is_ua_null(&self) -> bool {
246        if self.type_def.structure_type == StructureType::Union {
247            self.discriminant == 0
248        } else {
249            false
250        }
251    }
252}
253
254impl BinaryEncodable for DynamicStructure {
255    fn byte_len(&self, ctx: &crate::Context<'_>) -> usize {
256        // Byte length is the sum of the individual structure fields
257        let mut size = 0;
258        let s = &self.type_def;
259
260        match s.structure_type {
261            StructureType::Structure => {
262                for (value, field) in self.data.iter().zip(s.fields.iter()) {
263                    size += self.field_variant_len(value, field, ctx);
264                }
265            }
266            StructureType::StructureWithOptionalFields => {
267                // encoding mask.
268                size += 4;
269                for (value, field) in self.data.iter().zip(s.fields.iter()) {
270                    if !field.is_optional || !matches!(value, Variant::Empty) {
271                        size += self.field_variant_len(value, field, ctx);
272                    }
273                }
274            }
275            StructureType::Union => {
276                // discriminant
277                size += 4;
278                if self.discriminant != 0 {
279                    let (Some(value), Some(field)) = (
280                        self.data.first(),
281                        s.fields.get(self.discriminant as usize - 1),
282                    ) else {
283                        return 0;
284                    };
285                    size += self.field_variant_len(value, field, ctx);
286                }
287            }
288            StructureType::StructureWithSubtypedValues => {
289                todo!("StructureWithSubtypedValues is unsupported")
290            }
291            StructureType::UnionWithSubtypedValues => {
292                todo!("UnionWithSubtypedValues is unsupported")
293            }
294        }
295
296        size
297    }
298
299    fn encode<S: std::io::Write + ?Sized>(
300        &self,
301        stream: &mut S,
302        ctx: &crate::Context<'_>,
303    ) -> crate::EncodingResult<()> {
304        let s = &self.type_def;
305        match s.structure_type {
306            StructureType::Structure => {
307                // Invariant used here: The data list must contain the correct fields with the correct values.
308                for (value, field) in self.data.iter().zip(s.fields.iter()) {
309                    self.encode_field(stream, value, field, ctx)?;
310                }
311            }
312            StructureType::StructureWithOptionalFields => {
313                let mut encoding_mask = 0u32;
314                let mut optional_idx = 0;
315                for (value, field) in self.data.iter().zip(s.fields.iter()) {
316                    if field.is_optional {
317                        if !matches!(value, Variant::Empty) {
318                            encoding_mask |= 1 << optional_idx;
319                        }
320                        optional_idx += 1;
321                    }
322                }
323                write_u32(stream, encoding_mask)?;
324                for (value, field) in self.data.iter().zip(s.fields.iter()) {
325                    if !field.is_optional || !matches!(value, Variant::Empty) {
326                        self.encode_field(stream, value, field, ctx)?;
327                    }
328                }
329            }
330            StructureType::Union => {
331                write_u32(stream, self.discriminant)?;
332                if self.discriminant != 0 {
333                    let (Some(value), Some(field)) = (
334                        self.data.first(),
335                        s.fields.get(self.discriminant as usize - 1),
336                    ) else {
337                        return Err(Error::encoding(
338                            "Discriminant was out of range of known fields",
339                        ));
340                    };
341
342                    self.encode_field(stream, value, field, ctx)?;
343                }
344            }
345            StructureType::StructureWithSubtypedValues => {
346                todo!("StructureWithSubtypedValues is unsupported")
347            }
348            StructureType::UnionWithSubtypedValues => {
349                todo!("UnionWithSubtypedValues is unsupported")
350            }
351        }
352
353        Ok(())
354    }
355}
356
357/// Type loader that can load types dynamically using data type definitions loaded at
358/// runtime.
359pub struct DynamicTypeLoader {
360    pub(super) type_tree: Arc<DataTypeTree>,
361}
362
363impl DynamicTypeLoader {
364    /// Create a new type loader that loads types from the given type tree.
365    pub fn new(type_tree: Arc<DataTypeTree>) -> Self {
366        Self { type_tree }
367    }
368
369    fn decode_field_value(
370        &self,
371        field: &ParsedStructureField,
372        stream: &mut dyn std::io::Read,
373        ctx: &Context<'_>,
374    ) -> EncodingResult<Variant> {
375        match field.scalar_type {
376            crate::VariantScalarTypeId::Boolean => Ok(Variant::from(
377                <bool as BinaryDecodable>::decode(stream, ctx)?,
378            )),
379            crate::VariantScalarTypeId::SByte => {
380                Ok(Variant::from(<i8 as BinaryDecodable>::decode(stream, ctx)?))
381            }
382            crate::VariantScalarTypeId::Byte => {
383                Ok(Variant::from(<u8 as BinaryDecodable>::decode(stream, ctx)?))
384            }
385            crate::VariantScalarTypeId::Int16 => Ok(Variant::from(
386                <i16 as BinaryDecodable>::decode(stream, ctx)?,
387            )),
388            crate::VariantScalarTypeId::UInt16 => Ok(Variant::from(
389                <u16 as BinaryDecodable>::decode(stream, ctx)?,
390            )),
391            crate::VariantScalarTypeId::Int32 => Ok(Variant::from(
392                <i32 as BinaryDecodable>::decode(stream, ctx)?,
393            )),
394            crate::VariantScalarTypeId::UInt32 => Ok(Variant::from(
395                <u32 as BinaryDecodable>::decode(stream, ctx)?,
396            )),
397            crate::VariantScalarTypeId::Int64 => Ok(Variant::from(
398                <i64 as BinaryDecodable>::decode(stream, ctx)?,
399            )),
400            crate::VariantScalarTypeId::UInt64 => Ok(Variant::from(
401                <u64 as BinaryDecodable>::decode(stream, ctx)?,
402            )),
403            crate::VariantScalarTypeId::Float => Ok(Variant::from(
404                <f32 as BinaryDecodable>::decode(stream, ctx)?,
405            )),
406            crate::VariantScalarTypeId::Double => Ok(Variant::from(
407                <f64 as BinaryDecodable>::decode(stream, ctx)?,
408            )),
409            crate::VariantScalarTypeId::String => Ok(Variant::from(
410                <UAString as BinaryDecodable>::decode(stream, ctx)?,
411            )),
412            crate::VariantScalarTypeId::DateTime => Ok(Variant::from(
413                <DateTime as BinaryDecodable>::decode(stream, ctx)?,
414            )),
415            crate::VariantScalarTypeId::Guid => Ok(Variant::from(
416                <Guid as BinaryDecodable>::decode(stream, ctx)?,
417            )),
418            crate::VariantScalarTypeId::ByteString => Ok(Variant::from(
419                <ByteString as BinaryDecodable>::decode(stream, ctx)?,
420            )),
421            crate::VariantScalarTypeId::XmlElement => Ok(Variant::from(
422                <XmlElement as BinaryDecodable>::decode(stream, ctx)?,
423            )),
424            crate::VariantScalarTypeId::NodeId => Ok(Variant::from(
425                <NodeId as BinaryDecodable>::decode(stream, ctx)?,
426            )),
427            crate::VariantScalarTypeId::ExpandedNodeId => Ok(Variant::from(
428                <ExpandedNodeId as BinaryDecodable>::decode(stream, ctx)?,
429            )),
430            crate::VariantScalarTypeId::StatusCode => Ok(Variant::from(
431                <StatusCode as BinaryDecodable>::decode(stream, ctx)?,
432            )),
433            crate::VariantScalarTypeId::QualifiedName => Ok(Variant::from(
434                <QualifiedName as BinaryDecodable>::decode(stream, ctx)?,
435            )),
436            crate::VariantScalarTypeId::LocalizedText => Ok(Variant::from(
437                <LocalizedText as BinaryDecodable>::decode(stream, ctx)?,
438            )),
439            crate::VariantScalarTypeId::ExtensionObject => {
440                let Some(field_ty) = self.type_tree.get_struct_type(&field.type_id) else {
441                    return Err(Error::decoding(format!(
442                        "Dynamic type field missing from type tree: {}",
443                        field.type_id
444                    )));
445                };
446
447                // If the field is abstract, it's encoded as an extension object,
448                // or so we assume.
449                if field_ty.is_abstract {
450                    Ok(Variant::from(<ExtensionObject as BinaryDecodable>::decode(
451                        stream, ctx,
452                    )?))
453                } else {
454                    // Else, load the extension object body directly.
455                    Ok(Variant::from(ctx.load_from_binary(
456                        &field_ty.node_id,
457                        stream,
458                        None,
459                    )?))
460                }
461            }
462            crate::VariantScalarTypeId::DataValue => Ok(Variant::from(
463                <DataValue as BinaryDecodable>::decode(stream, ctx)?,
464            )),
465            crate::VariantScalarTypeId::Variant => Ok(Variant::Variant(Box::new(
466                <Variant as BinaryDecodable>::decode(stream, ctx)?,
467            ))),
468            crate::VariantScalarTypeId::DiagnosticInfo => Ok(Variant::from(
469                <DiagnosticInfo as BinaryDecodable>::decode(stream, ctx)?,
470            )),
471        }
472    }
473
474    fn decode_field(
475        &self,
476        field: &ParsedStructureField,
477        stream: &mut dyn std::io::Read,
478        ctx: &Context<'_>,
479    ) -> EncodingResult<Variant> {
480        if field.value_rank > 0 {
481            let (len, array_dims) = if field.value_rank > 1 {
482                let Some(array_dims) = <Option<Vec<i32>> as BinaryDecodable>::decode(stream, ctx)?
483                else {
484                    return Err(Error::decoding("Array has invalid ArrayDimensions"));
485                };
486                if array_dims.len() != field.value_rank as usize {
487                    return Err(Error::decoding(
488                        "Array has incorrect ArrayDimensions, must match value rank",
489                    ));
490                }
491                let mut len = 1;
492                let mut final_dims = Vec::with_capacity(array_dims.len());
493                for dim in &array_dims {
494                    if *dim <= 0 {
495                        return Err(Error::decoding("Array has incorrect ArrayDimensions, all dimensions must be greater than zero"));
496                    }
497                    len *= *dim as u32;
498                    final_dims.push(*dim as u32);
499                }
500                (len as usize, Some(final_dims))
501            } else {
502                let len = <u32 as BinaryDecodable>::decode(stream, ctx)?;
503                (len as usize, None)
504            };
505
506            if len > ctx.options().max_array_length {
507                return Err(Error::decoding(format!(
508                    "Array length {} exceeds decoding limit {}",
509                    len,
510                    ctx.options().max_array_length
511                )));
512            }
513
514            let mut res = Vec::with_capacity(len);
515            for _ in 0..len {
516                res.push(self.decode_field_value(field, stream, ctx)?);
517            }
518            if let Some(dims) = array_dims {
519                Ok(Variant::Array(Box::new(
520                    Array::new_multi(field.scalar_type, res, dims).map_err(Error::decoding)?,
521                )))
522            } else {
523                Ok(Variant::Array(Box::new(
524                    Array::new(field.scalar_type, res).map_err(Error::decoding)?,
525                )))
526            }
527        } else {
528            self.decode_field_value(field, stream, ctx)
529        }
530    }
531
532    fn decode_type_inner(
533        &self,
534        stream: &mut dyn std::io::Read,
535        ctx: &Context<'_>,
536        t: &Arc<StructTypeInfo>,
537    ) -> crate::EncodingResult<Box<dyn crate::DynEncodable>> {
538        match t.structure_type {
539            StructureType::Structure => {
540                let mut values = Vec::with_capacity(t.fields.len());
541                for field in &t.fields {
542                    values.push(self.decode_field(field, stream, ctx)?);
543                }
544                Ok(Box::new(DynamicStructure {
545                    type_def: t.clone(),
546                    discriminant: 0,
547                    type_tree: self.type_tree.clone(),
548                    data: values,
549                }))
550            }
551            StructureType::StructureWithOptionalFields => {
552                let mask = <u32 as BinaryDecodable>::decode(stream, ctx)?;
553                let mut values = Vec::with_capacity(t.fields.len());
554                let mut optional_idx = 0;
555                for field in t.fields.iter() {
556                    if field.is_optional {
557                        if (1 << optional_idx) & mask != 0 {
558                            values.push(self.decode_field(field, stream, ctx)?);
559                        } else {
560                            values.push(Variant::Empty);
561                        }
562                        optional_idx += 1;
563                    } else {
564                        values.push(self.decode_field(field, stream, ctx)?);
565                    }
566                }
567                Ok(Box::new(DynamicStructure {
568                    type_def: t.clone(),
569                    discriminant: 0,
570                    type_tree: self.type_tree.clone(),
571                    data: values,
572                }))
573            }
574            StructureType::Union => {
575                let discriminant = <u32 as BinaryDecodable>::decode(stream, ctx)?;
576                if discriminant == 0 {
577                    return Ok(Box::new(DynamicStructure::new_null_union(
578                        t.clone(),
579                        self.type_tree.clone(),
580                    )));
581                }
582                let Some(field) = t.fields.get(discriminant as usize - 1) else {
583                    return Err(Error::decoding(format!(
584                        "Invalid discriminant: {discriminant}"
585                    )));
586                };
587                let values = vec![self.decode_field(field, stream, ctx)?];
588                Ok(Box::new(DynamicStructure {
589                    type_def: t.clone(),
590                    discriminant,
591                    type_tree: self.type_tree.clone(),
592                    data: values,
593                }))
594            }
595            StructureType::StructureWithSubtypedValues => {
596                todo!("StructureWithSubtypedValues is unsupported")
597            }
598            StructureType::UnionWithSubtypedValues => {
599                todo!("UnionWithSubtypedValues is unsupported")
600            }
601        }
602    }
603}
604
605impl TypeLoader for DynamicTypeLoader {
606    fn load_from_binary(
607        &self,
608        node_id: &NodeId,
609        stream: &mut dyn std::io::Read,
610        ctx: &Context<'_>,
611        _length: Option<usize>,
612    ) -> Option<crate::EncodingResult<Box<dyn crate::DynEncodable>>> {
613        let ty_node_id = if let Some(mapped) = self.type_tree.encoding_to_data_type().get(node_id) {
614            mapped
615        } else {
616            node_id
617        };
618        let t = self.type_tree.get_struct_type(ty_node_id)?;
619
620        if !t.is_supported() {
621            return None;
622        }
623
624        Some(self.decode_type_inner(stream, ctx, t))
625    }
626
627    fn priority(&self) -> crate::TypeLoaderPriority {
628        crate::TypeLoaderPriority::Dynamic(50)
629    }
630
631    #[cfg(feature = "xml")]
632    fn load_from_xml(
633        &self,
634        node_id: &crate::NodeId,
635        stream: &mut crate::xml::XmlStreamReader<&mut dyn std::io::Read>,
636        ctx: &Context<'_>,
637        _name: &str,
638    ) -> Option<crate::EncodingResult<Box<dyn crate::DynEncodable>>> {
639        let ty_node_id = if let Some(mapped) = self.type_tree.encoding_to_data_type().get(node_id) {
640            mapped
641        } else {
642            node_id
643        };
644        let t = self.type_tree.get_struct_type(ty_node_id)?;
645
646        if !t.is_supported() {
647            return None;
648        }
649
650        Some(self.xml_decode_type_inner(stream, ctx, t))
651    }
652
653    #[cfg(feature = "json")]
654    fn load_from_json(
655        &self,
656        node_id: &crate::NodeId,
657        stream: &mut crate::json::JsonStreamReader<&mut dyn std::io::Read>,
658        ctx: &Context<'_>,
659    ) -> Option<crate::EncodingResult<Box<dyn crate::DynEncodable>>> {
660        let ty_node_id = if let Some(mapped) = self.type_tree.encoding_to_data_type().get(node_id) {
661            mapped
662        } else {
663            node_id
664        };
665        let t = self.type_tree.get_struct_type(ty_node_id)?;
666
667        if !t.is_supported() {
668            return None;
669        }
670
671        Some(self.json_decode_type_inner(stream, ctx, t))
672    }
673}
674
675#[cfg(test)]
676pub(crate) mod tests {
677    use std::{
678        io::{Cursor, Seek},
679        sync::{Arc, LazyLock},
680    };
681
682    use opcua_macros::ua_encodable;
683
684    use crate::{
685        binary_decode_to_enc, json_decode_to_enc, xml_decode_to_enc, Array, BinaryDecodable,
686        BinaryEncodable, ContextOwned, DataTypeDefinition, DataTypeId, DecodingOptions,
687        EUInformation, ExpandedMessageInfo, ExtensionObject, LocalizedText, NamespaceMap, NodeId,
688        ObjectId, StaticTypeLoader, StructureDefinition, StructureField, TypeLoaderCollection,
689        TypeLoaderInstance, UAString, Variant, VariantScalarTypeId,
690    };
691
692    use crate::custom::type_tree::{
693        DataTypeTree, EncodingIds, GenericTypeInfo, ParentIds, TypeInfo,
694    };
695
696    use super::{DynamicStructure, DynamicTypeLoader};
697
698    pub(crate) fn make_type_tree() -> DataTypeTree {
699        // Add a few builtins we need.
700        let mut type_tree = DataTypeTree::new(ParentIds::new());
701        type_tree.add_type(DataTypeId::Int32.into(), GenericTypeInfo::new(false));
702        type_tree.add_type(DataTypeId::Boolean.into(), GenericTypeInfo::new(false));
703        type_tree.add_type(
704            DataTypeId::LocalizedText.into(),
705            GenericTypeInfo::new(false),
706        );
707        type_tree.add_type(DataTypeId::String.into(), GenericTypeInfo::new(false));
708        type_tree
709    }
710
711    pub(crate) fn add_eu_information(type_tree: &mut DataTypeTree) {
712        type_tree.parent_ids_mut().add_type(
713            DataTypeId::EUInformation.into(),
714            DataTypeId::Structure.into(),
715        );
716        type_tree.add_type(
717            DataTypeId::EUInformation.into(),
718            TypeInfo::from_type_definition(
719                DataTypeDefinition::Structure(StructureDefinition {
720                    default_encoding_id: NodeId::null(),
721                    base_data_type: DataTypeId::Structure.into(),
722                    structure_type: crate::StructureType::Structure,
723                    fields: Some(vec![
724                        StructureField {
725                            name: "NamespaceUri".into(),
726                            data_type: DataTypeId::String.into(),
727                            value_rank: -1,
728                            ..Default::default()
729                        },
730                        StructureField {
731                            name: "UnitId".into(),
732                            data_type: DataTypeId::Int32.into(),
733                            value_rank: -1,
734                            ..Default::default()
735                        },
736                        StructureField {
737                            name: "DisplayName".into(),
738                            data_type: DataTypeId::LocalizedText.into(),
739                            value_rank: -1,
740                            ..Default::default()
741                        },
742                        StructureField {
743                            name: "Description".into(),
744                            data_type: DataTypeId::LocalizedText.into(),
745                            value_rank: -1,
746                            ..Default::default()
747                        },
748                    ]),
749                }),
750                "EUInformation".to_owned(),
751                Some(EncodingIds {
752                    binary_id: ObjectId::EUInformation_Encoding_DefaultBinary.into(),
753                    json_id: ObjectId::EUInformation_Encoding_DefaultJson.into(),
754                    xml_id: ObjectId::EUInformation_Encoding_DefaultXml.into(),
755                }),
756                false,
757                &DataTypeId::EUInformation.into(),
758                type_tree.parent_ids(),
759            )
760            .unwrap(),
761        );
762    }
763
764    #[test]
765    fn dynamic_struct_round_trip() {
766        let mut type_tree = make_type_tree();
767        add_eu_information(&mut type_tree);
768        // Add a structure for EUInformation
769
770        let loader = DynamicTypeLoader::new(Arc::new(type_tree));
771        let mut loaders = TypeLoaderCollection::new_empty();
772        loaders.add_type_loader(loader);
773        let ctx = ContextOwned::new(NamespaceMap::new(), loaders, DecodingOptions::test());
774
775        let mut write_buf = Vec::<u8>::new();
776        let mut cursor = Cursor::new(&mut write_buf);
777
778        let obj = ExtensionObject::from_message(EUInformation {
779            namespace_uri: "my.namespace.uri".into(),
780            unit_id: 5,
781            display_name: "Degrees Celsius".into(),
782            description: "Description".into(),
783        });
784
785        // Encode the object, will use the regular encode implementation for EUInformation.
786        BinaryEncodable::encode(&obj, &mut cursor, &ctx.context()).unwrap();
787        cursor.seek(std::io::SeekFrom::Start(0)).unwrap();
788
789        let obj2: ExtensionObject = BinaryDecodable::decode(&mut cursor, &ctx.context()).unwrap();
790
791        // Decode it back, resulting in a dynamic structure.
792        let value = obj2.inner_as::<DynamicStructure>().unwrap();
793        assert_eq!(value.data.len(), 4);
794        assert_eq!(value.data[0], Variant::from("my.namespace.uri"));
795        assert_eq!(value.data[1], Variant::from(5i32));
796        assert_eq!(
797            value.data[2],
798            Variant::from(LocalizedText::from("Degrees Celsius"))
799        );
800        assert_eq!(
801            value.data[3],
802            Variant::from(LocalizedText::from("Description"))
803        );
804
805        // Re-encode it
806        cursor.seek(std::io::SeekFrom::Start(0)).unwrap();
807        BinaryEncodable::encode(&obj2, &mut cursor, &ctx.context()).unwrap();
808
809        // Make a new context, this time with the regular decoder for EUInformation
810        let ctx = ContextOwned::new_default(NamespaceMap::new(), DecodingOptions::test());
811        cursor.seek(std::io::SeekFrom::Start(0)).unwrap();
812        let obj3: ExtensionObject = BinaryDecodable::decode(&mut cursor, &ctx.context()).unwrap();
813
814        assert_eq!(obj, obj3);
815    }
816
817    #[test]
818    fn dynamic_nested_struct_round_trip() {
819        let mut type_tree = make_type_tree();
820        add_eu_information(&mut type_tree);
821        let type_node_id = NodeId::new(1, 5);
822        type_tree
823            .parent_ids_mut()
824            .add_type(type_node_id.clone(), DataTypeId::Structure.into());
825        type_tree.add_type(
826            type_node_id.clone(),
827            TypeInfo::from_type_definition(
828                DataTypeDefinition::Structure(StructureDefinition {
829                    default_encoding_id: NodeId::null(),
830                    base_data_type: DataTypeId::Structure.into(),
831                    structure_type: crate::StructureType::Structure,
832                    fields: Some(vec![
833                        StructureField {
834                            name: "Info".into(),
835                            data_type: DataTypeId::EUInformation.into(),
836                            value_rank: -1,
837                            ..Default::default()
838                        },
839                        StructureField {
840                            name: "InfoArray".into(),
841                            data_type: DataTypeId::EUInformation.into(),
842                            value_rank: 1,
843                            ..Default::default()
844                        },
845                        StructureField {
846                            name: "AbstractField".into(),
847                            data_type: DataTypeId::BaseDataType.into(),
848                            value_rank: -1,
849                            ..Default::default()
850                        },
851                        StructureField {
852                            name: "PrimitiveArray".into(),
853                            data_type: DataTypeId::Int32.into(),
854                            value_rank: 2,
855                            ..Default::default()
856                        },
857                    ]),
858                }),
859                "MyType".to_owned(),
860                Some(EncodingIds {
861                    binary_id: NodeId::new(1, 6),
862                    json_id: NodeId::new(1, 7),
863                    xml_id: NodeId::new(1, 8),
864                }),
865                false,
866                &type_node_id,
867                type_tree.parent_ids(),
868            )
869            .unwrap(),
870        );
871        let type_tree = Arc::new(type_tree);
872        let loader = DynamicTypeLoader::new(type_tree.clone());
873        let mut loaders = TypeLoaderCollection::new();
874        loaders.add_type_loader(loader);
875        let ctx = ContextOwned::new(NamespaceMap::new(), loaders, DecodingOptions::test());
876
877        let obj = DynamicStructure::new_struct(
878            type_tree.get_struct_type(&type_node_id).unwrap().clone(),
879            type_tree,
880            vec![
881                Variant::from(ExtensionObject::from_message(EUInformation {
882                    namespace_uri: "my.namespace.uri".into(),
883                    unit_id: 5,
884                    display_name: "Degrees Celsius".into(),
885                    description: "Description".into(),
886                })),
887                Variant::from(vec![
888                    ExtensionObject::from_message(EUInformation {
889                        namespace_uri: "my.namespace.uri".into(),
890                        unit_id: 5,
891                        display_name: "Degrees Celsius".into(),
892                        description: "Description".into(),
893                    }),
894                    ExtensionObject::from_message(EUInformation {
895                        namespace_uri: "my.namespace.uri.2".into(),
896                        unit_id: 6,
897                        display_name: "Degrees Celsius 2".into(),
898                        description: "Description 2".into(),
899                    }),
900                ]),
901                Variant::Variant(Box::new(Variant::from(123))),
902                Variant::from(
903                    Array::new_multi(
904                        VariantScalarTypeId::Int32,
905                        [1i32, 2, 3, 4, 5, 6]
906                            .into_iter()
907                            .map(Variant::from)
908                            .collect::<Vec<_>>(),
909                        vec![2, 3],
910                    )
911                    .unwrap(),
912                ),
913            ],
914        )
915        .unwrap();
916        let obj = ExtensionObject::from_message(obj);
917
918        let mut write_buf = Vec::<u8>::new();
919        let mut cursor = Cursor::new(&mut write_buf);
920
921        BinaryEncodable::encode(&obj, &mut cursor, &ctx.context()).unwrap();
922        cursor.seek(std::io::SeekFrom::Start(0)).unwrap();
923        let obj2: ExtensionObject = BinaryDecodable::decode(&mut cursor, &ctx.context()).unwrap();
924
925        assert_eq!(obj, obj2);
926    }
927
928    pub(crate) fn get_namespaces() -> NamespaceMap {
929        let mut namespaces = NamespaceMap::new();
930        namespaces.add_namespace(TYPE_NAMESPACE);
931        namespaces
932    }
933
934    pub(crate) fn get_custom_union() -> ContextOwned {
935        let mut type_tree = make_type_tree();
936        type_tree
937            .parent_ids_mut()
938            .add_type(NodeId::new(1, 1), DataTypeId::Structure.into());
939        type_tree.add_type(
940            NodeId::new(1, 1),
941            TypeInfo::from_type_definition(
942                DataTypeDefinition::Structure(StructureDefinition {
943                    default_encoding_id: NodeId::null(),
944                    base_data_type: DataTypeId::Structure.into(),
945                    structure_type: crate::StructureType::Union,
946                    fields: Some(vec![
947                        StructureField {
948                            name: "Integer".into(),
949                            data_type: DataTypeId::Int32.into(),
950                            value_rank: -1,
951                            ..Default::default()
952                        },
953                        StructureField {
954                            name: "StringVariant".into(),
955                            data_type: DataTypeId::String.into(),
956                            value_rank: -1,
957                            ..Default::default()
958                        },
959                    ]),
960                }),
961                "MyUnion".to_owned(),
962                Some(EncodingIds {
963                    binary_id: NodeId::new(1, 2),
964                    json_id: NodeId::new(1, 3),
965                    xml_id: NodeId::new(1, 4),
966                }),
967                false,
968                &NodeId::new(1, 1),
969                type_tree.parent_ids(),
970            )
971            .unwrap(),
972        );
973
974        let loader = DynamicTypeLoader::new(Arc::new(type_tree));
975        let mut loaders = TypeLoaderCollection::new_empty();
976        loaders.add_type_loader(loader);
977
978        ContextOwned::new(get_namespaces(), loaders, DecodingOptions::test())
979    }
980
981    mod opcua {
982        pub(super) use crate as types;
983    }
984
985    const TYPE_NAMESPACE: &str = "my.custom.namespace.uri";
986
987    #[derive(Debug, Clone, PartialEq)]
988    #[ua_encodable]
989    pub(crate) enum MyUnion {
990        Null,
991        Integer(i32),
992        StringVariant(UAString),
993    }
994
995    impl ExpandedMessageInfo for MyUnion {
996        fn full_type_id(&self) -> crate::ExpandedNodeId {
997            crate::ExpandedNodeId::new_with_namespace(TYPE_NAMESPACE, 2)
998        }
999
1000        fn full_json_type_id(&self) -> crate::ExpandedNodeId {
1001            crate::ExpandedNodeId::new_with_namespace(TYPE_NAMESPACE, 3)
1002        }
1003
1004        fn full_xml_type_id(&self) -> crate::ExpandedNodeId {
1005            crate::ExpandedNodeId::new_with_namespace(TYPE_NAMESPACE, 4)
1006        }
1007
1008        fn full_data_type_id(&self) -> crate::ExpandedNodeId {
1009            crate::ExpandedNodeId::new_with_namespace(TYPE_NAMESPACE, 1)
1010        }
1011    }
1012
1013    static TYPES: LazyLock<TypeLoaderInstance> = LazyLock::new(|| {
1014        let mut inst = opcua::types::TypeLoaderInstance::new();
1015        inst.add_binary_type(1, 2, binary_decode_to_enc::<MyUnion>);
1016        inst.add_json_type(1, 3, json_decode_to_enc::<MyUnion>);
1017        inst.add_xml_type(1, 4, xml_decode_to_enc::<MyUnion>);
1018        inst
1019    });
1020
1021    pub(crate) struct MyUnionTypeLoader;
1022
1023    impl StaticTypeLoader for MyUnionTypeLoader {
1024        fn instance() -> &'static TypeLoaderInstance {
1025            &TYPES
1026        }
1027
1028        fn namespace() -> &'static str {
1029            TYPE_NAMESPACE
1030        }
1031    }
1032
1033    #[test]
1034    fn union_round_trip() {
1035        let ctx = get_custom_union();
1036
1037        let mut write_buf = Vec::<u8>::new();
1038        let mut cursor = Cursor::new(&mut write_buf);
1039
1040        let obj = ExtensionObject::from_message(MyUnion::Integer(123));
1041
1042        // Encode the object, using the regular BinaryEncodable implementation
1043        BinaryEncodable::encode(&obj, &mut cursor, &ctx.context()).unwrap();
1044        cursor.seek(std::io::SeekFrom::Start(0)).unwrap();
1045
1046        let obj2: ExtensionObject = BinaryDecodable::decode(&mut cursor, &ctx.context()).unwrap();
1047
1048        // Decode it back, resulting in a dynamic structure.
1049        let value = obj2.inner_as::<DynamicStructure>().unwrap();
1050        assert_eq!(value.data.len(), 1);
1051
1052        assert_eq!(value.data[0], Variant::from(123i32));
1053        assert_eq!(value.discriminant, 1);
1054
1055        cursor.seek(std::io::SeekFrom::Start(0)).unwrap();
1056        BinaryEncodable::encode(&obj2, &mut cursor, &ctx.context()).unwrap();
1057
1058        // Make a new context, this time with the regular decoder for MyUnion
1059        let mut ctx = ContextOwned::new_default(get_namespaces(), DecodingOptions::test());
1060        ctx.loaders_mut().add_type_loader(MyUnionTypeLoader);
1061        cursor.seek(std::io::SeekFrom::Start(0)).unwrap();
1062        let obj3: ExtensionObject = BinaryDecodable::decode(&mut cursor, &ctx.context()).unwrap();
1063
1064        assert_eq!(obj, obj3);
1065    }
1066
1067    #[test]
1068    fn union_null() {
1069        let ctx = get_custom_union();
1070
1071        let mut write_buf = Vec::<u8>::new();
1072        let mut cursor = Cursor::new(&mut write_buf);
1073
1074        let obj = ExtensionObject::from_message(MyUnion::Null);
1075
1076        // Encode the object, using the regular BinaryEncodable implementation
1077        BinaryEncodable::encode(&obj, &mut cursor, &ctx.context()).unwrap();
1078        cursor.seek(std::io::SeekFrom::Start(0)).unwrap();
1079
1080        let obj2: ExtensionObject = BinaryDecodable::decode(&mut cursor, &ctx.context()).unwrap();
1081
1082        // Decode it back, resulting in a dynamic structure.
1083        let value = obj2.inner_as::<DynamicStructure>().unwrap();
1084        assert_eq!(value.data.len(), 0);
1085        assert_eq!(value.discriminant, 0);
1086
1087        cursor.seek(std::io::SeekFrom::Start(0)).unwrap();
1088        BinaryEncodable::encode(&obj2, &mut cursor, &ctx.context()).unwrap();
1089
1090        // Make a new context, this time with the regular decoder for MyUnion
1091        let mut ctx = ContextOwned::new_default(get_namespaces(), DecodingOptions::test());
1092        ctx.loaders_mut().add_type_loader(MyUnionTypeLoader);
1093        cursor.seek(std::io::SeekFrom::Start(0)).unwrap();
1094        let obj3: ExtensionObject = BinaryDecodable::decode(&mut cursor, &ctx.context()).unwrap();
1095
1096        assert_eq!(obj, obj3);
1097    }
1098}