From 413331efc31103a0a3f9ee33da3eef72133e7c24 Mon Sep 17 00:00:00 2001 From: Einar Omang Date: Mon, 3 Mar 2025 12:56:27 +0100 Subject: [PATCH] Support encoding and decoding XML unions Not a complicated change. I also remove all traces of FromXml in this PR, since those were left behind after the refactor. We no longer use them, and don't want the burden of maintaining the macros. --- async-opcua-macros/src/encoding/mod.rs | 26 +--- async-opcua-macros/src/encoding/xml.rs | 163 +++++++++++++++++-------- async-opcua-macros/src/lib.rs | 13 -- async-opcua-types/src/lib.rs | 3 - async-opcua-types/src/tests/xml.rs | 66 +++++++++- async-opcua/src/lib.rs | 2 - 6 files changed, 180 insertions(+), 93 deletions(-) diff --git a/async-opcua-macros/src/encoding/mod.rs b/async-opcua-macros/src/encoding/mod.rs index 2723e629..ccc64a63 100644 --- a/async-opcua-macros/src/encoding/mod.rs +++ b/async-opcua-macros/src/encoding/mod.rs @@ -15,8 +15,6 @@ use proc_macro2::{Span, TokenStream}; use quote::quote; use syn::DeriveInput; use unions::AdvancedEnum; -#[cfg(feature = "xml")] -use xml::{generate_simple_enum_xml_impl, generate_xml_impl}; use crate::utils::StructItem; @@ -81,8 +79,6 @@ pub enum EncodingToImpl { #[cfg(feature = "json")] JsonDecode, #[cfg(feature = "xml")] - FromXml, - #[cfg(feature = "xml")] XmlEncode, #[cfg(feature = "xml")] XmlDecode, @@ -141,11 +137,7 @@ pub fn generate_encoding_impl( } #[cfg(feature = "xml")] (EncodingToImpl::XmlEncode, EncodingInput::AdvancedEnum(s)) => { - // xml::generate_union_xml_encode_impl(s) - Err(syn::Error::new_spanned( - s.ident, - "XmlEncodable is not supported on unions yet", - )) + xml::generate_union_xml_encode_impl(s) } #[cfg(feature = "xml")] (EncodingToImpl::XmlDecode, EncodingInput::Struct(s)) => xml::generate_xml_decode_impl(s), @@ -155,11 +147,7 @@ pub fn generate_encoding_impl( } #[cfg(feature = "xml")] (EncodingToImpl::XmlDecode, EncodingInput::AdvancedEnum(s)) => { - // xml::generate_union_xml_decode_impl(s) - Err(syn::Error::new_spanned( - s.ident, - "XmlDecodable is not supported on unions yet", - )) + xml::generate_union_xml_decode_impl(s) } #[cfg(feature = "xml")] (EncodingToImpl::XmlType, EncodingInput::Struct(s)) => { @@ -174,16 +162,6 @@ pub fn generate_encoding_impl( xml::generate_xml_type_impl(s.ident, s.attr) } - #[cfg(feature = "xml")] - (EncodingToImpl::FromXml, EncodingInput::Struct(s)) => generate_xml_impl(s), - #[cfg(feature = "xml")] - (EncodingToImpl::FromXml, EncodingInput::SimpleEnum(s)) => generate_simple_enum_xml_impl(s), - #[cfg(feature = "xml")] - (EncodingToImpl::FromXml, EncodingInput::AdvancedEnum(s)) => Err(syn::Error::new_spanned( - s.ident, - "FromXml is not supported on unions yet", - )), - (EncodingToImpl::UaEnum, EncodingInput::SimpleEnum(s)) => derive_ua_enum_impl(s), (EncodingToImpl::UaEnum, _) => Err(syn::Error::new( Span::call_site(), diff --git a/async-opcua-macros/src/encoding/xml.rs b/async-opcua-macros/src/encoding/xml.rs index dbc87f83..a359c2ab 100644 --- a/async-opcua-macros/src/encoding/xml.rs +++ b/async-opcua-macros/src/encoding/xml.rs @@ -4,56 +4,9 @@ use syn::Ident; use quote::quote; -use super::{attribute::EncodingItemAttribute, enums::SimpleEnum, EncodingStruct}; - -pub fn generate_xml_impl(strct: EncodingStruct) -> syn::Result { - let ident = strct.ident; - let mut body = quote! {}; - let mut build = quote! {}; - for field in strct.fields { - let name = field - .attr - .rename - .unwrap_or_else(|| field.ident.to_string().to_case(Case::Pascal)); - let ident = field.ident; - body.extend(quote! { - let #ident = opcua::types::xml::XmlField::get_xml_field(element, #name, ctx)?; - }); - build.extend(quote! { - #ident, - }); - } - Ok(quote! { - impl opcua::types::xml::FromXml for #ident { - fn from_xml<'a>( - element: &opcua::types::xml::XmlElement, - ctx: &opcua::types::Context<'a> - ) -> Result { - #body - Ok(Self { - #build - }) - } - } - }) -} - -pub fn generate_simple_enum_xml_impl(en: SimpleEnum) -> syn::Result { - let ident = en.ident; - let repr = en.repr; - - Ok(quote! { - impl opcua::types::xml::FromXml for #ident { - fn from_xml<'a>( - element: &opcua::types::xml::XmlElement, - ctx: &opcua::types::Context<'a> - ) -> Result { - let val = #repr::from_xml(element, ctx)?; - Self::try_from(val).map_err(opcua::types::Error::decoding) - } - } - }) -} +use super::{ + attribute::EncodingItemAttribute, enums::SimpleEnum, unions::AdvancedEnum, EncodingStruct, +}; pub fn generate_xml_encode_impl(strct: EncodingStruct) -> syn::Result { let ident = strct.ident; @@ -281,3 +234,113 @@ pub fn generate_xml_type_impl(idt: Ident, attr: EncodingItemAttribute) -> syn::R } }) } + +pub fn generate_union_xml_decode_impl(en: AdvancedEnum) -> syn::Result { + let ident = en.ident; + + let mut decode_arms = quote! {}; + + for variant in en.variants { + if variant.is_null { + continue; + } + + let name = variant + .attr + .rename + .unwrap_or_else(|| variant.name.to_string()); + let var_idt = variant.name; + + decode_arms.extend(quote! { + #name => value = Some(Self::#var_idt(opcua::types::xml::XmlDecodable::decode(stream, ctx)?)), + }); + } + + let fallback = if let Some(null_variant) = en.null_variant { + quote! { + Ok(Self::#null_variant) + } + } else { + quote! { + Err(opcua::types::Error::decoding(format!("Missing union value"))) + } + }; + + Ok(quote! { + impl opcua::types::xml::XmlDecodable for #ident { + fn decode( + stream: &mut opcua::types::xml::XmlStreamReader<&mut dyn std::io::Read>, + ctx: &opcua::types::Context<'_>, + ) -> opcua::types::EncodingResult { + use opcua::types::xml::XmlReadExt; + + let mut value = None; + stream.iter_children(|__key, stream, ctx| { + match __key.as_str() { + #decode_arms + _ => { + stream.skip_value()?; + } + } + Ok(()) + }, ctx)?; + + let Some(value) = value else { + return #fallback; + }; + + Ok(value) + } + } + }) +} + +pub fn generate_union_xml_encode_impl(en: AdvancedEnum) -> syn::Result { + let ident = en.ident; + + let mut encode_arms = quote! {}; + + let mut idx = 0u32; + for variant in en.variants { + let name = variant + .attr + .rename + .unwrap_or_else(|| variant.name.to_string()); + let var_idt = variant.name; + if variant.is_null { + encode_arms.extend(quote! { + Self::#var_idt => { + stream.encode_child("SwitchField", &0u32, ctx)?; + } + }); + continue; + } + + idx += 1; + + encode_arms.extend(quote! { + Self::#var_idt(inner) => { + stream.encode_child("SwitchField", &#idx, ctx)?; + stream.encode_child(#name, inner, ctx)?; + }, + }); + } + + Ok(quote! { + impl opcua::types::xml::XmlEncodable for #ident { + fn encode( + &self, + stream: &mut opcua::types::xml::XmlStreamWriter<&mut dyn std::io::Write>, + ctx: &opcua::types::Context<'_> + ) -> opcua::types::EncodingResult<()> { + use opcua::types::xml::XmlWriteExt; + + match self { + #encode_arms + } + + Ok(()) + } + } + }) +} diff --git a/async-opcua-macros/src/lib.rs b/async-opcua-macros/src/lib.rs index 06ce30e3..fe9f23f7 100644 --- a/async-opcua-macros/src/lib.rs +++ b/async-opcua-macros/src/lib.rs @@ -74,19 +74,6 @@ pub fn derive_event_field(item: TokenStream) -> TokenStream { } } -#[cfg(feature = "xml")] -#[proc_macro_derive(FromXml, attributes(opcua))] -/// Derive the `FromXml` trait on this struct or enum, creating a conversion from -/// NodeSet2 XML files. -/// -/// All fields must be marked with `opcua(ignore)` or implement `FromXml`. -pub fn derive_from_xml(item: TokenStream) -> TokenStream { - match generate_encoding_impl(parse_macro_input!(item), EncodingToImpl::FromXml) { - Ok(r) => r.into(), - Err(e) => e.to_compile_error().into(), - } -} - #[cfg(feature = "json")] #[proc_macro_derive(JsonEncodable, attributes(opcua))] /// Derive the `JsonEncodable` trait on this struct or enum, creating code diff --git a/async-opcua-types/src/lib.rs b/async-opcua-types/src/lib.rs index b23e9f42..1d19c233 100644 --- a/async-opcua-types/src/lib.rs +++ b/async-opcua-types/src/lib.rs @@ -275,9 +275,6 @@ pub mod variant; #[cfg(feature = "xml")] pub mod xml; -#[cfg(feature = "xml")] -pub use opcua_macros::FromXml; - #[cfg(feature = "json")] pub use opcua_macros::{JsonDecodable, JsonEncodable}; diff --git a/async-opcua-types/src/tests/xml.rs b/async-opcua-types/src/tests/xml.rs index b554a5f1..4d255af0 100644 --- a/async-opcua-types/src/tests/xml.rs +++ b/async-opcua-types/src/tests/xml.rs @@ -1,12 +1,14 @@ use std::io::{Cursor, Read, Write}; use std::str::FromStr; +use opcua_macros::{XmlDecodable, XmlEncodable, XmlType}; use opcua_xml::XmlStreamReader; use crate::xml::{XmlDecodable, XmlEncodable}; use crate::{ Argument, Array, ByteString, DataTypeId, DataValue, DateTime, EUInformation, ExpandedNodeId, - ExtensionObject, Guid, LocalizedText, NodeId, QualifiedName, StatusCode, UAString, Variant, + ExtensionObject, Guid, LocalizedText, NodeId, QualifiedName, StatusCode, UAString, UaNullable, + Variant, }; use crate::{Context, ContextOwned, DecodingOptions, EncodingResult}; @@ -396,3 +398,65 @@ fn from_xml_variant() { "#, ); } + +#[test] +fn test_custom_union() { + mod opcua { + pub use crate as types; + } + + #[derive(Debug, PartialEq, Clone, XmlDecodable, XmlEncodable, UaNullable, XmlType)] + pub enum MyUnion { + Var1(i32), + #[opcua(rename = "EUInfo")] + Var2(EUInformation), + Var3(f64), + } + + xml_round_trip( + &MyUnion::Var1(123), + r#"1123"#, + ); + + xml_round_trip( + &MyUnion::Var2(EUInformation { + namespace_uri: "https://my.namespace.uri".into(), + unit_id: 1, + display_name: LocalizedText::from("MyUnit"), + description: LocalizedText::new("en", "MyDesc"), + }), + r#" + 2 + + https://my.namespace.uri + 1 + MyUnit + enMyDesc + + "#, + ); + + xml_round_trip( + &MyUnion::Var3(123.123), + r#"1123.123"#, + ); +} + +#[test] +fn test_custom_union_nullable() { + mod opcua { + pub use crate as types; + } + + #[derive(Debug, PartialEq, Clone, XmlDecodable, XmlEncodable, UaNullable, XmlType)] + pub enum MyUnion { + Var1(i32), + Null, + } + + xml_round_trip( + &MyUnion::Var1(123), + r#"1123"#, + ); + xml_round_trip(&MyUnion::Null, r#"0"#); +} diff --git a/async-opcua/src/lib.rs b/async-opcua/src/lib.rs index db865fed..68e2f6f8 100644 --- a/async-opcua/src/lib.rs +++ b/async-opcua/src/lib.rs @@ -23,8 +23,6 @@ extern crate tempdir; pub use opcua_core::sync; -#[cfg(feature = "xml")] -pub use opcua_macros::FromXml; #[cfg(feature = "server")] pub use opcua_macros::{Event, EventField};