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

Xml encoding #47

Merged
merged 3 commits into from
Feb 27, 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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
17 changes: 17 additions & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ log = "^0.4"
parking_lot = { version = "^0.12", features = ["send_guard"] }
postcard = { version = "^1", features = ["use-std"] }
proc-macro2 = "^1"
quick-xml = "0.37.2"
quote = "^1"
regex = "^1"
roxmltree = "^0.20"
Expand Down
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