Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unified nullable trait for encoding #51

Merged
merged 1 commit into from
Mar 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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