opcua_types/
extension_object.rs

1// OPCUA for Rust
2// SPDX-License-Identifier: MPL-2.0
3// Copyright (C) 2017-2024 Adam Lock
4
5//! Contains the implementation of `ExtensionObject`.
6
7use std::{
8    any::{Any, TypeId},
9    fmt,
10    io::{Read, Write},
11};
12
13use crate::{write_i32, write_u8, Error, ExpandedMessageInfo, ExpandedNodeId, UaNullable};
14
15use super::{
16    encoding::{BinaryDecodable, BinaryEncodable, EncodingResult},
17    node_id::NodeId,
18    ObjectId,
19};
20
21#[derive(Debug)]
22/// Error returned when working with extension objects.
23pub struct ExtensionObjectError;
24
25impl fmt::Display for ExtensionObjectError {
26    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27        write!(f, "ExtensionObjectError")
28    }
29}
30
31/// Trait for an OPC-UA struct that can be dynamically encoded back to binary (or JSON).
32/// ExtensionObject wraps a dynamic object for this trait.
33/// Note that this trait is automatically implemented for anything that implements
34/// [BinaryEncodable], [JsonEncodable] (with the `json` feature), [Send], [Sync], [Clone],
35/// [ExpandedMessageInfo], [std::fmt::Debug] and [PartialEq].
36///
37/// All of these are automatically derived during codegen, if you
38/// want to manually implement a type that can be stored as an extension object,
39/// you need to implement or derive all of these traits.
40pub trait DynEncodable: Any + Send + Sync + std::fmt::Debug {
41    /// Encode the struct using OPC-UA binary encoding.
42    fn encode_binary(
43        &self,
44        stream: &mut dyn std::io::Write,
45        ctx: &crate::Context<'_>,
46    ) -> EncodingResult<()>;
47
48    #[cfg(feature = "json")]
49    /// Encode the struct using reversible OPC-UA JSON encoding.
50    fn encode_json(
51        &self,
52        stream: &mut crate::json::JsonStreamWriter<&mut dyn std::io::Write>,
53        ctx: &crate::Context<'_>,
54    ) -> EncodingResult<()>;
55
56    #[cfg(feature = "xml")]
57    /// Encode the struct using OPC-UA XML encoding.
58    fn encode_xml(
59        &self,
60        stream: &mut crate::xml::XmlStreamWriter<&mut dyn std::io::Write>,
61        ctx: &crate::Context<'_>,
62    ) -> EncodingResult<()>;
63
64    #[cfg(feature = "xml")]
65    /// The XML tag name for this struct.
66    fn xml_tag_name(&self) -> &str;
67
68    /// Get the binary byte length of this struct.
69    fn byte_len_dyn(&self, ctx: &crate::Context<'_>) -> usize;
70
71    /// Get the binary encoding ID of this struct.
72    fn binary_type_id(&self) -> ExpandedNodeId;
73
74    #[cfg(feature = "json")]
75    /// Get the JSON encoding ID of this struct.
76    fn json_type_id(&self) -> ExpandedNodeId;
77
78    #[cfg(feature = "xml")]
79    /// Get the XML encoding ID of this struct.
80    fn xml_type_id(&self) -> ExpandedNodeId;
81
82    /// Get the data type ID of this struct.
83    fn data_type_id(&self) -> ExpandedNodeId;
84
85    /// Method to cast this to a dyn Any box, required for downcasting.
86    fn as_dyn_any(self: Box<Self>) -> Box<dyn Any + Send + Sync + 'static>;
87
88    /// Method to cast this to a dyn Any trait object, required for downcasting by reference.
89    fn as_dyn_any_ref(&self) -> &(dyn Any + Send + Sync);
90
91    /// Clone this to a dyn box. Required in order to implement Clone for ExtensionObject.
92    fn clone_box(&self) -> Box<dyn DynEncodable>;
93
94    /// Compare this with dynamic object. Invokes the PartialEq implementation of self and other,
95    /// if other has type `Self`.
96    fn dyn_eq(&self, other: &dyn DynEncodable) -> bool;
97
98    /// Get the type name of the type, by calling `std::any::type_name` on `Self`.
99    /// Very useful for debugging.
100    fn type_name(&self) -> &'static str;
101
102    /// Override the extension object encoding used for this type.
103    /// This only makes sense if the type can only ever be encoded using a single
104    /// built-in encoding.
105    fn override_encoding(&self) -> Option<crate::encoding::BuiltInDataEncoding>;
106}
107
108macro_rules! blanket_dyn_encodable {
109    ($bound:tt $(+ $others:tt)*) => {
110        impl<T> DynEncodable for T
111        where
112            T: $bound  $(+ $others)* + ExpandedMessageInfo + Any + std::fmt::Debug + Send + Sync + Clone + PartialEq,
113        {
114            fn encode_binary(&self, stream: &mut dyn std::io::Write, ctx: &crate::Context<'_>) -> EncodingResult<()> {
115                BinaryEncodable::encode(self, stream, ctx)
116            }
117
118            #[cfg(feature = "json")]
119            fn encode_json(
120                &self,
121                stream: &mut crate::json::JsonStreamWriter<&mut dyn std::io::Write>,
122                ctx: &crate::Context<'_>
123            ) -> EncodingResult<()> {
124                JsonEncodable::encode(self, stream, ctx)
125            }
126
127            #[cfg(feature = "xml")]
128            fn encode_xml(
129                &self,
130                stream: &mut crate::xml::XmlStreamWriter<&mut dyn std::io::Write>,
131                ctx: &crate::Context<'_>,
132            ) -> EncodingResult<()> {
133                XmlEncodable::encode(self, stream, ctx)
134            }
135
136            #[cfg(feature = "xml")]
137            fn xml_tag_name(&self,) -> &str {
138                self.tag()
139            }
140
141            fn byte_len_dyn(&self, ctx: &crate::Context<'_>,) -> usize {
142                BinaryEncodable::byte_len(self, ctx)
143            }
144
145            fn binary_type_id(&self) -> ExpandedNodeId {
146                self.full_type_id()
147            }
148
149            #[cfg(feature = "json")]
150            fn json_type_id(&self) -> ExpandedNodeId {
151                self.full_json_type_id()
152            }
153
154            #[cfg(feature = "xml")]
155            fn xml_type_id(&self) -> ExpandedNodeId {
156                self.full_xml_type_id()
157            }
158
159            fn data_type_id(&self) -> ExpandedNodeId {
160                self.full_data_type_id()
161            }
162
163            fn as_dyn_any(self: Box<Self>) -> Box<dyn Any + Send + Sync + 'static> {
164                self
165            }
166
167            fn as_dyn_any_ref(&self) -> &(dyn Any + Send + Sync) {
168                self
169            }
170
171            fn clone_box(&self) -> Box<dyn DynEncodable> {
172                Box::new(self.clone())
173            }
174
175            fn dyn_eq(&self, other: &dyn DynEncodable) -> bool {
176                if let Some(o) = other.as_dyn_any_ref().downcast_ref::<Self>() {
177                    o == self
178                } else {
179                    false
180                }
181            }
182
183            fn type_name(&self) -> &'static str {
184                std::any::type_name::<Self>()
185            }
186
187            fn override_encoding(&self) -> Option<crate::encoding::BuiltInDataEncoding> {
188                BinaryEncodable::override_encoding(self)
189            }
190        }
191    };
192}
193
194#[cfg(feature = "json")]
195use crate::json::JsonEncodable;
196
197#[cfg(feature = "xml")]
198use crate::xml::XmlEncodable;
199
200#[cfg(feature = "xml")]
201macro_rules! blanket_call_2 {
202    ($($res:tt)*) => {
203        blanket_dyn_encodable!($($res)* + XmlEncodable);
204    }
205}
206#[cfg(not(feature = "xml"))]
207macro_rules! blanket_call_2 {
208    ($($res:tt)*) => {
209        blanket_dyn_encodable!($($res)*);
210    }
211}
212
213#[cfg(feature = "json")]
214macro_rules! blanket_call_1 {
215    ($($res:tt)*) => {
216        blanket_call_2!($($res)* + JsonEncodable);
217    }
218}
219#[cfg(not(feature = "json"))]
220macro_rules! blanket_call_1 {
221    ($($res:tt)*) => {
222        blanket_call_2!($($res)*);
223    }
224}
225
226blanket_call_1!(BinaryEncodable);
227
228impl PartialEq for dyn DynEncodable {
229    fn eq(&self, other: &dyn DynEncodable) -> bool {
230        self.dyn_eq(other)
231    }
232}
233
234impl std::error::Error for ExtensionObjectError {}
235
236impl UaNullable for ExtensionObject {}
237
238#[cfg(feature = "json")]
239mod json {
240    use std::io::{Cursor, Read};
241
242    use crate::{json::*, ByteString, Error, NodeId};
243
244    use super::ExtensionObject;
245
246    impl JsonEncodable for ExtensionObject {
247        fn encode(
248            &self,
249            stream: &mut JsonStreamWriter<&mut dyn std::io::Write>,
250            ctx: &crate::Context<'_>,
251        ) -> super::EncodingResult<()> {
252            let Some(body) = &self.body else {
253                stream.null_value()?;
254                return Ok(());
255            };
256
257            let type_id = body.json_type_id();
258
259            let id = type_id.try_resolve(ctx.namespaces()).ok_or_else(|| {
260                Error::encoding(format!("Missing namespace for encoding ID: {type_id}"))
261            })?;
262
263            stream.begin_object()?;
264
265            stream.name("UaTypeId")?;
266            JsonEncodable::encode(id.as_ref(), stream, ctx)?;
267
268            if let Some(encoding) = body.override_encoding() {
269                match encoding {
270                    crate::BuiltInDataEncoding::Binary => {
271                        stream.name("UaEncoding")?;
272                        stream.number_value(1)?;
273                    }
274                    crate::BuiltInDataEncoding::XML => {
275                        stream.name("UaEncoding")?;
276                        stream.number_value(2)?;
277                    }
278                    crate::BuiltInDataEncoding::JSON => (),
279                }
280            }
281
282            stream.name("UaBody")?;
283            body.encode_json(stream, ctx)?;
284
285            stream.end_object()?;
286
287            Ok(())
288        }
289    }
290
291    impl JsonDecodable for ExtensionObject {
292        fn decode(
293            stream: &mut JsonStreamReader<&mut dyn std::io::Read>,
294            ctx: &Context<'_>,
295        ) -> super::EncodingResult<Self> {
296            if stream.peek()? == ValueType::Null {
297                stream.next_null()?;
298                return Ok(Self::null());
299            }
300
301            let mut type_id: Option<NodeId> = None;
302            let mut encoding: Option<u32> = None;
303            let mut raw_body = None;
304            let mut raw_string_body: Option<String> = None;
305            let mut body = None;
306
307            stream.begin_object()?;
308
309            while stream.has_next()? {
310                match stream.next_name()? {
311                    "UaTypeId" => type_id = Some(JsonDecodable::decode(stream, ctx)?),
312                    "UaEncoding" => encoding = Some(JsonDecodable::decode(stream, ctx)?),
313                    "UaBody" => match stream.peek()? {
314                        ValueType::Object => {
315                            if encoding.is_some_and(|e| e != 0) {
316                                return Err(Error::decoding(format!(
317                                    "Invalid encoding, expected 0 or null, got {encoding:?}"
318                                )));
319                            }
320                            if let Some(type_id) = &type_id {
321                                body = Some(ctx.load_from_json(type_id, stream)?);
322                            } else {
323                                raw_body = Some(consume_raw_value(stream)?);
324                            }
325                        }
326                        _ => {
327                            raw_string_body = Some(JsonDecodable::decode(stream, ctx)?);
328                        }
329                    },
330                    _ => stream.skip_value()?,
331                }
332            }
333
334            stream.end_object()?;
335
336            let Some(type_id) = type_id else {
337                return Err(Error::decoding("Missing type ID in extension object"));
338            };
339
340            let encoding = encoding.unwrap_or_default();
341
342            if let Some(body) = body {
343                Ok(body)
344            } else if let Some(raw_body) = raw_body {
345                if encoding != 0 {
346                    return Err(Error::decoding(format!(
347                        "Invalid encoding, expected 0 or null, got {encoding}"
348                    )));
349                }
350                let mut cursor = Cursor::new(raw_body);
351                let mut inner_stream = JsonStreamReader::new(&mut cursor as &mut dyn Read);
352                Ok(ctx.load_from_json(&type_id, &mut inner_stream)?)
353            } else if let Some(string_body) = raw_string_body {
354                if encoding == 1 {
355                    let bytes = ByteString::from_base64_ignore_whitespace(string_body).ok_or_else(
356                        || Error::decoding("Invalid base64 string is JSON extension object"),
357                    )?;
358                    let Some(raw) = bytes.value else {
359                        return Err(Error::decoding("Missing extension object body"));
360                    };
361                    let len = raw.len();
362                    let mut cursor = Cursor::new(raw);
363                    Ok(ctx.load_from_binary(&type_id, &mut cursor as &mut dyn Read, Some(len))?)
364                } else if encoding == 2 {
365                    #[cfg(feature = "xml")]
366                    {
367                        let mut cursor = Cursor::new(string_body.as_bytes());
368                        let mut inner_stream =
369                            crate::xml::XmlStreamReader::new(&mut cursor as &mut dyn Read);
370                        if let Some(name) = crate::xml::enter_first_tag(&mut inner_stream)? {
371                            Ok(ctx.load_from_xml(&type_id, &mut inner_stream, &name)?)
372                        } else {
373                            Ok(ExtensionObject::null())
374                        }
375                    }
376                    #[cfg(not(feature = "xml"))]
377                    {
378                        tracing::warn!("XML feature is not enabled, deserializing XML payloads in JSON extension objects is not supported");
379                        Ok(ExtensionObject::null())
380                    }
381                } else {
382                    Err(Error::decoding(format!("Unsupported extension object encoding, expected 1 or 2 for string, got {encoding}")))
383                }
384            } else {
385                Err(Error::decoding("Missing extension object body"))
386            }
387        }
388    }
389}
390
391#[cfg(feature = "xml")]
392mod xml {
393    use opcua_xml::events::Event;
394
395    use crate::{xml::*, ByteString, NodeId};
396    use std::{
397        io::{Read, Write},
398        str::from_utf8,
399    };
400
401    use super::ExtensionObject;
402
403    impl XmlType for ExtensionObject {
404        const TAG: &'static str = "ExtensionObject";
405    }
406
407    impl XmlEncodable for ExtensionObject {
408        fn encode(
409            &self,
410            writer: &mut XmlStreamWriter<&mut dyn Write>,
411            ctx: &crate::Context<'_>,
412        ) -> super::EncodingResult<()> {
413            let Some(body) = &self.body else {
414                // An empty extension object is legal, meaning a null value.
415                return Ok(());
416            };
417            let type_id = body.xml_type_id();
418            let id = type_id.try_resolve(ctx.namespaces()).ok_or_else(|| {
419                Error::encoding(format!("Missing namespace for encoding ID: {type_id}"))
420            })?;
421
422            writer.encode_child("TypeId", id.as_ref(), ctx)?;
423            writer.write_start("Body")?;
424            writer.write_start(body.xml_tag_name())?;
425            body.encode_xml(writer, ctx)?;
426            writer.write_end(body.xml_tag_name())?;
427            writer.write_end("Body")?;
428
429            Ok(())
430        }
431    }
432
433    impl XmlDecodable for ExtensionObject {
434        fn decode(
435            read: &mut XmlStreamReader<&mut dyn Read>,
436            ctx: &Context<'_>,
437        ) -> Result<Self, Error> {
438            let mut type_id = None;
439            let mut body = None;
440            read.iter_children(
441                |key, reader, ctx| {
442                    match key.as_str() {
443                        "TypeId" => type_id = Some(NodeId::decode(reader, ctx)?),
444                        "Body" => {
445                            // First, read the top level element.
446                            let top_level = loop {
447                                match reader.next_event()? {
448                                    Event::Start(s) => break s,
449                                    Event::End(_) => {
450                                        return Ok(()); // End of the body itself
451                                    }
452                                    _ => (),
453                                }
454                            };
455                            let Some(type_id) = type_id.take() else {
456                                return Err(Error::decoding("Missing type ID in extension object"));
457                            };
458
459                            if top_level.local_name().as_ref() == b"ByteString" {
460                                // If it's a bytestring, decode it as a binary body.
461                                let val = ByteString::decode(reader, ctx)?;
462                                if let Some(raw) = val.value {
463                                    let len = raw.len();
464                                    let mut cursor = std::io::Cursor::new(raw);
465                                    body = Some(ctx.load_from_binary(
466                                        &type_id,
467                                        &mut cursor,
468                                        Some(len),
469                                    )?);
470                                }
471                            } else {
472                                let local_name = top_level.local_name();
473                                let name = from_utf8(local_name.as_ref())?.to_owned();
474                                // Decode from XML.
475                                body = Some(ctx.load_from_xml(&type_id, reader, &name)?);
476                            }
477                            // Read to the end of the body.
478                            reader.skip_value()?;
479                        }
480                        _ => reader.skip_value()?,
481                    };
482                    Ok(())
483                },
484                ctx,
485            )?;
486            Ok(body.unwrap_or_else(ExtensionObject::null))
487        }
488    }
489}
490
491/// An extension object holds an OPC-UA structure deserialize to a [DynEncodable].
492/// This makes it possible to deserialize an extension object, the serialize it back in a different
493/// format, without reflecting over or inspecting the inner type.
494///
495/// Note that in order for a type to be deserialized into an ExtensionObject, the
496/// [crate::Context] given during deserialization needs to contain a [crate::TypeLoader]
497/// that can handle the type.
498#[derive(PartialEq, Debug)]
499pub struct ExtensionObject {
500    /// The raw extension object body.
501    pub body: Option<Box<dyn DynEncodable>>,
502}
503
504impl Clone for ExtensionObject {
505    fn clone(&self) -> Self {
506        Self {
507            body: self.body.as_ref().map(|b| b.clone_box()),
508        }
509    }
510}
511
512impl Default for ExtensionObject {
513    fn default() -> Self {
514        Self::null()
515    }
516}
517
518impl BinaryEncodable for ExtensionObject {
519    fn byte_len(&self, ctx: &crate::Context<'_>) -> usize {
520        let type_id = self.binary_type_id();
521        let id = type_id.try_resolve(ctx.namespaces());
522
523        // Just default to null here, we'll fail later.
524        let mut size = id.map(|n| n.byte_len(ctx)).unwrap_or(2usize);
525        size += match &self.body {
526            Some(b) => 5 + b.byte_len_dyn(ctx),
527            None => 1,
528        };
529
530        size
531    }
532
533    fn encode<S: Write + ?Sized>(
534        &self,
535        mut stream: &mut S,
536        ctx: &crate::Context<'_>,
537    ) -> EncodingResult<()> {
538        let type_id = self.binary_type_id();
539        let id = type_id.try_resolve(ctx.namespaces());
540        let Some(id) = id else {
541            return Err(Error::encoding(format!("Unknown encoding ID: {type_id}")));
542        };
543
544        BinaryEncodable::encode(id.as_ref(), stream, ctx)?;
545
546        match &self.body {
547            Some(b) => {
548                if matches!(b.override_encoding(), Some(crate::BuiltInDataEncoding::XML)) {
549                    write_u8(stream, 0x2)?;
550                } else {
551                    write_u8(stream, 0x1)?;
552                }
553                write_i32(stream, b.byte_len_dyn(ctx) as i32)?;
554                b.encode_binary(&mut stream as &mut dyn Write, ctx)
555            }
556            None => write_u8(stream, 0x0),
557        }
558    }
559}
560impl BinaryDecodable for ExtensionObject {
561    fn decode<S: Read + ?Sized>(
562        mut stream: &mut S,
563        ctx: &crate::Context<'_>,
564    ) -> EncodingResult<Self> {
565        // Extension object is depth checked to prevent deep recursion
566        let _depth_lock = ctx.options().depth_lock()?;
567        let node_id = NodeId::decode(stream, ctx)?;
568        let encoding_type = u8::decode(stream, ctx)?;
569        let body = match encoding_type {
570            0x0 => None,
571            0x1 => {
572                let size = i32::decode(stream, ctx)?;
573                if size <= 0 {
574                    None
575                } else {
576                    Some(ctx.load_from_binary(&node_id, &mut stream, Some(size as usize))?)
577                }
578            }
579            0x2 => {
580                #[cfg(feature = "xml")]
581                {
582                    let body = crate::UAString::decode(stream, ctx)?;
583                    let Some(body) = body.value() else {
584                        return Ok(ExtensionObject::null());
585                    };
586                    let mut cursor = std::io::Cursor::new(body.as_bytes());
587                    let mut inner_stream =
588                        crate::xml::XmlStreamReader::new(&mut cursor as &mut dyn Read);
589                    if let Some(name) = crate::xml::enter_first_tag(&mut inner_stream)? {
590                        Some(ctx.load_from_xml(&node_id, &mut inner_stream, &name)?)
591                    } else {
592                        None
593                    }
594                }
595
596                #[cfg(not(feature = "xml"))]
597                {
598                    let size = i32::decode(stream, ctx)?;
599                    if size > 0 {
600                        let mut bytes = vec![0u8; size as usize];
601                        stream.read_exact(&mut bytes)?;
602                        Some(ExtensionObject::new(crate::type_loader::XmlBody::new(
603                            bytes,
604                            node_id,
605                            // This is only relevant if we are re-encoding the element as XML,
606                            // which we can't do anyway since the XML feature is disabled.
607                            "XmlElement".to_owned(),
608                        )))
609                    } else {
610                        None
611                    }
612                }
613            }
614            _ => {
615                return Err(Error::decoding(format!(
616                    "Invalid encoding type {encoding_type} in stream"
617                )));
618            }
619        };
620        Ok(body.unwrap_or_else(ExtensionObject::null))
621    }
622}
623
624impl ExtensionObject {
625    /// Create an extension object from a structure.
626    pub fn new<T>(encodable: T) -> ExtensionObject
627    where
628        T: DynEncodable,
629    {
630        Self {
631            body: Some(Box::new(encodable)),
632        }
633    }
634
635    /// Creates a null extension object, i.e. one with no value or payload
636    pub fn null() -> ExtensionObject {
637        ExtensionObject { body: None }
638    }
639
640    /// Tests for an empty extension object.
641    pub fn is_null(&self) -> bool {
642        self.body.is_none()
643    }
644
645    /// Get the binary type ID of the inner type.
646    pub fn binary_type_id(&self) -> ExpandedNodeId {
647        self.body
648            .as_ref()
649            .map(|b| b.binary_type_id())
650            .unwrap_or_else(ExpandedNodeId::null)
651    }
652
653    /// Returns the object id of the thing this extension object contains, assuming the
654    /// object id can be recognised from the node id.
655    pub fn object_id(&self) -> Result<ObjectId, ExtensionObjectError> {
656        self.body
657            .as_ref()
658            .ok_or(ExtensionObjectError)?
659            .binary_type_id()
660            .node_id
661            .as_object_id()
662            .map_err(|_| ExtensionObjectError)
663    }
664
665    /// Create an extension object from a structure.
666    pub fn from_message<T>(encodable: T) -> ExtensionObject
667    where
668        T: DynEncodable,
669    {
670        Self {
671            body: Some(Box::new(encodable)),
672        }
673    }
674
675    /// Consume the extension object and return the inner value downcast to `T`,
676    /// if the inner type is present and is an instance of `T`.
677    ///
678    /// You can use [match_extension_object_owned] for conveniently casting to one or more expected types.
679    pub fn into_inner_as<T: Send + Sync + 'static>(self) -> Option<Box<T>> {
680        self.body.and_then(|b| b.as_dyn_any().downcast().ok())
681    }
682
683    /// Return the inner value by reference downcast to `T`,
684    /// if the inner type is present and is an instance of `T`.
685    ///
686    /// You can use [match_extension_object] for conveniently casting to one or more expected types.
687    pub fn inner_as<T: Send + Sync + 'static>(&self) -> Option<&T> {
688        self.body
689            .as_ref()
690            .and_then(|b| b.as_dyn_any_ref().downcast_ref())
691    }
692
693    /// Get the rust [std::any::TypeId] of the inner type, if the extension object is not null.
694    pub fn type_id(&self) -> Option<TypeId> {
695        self.body.as_ref().map(|b| (**b).type_id())
696    }
697
698    /// Return `true` if the inner value is an instance of `T`
699    pub fn inner_is<T: 'static>(&self) -> bool {
700        self.type_id() == Some(TypeId::of::<T>())
701    }
702
703    /// Get the name of the Rust type stored in the extension object, unless it is empty.
704    pub fn type_name(&self) -> Option<&'static str> {
705        self.body.as_ref().map(|b| b.type_name())
706    }
707
708    /// Get the full data type ID of the inner type.
709    /// Note that for custom types this will not be resolved, so you need
710    /// to call [`ExpandedNodeId::try_resolve`] to get the actual `NodeId`.
711    pub fn data_type(&self) -> Option<ExpandedNodeId> {
712        self.body.as_ref().map(|b| b.data_type_id())
713    }
714}
715
716/// Macro for consuming an extension object and taking different actions depending on the
717/// inner type, like a match over types.
718///
719/// # Example
720///
721/// ```
722/// # mod opcua { pub(super) use opcua_types as types; }
723/// use opcua::types::{EUInformation, ExtensionObject, match_extension_object_owned};
724/// let obj = opcua::types::ExtensionObject::from_message(EUInformation {
725///     namespace_uri: "Degrees C".into(),
726///     ..Default::default()
727/// });
728/// match_extension_object_owned!(obj,
729///     _v: opcua::types::Argument => println!("Object is argument"),
730///     _v: EUInformation => println!("Object is EUInformation"),
731///     _ => println!("Body is something else: {:?}", obj.type_name()),
732/// )
733/// ```
734#[macro_export]
735macro_rules! match_extension_object_owned {
736    (_final { $($nom:tt)* }) => {
737        $($nom)*
738    };
739    (_inner $obj:ident, { $($nom:tt)* }, _ => $t:expr $(,)?) => {
740        match_extension_object_owned!(_final {
741            $($nom)*
742            else {
743                $t
744            }
745        })
746    };
747    (_inner $obj:ident, { $($nom:tt)* }, $tok:ident: $typ:ty => $t:expr $(,)?) => {
748        match_extension_object_owned!(_final {
749            $($nom)*
750            else if $obj.inner_is::<$typ>() {
751                let $tok: $typ = *$obj.into_inner_as::<$typ>().unwrap();
752                $t
753            }
754        })
755    };
756    (_inner $obj:ident, { $($nom:tt)* }, $tok:ident: $typ:ty => $t:expr, $($r:tt)*) => {
757        match_extension_object_owned!(_inner $obj, {
758            $($nom)*
759            else if $obj.inner_is::<$typ>() {
760                let $tok: $typ = *$obj.into_inner_as::<$typ>().unwrap();
761                $t
762            }
763        }, $($r)*)
764    };
765    ($obj:ident, $tok:ident: $typ:ty => $t:expr, $($r:tt)*) => {
766        match_extension_object_owned!(_inner $obj, {
767            if $obj.inner_is::<$typ>() {
768                let $tok: $typ = *$obj.into_inner_as::<$typ>().unwrap();
769                $t
770            }
771        }, $($r)*)
772    };
773}
774
775pub use match_extension_object_owned;
776
777/// Macro for inspecting an extension object by reference and taking different actions depending on the
778/// inner type, like a match over types.
779///
780/// # Example
781///
782/// ```
783/// # mod opcua { pub(super) use opcua_types as types; }
784/// use opcua::types::{EUInformation, ExtensionObject, match_extension_object};
785/// let obj = opcua::types::ExtensionObject::from_message(EUInformation {
786///     namespace_uri: "Degrees C".into(),
787///     ..Default::default()
788/// });
789/// match_extension_object!(obj,
790///     _v: opcua::types::Argument => println!("Object is argument"),
791///     _v: EUInformation => println!("Object is EUInformation"),
792///     _ => println!("Body is something else: {:?}", obj.type_name()),
793/// )
794/// ```
795#[macro_export]
796macro_rules! match_extension_object {
797    (_final { $($nom:tt)* }) => {
798        $($nom)*
799    };
800    (_inner $obj:ident, { $($nom:tt)* }, _ => $t:expr $(,)?) => {
801        match_extension_object!(_final {
802            $($nom)*
803            else {
804                $t
805            }
806        })
807    };
808    (_inner $obj:ident, { $($nom:tt)* }, $tok:ident: $typ:ty => $t:expr $(,)?) => {
809        match_extension_object!(_final {
810            $($nom)*
811            else if let Some($tok) = $obj.inner_as::<$typ>() {
812                $t
813            }
814        })
815    };
816    (_inner $obj:ident, { $($nom:tt)* }, $tok:ident: $typ:ty => $t:expr, $($r:tt)*) => {
817        match_extension_object!(_inner $obj, {
818            $($nom)*
819            else if let Some($tok) = $obj.inner_as::<$typ>() {
820                $t
821            }
822        }, $($r)*)
823    };
824    ($obj:ident, $tok:ident: $typ:ty => $t:expr, $($r:tt)*) => {
825        match_extension_object!(_inner $obj, {
826            if let Some($tok) = $obj.inner_as::<$typ>() {
827                $t
828            }
829        }, $($r)*)
830    };
831}
832
833pub use match_extension_object;