diff --git a/examples/src/bbigras_namespace.rs b/examples/src/bbigras_namespace.rs index 23d4ad0..e294597 100644 --- a/examples/src/bbigras_namespace.rs +++ b/examples/src/bbigras_namespace.rs @@ -1,3 +1,4 @@ +#![allow(dead_code)] // related to issue https://github.com/media-io/yaserde/issues/15 use yaserde::*; diff --git a/examples/src/boscop.rs b/examples/src/boscop.rs index 07f516d..b083344 100644 --- a/examples/src/boscop.rs +++ b/examples/src/boscop.rs @@ -1,3 +1,4 @@ +#![allow(dead_code)] // related to issue https://github.com/media-io/yaserde/issues/3 use yaserde::*; diff --git a/examples/src/generic.rs b/examples/src/generic.rs index 5c242e1..f55a53b 100644 --- a/examples/src/generic.rs +++ b/examples/src/generic.rs @@ -1,3 +1,4 @@ +#![allow(dead_code)] use yaserde::*; #[derive(YaSerialize, YaDeserialize, Debug, Default, Clone, Eq, PartialEq)] diff --git a/examples/src/ln_dom.rs b/examples/src/ln_dom.rs index 78db84e..aaae195 100644 --- a/examples/src/ln_dom.rs +++ b/examples/src/ln_dom.rs @@ -1,3 +1,4 @@ +#![allow(dead_code)] // related to issue https://github.com/media-io/yaserde/issues/11 use yaserde::*; diff --git a/examples/src/same_element_different_namespaces.rs b/examples/src/same_element_different_namespaces.rs index 31cb4e1..be3668e 100644 --- a/examples/src/same_element_different_namespaces.rs +++ b/examples/src/same_element_different_namespaces.rs @@ -1,3 +1,4 @@ +#![allow(dead_code)] // related to issue https://github.com/media-io/yaserde/issues/186 use yaserde::*; diff --git a/examples/tests/data/svd.xml b/examples/tests/data/svd.xml index 8743645..0d806ae 100644 --- a/examples/tests/data/svd.xml +++ b/examples/tests/data/svd.xml @@ -1,4 +1,4 @@ - + Renesas diff --git a/yaserde/src/lib.rs b/yaserde/src/lib.rs index 6c86ac9..669e30e 100644 --- a/yaserde/src/lib.rs +++ b/yaserde/src/lib.rs @@ -328,7 +328,7 @@ macro_rules! serialize_and_validate { log::debug!("serialize_and_validate @ {}:{}", file!(), line!()); let data: Result = yaserde::ser::to_string(&$model); - let content = &format!(r#"{}"#, $content); + let content = &format!(r#"{}"#, $content); assert_eq!( data, Ok(content.split("\n").map(|s| s.trim()).collect::()) diff --git a/yaserde/tests/cdata.rs b/yaserde/tests/cdata.rs index a73868d..e06c4d0 100644 --- a/yaserde/tests/cdata.rs +++ b/yaserde/tests/cdata.rs @@ -18,7 +18,7 @@ fn test_cdata_serialization() { msgdata: "Some unescaped content".to_string(), }; let xml_output = yaserde::ser::to_string(&test_data).expect("Serialization failed"); - let expected_output = r#"Some unescaped content]]>"#; + let expected_output = r#"Some unescaped content]]>"#; assert_eq!(xml_output, expected_output); } diff --git a/yaserde/tests/deserializer.rs b/yaserde/tests/deserializer.rs index 5b0e3af..d0754f8 100644 --- a/yaserde/tests/deserializer.rs +++ b/yaserde/tests/deserializer.rs @@ -493,6 +493,7 @@ fn de_enum() { Black, } + #[allow(dead_code)] #[derive(YaDeserialize, PartialEq, Debug)] pub struct RGBColor { red: String, @@ -1101,6 +1102,76 @@ fn de_attribute_sequence() { deserialize_and_validate!(content, model, Outer); } +#[test] +fn de_option_vec_as_attribute() { + init(); + + #[derive(YaDeserialize, PartialEq, Debug)] + #[yaserde(rename = "TestTag")] + pub struct OptionVecAttributeStruct { + #[yaserde(attribute = true)] + field: Option>, + } + + // Test case 1: Some(populated_vec) -> field="1 2 3 4" + let content = r#""#; + let model = OptionVecAttributeStruct { + field: Some(vec![1, 2, 3, 4]), + }; + convert_and_validate!(content, OptionVecAttributeStruct, model); + + // Test case 2: Some(empty_vec) -> field="" + let content = r#""#; + let model = OptionVecAttributeStruct { + field: Some(vec![]), + }; + convert_and_validate!(content, OptionVecAttributeStruct, model); + + // Test case 3: None -> no attribute + let content = r#""#; + let model = OptionVecAttributeStruct { field: None }; + convert_and_validate!(content, OptionVecAttributeStruct, model); +} + +#[test] +fn de_option_vec_enum_as_attribute() { + init(); + + #[derive(YaDeserialize, PartialEq, Debug, Default)] + enum MyEnum { + #[default] + One, + Two, + Three, + } + + #[derive(YaDeserialize, PartialEq, Debug)] + #[yaserde(rename = "TestTag")] + pub struct OptionVecEnumAttributeStruct { + #[yaserde(attribute = true)] + field: Option>, + } + + // Test case 1: Some(vec![MyEnum::One, MyEnum::Two, MyEnum::Three]) -> field="One Two Three" + let content = r#""#; + let model = OptionVecEnumAttributeStruct { + field: Some(vec![MyEnum::One, MyEnum::Two, MyEnum::Three]), + }; + convert_and_validate!(content, OptionVecEnumAttributeStruct, model); + + // Test case 2: Some(empty_vec) -> field="" + let content = r#""#; + let model = OptionVecEnumAttributeStruct { + field: Some(vec![]), + }; + convert_and_validate!(content, OptionVecEnumAttributeStruct, model); + + // Test case 3: None -> no attribute + let content = r#""#; + let model = OptionVecEnumAttributeStruct { field: None }; + convert_and_validate!(content, OptionVecEnumAttributeStruct, model); +} + #[test] fn de_nested_macro_rules() { init(); @@ -1108,6 +1179,7 @@ fn de_nested_macro_rules() { macro_rules! float_attrs { ($type:ty) => { #[derive(PartialEq, Debug, YaDeserialize)] + #[allow(dead_code)] pub struct Outer { #[yaserde(attribute = true)] pub inner: Option<$type>, diff --git a/yaserde/tests/serializer.rs b/yaserde/tests/serializer.rs index b575206..f2770e6 100644 --- a/yaserde/tests/serializer.rs +++ b/yaserde/tests/serializer.rs @@ -415,3 +415,138 @@ fn ser_custom() { let content = "2020110"; serialize_and_validate!(model, content); } + +#[test] +fn ser_vec_as_attribute() { + #[derive(YaSerialize, PartialEq, Debug)] + #[yaserde(rename = "TestTag")] + pub struct VecAttributeStruct { + #[yaserde(attribute = true)] + numbers: Vec, + #[yaserde(attribute = true)] + strings: Vec, + #[yaserde(attribute = true)] + bools: Vec, + #[yaserde(attribute = true)] + floats: Vec, + } + + let model = VecAttributeStruct { + numbers: vec![1, 2, 3, 4], + strings: vec!["hello".to_string(), "world".to_string()], + bools: vec![true, false, true], + floats: vec![6.14, 2.71], + }; + + // Expected XML with space-separated attribute values + let content = r#""#; + serialize_and_validate!(model, content); +} + +#[test] +fn ser_vec_as_attribute_nested() { + #[derive(YaSerialize, PartialEq, Debug)] + #[yaserde(rename = "TestTag")] + struct VecAttributeStruct { + #[yaserde(attribute = true)] + outer: Vec, + } + + #[derive(YaSerialize, PartialEq, Debug)] + #[yaserde(rename = "TestTag")] + enum Inner { + One, + Two, + } + + let model = VecAttributeStruct { + outer: vec![Inner::One, Inner::Two], + }; + + // Expected XML with space-separated attribute values + let content = r#""#; + serialize_and_validate!(model, content); +} + +#[test] +fn ser_option_vec_as_attribute() { + #[derive(YaSerialize, PartialEq, Debug)] + #[yaserde(rename = "TestTag")] + pub struct OptionVecAttributeStruct { + #[yaserde(attribute = true)] + field: Option>, + } + + // Expected XML with space-separated attribute values + let model = OptionVecAttributeStruct { + field: Some(vec![1, 2, 3, 4]), + }; + let content = r#""#; + serialize_and_validate!(model, content); + + let model = OptionVecAttributeStruct { + field: Some(vec![]), + }; + let content = r#""#; + serialize_and_validate!(model, content); + + // Expected XML with no attributes + let model = OptionVecAttributeStruct { field: None }; + let content = r#""#; + serialize_and_validate!(model, content); +} + +#[test] +fn ser_option_vec_complex() { + #[derive(Default, PartialEq, Debug, YaSerialize)] + pub struct Start { + #[yaserde(attribute = true, rename = "value")] + pub value: String, + } + + #[derive(Default, PartialEq, Debug, YaSerialize)] + #[yaserde(rename = "String")] + pub struct StringStruct { + #[yaserde(rename = "Start")] + pub start: Option>, + } + + // Test serialization with Some(vec) + let model = StringStruct { + start: Some(vec![ + Start { + value: "First string".to_string(), + }, + Start { + value: "Second string".to_string(), + }, + Start { + value: "Third string".to_string(), + }, + ]), + }; + + let content = yaserde::ser::to_string(&model).unwrap(); + assert_eq!( + content, + "" + ); + + // Test serialization with None + let model_none = StringStruct { start: None }; + let content_none = yaserde::ser::to_string(&model_none).unwrap(); + assert_eq!( + content_none, + "" + ); + + // Test serialization with Some(empty_vec) + let model_empty = StringStruct { + start: Some(vec![]), + }; + let content_empty = yaserde::ser::to_string(&model_empty).unwrap(); + assert_eq!( + content_empty, + "" + ); +} diff --git a/yaserde_derive/src/common/field.rs b/yaserde_derive/src/common/field.rs index 06f650c..5ed8715 100644 --- a/yaserde_derive/src/common/field.rs +++ b/yaserde_derive/src/common/field.rs @@ -101,11 +101,11 @@ impl YaSerdeField { .map(|p| format!("{}_", p.to_upper_camel_case())) .unwrap_or_default(); - let attribute = self - .attributes - .attribute - .then_some("Attribute_".to_string()) - .unwrap_or_default(); + let attribute = if self.attributes.attribute { + "Attribute_" + } else { + Default::default() + }; Ident::new( &format!( diff --git a/yaserde_derive/src/de/expand_struct.rs b/yaserde_derive/src/de/expand_struct.rs index fca7486..58da43e 100644 --- a/yaserde_derive/src/de/expand_struct.rs +++ b/yaserde_derive/src/de/expand_struct.rs @@ -25,7 +25,20 @@ pub fn parse( .map(|field| YaSerdeField::new(field.clone())) .filter_map(|field| match field.get_type() { Field::FieldStruct { struct_name } => build_default_value(&field, Some(quote!(#struct_name))), - Field::FieldOption { .. } => build_default_value(&field, None), + Field::FieldOption { data_type } => match *data_type { + Field::FieldVec { + data_type: inner_data_type, + } => match *inner_data_type { + Field::FieldStruct { ref struct_name } => { + build_default_value(&field, Some(quote!(::std::vec::Vec<#struct_name>))) + } + simple_type => { + let type_token: TokenStream = simple_type.into(); + build_default_value(&field, Some(quote!(::std::vec::Vec<#type_token>))) + } + }, + _ => build_default_value(&field, None), + }, Field::FieldVec { data_type } => match *data_type { Field::FieldStruct { ref struct_name } => { build_default_vec_value(&field, Some(quote!(::std::vec::Vec<#struct_name>))) @@ -127,7 +140,12 @@ pub fn parse( Field::FieldStruct { struct_name } => struct_visitor(struct_name), Field::FieldOption { data_type } => match *data_type { Field::FieldStruct { struct_name } => struct_visitor(struct_name), - Field::FieldOption { .. } | Field::FieldVec { .. } => None, + Field::FieldOption { .. } => None, + Field::FieldVec { data_type } => match *data_type { + Field::FieldStruct { struct_name } => struct_visitor(struct_name), + Field::FieldOption { .. } | Field::FieldVec { .. } => None, + simple_type => simple_type_visitor(simple_type), + }, simple_type => simple_type_visitor(simple_type), }, Field::FieldVec { data_type } => match *data_type { @@ -191,9 +209,41 @@ pub fn parse( Field::FieldStruct { struct_name } => { visit_struct(struct_name, quote! { = ::std::option::Option::Some(value) }) } - Field::FieldOption { data_type } => { - visit_sub(data_type, quote! { = ::std::option::Option::Some(value) }) - } + Field::FieldOption { data_type } => match *data_type { + Field::FieldVec { + data_type: inner_data_type, + } => match *inner_data_type { + Field::FieldStruct { struct_name } => { + // Handle Option> + visit_struct( + struct_name, + quote! { + = if #value_label.is_some() { + #value_label.as_mut().unwrap().push(value); + #value_label + } else { + Some(vec![value]) + } + }, + ) + } + simple_type => { + // Handle Option> + visit_simple( + simple_type, + quote! { + = if #value_label.is_some() { + #value_label.as_mut().unwrap().push(value); + #value_label + } else { + Some(vec![value]) + } + }, + ) + } + }, + _ => visit_sub(data_type, quote! { = ::std::option::Option::Some(value) }), + }, Field::FieldVec { data_type } => visit_sub(data_type, quote! { .push(value) }), simple_type => visit_simple(simple_type, quote! { = ::std::option::Option::Some(value) }), } @@ -259,6 +309,23 @@ pub fn parse( }) }; + let visit_option_vec = |visitor: &Ident, visitor_label: &Ident| { + Some(quote! { + for attr in attributes { + if attr.name.local_name == #label_name { + if #label.is_none() { + #label = Some(Vec::new()); + } + for value in attr.value.split_whitespace() { + let visitor = #visitor_label{}; + let value = visitor.#visitor(value)?; + #label.as_mut().unwrap().push(value); + } + } + } + }) + }; + let visit_string = || { Some(quote! { for attr in attributes { @@ -286,7 +353,18 @@ pub fn parse( }; let visit_sub = |sub_type: Box, action: TokenStream| match *sub_type { - Field::FieldOption { .. } | Field::FieldVec { .. } => unimplemented!(), + Field::FieldOption { .. } => unimplemented!(), + Field::FieldVec { data_type } => match data_type.as_ref() { + Field::FieldStruct { struct_name } => visit_option_vec( + &Ident::new("visit_str", field.get_span()), + &field.get_visitor_ident(Some(struct_name)), + ), + Field::FieldOption { .. } | Field::FieldVec { .. } => unimplemented!("Not supported"), + simple_type => visit_option_vec( + &simple_type.get_simple_type_visitor(), + &field.get_visitor_ident(None), + ), + }, Field::FieldStruct { struct_name } => visit_struct(struct_name, action), simple_type => visit_simple(simple_type, action), }; diff --git a/yaserde_derive/src/ser/element.rs b/yaserde_derive/src/ser/element.rs index 0f2fb0c..24e8cd3 100644 --- a/yaserde_derive/src/ser/element.rs +++ b/yaserde_derive/src/ser/element.rs @@ -2,10 +2,6 @@ use crate::common::YaSerdeField; use proc_macro2::{Ident, TokenStream}; use quote::quote; -pub fn enclose_formatted_characters(label: &Ident, label_name: String) -> TokenStream { - enclose_xml_event(label_name, quote!(format!("{}", &self.#label))) -} - pub fn enclose_formatted_characters_for_value(label: &Ident, label_name: String) -> TokenStream { enclose_xml_event(label_name, quote!(format!("{}", #label))) } diff --git a/yaserde_derive/src/ser/expand_struct.rs b/yaserde_derive/src/ser/expand_struct.rs index 2a4507b..9cf8b51 100644 --- a/yaserde_derive/src/ser/expand_struct.rs +++ b/yaserde_derive/src/ser/expand_struct.rs @@ -75,21 +75,31 @@ pub fn serialize( } }), ), - Field::FieldVec { .. } => { - let item_ident = Ident::new("yaserde_item", field.get_span()); - let inner = enclose_formatted_characters(&item_ident, label_name); - - field.ser_wrap_default_attribute( - None, - quote!({ - if let ::std::option::Option::Some(ref yaserde_list) = self.#label { - for yaserde_item in yaserde_list.iter() { - #inner - } - } - }), - ) - } + Field::FieldVec { data_type } => match *data_type { + Field::FieldString + | Field::FieldBool + | Field::FieldI8 + | Field::FieldU8 + | Field::FieldI16 + | Field::FieldU16 + | Field::FieldI32 + | Field::FieldU32 + | Field::FieldI64 + | Field::FieldU64 + | Field::FieldF32 + | Field::FieldF64 => { + ser_option_vec_attribute(&field, &label, &label_name, quote!(item.to_string())) + } + Field::FieldStruct { .. } => ser_option_vec_attribute( + &field, + &label, + &label_name, + quote!(::yaserde::ser::to_string_content(item).unwrap_or_default()), + ), + _ => { + unimplemented!("Complex data types in Option> attributes not yet supported") + } + }, Field::FieldStruct { .. } => field.ser_wrap_default_attribute( Some(quote! { self.#label @@ -115,10 +125,46 @@ pub fn serialize( struct_start_event.attr(#label_name, &yaserde_inner) }), ), - Field::FieldVec { .. } => { - // TODO - quote!() - } + Field::FieldVec { data_type } => match *data_type { + Field::FieldString + | Field::FieldBool + | Field::FieldI8 + | Field::FieldU8 + | Field::FieldI16 + | Field::FieldU16 + | Field::FieldI32 + | Field::FieldU32 + | Field::FieldI64 + | Field::FieldU64 + | Field::FieldF32 + | Field::FieldF64 => field.ser_wrap_default_attribute( + Some(quote! { + self.#label + .iter() + .map(|item| item.to_string()) + .collect::<::std::vec::Vec<_>>() + .join(" ") + }), + quote!({ + struct_start_event.attr(#label_name, &yaserde_inner) + }), + ), + Field::FieldOption { .. } | Field::FieldVec { .. } => { + unimplemented!("Nested Option or Vec in Vec not supported for attributes") + } + Field::FieldStruct { .. } => field.ser_wrap_default_attribute( + Some(quote! { + self.#label + .iter() + .map(|item| ::yaserde::ser::to_string_content(item)) + .collect::<::std::result::Result<::std::vec::Vec<_>, _>>()? + .join(" ") + }), + quote!({ + struct_start_event.attr(#label_name, &yaserde_inner) + }), + ), + }, } } else { match field.get_type() { @@ -213,18 +259,34 @@ pub fn serialize( }) } Field::FieldVec { .. } => { - let item_ident = Ident::new("yaserde_item", field.get_span()); - let inner = enclose_formatted_characters_for_value(&item_ident, label_name); + // Only use attribute serialization if the field is marked as an attribute + if field.is_attribute() { + let item_ident = Ident::new("yaserde_item", field.get_span()); + let inner = enclose_formatted_characters_for_value(&item_ident, label_name); - Some(quote! { - #conditions { - if let ::std::option::Option::Some(ref yaserde_items) = &self.#label { - for yaserde_item in yaserde_items.iter() { - #inner + Some(quote! { + #conditions { + if let ::std::option::Option::Some(ref yaserde_items) = &self.#label { + for yaserde_item in yaserde_items.iter() { + #inner + } } } - } - }) + }) + } else { + // For non-attribute Option>, use standard serialization + Some(quote! { + #conditions { + if let ::std::option::Option::Some(ref items) = &self.#label { + for item in items.iter() { + writer.set_start_event_name(::std::option::Option::Some(#label_name.to_string())); + writer.set_skip_start_end(false); + ::yaserde::YaSerialize::serialize(item, writer)?; + } + } + } + }) + } } Field::FieldStruct { .. } => Some(if field.is_flatten() { quote! { @@ -364,3 +426,38 @@ pub fn serialize( generics, ) } + +/// Helper function to generate serialization code for Option> attributes +fn ser_option_vec_attribute( + field: &YaSerdeField, + label: &Option, + label_name: &str, + item_serializer: TokenStream, +) -> TokenStream { + let yaserde_inner_expr = quote! { + self.#label + .as_ref() + .map_or_else( + || ::std::string::String::new(), + |yaserde_list| { + yaserde_list + .iter() + .map(|item| #item_serializer) + .collect::<::std::vec::Vec<_>>() + .join(" ") + } + ) + }; + + let attribute_expr = quote!({ + if self.#label.is_some() && !yaserde_inner.is_empty() { + struct_start_event.attr(#label_name, &yaserde_inner) + } else if self.#label.is_some() { + struct_start_event.attr(#label_name, "") + } else { + struct_start_event + } + }); + + field.ser_wrap_default_attribute(Some(yaserde_inner_expr), attribute_expr) +}