From a9ef680c3031607169a2e3b5fdde03955219dab8 Mon Sep 17 00:00:00 2001 From: Lennart <18233294+lennart-k@users.noreply.github.com> Date: Sun, 24 Nov 2024 15:09:34 +0100 Subject: [PATCH] Some initial work on xml parsing --- Cargo.lock | 22 ++ Cargo.toml | 1 + crates/dav/Cargo.toml | 1 + crates/xml/Cargo.toml | 12 ++ crates/xml/derive/Cargo.toml | 14 ++ crates/xml/derive/src/de/attrs.rs | 231 +++++++++++++++++++++ crates/xml/derive/src/de/de_enum.rs | 74 +++++++ crates/xml/derive/src/de/de_struct.rs | 278 ++++++++++++++++++++++++++ crates/xml/derive/src/de/mod.rs | 6 + crates/xml/derive/src/lib.rs | 18 ++ crates/xml/src/lib.rs | 136 +++++++++++++ crates/xml/tests/de_enum.rs | 45 +++++ crates/xml/tests/de_struct.rs | 171 ++++++++++++++++ crates/xml/tests/propfind.rs | 123 ++++++++++++ 14 files changed, 1132 insertions(+) create mode 100644 crates/xml/Cargo.toml create mode 100644 crates/xml/derive/Cargo.toml create mode 100644 crates/xml/derive/src/de/attrs.rs create mode 100644 crates/xml/derive/src/de/de_enum.rs create mode 100644 crates/xml/derive/src/de/de_struct.rs create mode 100644 crates/xml/derive/src/de/mod.rs create mode 100644 crates/xml/derive/src/lib.rs create mode 100644 crates/xml/src/lib.rs create mode 100644 crates/xml/tests/de_enum.rs create mode 100644 crates/xml/tests/de_struct.rs create mode 100644 crates/xml/tests/propfind.rs diff --git a/Cargo.lock b/Cargo.lock index f0e11c0..e6d4e60 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2514,6 +2514,7 @@ dependencies = [ "log", "quick-xml", "rustical_store", + "rustical_xml", "serde", "strum", "thiserror", @@ -2577,6 +2578,16 @@ dependencies = [ "tracing", ] +[[package]] +name = "rustical_xml" +version = "0.1.0" +dependencies = [ + "heck", + "quick-xml", + "thiserror", + "xml_derive", +] + [[package]] name = "rustix" version = "0.38.37" @@ -3845,6 +3856,17 @@ dependencies = [ "memchr", ] +[[package]] +name = "xml_derive" +version = "0.1.0" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "strum", + "syn", +] + [[package]] name = "zerocopy" version = "0.7.35" diff --git a/Cargo.toml b/Cargo.toml index 9ce72c1..59477ff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -90,6 +90,7 @@ rustical_store_sqlite = { path = "./crates/store_sqlite/" } rustical_caldav = { path = "./crates/caldav/" } rustical_carddav = { path = "./crates/carddav/" } rustical_frontend = { path = "./crates/frontend/" } +rustical_xml = { path = "./crates/xml/" } chrono-tz = "0.10.0" rand = "0.8" argon2 = "0.5" diff --git a/crates/dav/Cargo.toml b/crates/dav/Cargo.toml index 5881329..5e29b61 100644 --- a/crates/dav/Cargo.toml +++ b/crates/dav/Cargo.toml @@ -7,6 +7,7 @@ repository.workspace = true publish = false [dependencies] +rustical_xml.workspace = true actix-web = { workspace = true } async-trait = { workspace = true } futures-util = { workspace = true } diff --git a/crates/xml/Cargo.toml b/crates/xml/Cargo.toml new file mode 100644 index 0000000..235a94c --- /dev/null +++ b/crates/xml/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "rustical_xml" +version.workspace = true +edition.workspace = true +description.workspace = true +repository.workspace = true + +[dependencies] +heck = "0.5.0" +quick-xml.workspace = true +thiserror.workspace = true +xml_derive = { path = "./derive" } diff --git a/crates/xml/derive/Cargo.toml b/crates/xml/derive/Cargo.toml new file mode 100644 index 0000000..c606064 --- /dev/null +++ b/crates/xml/derive/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "xml_derive" +version = "0.1.0" +edition = "2021" + +[lib] +proc-macro = true + +[dependencies] +syn = { version = "2.0", features = ["full"] } +quote = "1.0" +proc-macro2 = "1.0" +heck = "0.5.0" +strum.workspace = true diff --git a/crates/xml/derive/src/de/attrs.rs b/crates/xml/derive/src/de/attrs.rs new file mode 100644 index 0000000..8929aab --- /dev/null +++ b/crates/xml/derive/src/de/attrs.rs @@ -0,0 +1,231 @@ +use core::panic; + +use heck::{ToKebabCase, ToPascalCase}; +use quote::ToTokens; +use strum::EnumString; +use syn::{ + punctuated::Punctuated, token::Comma, Attribute, Expr, ExprLit, Lit, LitByteStr, LitStr, Meta, +}; + +const ATTR_SCOPE: &str = "xml"; + +#[derive(Default)] +pub struct VariantAttrs { + pub rename: Option, + pub ns: Option, +} + +pub fn get_scoped_attrs(attrs: &[Attribute]) -> Option> { + attrs + .iter() + .find(|attr| attr.path().is_ident(ATTR_SCOPE)) + .map(|attr| { + attr.parse_args_with(Punctuated::::parse_terminated) + .unwrap() + }) +} + +pub fn parse_variant_attrs(attrs: &[Attribute]) -> VariantAttrs { + let mut variant_attrs = VariantAttrs::default(); + + let attrs = get_scoped_attrs(attrs); + + let attrs = if let Some(attrs) = attrs { + attrs + } else { + return variant_attrs; + }; + + for meta in attrs { + match meta { + // single flag + Meta::Path(path) => { + panic!("unrecognized variant flag: {}", path.to_token_stream()); + } + Meta::List(list) => { + panic!("list-type attrs not supported: {}", list.to_token_stream()); + } + Meta::NameValue(name_value) => { + if name_value.path.is_ident("ns") { + if let Expr::Lit(ExprLit { + lit: Lit::Str(lit_str), + .. + }) = name_value.value + { + variant_attrs.ns = Some(lit_str.value()); + } + } else if name_value.path.is_ident("rename") { + if let Expr::Lit(ExprLit { + lit: Lit::Str(lit_str), + .. + }) = name_value.value + { + variant_attrs.rename = Some(lit_str.value()); + } + } else { + panic!( + "unrecognized variant attribute: {}", + name_value.to_token_stream() + ); + } + } + } + } + variant_attrs +} + +#[derive(EnumString)] +pub enum CaseStyle { + #[strum(serialize = "kebab-case")] + KebabCase, + #[strum(serialize = "PascalCase")] + PascalCase, +} + +impl CaseStyle { + fn transform_text(&self, input: &str) -> String { + match self { + Self::KebabCase => input.to_kebab_case(), + Self::PascalCase => input.to_pascal_case(), + } + } +} + +#[derive(Default)] +pub struct EnumAttrs { + pub case_style: Option, + pub ns_strict: bool, +} + +fn parse_enum_attrs(attrs: &[Attribute]) -> EnumAttrs { + let enum_attrs = EnumAttrs::default(); + + enum_attrs +} + +#[derive(Default)] +pub struct StructAttrs { + pub root: Option, +} + +pub fn parse_struct_attrs(attrs: &[Attribute]) -> StructAttrs { + let mut struct_attrs = StructAttrs::default(); + + let attrs = get_scoped_attrs(attrs); + let attrs = if let Some(attrs) = attrs { + attrs + } else { + return struct_attrs; + }; + + for meta in attrs { + match meta { + // single flag + Meta::Path(_path) => { + panic!("invalid path attribute") + } + Meta::List(list) => { + panic!("list-type attrs not supported: {}", list.to_token_stream()); + } + Meta::NameValue(name_value) => { + if name_value.path.is_ident("root") { + if let Expr::Lit(ExprLit { + lit: Lit::ByteStr(lit_str), + .. + }) = name_value.value + { + struct_attrs.root = Some(lit_str); + } + } else { + panic!( + "unrecognized field attribute: {}", + name_value.to_token_stream() + ); + } + } + } + } + + struct_attrs +} + +#[derive(Default)] +pub struct FieldAttrs { + pub rename: Option, + pub ns: Option, + pub text: bool, + pub untagged: bool, + pub flatten: bool, + pub default: Option, +} + +pub fn parse_field_attrs(attrs: &[Attribute]) -> FieldAttrs { + let mut field_attrs = FieldAttrs::default(); + + let attrs = get_scoped_attrs(attrs); + let attrs = if let Some(attrs) = attrs { + attrs + } else { + return field_attrs; + }; + + for meta in attrs { + match meta { + // single flag + Meta::Path(path) => { + if path.is_ident("text") { + field_attrs.text = true; + } + if path.is_ident("untagged") { + field_attrs.untagged = true; + } + if path.is_ident("flatten") { + field_attrs.flatten = true; + } + } + Meta::List(list) => { + panic!("list-type attrs not supported: {}", list.to_token_stream()); + } + Meta::NameValue(name_value) => { + if name_value.path.is_ident("ns") { + if let Expr::Lit(ExprLit { + lit: Lit::Str(lit_str), + .. + }) = name_value.value + { + field_attrs.ns = Some(lit_str.value()); + } + } else if name_value.path.is_ident("rename") { + if let Expr::Lit(ExprLit { + lit: Lit::Str(lit_str), + .. + }) = name_value.value + { + field_attrs.rename = Some(lit_str.value()); + } else { + panic!("invalid rename attribute"); + } + } else if name_value.path.is_ident("default") { + if let Expr::Lit(ExprLit { + lit: Lit::Str(lit_str), + .. + }) = name_value.value + { + let a: syn::ExprPath = syn::parse_str(&lit_str.value()) + .expect("could not parse default attribute as expression"); + field_attrs.default = Some(a); + } else { + panic!("invalid default attribute"); + } + } else { + panic!( + "unrecognized field attribute: {}", + name_value.to_token_stream() + ); + } + } + } + } + + field_attrs +} diff --git a/crates/xml/derive/src/de/de_enum.rs b/crates/xml/derive/src/de/de_enum.rs new file mode 100644 index 0000000..f5595c1 --- /dev/null +++ b/crates/xml/derive/src/de/de_enum.rs @@ -0,0 +1,74 @@ +use crate::de::attrs::parse_variant_attrs; +use proc_macro2::Span; +use quote::quote; +use syn::{DataEnum, DeriveInput, Fields, FieldsUnnamed, Variant}; + +pub fn enum_variant_branch(variant: &Variant) -> proc_macro2::TokenStream { + let ident = &variant.ident; + + match &variant.fields { + Fields::Named(named) => { + panic!("struct variants are not supported, please use a tuple variant with a struct") + } + Fields::Unnamed(FieldsUnnamed { unnamed, .. }) => { + if unnamed.len() != 1 { + panic!("tuple variants should contain exactly one element"); + } + let field = unnamed.iter().next().unwrap(); + quote! { + let val = #field::deserialize(reader, start, empty)?; + Ok(Self::#ident(val)) + } + } + Fields::Unit => { + quote! { + // Make sure that content is still consumed + ::rustical_xml::Unit::deserialize(reader, start, empty)?; + Ok(Self::#ident) + } + } + } +} + +pub fn impl_de_enum(input: &DeriveInput, data: &DataEnum) -> proc_macro2::TokenStream { + let (impl_generics, type_generics, where_clause) = input.generics.split_for_impl(); + let name = &input.ident; + + let variants = data.variants.iter().map(|variant| { + let attrs = parse_variant_attrs(&variant.attrs); + + let variant_name = attrs.rename.unwrap_or(variant.ident.to_string()); + let variant_name = syn::LitByteStr::new(variant_name.as_bytes(), Span::call_site()); + let branch = enum_variant_branch(variant); + // dbg!(variant.fields.to_token_stream()); + + quote! { + #variant_name => { + #branch + } + } + }); + + quote! { + impl #impl_generics ::rustical_xml::XmlDeserialize for #name #type_generics #where_clause { + fn deserialize( + reader: &mut quick_xml::NsReader, + start: &quick_xml::events::BytesStart, + empty: bool + ) -> Result { + use quick_xml::events::Event; + + let (_ns, name) = reader.resolve_element(start.name()); + + + match name.as_ref() { + #(#variants)* + name => { + // Handle invalid variant name + Err(rustical_xml::XmlError::InvalidVariant(String::from_utf8_lossy(name).to_string())) + } + } + } + } + } +} diff --git a/crates/xml/derive/src/de/de_struct.rs b/crates/xml/derive/src/de/de_struct.rs new file mode 100644 index 0000000..d219b8a --- /dev/null +++ b/crates/xml/derive/src/de/de_struct.rs @@ -0,0 +1,278 @@ +use super::attrs::{parse_field_attrs, parse_struct_attrs, FieldAttrs}; +use core::panic; +use heck::ToKebabCase; +use proc_macro2::Span; +use quote::quote; +use syn::{AngleBracketedGenericArguments, DataStruct, DeriveInput, TypePath}; + +fn get_generic_type(ty: &syn::Type) -> Option<&syn::Type> { + if let syn::Type::Path(TypePath { path, .. }) = ty { + if let Some(seg) = path.segments.last() { + if let syn::PathArguments::AngleBracketed(AngleBracketedGenericArguments { + args, .. + }) = &seg.arguments + { + if let Some(syn::GenericArgument::Type(t)) = &args.first() { + return Some(t); + } + } + } + } + None +} + +pub struct Field { + pub field: syn::Field, + pub attrs: FieldAttrs, +} + +impl Field { + fn from_syn_field(field: syn::Field) -> Self { + Self { + attrs: parse_field_attrs(&field.attrs), + field, + } + } + fn de_name(&self) -> String { + self.attrs + .rename + .to_owned() + .unwrap_or(self.field_ident().to_string().to_kebab_case()) + } + + fn field_ident(&self) -> &syn::Ident { + self.field + .ident + .as_ref() + .expect("tuple structs not supported") + } + + fn ty(&self) -> &syn::Type { + &self.field.ty + } + + fn builder_field(&self) -> proc_macro2::TokenStream { + let field_ident = self.field_ident(); + let ty = self.ty(); + if self.attrs.default.is_some() { + quote! { + #field_ident: #ty, + } + } else if self.attrs.flatten { + let generic_type = get_generic_type(ty).expect("flatten attribute only implemented for explicit generics (rustical_xml will assume the first generic as the inner type)"); + quote! { + #field_ident: Vec<#generic_type>, + } + } else { + quote! { + #field_ident: Option<#ty>, + } + } + } + + fn builder_field_init(&self) -> proc_macro2::TokenStream { + let field_ident = self.field_ident(); + if let Some(default) = &self.attrs.default { + quote! { + #field_ident: #default(), + } + } else if self.attrs.flatten { + quote! { + #field_ident: vec![], + } + } else { + quote! { + #field_ident: None, + } + } + } + + fn builder_field_build(&self) -> proc_macro2::TokenStream { + let field_ident = self.field_ident(); + if self.attrs.flatten { + quote! { + #field_ident: FromIterator::from_iter(builder.#field_ident.into_iter()) + } + } else if self.attrs.default.is_some() { + quote! { + #field_ident: builder.#field_ident, + } + } else { + quote! { + #field_ident: builder.#field_ident.expect("todo: handle missing field"), + } + } + } + + fn named_branch(&self) -> Option { + if self.attrs.text { + return None; + } + if self.attrs.untagged { + return None; + } + let field_name = syn::LitByteStr::new(self.de_name().as_bytes(), Span::call_site()); + let field_ident = self.field_ident(); + let deserializer = self.ty(); + Some(if self.attrs.default.is_some() { + quote! { + #field_name => { + builder.#field_ident = <#deserializer as rustical_xml::XmlDeserialize>::deserialize(reader, &start, empty)?; + }, + } + } else if self.attrs.flatten { + let deserializer = get_generic_type(self.ty()).expect("flatten attribute only implemented for explicit generics (rustical_xml will assume the first generic as the inner type)"); + quote! { + #field_name => { + builder.#field_ident.push(<#deserializer as rustical_xml::XmlDeserialize>::deserialize(reader, &start, empty)?); + }, + } + } else { + quote! { + #field_name => { + builder.#field_ident = Some(<#deserializer as rustical_xml::XmlDeserialize>::deserialize(reader, &start, empty)?); + }, + } + }) + } + + fn untagged_branch(&self) -> Option { + if !self.attrs.untagged { + return None; + } + let field_ident = self.field_ident(); + let deserializer = self.ty(); + Some(quote! { + _ => { + builder.#field_ident = Some(<#deserializer as rustical_xml::XmlDeserialize>::deserialize(reader, &start, empty)?); + }, + }) + } + + fn text_branch(&self) -> Option { + if !self.attrs.text { + return None; + } + let field_ident = self.field_ident(); + Some(quote! { + builder.#field_ident = Some(text.to_owned().into()); + }) + } +} + +pub fn impl_de_struct(input: &DeriveInput, data: &DataStruct) -> proc_macro2::TokenStream { + let (impl_generics, type_generics, where_clause) = input.generics.split_for_impl(); + let name = &input.ident; + + let struct_attrs = parse_struct_attrs(&input.attrs); + + let fields: Vec<_> = data + .fields + .iter() + .map(|field| Field::from_syn_field(field.to_owned())) + .collect(); + + let builder_fields = fields.iter().map(Field::builder_field); + let builder_field_inits = fields.iter().map(Field::builder_field_init); + let named_field_branches = fields.iter().filter_map(Field::named_branch); + let untagged_field_branches: Vec<_> = + fields.iter().filter_map(Field::untagged_branch).collect(); + if untagged_field_branches.len() > 1 { + panic!("Currently only one untagged field supported!"); + } + let text_field_branches = fields.iter().filter_map(Field::text_branch); + let builder_field_builds = fields.iter().map(Field::builder_field_build); + + let xml_root_impl = if let Some(root) = struct_attrs.root { + quote! { + impl #impl_generics ::rustical_xml::XmlRoot for #name #type_generics #where_clause { + fn root_tag() -> &'static [u8] { + #root + } + } + } + } else { + quote! {} + }; + + quote! { + #xml_root_impl + + impl #impl_generics ::rustical_xml::XmlDeserialize for #name #type_generics #where_clause { + fn deserialize( + reader: &mut quick_xml::NsReader, + start: &quick_xml::events::BytesStart, + empty: bool + ) -> Result { + use quick_xml::events::Event; + use rustical_xml::XmlError; + + let mut buf = Vec::new(); + + // initialise fields + struct StructBuilder { + #(#builder_fields)* + } + + let mut builder = StructBuilder { + #(#builder_field_inits)* + }; + + if !empty { + loop { + let event = reader.read_event_into(&mut buf)?; + match &event { + Event::End(e) if e.name() == start.name() => { + break; + } + Event::Eof => return Err(XmlError::Eof), + // start of a child element + Event::Start(start) | Event::Empty(start) => { + let empty = matches!(event, Event::Empty(_)); + let (_ns, name) = reader.resolve_element(start.name()); + match name.as_ref() { + #(#named_field_branches)* + #(#untagged_field_branches)* + _ => { + // invalid field name + return Err(XmlError::UnknownError) + } + } + } + Event::Text(bytes_text) => { + let text = bytes_text.unescape()?; + #(#text_field_branches)* + } + Event::CData(cdata) => { + return Err(XmlError::UnsupportedEvent("CDATA")); + } + Event::Comment(_) => { + // ignore + } + Event::Decl(_) => { + // Error: not supported + return Err(XmlError::UnsupportedEvent("Declaration")); + } + Event::PI(_) => { + // Error: not supported + return Err(XmlError::UnsupportedEvent("Processing instruction")); + } + Event::DocType(doctype) => { + // Error: start of new document + return Err(XmlError::UnsupportedEvent("Doctype in the middle of the document")); + } + Event::End(end) => { + // Error: premature end + return Err(XmlError::Other("Unexpected closing tag for wrong element".to_owned())); + } + } + } + } + + Ok(Self { + #(#builder_field_builds)* + }) + } + } + } +} diff --git a/crates/xml/derive/src/de/mod.rs b/crates/xml/derive/src/de/mod.rs new file mode 100644 index 0000000..9666e9e --- /dev/null +++ b/crates/xml/derive/src/de/mod.rs @@ -0,0 +1,6 @@ +pub mod attrs; +mod de_enum; +mod de_struct; + +pub use de_enum::impl_de_enum; +pub use de_struct::impl_de_struct; diff --git a/crates/xml/derive/src/lib.rs b/crates/xml/derive/src/lib.rs new file mode 100644 index 0000000..5d355f8 --- /dev/null +++ b/crates/xml/derive/src/lib.rs @@ -0,0 +1,18 @@ +use core::panic; +use syn::{parse_macro_input, DeriveInput}; + +mod de; + +use de::{impl_de_enum, impl_de_struct}; + +#[proc_macro_derive(XmlDeserialize, attributes(xml))] +pub fn derive_xml_deserialize(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let input = parse_macro_input!(input as DeriveInput); + + match &input.data { + syn::Data::Enum(e) => impl_de_enum(&input, e), + syn::Data::Struct(s) => impl_de_struct(&input, s), + syn::Data::Union(_) => panic!("Union not supported"), + } + .into() +} diff --git a/crates/xml/src/lib.rs b/crates/xml/src/lib.rs new file mode 100644 index 0000000..fb5c9ac --- /dev/null +++ b/crates/xml/src/lib.rs @@ -0,0 +1,136 @@ +use quick_xml::events::{BytesStart, Event}; +use std::io::BufRead; +use thiserror::Error; +pub use xml_derive::XmlDeserialize; + +#[derive(Debug, Error)] +pub enum XmlError { + #[error(transparent)] + QuickXmlDeError(#[from] quick_xml::de::DeError), + #[error(transparent)] + QuickXmlError(#[from] quick_xml::Error), + #[error("Unknown error")] + UnknownError, + #[error("Invalid tag {0}. Expected {1}")] + InvalidTag(String, String), + #[error("Missing field {0}")] + MissingField(&'static str), + #[error("End of file, expected closing tags")] + Eof, + #[error("Unsupported xml event: {0}")] + UnsupportedEvent(&'static str), + #[error("{0}")] + Other(String), + #[error("Invalid variant: {0}")] + InvalidVariant(String), +} + +pub trait XmlDeserialize: Sized { + fn deserialize( + reader: &mut quick_xml::NsReader, + start: &BytesStart, + empty: bool, + ) -> Result; +} + +pub trait XmlRoot: XmlDeserialize { + fn parse(mut reader: quick_xml::NsReader) -> Result { + let mut buf = Vec::new(); + let event = reader.read_event_into(&mut buf)?; + match event { + Event::Start(start) => { + let (_ns, name) = reader.resolve_element(start.name()); + if name.as_ref() != Self::root_tag() { + return Err(XmlError::InvalidTag( + String::from_utf8_lossy(name.as_ref()).to_string(), + String::from_utf8_lossy(Self::root_tag()).to_string(), + )); + }; + // TODO: check namespace + + return Self::deserialize(&mut reader, &start, false); + } + Event::Empty(start) => { + let (_ns, name) = reader.resolve_element(start.name()); + if name.as_ref() != Self::root_tag() { + return Err(XmlError::InvalidTag( + String::from_utf8_lossy(name.as_ref()).to_string(), + String::from_utf8_lossy(Self::root_tag()).to_string(), + )); + }; + // TODO: check namespace + + return Self::deserialize(&mut reader, &start, true); + } + _ => {} + }; + Err(XmlError::UnknownError) + } + + fn parse_str(input: &str) -> Result { + let mut reader = quick_xml::NsReader::from_str(input); + reader.config_mut().trim_text(true); + Self::parse(reader) + } + + fn root_tag() -> &'static [u8]; +} + +impl XmlDeserialize for Option { + fn deserialize( + reader: &mut quick_xml::NsReader, + start: &BytesStart, + empty: bool, + ) -> Result { + Ok(Some(T::deserialize(reader, start, empty)?)) + } +} + +pub struct Unit; + +impl XmlDeserialize for Unit { + fn deserialize( + reader: &mut quick_xml::NsReader, + start: &BytesStart, + empty: bool, + ) -> Result { + if empty { + return Ok(Unit); + } + let mut buf = Vec::new(); + loop { + match reader.read_event_into(&mut buf)? { + Event::End(e) if e.name() == start.name() => return Ok(Unit), + Event::Eof => return Err(XmlError::Eof), + _ => {} + }; + } + } +} + +impl XmlDeserialize for String { + fn deserialize( + reader: &mut quick_xml::NsReader, + start: &BytesStart, + empty: bool, + ) -> Result { + if empty { + return Ok(String::new()); + } + let mut content = String::new(); + let mut buf = Vec::new(); + loop { + match reader.read_event_into(&mut buf)? { + Event::End(e) if e.name() == start.name() => { + break; + } + Event::Eof => return Err(XmlError::Eof), + Event::Text(text) => { + content.push_str(&text.unescape()?); + } + _a => return Err(XmlError::UnknownError), + }; + } + Ok(content) + } +} diff --git a/crates/xml/tests/de_enum.rs b/crates/xml/tests/de_enum.rs new file mode 100644 index 0000000..e1819ea --- /dev/null +++ b/crates/xml/tests/de_enum.rs @@ -0,0 +1,45 @@ +use rustical_xml::XmlRoot; +use xml_derive::XmlDeserialize; + +#[test] +fn test_struct_untagged_enum() { + #[derive(Debug, XmlDeserialize, PartialEq)] + struct Propfind { + prop: Prop, + } + + #[derive(Debug, XmlDeserialize, PartialEq)] + struct Prop { + #[xml(untagged)] + prop: PropEnum, + } + + #[derive(Debug, XmlDeserialize, PartialEq)] + enum PropEnum { + A, + B, + } + + impl XmlRoot for Propfind { + fn root_tag() -> &'static [u8] { + b"propfind" + } + } + + let doc = Propfind::parse_str( + r#" + + + + + + "#, + ) + .unwrap(); + assert_eq!( + doc, + Propfind { + prop: Prop { prop: PropEnum::A } + } + ); +} diff --git a/crates/xml/tests/de_struct.rs b/crates/xml/tests/de_struct.rs new file mode 100644 index 0000000..a97de1f --- /dev/null +++ b/crates/xml/tests/de_struct.rs @@ -0,0 +1,171 @@ +use std::collections::HashSet; + +use rustical_xml::XmlRoot; +use xml_derive::XmlDeserialize; + +#[test] +fn test_struct_text_field() { + #[derive(Debug, XmlDeserialize, PartialEq)] + struct Document { + #[xml(text)] + text: String, + #[xml(text)] + text2: String, + } + + impl XmlRoot for Document { + fn root_tag() -> &'static [u8] { + b"document" + } + } + + let doc = Document::parse_str(r#"Hello!"#).unwrap(); + assert_eq!( + doc, + Document { + text: "Hello!".to_owned(), + text2: "Hello!".to_owned() + } + ); +} + +#[test] +fn test_struct_document() { + #[derive(Debug, XmlDeserialize, PartialEq)] + struct Document { + child: Child, + } + + #[derive(Debug, XmlDeserialize, PartialEq, Default)] + struct Child { + #[xml(text)] + text: String, + } + + impl XmlRoot for Document { + fn root_tag() -> &'static [u8] { + b"document" + } + } + + let doc = Document::parse_str(r#"Hello!"#).unwrap(); + assert_eq!( + doc, + Document { + child: Child { + text: "Hello!".to_owned() + } + } + ); +} + +#[test] +fn test_struct_rename_field() { + #[derive(Debug, XmlDeserialize, PartialEq)] + struct Document { + #[xml(rename = "ok-wow")] + child: Child, + } + + #[derive(Debug, XmlDeserialize, PartialEq, Default)] + struct Child { + #[xml(text)] + text: String, + } + + impl XmlRoot for Document { + fn root_tag() -> &'static [u8] { + b"document" + } + } + + let doc = Document::parse_str(r#"Hello!"#).unwrap(); + assert_eq!( + doc, + Document { + child: Child { + text: "Hello!".to_owned() + }, + } + ); +} + +#[test] +fn test_struct_optional_field() { + #[derive(Debug, XmlDeserialize, PartialEq)] + struct Document { + #[xml(default = "Default::default")] + child: Option, + } + + impl XmlRoot for Document { + fn root_tag() -> &'static [u8] { + b"document" + } + } + + #[derive(Debug, XmlDeserialize, PartialEq, Default)] + struct Child; + + let doc = Document::parse_str(r#""#).unwrap(); + assert_eq!(doc, Document { child: Some(Child) }); + + let doc = Document::parse_str(r#""#).unwrap(); + assert_eq!(doc, Document { child: None }); +} + +#[test] +fn test_struct_vec() { + #[derive(Debug, XmlDeserialize, PartialEq)] + #[xml(root = b"document")] + struct Document { + #[xml(rename = "child", flatten)] + children: Vec, + } + + #[derive(Debug, XmlDeserialize, PartialEq, Default)] + struct Child; + + let doc = Document::parse_str( + r#" + + + + "#, + ) + .unwrap(); + assert_eq!( + doc, + Document { + children: vec![Child, Child] + } + ); +} + +#[test] +fn test_struct_set() { + #[derive(Debug, XmlDeserialize, PartialEq)] + #[xml(root = b"document")] + struct Document { + #[xml(rename = "child", flatten)] + children: HashSet, + } + + #[derive(Debug, XmlDeserialize, PartialEq, Default, Eq, Hash)] + struct Child; + + let doc = Document::parse_str( + r#" + + + + "#, + ) + .unwrap(); + assert_eq!( + doc, + Document { + children: HashSet::from_iter(vec![Child].into_iter()) + } + ); +} diff --git a/crates/xml/tests/propfind.rs b/crates/xml/tests/propfind.rs new file mode 100644 index 0000000..a5a8166 --- /dev/null +++ b/crates/xml/tests/propfind.rs @@ -0,0 +1,123 @@ +use quick_xml::events::{BytesStart, Event}; +use rustical_xml::{Unit, XmlDeserialize, XmlError, XmlRoot}; + +#[derive(Debug, XmlDeserialize)] +#[xml(rename_all = "kebab-case")] +pub enum Prop { + #[xml(rename = "displayname")] + Displayname(String), + #[xml(ns = "DAV:Push", rename = "transports")] + Transports, +} + +#[derive(Debug)] +pub struct PropfindElement { + // child with name propfind and namespace DAV: + pub prop: Vec, + pub test: Option, +} + +impl XmlDeserialize for PropfindElement { + fn deserialize( + reader: &mut quick_xml::NsReader, + start: &BytesStart, + empty: bool, + ) -> Result { + // init values for the struct attributes + let mut attr_prop: Option> = None; + let mut attr_test: Option = None; + + if !empty { + let mut buf = Vec::new(); + loop { + match reader.read_event_into(&mut buf)? { + Event::End(e) if e.name() == start.name() => { + break; + } + Event::Eof => return Err(XmlError::Eof), + Event::Start(start) => { + let (_ns, name) = reader.resolve_element(start.name()); + match name.as_ref() { + b"prop" => { + if attr_prop.is_none() { + attr_prop = Some(Vec::::deserialize(reader, &start, false)?); + } + } + b"test" => { + if attr_test.is_none() { + attr_test = Some(Prop::deserialize(reader, &start, false)?); + } + } + _ => { + return Err(XmlError::InvalidTag( + String::from_utf8_lossy(name.as_ref()).to_string(), + "prop".to_string(), + )); + } + } + } + Event::Empty(start) => { + let (_ns, name) = reader.resolve_element(start.name()); + match name.as_ref() { + b"prop" => { + if attr_prop.is_none() { + attr_prop = Some(Vec::::deserialize(reader, &start, true)?); + } + } + b"test" => { + if attr_test.is_none() { + attr_test = Some(Prop::deserialize(reader, &start, true)?); + } + } + _ => { + return Err(XmlError::InvalidTag( + String::from_utf8_lossy(name.as_ref()).to_string(), + "prop".to_string(), + )); + } + } + } + a => { + dbg!(a); + } + }; + } + } + + let attr_prop = attr_prop.ok_or(XmlError::MissingField("prop"))?; + Ok(Self { + prop: attr_prop, + test: None, + }) + } +} + +impl XmlRoot for PropfindElement { + fn root_tag() -> &'static [u8] { + b"propfind" + } +} + +#[test] +fn test_propfind() { + let propfind: PropfindElement = PropfindElement::parse_str( + r#" + + + Hello! + + + + + Okay wow! + + + "#, + ) + .unwrap(); + dbg!(propfind); +} + +fn asd() { + let a: Option = None; +}