Skip to content

Commit

Permalink
Replace FromXml with XmlEncodable and XmlDecodable
Browse files Browse the repository at this point in the history
This is a major rewrite of XML parsing, adding support for XML
serialization, and bringing it up to the same level as JSON and binary.
I also fixed a few bugs I hit on in a few places. There are still some
things remaining:

 - Unions
 - XML in binary/json encoded extension objects.
 - Proper support for the XmlElement variant (currently it doesn't work
   properly for XML). Note that this variant type is deprecated.

This PR is already way too big. There was no real way to make it
smaller, since once you start replacing FromXml there's no way to stop
while still having the library compile.
  • Loading branch information
einarmo committed Feb 27, 2025
1 parent c60312e commit 60089b7
Show file tree
Hide file tree
Showing 362 changed files with 5,477 additions and 1,230 deletions.
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ This is a list of things that are known to be missing, or ideas that could be im
- Implement a better framework for security checks on the server.
- Write a sophisticated server example with a persistent store. This would be a great way to verify the flexibility of the server.
- Write some "bad ideas" servers, it would be nice to showcase how flexible this is.
- Re-implement XML. The current approach using roxmltree is easy to write, but not actually what we need if we really wanted to implement OPC-UA XML encoding. A stream-based low level XML parser like `quick-xml` would probably be a better option. An implementation could probably borrow a lot from the JSON implementation.
- Finish up XML implementation.
- Support for XML bodies in binary and JSON extension objects.
- Support for unions in derive macros.
- Support for XmlElement in Variant.
- Write a framework for method calls. The foundation for this has been laid with `TryFromVariant`, if we really wanted to we could use clever trait magic to let users simply define a rust method that takes in values that each implement a trait `MethodArg`, with a blanket impl for `TryFromVariant`, and return a tuple of results. Could be really powerful, but methods are a little niche.
- Implement `Query`. I never got around to this, because the service is just so complex. Currently there is no way to actually implement it, since it won't work unless _all_ node managers implement it, and the core node managers don't.
- Look into running certain services concurrently. Currently they are sequential because that makes everything much simpler, but the services that don't have any cross node-manager interaction could run on all node managers concurrently.
Expand Down
2 changes: 2 additions & 0 deletions async-opcua-client/src/custom_types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ struct RawTypeData {
type_definition: Option<DataTypeDefinition>,
is_abstract: bool,
encoding_ids: Option<EncodingIds>,
name: String,
}

