opcua_types/
node_id.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 `NodeId`.
6
7use std::{
8    self,
9    convert::TryFrom,
10    fmt,
11    io::{Read, Write},
12    str::FromStr,
13    sync::{
14        atomic::{AtomicUsize, Ordering},
15        LazyLock,
16    },
17};
18
19use crate::{
20    byte_string::ByteString,
21    encoding::{BinaryDecodable, BinaryEncodable, EncodingResult},
22    guid::Guid,
23    read_u16, read_u32, read_u8,
24    status_code::StatusCode,
25    string::*,
26    write_u16, write_u32, write_u8, DataTypeId, Error, MethodId, ObjectId, ObjectTypeId,
27    ReferenceTypeId, UaNullable, VariableId, VariableTypeId,
28};
29
30/// The kind of identifier, numeric, string, guid or byte
31#[derive(Eq, PartialEq, Clone, Debug, Hash)]
32pub enum Identifier {
33    /// Numeric node ID identifier. i=123
34    Numeric(u32),
35    /// String node ID identifier, s=...
36    String(UAString),
37    /// GUID node ID identifier, g=...
38    Guid(Guid),
39    /// Opaque node ID identifier, o=...
40    ByteString(ByteString),
41}
42
43impl fmt::Display for Identifier {
44    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
45        match self {
46            Identifier::Numeric(v) => write!(f, "i={}", *v),
47            Identifier::String(v) => write!(f, "s={v}"),
48            Identifier::Guid(v) => write!(f, "g={v:?}"),
49            Identifier::ByteString(v) => write!(f, "b={}", v.as_base64()),
50        }
51    }
52}
53
54impl FromStr for Identifier {
55    type Err = ();
56
57    fn from_str(s: &str) -> Result<Self, Self::Err> {
58        if s.len() < 2 {
59            Err(())
60        } else {
61            let k = &s[..2];
62            let v = &s[2..];
63            match k {
64                "i=" => v.parse::<u32>().map(|v| v.into()).map_err(|_| ()),
65                "s=" => Ok(UAString::from(v).into()),
66                "g=" => Guid::from_str(v).map(|v| v.into()).map_err(|_| ()),
67                "b=" => ByteString::from_base64(v).map(|v| v.into()).ok_or(()),
68                _ => Err(()),
69            }
70        }
71    }
72}
73
74impl From<i32> for Identifier {
75    fn from(v: i32) -> Self {
76        Identifier::Numeric(v as u32)
77    }
78}
79
80impl From<u32> for Identifier {
81    fn from(v: u32) -> Self {
82        Identifier::Numeric(v)
83    }
84}
85
86impl<'a> From<&'a str> for Identifier {
87    fn from(v: &'a str) -> Self {
88        Identifier::from(UAString::from(v))
89    }
90}
91
92impl From<&String> for Identifier {
93    fn from(v: &String) -> Self {
94        Identifier::from(UAString::from(v))
95    }
96}
97
98impl From<String> for Identifier {
99    fn from(v: String) -> Self {
100        Identifier::from(UAString::from(v))
101    }
102}
103
104impl From<UAString> for Identifier {
105    fn from(v: UAString) -> Self {
106        Identifier::String(v)
107    }
108}
109
110impl From<Guid> for Identifier {
111    fn from(v: Guid) -> Self {
112        Identifier::Guid(v)
113    }
114}
115
116impl From<ByteString> for Identifier {
117    fn from(v: ByteString) -> Self {
118        Identifier::ByteString(v)
119    }
120}
121
122#[derive(Debug)]
123/// Error returned from working with node IDs.
124pub struct NodeIdError;
125
126impl fmt::Display for NodeIdError {
127    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
128        write!(f, "NodeIdError")
129    }
130}
131
132impl std::error::Error for NodeIdError {}
133
134/// An identifier for a node in the address space of an OPC UA Server.
135#[derive(PartialEq, Eq, Clone, Debug, Hash)]
136pub struct NodeId {
137    /// The index for a namespace
138    pub namespace: u16,
139    /// The identifier for the node in the address space
140    pub identifier: Identifier,
141}
142
143impl fmt::Display for NodeId {
144    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
145        if self.namespace != 0 {
146            write!(f, "ns={};{}", self.namespace, self.identifier)
147        } else {
148            write!(f, "{}", self.identifier)
149        }
150    }
151}
152
153impl UaNullable for NodeId {
154    fn is_ua_null(&self) -> bool {
155        self.is_null()
156    }
157}
158
159// JSON serialization schema as per spec:
160//
161// "Type"
162//      The IdentifierType encoded as a JSON number.
163//      Allowed values are:
164//            0 - UInt32 Identifier encoded as a JSON number.
165//            1 - A String Identifier encoded as a JSON string.
166//            2 - A Guid Identifier encoded as described in 5.4.2.7.
167//            3 - A ByteString Identifier encoded as described in 5.4.2.8.
168//      This field is omitted for UInt32 identifiers.
169// "Id"
170//      The Identifier.
171//      The value of the id field specifies the encoding of this field.
172// "Namespace"
173//      The NamespaceIndex for the NodeId.
174//      The field is encoded as a JSON number for the reversible encoding.
175//      The field is omitted if the NamespaceIndex equals 0.
176//      For the non-reversible encoding, the field is the NamespaceUri associated with the NamespaceIndex, encoded as a JSON string.
177//      A NamespaceIndex of 1 is always encoded as a JSON number.
178
179#[cfg(feature = "json")]
180mod json {
181    use std::io::{Read, Write};
182    use std::str::FromStr;
183
184    use tracing::warn;
185
186    use crate::{json::*, ByteString, Error, Guid};
187
188    use super::{Identifier, NodeId, UAString};
189    enum RawIdentifier {
190        String(String),
191        Integer(u32),
192    }
193
194    impl JsonEncodable for NodeId {
195        fn encode(
196            &self,
197            stream: &mut JsonStreamWriter<&mut dyn Write>,
198            ctx: &crate::json::Context<'_>,
199        ) -> super::EncodingResult<()> {
200            stream.begin_object()?;
201            match &self.identifier {
202                super::Identifier::Numeric(n) => {
203                    stream.name("Id")?;
204                    stream.number_value(*n)?;
205                }
206                super::Identifier::String(uastring) => {
207                    stream.name("IdType")?;
208                    stream.number_value(1)?;
209                    stream.name("Id")?;
210                    JsonEncodable::encode(uastring, stream, ctx)?;
211                }
212                super::Identifier::Guid(guid) => {
213                    stream.name("IdType")?;
214                    stream.number_value(2)?;
215                    stream.name("Id")?;
216                    JsonEncodable::encode(guid, stream, ctx)?;
217                }
218                super::Identifier::ByteString(byte_string) => {
219                    stream.name("IdType")?;
220                    stream.number_value(3)?;
221                    stream.name("Id")?;
222                    JsonEncodable::encode(byte_string, stream, ctx)?;
223                }
224            }
225            if self.namespace != 0 {
226                stream.name("Namespace")?;
227                stream.number_value(self.namespace)?;
228            }
229            stream.end_object()?;
230            Ok(())
231        }
232    }
233
234    impl JsonDecodable for NodeId {
235        fn decode(
236            stream: &mut JsonStreamReader<&mut dyn Read>,
237            _ctx: &Context<'_>,
238        ) -> super::EncodingResult<Self> {
239            match stream.peek()? {
240                ValueType::Null => {
241                    stream.next_null()?;
242                    return Ok(Self::null());
243                }
244                _ => stream.begin_object()?,
245            }
246
247            let mut id_type: Option<u16> = None;
248            let mut namespace: Option<u16> = None;
249            let mut value: Option<RawIdentifier> = None;
250
251            while stream.has_next()? {
252                match stream.next_name()? {
253                    "IdType" => {
254                        id_type = Some(stream.next_number()??);
255                    }
256                    "Namespace" => {
257                        namespace = Some(stream.next_number()??);
258                    }
259                    "Id" => match stream.peek()? {
260                        ValueType::Null => {
261                            stream.next_null()?;
262                            value = Some(RawIdentifier::Integer(0));
263                        }
264                        ValueType::Number => {
265                            value = Some(RawIdentifier::Integer(stream.next_number()??));
266                        }
267                        _ => {
268                            value = Some(RawIdentifier::String(stream.next_string()?));
269                        }
270                    },
271                    _ => stream.skip_value()?,
272                }
273            }
274
275            let identifier = match id_type {
276                Some(1) => {
277                    let Some(RawIdentifier::String(s)) = value else {
278                        return Err(Error::decoding("Invalid NodeId, empty identifier"));
279                    };
280                    let s = UAString::from(s);
281                    if s.is_null() || s.is_empty() {
282                        return Err(Error::decoding("Invalid NodeId, empty identifier"));
283                    }
284                    Identifier::String(s)
285                }
286                Some(2) => {
287                    let Some(RawIdentifier::String(s)) = value else {
288                        return Err(Error::decoding("Invalid NodeId, empty identifier"));
289                    };
290                    if s.is_empty() {
291                        return Err(Error::decoding("Invalid NodeId, empty identifier"));
292                    }
293                    let s = Guid::from_str(&s).map_err(|_| {
294                        warn!("Unable to decode GUID identifier");
295                        Error::decoding("Unable to decode GUID identifier")
296                    })?;
297                    Identifier::Guid(s)
298                }
299                Some(3) => {
300                    let Some(RawIdentifier::String(s)) = value else {
301                        return Err(Error::decoding("Invalid NodeId, empty identifier"));
302                    };
303                    if s.is_empty() {
304                        return Err(Error::decoding("Invalid NodeId, empty identifier"));
305                    }
306                    let s: ByteString = ByteString::from_base64(&s)
307                        .ok_or_else(|| Error::decoding("Unable to decode bytestring identifier"))?;
308                    Identifier::ByteString(s)
309                }
310                None | Some(0) => {
311                    let Some(RawIdentifier::Integer(s)) = value else {
312                        return Err(Error::decoding("Invalid NodeId, empty identifier"));
313                    };
314                    Identifier::Numeric(s)
315                }
316                Some(r) => {
317                    return Err(Error::decoding(format!(
318                        "Failed to deserialize NodeId, got unexpected IdType {r}"
319                    )));
320                }
321            };
322
323            stream.end_object()?;
324            Ok(Self {
325                namespace: namespace.unwrap_or_default(),
326                identifier,
327            })
328        }
329    }
330}
331
332#[cfg(feature = "xml")]
333mod xml {
334    use crate::xml::*;
335    use std::{
336        io::{Read, Write},
337        str::FromStr,
338    };
339
340    use super::NodeId;
341
342    impl XmlType for NodeId {
343        const TAG: &'static str = "NodeId";
344    }
345
346    impl XmlEncodable for NodeId {
347        fn encode(
348            &self,
349            writer: &mut XmlStreamWriter<&mut dyn Write>,
350            ctx: &crate::xml::Context<'_>,
351        ) -> Result<(), Error> {
352            let namespace_index = ctx.resolve_namespace_index_inverse(self.namespace)?;
353
354            let self_str = if namespace_index > 0 {
355                format!("ns={};{}", namespace_index, self.identifier)
356            } else {
357                self.identifier.to_string()
358            };
359            let val = ctx.resolve_alias_inverse(&self_str);
360            writer.encode_child("Identifier", val, ctx)
361        }
362    }
363
364    impl XmlDecodable for NodeId {
365        fn decode(
366            read: &mut XmlStreamReader<&mut dyn Read>,
367            context: &Context<'_>,
368        ) -> Result<Self, Error>
369        where
370            Self: Sized,
371        {
372            let val: Option<String> = read.decode_single_child("Identifier", context)?;
373            let Some(val) = val else {
374                return Ok(NodeId::null());
375            };
376
377            let val_str = context.resolve_alias(&val);
378            let mut id = NodeId::from_str(val_str)
379                .map_err(|e| Error::new(e, format!("Invalid node ID: {val_str}")))?;
380            id.namespace = context.resolve_namespace_index(id.namespace)?;
381            Ok(id)
382        }
383    }
384}
385
386impl BinaryEncodable for NodeId {
387    fn byte_len(&self, ctx: &crate::Context<'_>) -> usize {
388        // Type determines the byte code
389        let size: usize = match self.identifier {
390            Identifier::Numeric(value) => {
391                if self.namespace == 0 && value <= 255 {
392                    2
393                } else if self.namespace <= 255 && value <= 65535 {
394                    4
395                } else {
396                    7
397                }
398            }
399            Identifier::String(ref value) => 3 + value.byte_len(ctx),
400            Identifier::Guid(ref value) => 3 + value.byte_len(ctx),
401            Identifier::ByteString(ref value) => 3 + value.byte_len(ctx),
402        };
403        size
404    }
405
406    fn encode<S: Write + ?Sized>(
407        &self,
408        stream: &mut S,
409        ctx: &crate::Context<'_>,
410    ) -> EncodingResult<()> {
411        // Type determines the byte code
412        match &self.identifier {
413            Identifier::Numeric(value) => {
414                if self.namespace == 0 && *value <= 255 {
415                    // node id fits into 2 bytes when the namespace is 0 and the value <= 255
416                    write_u8(stream, 0x0)?;
417                    write_u8(stream, *value as u8)
418                } else if self.namespace <= 255 && *value <= 65535 {
419                    // node id fits into 4 bytes when namespace <= 255 and value <= 65535
420                    write_u8(stream, 0x1)?;
421                    write_u8(stream, self.namespace as u8)?;
422                    write_u16(stream, *value as u16)
423                } else {
424                    // full node id
425                    write_u8(stream, 0x2)?;
426                    write_u16(stream, self.namespace)?;
427                    write_u32(stream, *value)
428                }
429            }
430            Identifier::String(value) => {
431                write_u8(stream, 0x3)?;
432                write_u16(stream, self.namespace)?;
433                value.encode(stream, ctx)
434            }
435            Identifier::Guid(value) => {
436                write_u8(stream, 0x4)?;
437                write_u16(stream, self.namespace)?;
438                value.encode(stream, ctx)
439            }
440            Identifier::ByteString(value) => {
441                write_u8(stream, 0x5)?;
442                write_u16(stream, self.namespace)?;
443                value.encode(stream, ctx)
444            }
445        }
446    }
447}
448
449impl BinaryDecodable for NodeId {
450    fn decode<S: Read + ?Sized>(stream: &mut S, ctx: &crate::Context<'_>) -> EncodingResult<Self> {
451        let identifier = read_u8(stream)?;
452        let node_id = match identifier {
453            0x0 => {
454                let namespace = 0;
455                let value = read_u8(stream)?;
456                NodeId::new(namespace, u32::from(value))
457            }
458            0x1 => {
459                let namespace = read_u8(stream)?;
460                let value = read_u16(stream)?;
461                NodeId::new(u16::from(namespace), u32::from(value))
462            }
463            0x2 => {
464                let namespace = read_u16(stream)?;
465                let value = read_u32(stream)?;
466                NodeId::new(namespace, value)
467            }
468            0x3 => {
469                let namespace = read_u16(stream)?;
470                let value = UAString::decode(stream, ctx)?;
471                NodeId::new(namespace, value)
472            }
473            0x4 => {
474                let namespace = read_u16(stream)?;
475                let value = Guid::decode(stream, ctx)?;
476                NodeId::new(namespace, value)
477            }
478            0x5 => {
479                let namespace = read_u16(stream)?;
480                let value = ByteString::decode(stream, ctx)?;
481                NodeId::new(namespace, value)
482            }
483            _ => {
484                return Err(Error::decoding(format!(
485                    "Unrecognized node id type {identifier}"
486                )));
487            }
488        };
489        Ok(node_id)
490    }
491}
492
493impl FromStr for NodeId {
494    type Err = StatusCode;
495    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
496        use regex::Regex;
497
498        // Parses a node from a string using the format specified in 5.3.1.10 part 6
499        //
500        // ns=<namespaceindex>;<type>=<value>
501        //
502        // Where type:
503        //   i = NUMERIC
504        //   s = STRING
505        //   g = GUID
506        //   b = OPAQUE (ByteString)
507        //
508        // If namespace == 0, the ns=0; will be omitted
509
510        static RE: LazyLock<Regex> =
511            LazyLock::new(|| Regex::new(r"^(ns=(?P<ns>[0-9]+);)?(?P<t>[isgb]=.+)$").unwrap());
512
513        let captures = RE.captures(s).ok_or(StatusCode::BadNodeIdInvalid)?;
514
515        // Check namespace (optional)
516        let namespace = if let Some(ns) = captures.name("ns") {
517            ns.as_str()
518                .parse::<u16>()
519                .map_err(|_| StatusCode::BadNodeIdInvalid)?
520        } else {
521            0
522        };
523
524        // Type identifier
525        let t = captures.name("t").unwrap();
526        Identifier::from_str(t.as_str())
527            .map(|t| NodeId::new(namespace, t))
528            .map_err(|_| StatusCode::BadNodeIdInvalid)
529    }
530}
531
532impl<'a> From<&'a str> for NodeId {
533    fn from(value: &'a str) -> Self {
534        (0u16, value).into()
535    }
536}
537
538impl From<UAString> for NodeId {
539    fn from(value: UAString) -> Self {
540        (0u16, value).into()
541    }
542}
543
544impl From<u32> for NodeId {
545    fn from(value: u32) -> Self {
546        (0, value).into()
547    }
548}
549
550impl From<Guid> for NodeId {
551    fn from(value: Guid) -> Self {
552        (0, value).into()
553    }
554}
555
556impl From<ByteString> for NodeId {
557    fn from(value: ByteString) -> Self {
558        (0, value).into()
559    }
560}
561
562impl From<&NodeId> for NodeId {
563    fn from(v: &NodeId) -> Self {
564        v.clone()
565    }
566}
567
568impl From<NodeId> for String {
569    fn from(value: NodeId) -> Self {
570        value.to_string()
571    }
572}
573
574impl<'a> From<(u16, &'a str)> for NodeId {
575    fn from(v: (u16, &'a str)) -> Self {
576        Self::new(v.0, UAString::from(v.1))
577    }
578}
579
580impl From<(u16, UAString)> for NodeId {
581    fn from(v: (u16, UAString)) -> Self {
582        Self::new(v.0, v.1)
583    }
584}
585
586impl From<(u16, u32)> for NodeId {
587    fn from(v: (u16, u32)) -> Self {
588        Self::new(v.0, v.1)
589    }
590}
591
592impl From<(u16, Guid)> for NodeId {
593    fn from(v: (u16, Guid)) -> Self {
594        Self::new(v.0, v.1)
595    }
596}
597
598impl From<(u16, ByteString)> for NodeId {
599    fn from(v: (u16, ByteString)) -> Self {
600        Self::new(v.0, v.1)
601    }
602}
603
604// Cheap comparisons intended for use when comparing node IDs to constants.
605impl PartialEq<(u16, &str)> for NodeId {
606    fn eq(&self, other: &(u16, &str)) -> bool {
607        self.namespace == other.0
608            && match &self.identifier {
609                Identifier::String(s) => s.as_ref() == other.1,
610                _ => false,
611            }
612    }
613}
614
615impl PartialEq<(u16, &[u8; 16])> for NodeId {
616    fn eq(&self, other: &(u16, &[u8; 16])) -> bool {
617        self.namespace == other.0
618            && match &self.identifier {
619                Identifier::Guid(s) => s.as_bytes() == other.1,
620                _ => false,
621            }
622    }
623}
624
625impl PartialEq<(u16, &[u8])> for NodeId {
626    fn eq(&self, other: &(u16, &[u8])) -> bool {
627        self.namespace == other.0
628            && match &self.identifier {
629                Identifier::ByteString(s) => {
630                    s.value.as_ref().is_some_and(|v| v.as_slice() == other.1)
631                }
632                _ => false,
633            }
634    }
635}
636
637impl PartialEq<(u16, u32)> for NodeId {
638    fn eq(&self, other: &(u16, u32)) -> bool {
639        self.namespace == other.0
640            && match &self.identifier {
641                Identifier::Numeric(s) => s == &other.1,
642                _ => false,
643            }
644    }
645}
646
647impl PartialEq<ObjectId> for NodeId {
648    fn eq(&self, other: &ObjectId) -> bool {
649        *self == (0u16, *other as u32)
650    }
651}
652
653impl PartialEq<ObjectTypeId> for NodeId {
654    fn eq(&self, other: &ObjectTypeId) -> bool {
655        *self == (0u16, *other as u32)
656    }
657}
658
659impl PartialEq<ReferenceTypeId> for NodeId {
660    fn eq(&self, other: &ReferenceTypeId) -> bool {
661        *self == (0u16, *other as u32)
662    }
663}
664
665impl PartialEq<VariableId> for NodeId {
666    fn eq(&self, other: &VariableId) -> bool {
667        *self == (0u16, *other as u32)
668    }
669}
670
671impl PartialEq<VariableTypeId> for NodeId {
672    fn eq(&self, other: &VariableTypeId) -> bool {
673        *self == (0u16, *other as u32)
674    }
675}
676
677impl PartialEq<DataTypeId> for NodeId {
678    fn eq(&self, other: &DataTypeId) -> bool {
679        *self == (0u16, *other as u32)
680    }
681}
682
683static NEXT_NODE_ID_NUMERIC: AtomicUsize = AtomicUsize::new(1);
684
685impl Default for NodeId {
686    fn default() -> Self {
687        NodeId::null()
688    }
689}
690
691impl NodeId {
692    /// Constructs a new NodeId from anything that can be turned into Identifier
693    /// u32, Guid, ByteString or String
694    pub fn new<T>(namespace: u16, value: T) -> NodeId
695    where
696        T: 'static + Into<Identifier>,
697    {
698        NodeId {
699            namespace,
700            identifier: value.into(),
701        }
702    }
703
704    /// Returns the node id for the root folder.
705    pub fn root_folder_id() -> NodeId {
706        ObjectId::RootFolder.into()
707    }
708
709    /// Returns the node id for the objects folder.
710    pub fn objects_folder_id() -> NodeId {
711        ObjectId::ObjectsFolder.into()
712    }
713
714    /// Returns the node id for the types folder.
715    pub fn types_folder_id() -> NodeId {
716        ObjectId::TypesFolder.into()
717    }
718
719    /// Returns the node id for the views folder.
720    pub fn views_folder_id() -> NodeId {
721        ObjectId::ViewsFolder.into()
722    }
723
724    /// Test if the node id is null, i.e. 0 namespace and 0 identifier
725    pub fn is_null(&self) -> bool {
726        self.namespace == 0 && self.identifier == Identifier::Numeric(0)
727    }
728
729    /// Returns a null node id
730    pub fn null() -> NodeId {
731        NodeId::new(0, 0u32)
732    }
733
734    /// Creates a numeric node id with an id incrementing up from 1000
735    pub fn next_numeric(namespace: u16) -> NodeId {
736        NodeId::new(
737            namespace,
738            NEXT_NODE_ID_NUMERIC.fetch_add(1, Ordering::SeqCst) as u32,
739        )
740    }
741
742    /// Extracts an ObjectId from a node id, providing the node id holds an object id
743    pub fn as_object_id(&self) -> std::result::Result<ObjectId, NodeIdError> {
744        match self.identifier {
745            Identifier::Numeric(id) if self.namespace == 0 => {
746                ObjectId::try_from(id).map_err(|_| NodeIdError)
747            }
748            _ => Err(NodeIdError),
749        }
750    }
751
752    /// Try to convert this to a builtin variable ID.
753    pub fn as_variable_id(&self) -> std::result::Result<VariableId, NodeIdError> {
754        match self.identifier {
755            Identifier::Numeric(id) if self.namespace == 0 => {
756                VariableId::try_from(id).map_err(|_| NodeIdError)
757            }
758            _ => Err(NodeIdError),
759        }
760    }
761
762    /// Try to convert this to a builtin reference type ID.
763    pub fn as_reference_type_id(&self) -> std::result::Result<ReferenceTypeId, NodeIdError> {
764        if self.is_null() {
765            Err(NodeIdError)
766        } else {
767            match self.identifier {
768                Identifier::Numeric(id) if self.namespace == 0 => {
769                    ReferenceTypeId::try_from(id).map_err(|_| NodeIdError)
770                }
771                _ => Err(NodeIdError),
772            }
773        }
774    }
775
776    /// Try to convert this to a builtin data type ID.
777    pub fn as_data_type_id(&self) -> std::result::Result<DataTypeId, NodeIdError> {
778        match self.identifier {
779            Identifier::Numeric(id) if self.namespace == 0 => {
780                DataTypeId::try_from(id).map_err(|_| NodeIdError)
781            }
782            _ => Err(NodeIdError),
783        }
784    }
785
786    /// Try to convert this to a builtin method ID.
787    pub fn as_method_id(&self) -> std::result::Result<MethodId, NodeIdError> {
788        match self.identifier {
789            Identifier::Numeric(id) if self.namespace == 0 => {
790                MethodId::try_from(id).map_err(|_| NodeIdError)
791            }
792            _ => Err(NodeIdError),
793        }
794    }
795
796    /// Test if the node id is numeric
797    pub fn is_numeric(&self) -> bool {
798        matches!(self.identifier, Identifier::Numeric(_))
799    }
800
801    /// Test if the node id is a string
802    pub fn is_string(&self) -> bool {
803        matches!(self.identifier, Identifier::String(_))
804    }
805
806    /// Test if the node id is a guid
807    pub fn is_guid(&self) -> bool {
808        matches!(self.identifier, Identifier::Guid(_))
809    }
810
811    /// Test if the node id us a byte string
812    pub fn is_byte_string(&self) -> bool {
813        matches!(self.identifier, Identifier::ByteString(_))
814    }
815
816    /// Get the numeric value of this node ID if it is numeric.
817    pub fn as_u32(&self) -> Option<u32> {
818        match &self.identifier {
819            Identifier::Numeric(i) => Some(*i),
820            _ => None,
821        }
822    }
823}