Skip to content

Commit

Permalink
Support encoding and decoding XML unions
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
einarmo committed Mar 3, 2025
1 parent 18459c7 commit 413331e
Show file tree
Hide file tree
Showing 6 changed files with 180 additions and 93 deletions.
26 changes: 2 additions & 24 deletions async-opcua-macros/src/encoding/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -81,8 +79,6 @@ pub enum EncodingToImpl {
#[cfg(feature = "json")]
JsonDecode,
#[cfg(feature = "xml")]
FromXml,
#[cfg(feature = "xml")]
XmlEncode,
#[cfg(feature = "xml")]
XmlDecode,
Expand Down Expand Up @@ -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),
Expand All @@ -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)) => {
Expand All @@ -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(),
Expand Down
163 changes: 113 additions & 50 deletions async-opcua-macros/src/encoding/xml.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<TokenStream> {
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<Self, opcua::types::Error> {
#body
Ok(Self {
#build
})
}
}
})
}

pub fn generate_simple_enum_xml_impl(en: SimpleEnum) -> syn::Result<TokenStream> {
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<Self, opcua::types::Error> {
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<TokenStream> {
let ident = strct.ident;
Expand Down Expand Up @@ -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<TokenStream> {
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<Self> {
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<TokenStream> {
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(())
}
}
})
}
13 changes: 0 additions & 13 deletions async-opcua-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 0 additions & 3 deletions async-opcua-types/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down
66 changes: 65 additions & 1 deletion async-opcua-types/src/tests/xml.rs
Original file line number Diff line number Diff line change
@@ -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};

Expand Down Expand Up @@ -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#"<SwitchField>1</SwitchField><Var1>123</Var1>"#,
);

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#"
<SwitchField>2</SwitchField>
<EUInfo>
<NamespaceUri>https://my.namespace.uri</NamespaceUri>
<UnitId>1</UnitId>
<DisplayName><Text>MyUnit</Text></DisplayName>
<Description><Locale>en</Locale><Text>MyDesc</Text></Description>
</EUInfo>
"#,
);

xml_round_trip(
&MyUnion::Var3(123.123),
r#"<SwitchField>1</SwitchField><Var3>123.123</Var3>"#,
);
}

#[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#"<SwitchField>1</SwitchField><Var1>123</Var1>"#,
);
xml_round_trip(&MyUnion::Null, r#"<SwitchField>0</SwitchField>"#);
}
2 changes: 0 additions & 2 deletions async-opcua/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down

0 comments on commit 413331e

Please sign in to comment.