From aea1c2c4f8956604341153b7f48b31326856e6f9 Mon Sep 17 00:00:00 2001 From: Einar Omang Date: Fri, 28 Feb 2025 10:26:38 +0100 Subject: [PATCH] Unified nullable trait for encoding There's currently duplicated logic between XML and JSON. This unifies them into a single trait, and adds a derive macro for that. It also expands the logic around enums/unions a bit, so that we cover more cases that are null according to the standard. --- async-opcua-codegen/src/types/gen.rs | 8 ++ async-opcua-macros/src/encoding/json.rs | 6 +- async-opcua-macros/src/encoding/mod.rs | 48 ++++++++++- async-opcua-macros/src/encoding/xml.rs | 4 +- async-opcua-macros/src/lib.rs | 14 ++- async-opcua-types/src/argument.rs | 2 +- async-opcua-types/src/byte_string.rs | 8 +- async-opcua-types/src/custom/custom_struct.rs | 12 ++- async-opcua-types/src/data_value.rs | 2 +- async-opcua-types/src/date_time.rs | 6 ++ async-opcua-types/src/diagnostic_info.rs | 8 +- async-opcua-types/src/encoding.rs | 57 +++++++++++++ async-opcua-types/src/expanded_node_id.rs | 16 ++-- async-opcua-types/src/extension_object.rs | 4 +- .../src/generated/types/enums.rs | 85 +++++++++++++++++++ async-opcua-types/src/guid.rs | 6 ++ async-opcua-types/src/json.rs | 30 +------ async-opcua-types/src/lib.rs | 1 + async-opcua-types/src/localized_text.rs | 2 +- async-opcua-types/src/node_id.rs | 16 ++-- async-opcua-types/src/qualified_name.rs | 16 ++-- async-opcua-types/src/request_header.rs | 2 +- async-opcua-types/src/response_header.rs | 2 +- async-opcua-types/src/status_code.rs | 8 +- async-opcua-types/src/string.rs | 15 ++-- async-opcua-types/src/tests/json.rs | 8 +- async-opcua-types/src/variant/json.rs | 4 - async-opcua-types/src/variant/mod.rs | 8 +- async-opcua-types/src/xml/builtins.rs | 22 +---- async-opcua-types/src/xml/encoding.rs | 10 +-- 30 files changed, 312 insertions(+), 118 deletions(-) diff --git a/async-opcua-codegen/src/types/gen.rs b/async-opcua-codegen/src/types/gen.rs index d2102c0ab..6c1914447 100644 --- a/async-opcua-codegen/src/types/gen.rs +++ b/async-opcua-codegen/src/types/gen.rs @@ -325,6 +325,14 @@ impl CodeGenerator { })?; let write_method = Ident::new(&format!("write_{}", item.typ), Span::call_site()); + impls.push(parse_quote! { + impl opcua::types::UaNullable for #enum_ident { + fn is_ua_null(&self) -> bool { + self.is_empty() + } + } + }); + impls.push(parse_quote! { impl opcua::types::BinaryEncodable for #enum_ident { fn byte_len(&self, _ctx: &opcua::types::Context<'_>) -> usize { diff --git a/async-opcua-macros/src/encoding/json.rs b/async-opcua-macros/src/encoding/json.rs index 53908da68..51287652f 100644 --- a/async-opcua-macros/src/encoding/json.rs +++ b/async-opcua-macros/src/encoding/json.rs @@ -27,7 +27,7 @@ pub fn generate_json_encode_impl(strct: EncodingStruct) -> syn::Result syn::Result syn::Result syn::Result { let input = EncodingInput::from_derive_input(item.clone())?; let mut output = quote! { - #[derive(opcua::types::BinaryEncodable, opcua::types::BinaryDecodable)] + #[derive(opcua::types::BinaryEncodable, opcua::types::BinaryDecodable, opcua::types::UaNullable)] #[cfg_attr( feature = "json", derive(opcua::types::JsonEncodable, opcua::types::JsonDecodable) @@ -222,3 +222,49 @@ pub(crate) fn derive_all_inner(item: DeriveInput) -> syn::Result { Ok(output) } + +pub(crate) fn derive_ua_nullable_inner(item: DeriveInput) -> syn::Result { + let input = EncodingInput::from_derive_input(item.clone())?; + match input { + EncodingInput::Struct(s) => { + let ident = s.ident; + Ok(quote! { + impl opcua::types::UaNullable for #ident {} + }) + } + EncodingInput::SimpleEnum(s) => { + let null_variant = s.variants.iter().find(|v| v.attr.default); + let ident = s.ident; + if let Some(null_variant) = null_variant { + let n_ident = &null_variant.name; + Ok(quote! { + impl opcua::types::UaNullable for #ident { + fn is_ua_null(&self) -> bool { + matches!(self, Self::#n_ident) + } + } + }) + } else { + Ok(quote! { + impl opcua::types::UaNullable for #ident {} + }) + } + } + EncodingInput::AdvancedEnum(s) => { + let ident = s.ident; + if let Some(null_variant) = s.null_variant { + Ok(quote! { + impl opcua::types::UaNullable for #ident { + fn is_ua_null(&self) -> bool { + matches!(self, Self::#null_variant) + } + } + }) + } else { + Ok(quote! { + impl opcua::types::UaNullable for #ident {} + }) + } + } + } +} diff --git a/async-opcua-macros/src/encoding/xml.rs b/async-opcua-macros/src/encoding/xml.rs index 9b680feb8..dbc87f832 100644 --- a/async-opcua-macros/src/encoding/xml.rs +++ b/async-opcua-macros/src/encoding/xml.rs @@ -102,14 +102,14 @@ pub fn generate_xml_encode_impl(strct: EncodingStruct) -> syn::Result TokenStream { } } +#[proc_macro_derive(UaNullable, attributes(opcua))] +/// Derive the `UaNullable` trait on this struct or enum. This indicates whether the +/// value is null/default in OPC-UA encoding. +pub fn derive_ua_nullable(item: TokenStream) -> TokenStream { + match derive_ua_nullable_inner(parse_macro_input!(item)) { + Ok(r) => r.into(), + Err(e) => e.to_compile_error().into(), + } +} + #[proc_macro_attribute] /// Derive all the standard encoding traits on this struct or enum. /// This will derive `BinaryEncodable`, `BinaryDecodable`, `JsonEncodable`, `JsonDecodable`, diff --git a/async-opcua-types/src/argument.rs b/async-opcua-types/src/argument.rs index 8c6d48569..05741c996 100644 --- a/async-opcua-types/src/argument.rs +++ b/async-opcua-types/src/argument.rs @@ -31,7 +31,7 @@ mod opcua { pub use crate as types; } -#[derive(Clone, Debug, PartialEq, Default)] +#[derive(Clone, Debug, PartialEq, Default, crate::UaNullable)] #[cfg_attr(feature = "json", derive(crate::JsonEncodable, crate::JsonDecodable))] #[cfg_attr( feature = "xml", diff --git a/async-opcua-types/src/byte_string.rs b/async-opcua-types/src/byte_string.rs index 4890bb116..c68a3e1a1 100644 --- a/async-opcua-types/src/byte_string.rs +++ b/async-opcua-types/src/byte_string.rs @@ -14,7 +14,7 @@ use base64::{engine::general_purpose::STANDARD, Engine}; use crate::{ encoding::{process_decode_io_result, process_encode_io_result, write_i32, EncodingResult}, read_i32, DecodingOptions, Error, Guid, OutOfRange, SimpleBinaryDecodable, - SimpleBinaryEncodable, + SimpleBinaryEncodable, UaNullable, }; /// A sequence of octets. @@ -34,6 +34,12 @@ impl AsRef<[u8]> for ByteString { } } +impl UaNullable for ByteString { + fn is_ua_null(&self) -> bool { + self.is_null() + } +} + #[cfg(feature = "json")] mod json { use std::io::{Read, Write}; diff --git a/async-opcua-types/src/custom/custom_struct.rs b/async-opcua-types/src/custom/custom_struct.rs index 2da0aff39..2e5a9084d 100644 --- a/async-opcua-types/src/custom/custom_struct.rs +++ b/async-opcua-types/src/custom/custom_struct.rs @@ -4,7 +4,7 @@ use crate::{ write_i32, write_u32, Array, BinaryDecodable, BinaryEncodable, ByteString, Context, DataValue, DateTime, DiagnosticInfo, EncodingResult, Error, ExpandedMessageInfo, ExpandedNodeId, ExtensionObject, Guid, LocalizedText, NodeId, QualifiedName, StatusCode, StructureType, - TypeLoader, UAString, Variant, XmlElement, + TypeLoader, UAString, UaNullable, Variant, XmlElement, }; use super::type_tree::{DataTypeTree, ParsedStructureField, StructTypeInfo}; @@ -224,6 +224,16 @@ impl DynamicStructure { } } +impl UaNullable for DynamicStructure { + fn is_ua_null(&self) -> bool { + if self.type_def.structure_type == StructureType::Union { + self.discriminant == 0 + } else { + false + } + } +} + impl BinaryEncodable for DynamicStructure { fn byte_len(&self, ctx: &crate::Context<'_>) -> usize { // Byte length is the sum of the individual structure fields diff --git a/async-opcua-types/src/data_value.rs b/async-opcua-types/src/data_value.rs index 20eadf9bd..fb96a6aa9 100644 --- a/async-opcua-types/src/data_value.rs +++ b/async-opcua-types/src/data_value.rs @@ -38,7 +38,7 @@ mod opcua { /// A data value is a value of a variable in the OPC UA server and contains information about its /// value, status and change timestamps. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, crate::UaNullable)] #[cfg_attr( feature = "json", derive(opcua_macros::JsonEncodable, opcua_macros::JsonDecodable) diff --git a/async-opcua-types/src/date_time.rs b/async-opcua-types/src/date_time.rs index 303b34bcd..f52ce9205 100644 --- a/async-opcua-types/src/date_time.rs +++ b/async-opcua-types/src/date_time.rs @@ -34,6 +34,12 @@ pub struct DateTime { date_time: DateTimeUtc, } +impl crate::UaNullable for DateTime { + fn is_ua_null(&self) -> bool { + self.is_null() + } +} + #[cfg(feature = "json")] mod json { use crate::{json::*, Error}; diff --git a/async-opcua-types/src/diagnostic_info.rs b/async-opcua-types/src/diagnostic_info.rs index 4f0444200..7ac0b6824 100644 --- a/async-opcua-types/src/diagnostic_info.rs +++ b/async-opcua-types/src/diagnostic_info.rs @@ -62,6 +62,12 @@ bitflags! { } } +impl crate::UaNullable for DiagnosticBits { + fn is_ua_null(&self) -> bool { + self.is_empty() + } +} + #[cfg(feature = "json")] mod json { use crate::json::*; @@ -127,7 +133,7 @@ mod opcua { } /// Diagnostic information. -#[derive(PartialEq, Debug, Clone)] +#[derive(PartialEq, Debug, Clone, crate::UaNullable)] #[cfg_attr( feature = "json", derive(opcua_macros::JsonEncodable, opcua_macros::JsonDecodable) diff --git a/async-opcua-types/src/encoding.rs b/async-opcua-types/src/encoding.rs index 8ef92b027..cb6c8b340 100644 --- a/async-opcua-types/src/encoding.rs +++ b/async-opcua-types/src/encoding.rs @@ -316,6 +316,63 @@ impl DecodingOptions { } } +/// Trait implemented by OPC-UA types, indicating whether +/// they are null or not, for use in encoding. +pub trait UaNullable { + /// Return true if this value is null, meaning it can be left out when + /// being encoded in JSON and XML encodings. + fn is_ua_null(&self) -> bool { + false + } +} + +impl UaNullable for Option +where + T: UaNullable, +{ + fn is_ua_null(&self) -> bool { + match self { + Some(s) => s.is_ua_null(), + None => true, + } + } +} + +impl UaNullable for Vec where T: UaNullable {} +impl UaNullable for Box +where + T: UaNullable, +{ + fn is_ua_null(&self) -> bool { + self.as_ref().is_ua_null() + } +} + +macro_rules! is_null_const { + ($t:ty, $c:expr) => { + impl UaNullable for $t { + fn is_ua_null(&self) -> bool { + *self == $c + } + } + }; +} + +is_null_const!(bool, false); +is_null_const!(u8, 0); +is_null_const!(u16, 0); +is_null_const!(u32, 0); +is_null_const!(u64, 0); +is_null_const!(i8, 0); +is_null_const!(i16, 0); +is_null_const!(i32, 0); +is_null_const!(i64, 0); +is_null_const!(f32, 0.0); +is_null_const!(f64, 0.0); + +impl UaNullable for String {} +impl UaNullable for str {} + /// OPC UA Binary Encoding interface. Anything that encodes to binary must implement this. It provides /// functions to calculate the size in bytes of the struct (for allocating memory), encoding to a stream /// and decoding from a stream. diff --git a/async-opcua-types/src/expanded_node_id.rs b/async-opcua-types/src/expanded_node_id.rs index 7d18173d3..dbe13da7a 100644 --- a/async-opcua-types/src/expanded_node_id.rs +++ b/async-opcua-types/src/expanded_node_id.rs @@ -21,7 +21,7 @@ use crate::{ read_u16, read_u32, read_u8, status_code::StatusCode, string::*, - write_u16, write_u32, write_u8, Context, Error, NamespaceMap, + write_u16, write_u32, write_u8, Context, Error, NamespaceMap, UaNullable, }; /// A NodeId that allows the namespace URI to be specified instead of an index. @@ -35,6 +35,12 @@ pub struct ExpandedNodeId { pub server_index: u32, } +impl UaNullable for ExpandedNodeId { + fn is_ua_null(&self) -> bool { + self.is_null() + } +} + #[cfg(feature = "json")] mod json { // JSON serialization schema as per spec: @@ -118,10 +124,6 @@ mod json { stream.end_object()?; Ok(()) } - - fn is_null_json(&self) -> bool { - self.is_null() - } } impl JsonDecodable for ExpandedNodeId { @@ -263,10 +265,6 @@ mod xml { }; node_id.encode(writer, context) } - - fn is_null_xml(&self) -> bool { - self.is_null() - } } impl XmlDecodable for ExpandedNodeId { diff --git a/async-opcua-types/src/extension_object.rs b/async-opcua-types/src/extension_object.rs index d77f09996..0221499b8 100644 --- a/async-opcua-types/src/extension_object.rs +++ b/async-opcua-types/src/extension_object.rs @@ -10,7 +10,7 @@ use std::{ io::{Read, Write}, }; -use crate::{write_i32, write_u8, Error, ExpandedMessageInfo, ExpandedNodeId}; +use crate::{write_i32, write_u8, Error, ExpandedMessageInfo, ExpandedNodeId, UaNullable}; use super::{ encoding::{BinaryDecodable, BinaryEncodable, EncodingResult}, @@ -224,6 +224,8 @@ impl PartialEq for dyn DynEncodable { impl std::error::Error for ExtensionObjectError {} +impl UaNullable for ExtensionObject {} + #[cfg(feature = "json")] mod json { use std::io::{Cursor, Read}; diff --git a/async-opcua-types/src/generated/types/enums.rs b/async-opcua-types/src/generated/types/enums.rs index 4f2974569..1f65e0f64 100644 --- a/async-opcua-types/src/generated/types/enums.rs +++ b/async-opcua-types/src/generated/types/enums.rs @@ -17,6 +17,11 @@ bitflags::bitflags! { NonatomicWrite = 512i32; const WriteFullArrayOnly = 1024i32; const NoSubDataTypes = 2048i32; const NonVolatile = 4096i32; const Constant = 8192i32; } } +impl opcua::types::UaNullable for AccessLevelExType { + fn is_ua_null(&self) -> bool { + self.is_empty() + } +} impl opcua::types::BinaryEncodable for AccessLevelExType { fn byte_len(&self, _ctx: &opcua::types::Context<'_>) -> usize { 4usize @@ -98,6 +103,11 @@ bitflags::bitflags! { const HistoryWrite = 8u8; const SemanticChange = 16u8; const StatusWrite = 32u8; const TimestampWrite = 64u8; } } +impl opcua::types::UaNullable for AccessLevelType { + fn is_ua_null(&self) -> bool { + self.is_empty() + } +} impl opcua::types::BinaryEncodable for AccessLevelType { fn byte_len(&self, _ctx: &opcua::types::Context<'_>) -> usize { 1usize @@ -178,6 +188,11 @@ bitflags::bitflags! { const None = 0i16; const SigningRequired = 1i16; const EncryptionRequired = 2i16; const SessionRequired = 4i16; const ApplyRestrictionsToBrowse = 8i16; } } +impl opcua::types::UaNullable for AccessRestrictionType { + fn is_ua_null(&self) -> bool { + self.is_empty() + } +} impl opcua::types::BinaryEncodable for AccessRestrictionType { fn byte_len(&self, _ctx: &opcua::types::Context<'_>) -> usize { 2usize @@ -257,6 +272,11 @@ bitflags::bitflags! { #[derive(Debug, Copy, Clone, PartialEq)] pub struct AlarmMask : i16 { const None = 0i16; const Active = 1i16; const Unacknowledged = 2i16; const Unconfirmed = 4i16; } } +impl opcua::types::UaNullable for AlarmMask { + fn is_ua_null(&self) -> bool { + self.is_empty() + } +} impl opcua::types::BinaryEncodable for AlarmMask { fn byte_len(&self, _ctx: &opcua::types::Context<'_>) -> usize { 2usize @@ -355,6 +375,11 @@ bitflags::bitflags! { const DataTypeDefinition = 4194304i32; const RolePermissions = 8388608i32; const AccessRestrictions = 16777216i32; const AccessLevelEx = 33554432i32; } } +impl opcua::types::UaNullable for AttributeWriteMask { + fn is_ua_null(&self) -> bool { + self.is_empty() + } +} impl opcua::types::BinaryEncodable for AttributeWriteMask { fn byte_len(&self, _ctx: &opcua::types::Context<'_>) -> usize { 4usize @@ -500,6 +525,11 @@ bitflags::bitflags! { ServerTimestamp = 4i32; const SourcePicoSeconds = 8i32; const ServerPicoSeconds = 16i32; const RawData = 32i32; } } +impl opcua::types::UaNullable for DataSetFieldContentMask { + fn is_ua_null(&self) -> bool { + self.is_empty() + } +} impl opcua::types::BinaryEncodable for DataSetFieldContentMask { fn byte_len(&self, _ctx: &opcua::types::Context<'_>) -> usize { 4usize @@ -579,6 +609,11 @@ bitflags::bitflags! { #[derive(Debug, Copy, Clone, PartialEq)] pub struct DataSetFieldFlags : i16 { const None = 0i16; const PromotedField = 1i16; } } +impl opcua::types::UaNullable for DataSetFieldFlags { + fn is_ua_null(&self) -> bool { + self.is_empty() + } +} impl opcua::types::BinaryEncodable for DataSetFieldFlags { fn byte_len(&self, _ctx: &opcua::types::Context<'_>) -> usize { 2usize @@ -697,6 +732,11 @@ bitflags::bitflags! { None = 0u8; const SubscribeToEvents = 1u8; const HistoryRead = 4u8; const HistoryWrite = 8u8; } } +impl opcua::types::UaNullable for EventNotifierType { + fn is_ua_null(&self) -> bool { + self.is_empty() + } +} impl opcua::types::BinaryEncodable for EventNotifierType { fn byte_len(&self, _ctx: &opcua::types::Context<'_>) -> usize { 1usize @@ -869,6 +909,11 @@ bitflags::bitflags! { 128i32; const PublisherId = 256i32; const WriterGroupName = 512i32; const MinorVersion = 1024i32; } } +impl opcua::types::UaNullable for JsonDataSetMessageContentMask { + fn is_ua_null(&self) -> bool { + self.is_empty() + } +} impl opcua::types::BinaryEncodable for JsonDataSetMessageContentMask { fn byte_len(&self, _ctx: &opcua::types::Context<'_>) -> usize { 4usize @@ -951,6 +996,11 @@ bitflags::bitflags! { 8i32; const DataSetClassId = 16i32; const ReplyTo = 32i32; const WriterGroupName = 64i32; } } +impl opcua::types::UaNullable for JsonNetworkMessageContentMask { + fn is_ua_null(&self) -> bool { + self.is_empty() + } +} impl opcua::types::BinaryEncodable for JsonNetworkMessageContentMask { fn byte_len(&self, _ctx: &opcua::types::Context<'_>) -> usize { 4usize @@ -1169,6 +1219,11 @@ bitflags::bitflags! { const RequiresLowerCaseCharacters = 64i32; const RequiresDigitCharacters = 128i32; const RequiresSpecialCharacters = 256i32; } } +impl opcua::types::UaNullable for PasswordOptionsMask { + fn is_ua_null(&self) -> bool { + self.is_empty() + } +} impl opcua::types::BinaryEncodable for PasswordOptionsMask { fn byte_len(&self, _ctx: &opcua::types::Context<'_>) -> usize { 4usize @@ -1262,6 +1317,11 @@ bitflags::bitflags! { 2048i32; const Call = 4096i32; const AddReference = 8192i32; const RemoveReference = 16384i32; const DeleteNode = 32768i32; const AddNode = 65536i32; } } +impl opcua::types::UaNullable for PermissionType { + fn is_ua_null(&self) -> bool { + self.is_empty() + } +} impl opcua::types::BinaryEncodable for PermissionType { fn byte_len(&self, _ctx: &opcua::types::Context<'_>) -> usize { 4usize @@ -1346,6 +1406,11 @@ bitflags::bitflags! { ReferencePubDataset = 512i32; const ReferenceSubDataset = 1024i32; const ReferenceSecurityGroup = 2048i32; const ReferencePushTarget = 4096i32; } } +impl opcua::types::UaNullable for PubSubConfigurationRefMask { + fn is_ua_null(&self) -> bool { + self.is_empty() + } +} impl opcua::types::BinaryEncodable for PubSubConfigurationRefMask { fn byte_len(&self, _ctx: &opcua::types::Context<'_>) -> usize { 4usize @@ -1526,6 +1591,11 @@ bitflags::bitflags! { = 16i32; const CheckRevocationStatusOnline = 32i32; const CheckRevocationStatusOffline = 64i32; } } +impl opcua::types::UaNullable for TrustListValidationOptions { + fn is_ua_null(&self) -> bool { + self.is_empty() + } +} impl opcua::types::BinaryEncodable for TrustListValidationOptions { fn byte_len(&self, _ctx: &opcua::types::Context<'_>) -> usize { 4usize @@ -1669,6 +1739,11 @@ bitflags::bitflags! { Status = 4i32; const MajorVersion = 8i32; const MinorVersion = 16i32; const SequenceNumber = 32i32; } } +impl opcua::types::UaNullable for UadpDataSetMessageContentMask { + fn is_ua_null(&self) -> bool { + self.is_empty() + } +} impl opcua::types::BinaryEncodable for UadpDataSetMessageContentMask { fn byte_len(&self, _ctx: &opcua::types::Context<'_>) -> usize { 4usize @@ -1752,6 +1827,11 @@ bitflags::bitflags! { const PicoSeconds = 256i32; const DataSetClassId = 512i32; const PromotedFields = 1024i32; } } +impl opcua::types::UaNullable for UadpNetworkMessageContentMask { + fn is_ua_null(&self) -> bool { + self.is_empty() + } +} impl opcua::types::BinaryEncodable for UadpNetworkMessageContentMask { fn byte_len(&self, _ctx: &opcua::types::Context<'_>) -> usize { 4usize @@ -1832,6 +1912,11 @@ bitflags::bitflags! { const None = 0i32; const NoDelete = 1i32; const Disabled = 2i32; const NoChangeByUser = 4i32; const MustChangePassword = 8i32; } } +impl opcua::types::UaNullable for UserConfigurationMask { + fn is_ua_null(&self) -> bool { + self.is_empty() + } +} impl opcua::types::BinaryEncodable for UserConfigurationMask { fn byte_len(&self, _ctx: &opcua::types::Context<'_>) -> usize { 4usize diff --git a/async-opcua-types/src/guid.rs b/async-opcua-types/src/guid.rs index 7bacabfe1..cc50c5b84 100644 --- a/async-opcua-types/src/guid.rs +++ b/async-opcua-types/src/guid.rs @@ -26,6 +26,12 @@ impl From for Uuid { } } +impl UaNullable for Guid { + fn is_ua_null(&self) -> bool { + self.uuid.is_nil() + } +} + #[cfg(feature = "json")] mod json { use std::io::{Read, Write}; diff --git a/async-opcua-types/src/json.rs b/async-opcua-types/src/json.rs index 4c7063f7c..839c181aa 100644 --- a/async-opcua-types/src/json.rs +++ b/async-opcua-types/src/json.rs @@ -15,10 +15,10 @@ pub use struson::{ writer::{JsonStreamWriter, JsonWriter}, }; -use crate::{EncodingResult, Error}; +use crate::{EncodingResult, Error, UaNullable}; /// Trait for OPC-UA json encoding. -pub trait JsonEncodable { +pub trait JsonEncodable: UaNullable { #[allow(unused)] /// Write the type to the provided JSON writer. fn encode( @@ -26,12 +26,6 @@ pub trait JsonEncodable { stream: &mut JsonStreamWriter<&mut dyn Write>, ctx: &crate::Context<'_>, ) -> EncodingResult<()>; - - /// This method should return `true` if the value is default - /// and should not be serialized. - fn is_null_json(&self) -> bool { - false - } } impl From for Error { @@ -88,10 +82,6 @@ where None => Ok(stream.null_value()?), } } - - fn is_null_json(&self) -> bool { - self.is_none() - } } impl JsonDecodable for Option @@ -165,10 +155,6 @@ where ) -> EncodingResult<()> { T::encode(self, stream, ctx) } - - fn is_null_json(&self) -> bool { - T::is_null_json(self) - } } impl JsonDecodable for Box @@ -209,10 +195,6 @@ macro_rules! json_enc_float { Ok(()) } - - fn is_null_json(&self) -> bool { - *self == 0.0 - } } impl JsonDecodable for $t { @@ -249,10 +231,6 @@ macro_rules! json_enc_number { stream.number_value(*self)?; Ok(()) } - - fn is_null_json(&self) -> bool { - *self == 0 - } } impl JsonDecodable for $t { @@ -306,10 +284,6 @@ impl JsonEncodable for bool { stream.bool_value(*self)?; Ok(()) } - - fn is_null_json(&self) -> bool { - !self - } } impl JsonDecodable for bool { diff --git a/async-opcua-types/src/lib.rs b/async-opcua-types/src/lib.rs index 69b9eb925..b23e9f428 100644 --- a/async-opcua-types/src/lib.rs +++ b/async-opcua-types/src/lib.rs @@ -288,6 +288,7 @@ pub use opcua_macros::ua_encodable; pub use opcua_macros::BinaryDecodable; pub use opcua_macros::BinaryEncodable; pub use opcua_macros::UaEnum; +pub use opcua_macros::UaNullable; mod ua_enum; pub use self::{ diff --git a/async-opcua-types/src/localized_text.rs b/async-opcua-types/src/localized_text.rs index 614440a5d..3c8569ca0 100644 --- a/async-opcua-types/src/localized_text.rs +++ b/async-opcua-types/src/localized_text.rs @@ -18,7 +18,7 @@ mod opcua { pub use crate as types; } /// A human readable text with an optional locale identifier. -#[derive(PartialEq, Default, Debug, Clone)] +#[derive(PartialEq, Default, Debug, Clone, crate::UaNullable)] #[cfg_attr( feature = "json", derive(opcua_macros::JsonEncodable, opcua_macros::JsonDecodable) diff --git a/async-opcua-types/src/node_id.rs b/async-opcua-types/src/node_id.rs index d9fd32f31..cabd2f617 100644 --- a/async-opcua-types/src/node_id.rs +++ b/async-opcua-types/src/node_id.rs @@ -24,7 +24,7 @@ use crate::{ status_code::StatusCode, string::*, write_u16, write_u32, write_u8, DataTypeId, Error, MethodId, ObjectId, ObjectTypeId, - ReferenceTypeId, VariableId, VariableTypeId, + ReferenceTypeId, UaNullable, VariableId, VariableTypeId, }; /// The kind of identifier, numeric, string, guid or byte @@ -150,6 +150,12 @@ impl fmt::Display for NodeId { } } +impl UaNullable for NodeId { + fn is_ua_null(&self) -> bool { + self.is_null() + } +} + // JSON serialization schema as per spec: // // "Type" @@ -223,10 +229,6 @@ mod json { stream.end_object()?; Ok(()) } - - fn is_null_json(&self) -> bool { - self.is_null() - } } impl JsonDecodable for NodeId { @@ -357,10 +359,6 @@ mod xml { let val = ctx.resolve_alias_inverse(&self_str); writer.encode_child("Identifier", val, ctx) } - - fn is_null_xml(&self) -> bool { - self.is_null() - } } impl XmlDecodable for NodeId { diff --git a/async-opcua-types/src/qualified_name.rs b/async-opcua-types/src/qualified_name.rs index 6273758af..a221d144b 100644 --- a/async-opcua-types/src/qualified_name.rs +++ b/async-opcua-types/src/qualified_name.rs @@ -15,7 +15,7 @@ use regex::Regex; use crate::{ encoding::{BinaryDecodable, BinaryEncodable, EncodingResult}, string::*, - NamespaceMap, + NamespaceMap, UaNullable, }; #[allow(unused)] @@ -46,6 +46,12 @@ pub struct QualifiedName { pub name: UAString, } +impl UaNullable for QualifiedName { + fn is_ua_null(&self) -> bool { + self.is_null() + } +} + #[cfg(feature = "xml")] mod xml { use crate::{xml::*, UAString}; @@ -67,10 +73,6 @@ mod xml { writer.encode_child("Name", &self.name, context)?; Ok(()) } - - fn is_null_xml(&self) -> bool { - self.is_null() - } } impl XmlDecodable for QualifiedName { @@ -133,10 +135,6 @@ mod json { stream.string_value(&self.to_string())?; Ok(()) } - - fn is_null_json(&self) -> bool { - self.name.is_null_json() - } } impl JsonDecodable for QualifiedName { diff --git a/async-opcua-types/src/request_header.rs b/async-opcua-types/src/request_header.rs index 3e7670d42..37bd47902 100644 --- a/async-opcua-types/src/request_header.rs +++ b/async-opcua-types/src/request_header.rs @@ -26,7 +26,7 @@ mod opcua { } /// The `RequestHeader` contains information common to every request from a client to the server. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, crate::UaNullable)] #[cfg_attr( feature = "json", derive(opcua_macros::JsonEncodable, opcua_macros::JsonDecodable) diff --git a/async-opcua-types/src/response_header.rs b/async-opcua-types/src/response_header.rs index 54c64e45e..1662372f7 100644 --- a/async-opcua-types/src/response_header.rs +++ b/async-opcua-types/src/response_header.rs @@ -27,7 +27,7 @@ mod opcua { } /// The `ResponseHeader` contains information common to every response from server to client. -#[derive(Debug, Clone, PartialEq, Default)] +#[derive(Debug, Clone, PartialEq, Default, crate::UaNullable)] #[cfg_attr( feature = "json", derive(opcua_macros::JsonEncodable, opcua_macros::JsonDecodable) diff --git a/async-opcua-types/src/status_code.rs b/async-opcua-types/src/status_code.rs index ab38c21b7..be3eea6a4 100644 --- a/async-opcua-types/src/status_code.rs +++ b/async-opcua-types/src/status_code.rs @@ -8,7 +8,7 @@ use std::{ io::{Read, Write}, }; -use crate::{DecodingOptions, SimpleBinaryDecodable}; +use crate::{DecodingOptions, SimpleBinaryDecodable, UaNullable}; use super::encoding::{read_u32, write_u32, EncodingResult, SimpleBinaryEncodable}; @@ -17,6 +17,12 @@ use super::encoding::{read_u32, write_u32, EncodingResult, SimpleBinaryEncodable /// parsing, and reading. pub struct StatusCode(u32); +impl UaNullable for StatusCode { + fn is_ua_null(&self) -> bool { + self.0 == 0 + } +} + #[cfg(feature = "json")] mod json { use crate::json::*; diff --git a/async-opcua-types/src/string.rs b/async-opcua-types/src/string.rs index 820149179..19592538b 100644 --- a/async-opcua-types/src/string.rs +++ b/async-opcua-types/src/string.rs @@ -12,6 +12,7 @@ use std::{ use crate::{ encoding::{process_decode_io_result, process_encode_io_result, write_i32, EncodingResult}, read_i32, DecodingOptions, Error, OutOfRange, SimpleBinaryDecodable, SimpleBinaryEncodable, + UaNullable, }; /// To avoid naming conflict hell, the OPC UA String type is typed `UAString` so it does not collide @@ -35,6 +36,12 @@ impl fmt::Display for UAString { } } +impl UaNullable for UAString { + fn is_ua_null(&self) -> bool { + self.is_null() + } +} + #[cfg(feature = "json")] mod json { use std::io::{Read, Write}; @@ -61,10 +68,6 @@ mod json { Ok(()) } - - fn is_null_json(&self) -> bool { - self.is_null() - } } impl JsonDecodable for UAString { @@ -159,10 +162,6 @@ mod xml { Ok(()) } - - fn is_null_xml(&self) -> bool { - self.is_null() - } } impl XmlDecodable for UAString { diff --git a/async-opcua-types/src/tests/json.rs b/async-opcua-types/src/tests/json.rs index 94c19fb38..d76db06e5 100644 --- a/async-opcua-types/src/tests/json.rs +++ b/async-opcua-types/src/tests/json.rs @@ -4,7 +4,7 @@ use std::{ }; use base64::Engine; -use opcua_macros::{JsonDecodable, JsonEncodable}; +use opcua_macros::{JsonDecodable, JsonEncodable, UaNullable}; use serde_json::{json, Value}; use struson::{ reader::JsonStreamReader, @@ -767,7 +767,7 @@ fn test_custom_struct_with_optional() { pub use crate as types; } - #[derive(Debug, PartialEq, Clone, JsonDecodable, JsonEncodable)] + #[derive(Debug, PartialEq, Clone, JsonDecodable, JsonEncodable, UaNullable)] pub struct MyStructWithOptionalFields { foo: i32, #[opcua(optional)] @@ -838,7 +838,7 @@ fn test_custom_union() { pub use crate as types; } - #[derive(Debug, PartialEq, Clone, JsonDecodable, JsonEncodable)] + #[derive(Debug, PartialEq, Clone, JsonDecodable, JsonEncodable, UaNullable)] pub enum MyUnion { Var1(i32), #[opcua(rename = "EUInfo")] @@ -903,7 +903,7 @@ fn test_custom_union_nullable() { pub use crate as types; } - #[derive(Debug, PartialEq, Clone, JsonDecodable, JsonEncodable)] + #[derive(Debug, PartialEq, Clone, JsonDecodable, JsonEncodable, UaNullable)] pub enum MyUnion { Var1(i32), Null, diff --git a/async-opcua-types/src/variant/json.rs b/async-opcua-types/src/variant/json.rs index b2dfdac82..3b204039d 100644 --- a/async-opcua-types/src/variant/json.rs +++ b/async-opcua-types/src/variant/json.rs @@ -100,10 +100,6 @@ impl JsonEncodable for Variant { Ok(()) } - - fn is_null_json(&self) -> bool { - matches!(self, Variant::Empty) - } } enum VariantOrArray { diff --git a/async-opcua-types/src/variant/mod.rs b/async-opcua-types/src/variant/mod.rs index 9025f6fa0..e4e5ab2d4 100644 --- a/async-opcua-types/src/variant/mod.rs +++ b/async-opcua-types/src/variant/mod.rs @@ -41,7 +41,7 @@ use crate::{ qualified_name::QualifiedName, status_code::StatusCode, string::{UAString, XmlElement}, - write_i32, write_u8, DataTypeId, DataValue, DiagnosticInfo, DynEncodable, Error, + write_i32, write_u8, DataTypeId, DataValue, DiagnosticInfo, DynEncodable, Error, UaNullable, }; /// A `Variant` holds built-in OPC UA data types, including single and multi dimensional arrays, /// data values and extension objects. @@ -314,6 +314,12 @@ impl Variant { } } +impl UaNullable for Variant { + fn is_ua_null(&self) -> bool { + self.is_empty() + } +} + impl BinaryEncodable for Variant { fn byte_len(&self, ctx: &crate::Context<'_>) -> usize { let mut size: usize = 0; diff --git a/async-opcua-types/src/xml/builtins.rs b/async-opcua-types/src/xml/builtins.rs index 7bd4cab90..e3b2e367f 100644 --- a/async-opcua-types/src/xml/builtins.rs +++ b/async-opcua-types/src/xml/builtins.rs @@ -21,10 +21,6 @@ macro_rules! xml_enc_number { writer.write_text(&self.to_string())?; Ok(()) } - - fn is_null_xml(&self) -> bool { - *self == 0 - } } impl XmlDecodable for $t { @@ -67,10 +63,6 @@ macro_rules! xml_enc_float { } Ok(()) } - - fn is_null_xml(&self) -> bool { - *self == 0.0 - } } impl XmlDecodable for $t { @@ -173,10 +165,6 @@ impl XmlEncodable for bool { writer.write_text(if *self { "true" } else { "false" })?; Ok(()) } - - fn is_null_xml(&self) -> bool { - !self - } } impl XmlType for Box @@ -212,10 +200,6 @@ where ) -> Result<(), Error> { self.as_ref().encode(writer, context) } - - fn is_null_xml(&self) -> bool { - self.as_ref().is_null_xml() - } } impl XmlType for Vec @@ -264,7 +248,7 @@ where context: &Context<'_>, ) -> super::EncodingResult<()> { for item in self { - if item.is_null_xml() { + if item.is_ua_null() { writer.write_empty(item.tag())?; } else { writer.encode_child(item.tag(), item, context)?; @@ -312,8 +296,4 @@ where } Ok(()) } - - fn is_null_xml(&self) -> bool { - self.is_none() - } } diff --git a/async-opcua-types/src/xml/encoding.rs b/async-opcua-types/src/xml/encoding.rs index 189fe0b3f..818d8cc7d 100644 --- a/async-opcua-types/src/xml/encoding.rs +++ b/async-opcua-types/src/xml/encoding.rs @@ -5,7 +5,7 @@ use std::{ use opcua_xml::{XmlReadError, XmlStreamReader, XmlStreamWriter, XmlWriteError}; -use crate::{Context, EncodingResult, Error}; +use crate::{Context, EncodingResult, Error, UaNullable}; impl From for Error { fn from(value: XmlReadError) -> Self { @@ -48,19 +48,13 @@ pub trait XmlDecodable: XmlType { } /// Trait for types that can be encoded to XML. -pub trait XmlEncodable: XmlType { +pub trait XmlEncodable: XmlType + UaNullable { /// Encode a value to an XML stream. fn encode( &self, writer: &mut XmlStreamWriter<&mut dyn Write>, context: &Context<'_>, ) -> EncodingResult<()>; - - /// This method should return `true` if the value is default - /// and should not be serialized. - fn is_null_xml(&self) -> bool { - false - } } /// Extensions for XmlStreamWriter.