Skip to content

Commit

Permalink
Unified nullable trait for encoding
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
einarmo committed Mar 2, 2025
1 parent 0731241 commit 18459c7
Show file tree
Hide file tree
Showing 30 changed files with 312 additions and 118 deletions.
8 changes: 8 additions & 0 deletions async-opcua-codegen/src/types/gen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
6 changes: 3 additions & 3 deletions async-opcua-macros/src/encoding/json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ pub fn generate_json_encode_impl(strct: EncodingStruct) -> syn::Result<TokenStre
}
let ident = &field.ident;
body.extend(quote! {
if self.#ident.as_ref().is_some_and(|f| !f.is_null_json()) {
if self.#ident.as_ref().is_some_and(|f| !opcua::types::UaNullable::is_ua_null(f)) {
encoding_mask |= 1 << #optional_index;
}
});
Expand All @@ -53,15 +53,15 @@ pub fn generate_json_encode_impl(strct: EncodingStruct) -> syn::Result<TokenStre
if field.attr.optional {
body.extend(quote! {
if let Some(item) = &self.#ident {
if !item.is_null_json() {
if !opcua::types::UaNullable::is_ua_null(item){
stream.name(#name)?;
opcua::types::json::JsonEncodable::encode(item, stream, ctx)?;
}
}
});
} else {
body.extend(quote! {
if !self.#ident.is_null_json() {
if !opcua::types::UaNullable::is_ua_null(&self.#ident) {
stream.name(#name)?;
opcua::types::json::JsonEncodable::encode(&self.#ident, stream, ctx)?;
}
Expand Down
48 changes: 47 additions & 1 deletion async-opcua-macros/src/encoding/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ pub fn generate_encoding_impl(
pub(crate) fn derive_all_inner(item: DeriveInput) -> syn::Result<TokenStream> {
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)
Expand All @@ -222,3 +222,49 @@ pub(crate) fn derive_all_inner(item: DeriveInput) -> syn::Result<TokenStream> {

Ok(output)
}

pub(crate) fn derive_ua_nullable_inner(item: DeriveInput) -> syn::Result<TokenStream> {
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 {}
})
}
}
}
}
4 changes: 2 additions & 2 deletions async-opcua-macros/src/encoding/xml.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,14 +102,14 @@ pub fn generate_xml_encode_impl(strct: EncodingStruct) -> syn::Result<TokenStrea
if field.attr.optional {
body.extend(quote! {
if let Some(item) = &self.#ident {
if !item.is_null_xml() {
if !opcua::types::UaNullable::is_ua_null(item) {
stream.encode_child(#name, item, ctx)?;
}
}
});
} else {
body.extend(quote! {
if !self.#ident.is_null_xml() {
if !opcua::types::UaNullable::is_ua_null(&self.#ident) {
stream.encode_child(#name, &self.#ident, ctx)?;
}
});
Expand Down
14 changes: 13 additions & 1 deletion async-opcua-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ mod encoding;
mod events;
mod utils;

use encoding::{derive_all_inner, generate_encoding_impl, EncodingToImpl};
use encoding::{
derive_all_inner, derive_ua_nullable_inner, generate_encoding_impl, EncodingToImpl,
};
use events::{derive_event_field_inner, derive_event_inner};
use proc_macro::TokenStream;
use syn::parse_macro_input;
Expand Down Expand Up @@ -186,6 +188,16 @@ pub fn derive_xml_type(item: TokenStream) -> 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`,
Expand Down
2 changes: 1 addition & 1 deletion async-opcua-types/src/argument.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
8 changes: 7 additions & 1 deletion async-opcua-types/src/byte_string.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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};
Expand Down
12 changes: 11 additions & 1 deletion async-opcua-types/src/custom/custom_struct.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion async-opcua-types/src/data_value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
6 changes: 6 additions & 0 deletions async-opcua-types/src/date_time.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down
8 changes: 7 additions & 1 deletion async-opcua-types/src/diagnostic_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;
Expand Down Expand Up @@ -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)
Expand Down
57 changes: 57 additions & 0 deletions async-opcua-types/src/encoding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> UaNullable for Option<T>
where
T: UaNullable,
{
fn is_ua_null(&self) -> bool {
match self {
Some(s) => s.is_ua_null(),
None => true,
}
}
}

impl<T> UaNullable for Vec<T> where T: UaNullable {}
impl<T> UaNullable for Box<T>
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.
Expand Down
16 changes: 7 additions & 9 deletions async-opcua-types/src/expanded_node_id.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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:
Expand Down Expand Up @@ -118,10 +124,6 @@ mod json {
stream.end_object()?;
Ok(())
}

fn is_null_json(&self) -> bool {
self.is_null()
}
}

impl JsonDecodable for ExpandedNodeId {
Expand Down Expand Up @@ -263,10 +265,6 @@ mod xml {
};
node_id.encode(writer, context)
}

fn is_null_xml(&self) -> bool {
self.is_null()
}
}

impl XmlDecodable for ExpandedNodeId {
Expand Down
4 changes: 3 additions & 1 deletion async-opcua-types/src/extension_object.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -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};
Expand Down
Loading

0 comments on commit 18459c7

Please sign in to comment.