impl<T: FnMut(&NodeId) -> bool> DataTypeTreeBuilder<T> {
Expand Down Expand Up @@ -282,6 +283,7 @@ impl<T: FnMut(&NodeId) -> bool> DataTypeTreeBuilder<T> {
if let Some(def) = type_data.type_definition {
let info = match TypeInfo::from_type_definition(
def,
type_data.name,
type_data.encoding_ids,
type_data.is_abstract,
&id,
Expand Down
10 changes: 8 additions & 2 deletions async-opcua-codegen/src/nodeset/value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -264,13 +264,19 @@ impl<'a> ValueBuilder<'a> {
}

fn render_extension_object(&self, obj: &ExtensionObject) -> Result<TokenStream, CodeGenError> {
let Some(body) = &obj.body else {
let Some(data) = obj.body.as_ref().and_then(|b| b.data.as_ref()) else {
return Ok(quote::quote! {
opcua::types::ExtensionObject::null()
});
};

let content = self.render_extension_object_inner(&body.data)?;
let element = XmlElement::parse(data)?;
let Some(element) = element else {
return Ok(quote::quote! {
opcua::types::ExtensionObject::null()
});
};
let content = self.render_extension_object_inner(&element)?;

Ok(quote! {
opcua::types::ExtensionObject::from_message(#content)
Expand Down
63 changes: 50 additions & 13 deletions async-opcua-codegen/src/types/gen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ impl CodeGenerator {

fn is_default_recursive(&self, name: &str) -> bool {
if self.default_excluded.contains(name) {
return false;
return true;
}

let Some(it) = self.import_map.get(name) else {
Expand Down Expand Up @@ -361,20 +361,39 @@ impl CodeGenerator {
}
});

// Xml impl
impls.push(parse_quote! {
#[cfg(feature = "xml")]
impl opcua::types::xml::FromXml for #enum_ident {
fn from_xml(
element: &opcua::types::xml::XmlElement,
ctx: &opcua::types::Context<'_>
impl opcua::types::xml::XmlDecodable for #enum_ident {
fn decode(
stream: &mut opcua::types::xml::XmlStreamReader<&mut dyn std::io::Read>,
ctx: &opcua::types::Context<'_>,
) -> opcua::types::EncodingResult<Self> {
let val = #ty::from_xml(element, ctx)?;
Ok(Self::from_bits_truncate(val))
Ok(Self::from_bits_truncate(#ty::decode(stream, ctx)?))
}
}
});

impls.push(parse_quote! {
#[cfg(feature = "xml")]
impl opcua::types::xml::XmlEncodable for #enum_ident {
fn encode(
&self,
stream: &mut opcua::types::xml::XmlStreamWriter<&mut dyn std::io::Write>,
ctx: &opcua::types::Context<'_>,
) -> opcua::types::EncodingResult<()> {
self.bits().encode(stream, ctx)
}
}
});

let name = &item.name;
impls.push(parse_quote! {
#[cfg(feature = "xml")]
impl opcua::types::xml::XmlType for #enum_ident {
const TAG: &'static str = #name;
}
});

impls.push(parse_quote! {
#[cfg(feature = "json")]
impl opcua::types::json::JsonDecodable for #enum_ident {
Expand Down Expand Up @@ -443,7 +462,10 @@ impl CodeGenerator {
)]
});
attrs.push(parse_quote! {
#[cfg_attr(feature = "xml", derive(opcua::types::FromXml))]
#[cfg_attr(
feature = "xml",
derive(opcua::types::XmlEncodable, opcua::types::XmlDecodable, opcua::types::XmlType)
)]
});
let ty: Type = syn::parse_str(&item.typ.to_string())?;
attrs.push(parse_quote! {
Expand Down Expand Up @@ -504,7 +526,13 @@ impl CodeGenerator {
}
}

let (enum_ident, _) = safe_ident(&item.name);
let (enum_ident, renamed) = safe_ident(&item.name);
if renamed {
let name = &item.name;
attrs.push(parse_quote! {
#[opcua(rename = #name)]
});
}

let res = ItemEnum {
attrs,
Expand Down Expand Up @@ -565,17 +593,26 @@ impl CodeGenerator {
#[cfg_attr(feature = "json", derive(opcua::types::JsonEncodable, opcua::types::JsonDecodable))]
});
attrs.push(parse_quote! {
#[cfg_attr(feature = "xml", derive(opcua::types::FromXml))]
#[cfg_attr(
feature = "xml",
derive(opcua::types::XmlEncodable, opcua::types::XmlDecodable, opcua::types::XmlType)
)]
});

if self.has_default(&item.name) {
if self.has_default(&item.name) && !self.default_excluded.contains(&item.name) {
attrs.push(parse_quote! {
#[derive(Default)]
});
}

let mut impls = Vec::new();
let (struct_ident, _) = safe_ident(&item.name);
let (struct_ident, renamed) = safe_ident(&item.name);
if renamed {
let name = &item.name;
attrs.push(parse_quote! {
#[opcua(rename = #name)]
});
}

for field in item.visible_fields() {
let typ: Type = match &field.typ {
Expand Down
4 changes: 2 additions & 2 deletions async-opcua-codegen/src/types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -282,14 +282,14 @@ fn xml_loader_impl(ids: &[&(EncodingIds, String)], namespace: &str) -> (TokenStr
fn load_from_xml(
&self,
node_id: &opcua::types::NodeId,
stream: &opcua::types::xml::XmlElement,
stream: &mut opcua::types::xml::XmlStreamReader<&mut dyn std::io::Read>,
ctx: &opcua::types::Context<'_>,
) -> Option<opcua::types::EncodingResult<Box<dyn opcua::types::DynEncodable>>> {
#index_check

let Some(num_id) = node_id.as_u32() else {
return Some(Err(opcua::types::Error::decoding(
format!("Unsupported encoding ID {node_id}, we only support numeric IDs"),
"Unsupported encoding ID. Only numeric encoding IDs are currently supported"
)));
};

Expand Down
35 changes: 35 additions & 0 deletions async-opcua-macros/src/encoding/attribute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,38 @@ impl Parse for EncodingVariantAttribute {
Ok(slf)
}
}

#[derive(Debug, Default)]
pub(crate) struct EncodingItemAttribute {
#[allow(unused)]
pub(crate) rename: Option<String>,
}

impl ItemAttr for EncodingItemAttribute {
fn combine(&mut self, other: Self) {
self.rename = other.rename;
}
}

impl Parse for EncodingItemAttribute {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let mut slf = Self::default();

loop {
let ident: Ident = input.parse()?;
match ident.to_string().as_str() {
"rename" => {
input.parse::<Token![=]>()?;
let val: LitStr = input.parse()?;
slf.rename = Some(val.value());
}
_ => return Err(syn::Error::new_spanned(ident, "Unknown attribute value")),
}
if !input.peek(Token![,]) {
break;
}
input.parse::<Token![,]>()?;
}
Ok(slf)
}
}
33 changes: 20 additions & 13 deletions async-opcua-macros/src/encoding/enums.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
use proc_macro2::TokenStream;
use syn::{Attribute, DataEnum, Expr, Ident, Type, Variant};
use syn::{Attribute, DataEnum, Ident, LitInt, Type, Variant};

use crate::utils::ItemAttr;
use quote::quote;
use quote::{quote, ToTokens};

use super::attribute::EncodingVariantAttribute;
use super::attribute::{EncodingItemAttribute, EncodingVariantAttribute};

pub struct SimpleEnumVariant {
pub value: Expr,
pub value: LitInt,
pub name: Ident,
pub attr: EncodingVariantAttribute,
}
Expand All @@ -16,6 +16,8 @@ pub struct SimpleEnum {
pub repr: Type,
pub variants: Vec<SimpleEnumVariant>,
pub ident: Ident,
#[allow(unused)]
pub attr: EncodingItemAttribute,
}

impl SimpleEnumVariant {
Expand All @@ -26,6 +28,7 @@ impl SimpleEnumVariant {
"Enum variant must have explicit discriminant",
));
};
let value = syn::parse2(value.into_token_stream())?;
if !variant.fields.is_empty() {
return Err(syn::Error::new_spanned(
variant.fields,
Expand Down Expand Up @@ -66,16 +69,19 @@ impl SimpleEnum {
.collect::<Result<Vec<_>, _>>()?;

let mut repr: Option<Type> = None;
let mut final_attr = EncodingItemAttribute::default();
for attr in attributes {
if attr.path().segments.len() == 1
&& attr
.path()
.segments
.first()
.is_some_and(|s| s.ident == "repr")
{
if attr.path().segments.len() != 1 {
continue;
}
let seg = attr.path().segments.first();
if seg.is_some_and(|s| s.ident == "repr") {
repr = Some(attr.parse_args()?);
}
if seg.is_some_and(|s| s.ident == "opcua") {
let data: EncodingItemAttribute = attr.parse_args()?;
final_attr.combine(data);
}
}

let Some(repr) = repr else {
Expand All @@ -89,6 +95,7 @@ impl SimpleEnum {
repr,
variants,
ident,
attr: final_attr,
})
}
}
Expand Down Expand Up @@ -117,9 +124,9 @@ pub fn derive_ua_enum_impl(en: SimpleEnum) -> syn::Result<TokenStream> {
let val = variant.value;
let name = variant.name;
let name_str = if let Some(rename) = variant.attr.rename {
rename
format!("{}_{}", rename, val.base10_digits())
} else {
name.to_string()
format!("{}_{}", name, val.base10_digits())
};
try_from_arms.extend(quote! {
#val => Self::#name,
Expand Down
14 changes: 8 additions & 6 deletions async-opcua-macros/src/encoding/json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,14 +150,16 @@ pub fn generate_json_decode_impl(strct: EncodingStruct) -> syn::Result<TokenStre

if field.attr.no_default {
let err = format!("Missing required field {name}");
let handle = if has_header {
quote! {
.map_err(|e| e.maybe_with_request_handle(__request_handle))?
}
} else {
quote! {}
};
build.extend(quote! {
#ident: #ident.unwrap_or_else(|| {
log::warn!(#err);
opcua::types::Error::new(
opcua::types::StatusCode::BadDecodingError,
None,
__request_handle,
)
opcua::types::Error::decoding(#err)#handle
})?,
});
} else {
Expand Down
Loading

0 comments on commit 60089b7

Please sign in to comment.