From 60628c8680d8d403c4d573b4582fea77069b2393 Mon Sep 17 00:00:00 2001 From: John Hughes Date: Sat, 6 Sep 2025 12:54:54 +0200 Subject: [PATCH 1/8] feat: add serialization support for Vec as an attribute --- yaserde/tests/serializer.rs | 27 ++++++++++++++++++++ yaserde_derive/src/ser/expand_struct.rs | 33 ++++++++++++++++++++++--- 2 files changed, 57 insertions(+), 3 deletions(-) diff --git a/yaserde/tests/serializer.rs b/yaserde/tests/serializer.rs index b575206..c48c5c1 100644 --- a/yaserde/tests/serializer.rs +++ b/yaserde/tests/serializer.rs @@ -415,3 +415,30 @@ 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![3.14, 2.71], + }; + + // Expected XML with space-separated attribute values + let content = r#""#; + serialize_and_validate!(model, content); +} diff --git a/yaserde_derive/src/ser/expand_struct.rs b/yaserde_derive/src/ser/expand_struct.rs index 2a4507b..1a46c08 100644 --- a/yaserde_derive/src/ser/expand_struct.rs +++ b/yaserde_derive/src/ser/expand_struct.rs @@ -115,9 +115,36 @@ 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 { .. } => { + unimplemented!("Struct fields in Vec not supported for attributes") + } } } } else { From 2ba7316befd516221c1930f6595c75b6b44edfad Mon Sep 17 00:00:00 2001 From: John Hughes Date: Sat, 6 Sep 2025 13:13:28 +0200 Subject: [PATCH 2/8] feat: add support for nested Vec attributes in serialization --- yaserde/tests/serializer.rs | 25 +++++++++++++++++++++++++ yaserde_derive/src/ser/expand_struct.rs | 17 +++++++++++++---- 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/yaserde/tests/serializer.rs b/yaserde/tests/serializer.rs index c48c5c1..3c60d18 100644 --- a/yaserde/tests/serializer.rs +++ b/yaserde/tests/serializer.rs @@ -442,3 +442,28 @@ fn ser_vec_as_attribute() { 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); +} diff --git a/yaserde_derive/src/ser/expand_struct.rs b/yaserde_derive/src/ser/expand_struct.rs index 1a46c08..0f31e31 100644 --- a/yaserde_derive/src/ser/expand_struct.rs +++ b/yaserde_derive/src/ser/expand_struct.rs @@ -142,10 +142,19 @@ pub fn serialize( Field::FieldOption { .. } | Field::FieldVec { .. } => { unimplemented!("Nested Option or Vec in Vec not supported for attributes") } - Field::FieldStruct { .. } => { - unimplemented!("Struct fields 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() { From ab171bda5e565a5709f200104604f9691107bf1e Mon Sep 17 00:00:00 2001 From: John Hughes Date: Sat, 6 Sep 2025 16:13:05 +0200 Subject: [PATCH 3/8] fix: clippy warnings --- examples/src/bbigras_namespace.rs | 1 + examples/src/boscop.rs | 1 + examples/src/generic.rs | 1 + examples/src/ln_dom.rs | 1 + examples/src/same_element_different_namespaces.rs | 1 + yaserde/tests/deserializer.rs | 2 ++ yaserde/tests/serializer.rs | 4 ++-- yaserde_derive/src/common/field.rs | 10 +++++----- 8 files changed, 14 insertions(+), 7 deletions(-) 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/yaserde/tests/deserializer.rs b/yaserde/tests/deserializer.rs index 5b0e3af..e28af22 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, @@ -1108,6 +1109,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 3c60d18..a945fc1 100644 --- a/yaserde/tests/serializer.rs +++ b/yaserde/tests/serializer.rs @@ -435,11 +435,11 @@ fn ser_vec_as_attribute() { numbers: vec![1, 2, 3, 4], strings: vec!["hello".to_string(), "world".to_string()], bools: vec![true, false, true], - floats: vec![3.14, 2.71], + floats: vec![6.14, 2.71], }; // Expected XML with space-separated attribute values - let content = r#""#; + let content = r#""#; serialize_and_validate!(model, content); } 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!( From 16b84b1d34b4270d1ffbd723153dfc30517fe30a Mon Sep 17 00:00:00 2001 From: John Hughes Date: Sat, 6 Sep 2025 21:55:19 +0200 Subject: [PATCH 4/8] fix: update XML encoding to be consistent (UTF-8) --- examples/tests/data/svd.xml | 2 +- yaserde/src/lib.rs | 2 +- yaserde/tests/cdata.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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); } From f67beadc805195bd3e40a1c72be5b841860446e3 Mon Sep 17 00:00:00 2001 From: John Hughes Date: Sun, 7 Sep 2025 11:25:43 +0200 Subject: [PATCH 5/8] feat: add support for Option as an attribute in serialization and deserialization --- yaserde/tests/deserializer.rs | 31 ++++++++++++++++++++ yaserde/tests/serializer.rs | 28 ++++++++++++++++++ yaserde_derive/src/de/expand_struct.rs | 37 ++++++++++++++++++++++-- yaserde_derive/src/ser/element.rs | 4 --- yaserde_derive/src/ser/expand_struct.rs | 38 ++++++++++++++++--------- 5 files changed, 118 insertions(+), 20 deletions(-) diff --git a/yaserde/tests/deserializer.rs b/yaserde/tests/deserializer.rs index e28af22..b332eab 100644 --- a/yaserde/tests/deserializer.rs +++ b/yaserde/tests/deserializer.rs @@ -1102,6 +1102,37 @@ 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_nested_macro_rules() { init(); diff --git a/yaserde/tests/serializer.rs b/yaserde/tests/serializer.rs index a945fc1..93abb57 100644 --- a/yaserde/tests/serializer.rs +++ b/yaserde/tests/serializer.rs @@ -467,3 +467,31 @@ fn ser_vec_as_attribute_nested() { 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); +} diff --git a/yaserde_derive/src/de/expand_struct.rs b/yaserde_derive/src/de/expand_struct.rs index fca7486..8cc436a 100644 --- a/yaserde_derive/src/de/expand_struct.rs +++ b/yaserde_derive/src/de/expand_struct.rs @@ -127,7 +127,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 { @@ -259,6 +264,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 +308,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 0f31e31..15ae6e0 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 { .. } => field.ser_wrap_default_attribute( + Some(quote! { + self.#label + .as_ref() + .map_or_else( + || ::std::string::String::new(), + |yaserde_list| { + yaserde_list + .iter() + .map(|item| item.to_string()) + .collect::<::std::vec::Vec<_>>() + .join(" ") } - } - }), - ) - } + ) + }), + 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::FieldStruct { .. } => field.ser_wrap_default_attribute( Some(quote! { self.#label From db3727903f86ce9658785a1745c7189d0fddd3da Mon Sep 17 00:00:00 2001 From: John Hughes Date: Sun, 7 Sep 2025 13:25:56 +0200 Subject: [PATCH 6/8] feat: add serialization and deserialization support for Option> attributes --- yaserde/tests/deserializer.rs | 39 ++++++++++++ yaserde/tests/serializer.rs | 34 ++++++++++ yaserde_derive/src/ser/expand_struct.rs | 85 +++++++++++++++++-------- 3 files changed, 133 insertions(+), 25 deletions(-) diff --git a/yaserde/tests/deserializer.rs b/yaserde/tests/deserializer.rs index b332eab..d0754f8 100644 --- a/yaserde/tests/deserializer.rs +++ b/yaserde/tests/deserializer.rs @@ -1133,6 +1133,45 @@ fn de_option_vec_as_attribute() { 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(); diff --git a/yaserde/tests/serializer.rs b/yaserde/tests/serializer.rs index 93abb57..8330fa2 100644 --- a/yaserde/tests/serializer.rs +++ b/yaserde/tests/serializer.rs @@ -495,3 +495,37 @@ fn ser_option_vec_as_attribute() { let content = r#""#; serialize_and_validate!(model, content); } + +#[test] +fn ser_option_vec_enum_as_attribute() { + #[derive(YaSerialize, PartialEq, Debug)] + enum MyEnum { + One, + Two, + Three, + } + + #[derive(YaSerialize, PartialEq, Debug)] + #[yaserde(rename = "TestTag")] + pub struct OptionVecEnumAttributeStruct { + #[yaserde(attribute = true)] + field: Option>, + } + + // Expected XML with space-separated attribute values + let model = OptionVecEnumAttributeStruct { + field: Some(vec![MyEnum::One, MyEnum::Two, MyEnum::Three]), + }; + let content = r#""#; + serialize_and_validate!(model, content); + + let model = OptionVecEnumAttributeStruct { + field: Some(vec![]), + }; + let content = r#""#; + serialize_and_validate!(model, content); + + let model = OptionVecEnumAttributeStruct { field: None }; + let content = r#""#; + serialize_and_validate!(model, content); +} diff --git a/yaserde_derive/src/ser/expand_struct.rs b/yaserde_derive/src/ser/expand_struct.rs index 15ae6e0..e0548f0 100644 --- a/yaserde_derive/src/ser/expand_struct.rs +++ b/yaserde_derive/src/ser/expand_struct.rs @@ -75,31 +75,31 @@ pub fn serialize( } }), ), - Field::FieldVec { .. } => field.ser_wrap_default_attribute( - Some(quote! { - self.#label - .as_ref() - .map_or_else( - || ::std::string::String::new(), - |yaserde_list| { - yaserde_list - .iter() - .map(|item| item.to_string()) - .collect::<::std::vec::Vec<_>>() - .join(" ") - } - ) - }), - 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::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 @@ -410,3 +410,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) +} From 61733045abd36bfe4df0093ee7b6af1c50d6ccb3 Mon Sep 17 00:00:00 2001 From: John Hughes Date: Sun, 7 Sep 2025 19:51:05 +0200 Subject: [PATCH 7/8] feat: implement serialization support for Option> attributes with attribute distinction --- yaserde/tests/serializer.rs | 69 ++++++++++++++++--------- yaserde_derive/src/de/expand_struct.rs | 43 +++++++++++++-- yaserde_derive/src/ser/expand_struct.rs | 34 ++++++++---- 3 files changed, 109 insertions(+), 37 deletions(-) diff --git a/yaserde/tests/serializer.rs b/yaserde/tests/serializer.rs index 8330fa2..f2770e6 100644 --- a/yaserde/tests/serializer.rs +++ b/yaserde/tests/serializer.rs @@ -497,35 +497,56 @@ fn ser_option_vec_as_attribute() { } #[test] -fn ser_option_vec_enum_as_attribute() { - #[derive(YaSerialize, PartialEq, Debug)] - enum MyEnum { - One, - Two, - Three, +fn ser_option_vec_complex() { + #[derive(Default, PartialEq, Debug, YaSerialize)] + pub struct Start { + #[yaserde(attribute = true, rename = "value")] + pub value: String, } - #[derive(YaSerialize, PartialEq, Debug)] - #[yaserde(rename = "TestTag")] - pub struct OptionVecEnumAttributeStruct { - #[yaserde(attribute = true)] - field: Option>, + #[derive(Default, PartialEq, Debug, YaSerialize)] + #[yaserde(rename = "String")] + pub struct StringStruct { + #[yaserde(rename = "Start")] + pub start: Option>, } - // Expected XML with space-separated attribute values - let model = OptionVecEnumAttributeStruct { - field: Some(vec![MyEnum::One, MyEnum::Two, MyEnum::Three]), + // 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 = r#""#; - serialize_and_validate!(model, content); - let model = OptionVecEnumAttributeStruct { - field: Some(vec![]), - }; - let content = r#""#; - serialize_and_validate!(model, content); + let content = yaserde::ser::to_string(&model).unwrap(); + assert_eq!( + content, + "" + ); - let model = OptionVecEnumAttributeStruct { field: None }; - let content = r#""#; - serialize_and_validate!(model, 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/de/expand_struct.rs b/yaserde_derive/src/de/expand_struct.rs index 8cc436a..ff529ac 100644 --- a/yaserde_derive/src/de/expand_struct.rs +++ b/yaserde_derive/src/de/expand_struct.rs @@ -25,7 +25,18 @@ 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>))) @@ -196,9 +207,33 @@ 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) }), } diff --git a/yaserde_derive/src/ser/expand_struct.rs b/yaserde_derive/src/ser/expand_struct.rs index e0548f0..9cf8b51 100644 --- a/yaserde_derive/src/ser/expand_struct.rs +++ b/yaserde_derive/src/ser/expand_struct.rs @@ -259,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! { From e6f27d99115516ea82ee5fa56fdb8e2a538c0101 Mon Sep 17 00:00:00 2001 From: John Hughes Date: Mon, 15 Sep 2025 16:43:52 +0200 Subject: [PATCH 8/8] fix formatting --- yaserde_derive/src/de/expand_struct.rs | 46 ++++++++++++++++---------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/yaserde_derive/src/de/expand_struct.rs b/yaserde_derive/src/de/expand_struct.rs index ff529ac..58da43e 100644 --- a/yaserde_derive/src/de/expand_struct.rs +++ b/yaserde_derive/src/de/expand_struct.rs @@ -26,7 +26,9 @@ pub fn parse( .filter_map(|field| match field.get_type() { Field::FieldStruct { struct_name } => build_default_value(&field, Some(quote!(#struct_name))), Field::FieldOption { data_type } => match *data_type { - Field::FieldVec { data_type: inner_data_type } => match *inner_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>))) } @@ -208,28 +210,36 @@ pub fn parse( visit_struct(struct_name, 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::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]) - } - }) + 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_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) }),