diff --git a/src/de.rs b/src/de.rs index 7439935..c9720ab 100644 --- a/src/de.rs +++ b/src/de.rs @@ -6,8 +6,14 @@ use serde::{ use std::collections::HashMap; use std::str::FromStr; +#[derive(Debug, Clone)] +struct Section { + name: String, + fields: HashMap, +} + pub struct Deserializer { - sections: HashMap>, + sections: Vec
, } pub fn from_str<'a, T>(s: &'a str) -> Result @@ -21,9 +27,11 @@ where impl Deserializer { fn from_str(input: &str) -> Result { - let mut sections = HashMap::new(); - let mut current_section = String::new(); - sections.insert(current_section.clone(), HashMap::new()); + let mut sections = vec![Section { + name: String::new(), + fields: HashMap::new(), + }]; + let mut current_section = 0usize; for line in input.lines() { let line = line.trim(); @@ -35,8 +43,11 @@ impl Deserializer { // Section header if line.starts_with('[') && line.ends_with(']') { - current_section = line[1..line.len() - 1].to_string(); - sections.insert(current_section.clone(), HashMap::new()); + sections.push(Section { + name: line[1..line.len() - 1].to_string(), + fields: HashMap::new(), + }); + current_section = sections.len() - 1; continue; } @@ -45,15 +56,100 @@ impl Deserializer { let key = line[..eq_pos].trim().to_string(); let value = Self::unescape_value(line[eq_pos + 1..].trim())?; - if let Some(section) = sections.get_mut(¤t_section) { - section.insert(key, value); - } + sections[current_section].fields.insert(key, value); } } Ok(Deserializer { sections }) } + fn section_base_name(name: &str) -> &str { + if let Some((base, suffix)) = name.rsplit_once('.') + && !suffix.is_empty() + && suffix.chars().all(|c| c.is_ascii_digit()) + { + return base; + } + + name + } + + fn pascal_case(name: &str) -> String { + name.split('_') + .filter(|part| !part.is_empty()) + .map(|part| { + let mut chars = part.chars(); + match chars.next() { + Some(first) => { + let mut result = first.to_uppercase().to_string(); + result.push_str(chars.as_str()); + result + } + None => String::new(), + } + }) + .collect() + } + + fn singular_candidates(name: &str) -> Vec { + let mut candidates = vec![name.to_string()]; + + let singular = if let Some(base) = name.strip_suffix("ies") { + format!("{base}y") + } else if let Some(base) = name.strip_suffix("ses") { + base.to_string() + } else if let Some(base) = name.strip_suffix('s') { + base.to_string() + } else { + String::new() + }; + + if !singular.is_empty() && singular != name { + candidates.push(singular.clone()); + let pascal = Self::pascal_case(&singular); + if pascal != singular { + candidates.push(pascal); + } + } + + let pascal = Self::pascal_case(name); + if pascal != name { + candidates.push(pascal); + } + + candidates.sort(); + candidates.dedup(); + candidates + } + + fn section_indices_matching(&self, name: &str) -> Vec { + let candidates = Self::singular_candidates(name); + self.sections + .iter() + .enumerate() + .filter_map(|(index, section)| { + let base = Self::section_base_name(§ion.name); + candidates + .iter() + .any(|candidate| candidate == base) + .then_some(index) + }) + .collect() + } + + fn section_index(&self, name: &str) -> Option { + self.sections + .iter() + .enumerate() + .find_map(|(index, section)| { + (Self::section_base_name(§ion.name) == name).then_some(index) + }) + } + + fn section(&self, index: usize) -> &Section { + &self.sections[index] + } + fn unescape_value(value: &str) -> Result { let mut unescaped = String::with_capacity(value.len()); let mut iter = value.chars(); @@ -237,11 +333,14 @@ impl<'de> de::Deserializer<'de> for &mut Deserializer { visitor.visit_newtype_struct(self) } - fn deserialize_seq(self, _visitor: V) -> Result + fn deserialize_seq(self, visitor: V) -> Result where V: de::Visitor<'de>, { - Err(Error::UnsupportedFeature("sequences".to_string())) + visitor.visit_seq(SequenceDeserializer::new( + self, + (1..self.sections.len()).collect(), + )) } fn deserialize_tuple(self, _len: usize, _visitor: V) -> Result @@ -273,33 +372,18 @@ impl<'de> de::Deserializer<'de> for &mut Deserializer { fn deserialize_struct( self, name: &'static str, - _fields: &'static [&'static str], + fields: &'static [&'static str], visitor: V, ) -> Result where V: de::Visitor<'de>, { - // For root struct or when the struct name exists as a section - if name.is_empty() || self.sections.contains_key(name) { - if name.is_empty() { - // Root struct - deserialize the whole INI file - visitor.visit_map(RootStructAccess::new(self)) - } else { - // Named section exists - visitor.visit_map(StructAccess::new(self, name)) - } + if name.is_empty() { + visitor.visit_map(RootStructAccess::new(self, fields)) + } else if let Some(index) = self.section_index(name) { + visitor.visit_map(StructAccess::new(self, index)) } else { - // Check if any section exists (for renamed structs) - // This handles cases where the struct might be renamed via serde - if self.sections.len() > 1 - || (self.sections.len() == 1 && !self.sections.contains_key("")) - { - // We have sections, assume root struct - visitor.visit_map(RootStructAccess::new(self)) - } else { - // No sections or only root section - visitor.visit_map(StructAccess::new(self, "")) - } + visitor.visit_map(RootStructAccess::new(self, fields)) } } @@ -332,13 +416,13 @@ impl<'de> de::Deserializer<'de> for &mut Deserializer { struct MapAccess<'a> { de: &'a mut Deserializer, - sections: Vec, + sections: Vec, index: usize, } impl<'a> MapAccess<'a> { fn new(de: &'a mut Deserializer) -> Self { - let sections: Vec = de.sections.keys().cloned().collect(); + let sections: Vec = (0..de.sections.len()).collect(); MapAccess { de, sections, @@ -350,7 +434,8 @@ impl<'a> MapAccess<'a> { // Enum to track field source enum FieldSource { Root(String), - Section, + Section(usize), + Sequence(Vec), } // Root struct access - handles both root fields and sections @@ -361,29 +446,36 @@ struct RootStructAccess<'a> { } impl<'a> RootStructAccess<'a> { - fn new(de: &'a mut Deserializer) -> Self { + fn new(de: &'a mut Deserializer, expected_fields: &'static [&'static str]) -> Self { let mut fields = Vec::new(); - - // Get root section fields - if let Some(root_section) = de.sections.get("") { - for (key, value) in root_section { - // Check if there's also a section with this name - if de.sections.contains_key(key) { - // Prefer section over root field for self-referential structs - fields.push((key.clone(), FieldSource::Section)); + let root_section = de.section(0); + + for &field_name in expected_fields { + let matching_sections = de.section_indices_matching(field_name); + if matching_sections.len() > 1 { + fields.push(( + field_name.to_string(), + FieldSource::Sequence(matching_sections), + )); + } else if matching_sections.len() == 1 { + let index = matching_sections[0]; + let section = de.section(index); + if section.fields.len() == 1 && section.fields.contains_key("value") { + fields.push((field_name.to_string(), FieldSource::Sequence(vec![index]))); } else { - fields.push((key.clone(), FieldSource::Root(value.clone()))); - } - } - } - - // Add sections that don't have corresponding root fields - for section_name in de.sections.keys() { - if !section_name.is_empty() { - // Check if we already added this as a field - if !fields.iter().any(|(name, _)| name == section_name) { - fields.push((section_name.clone(), FieldSource::Section)); + let section_name = section.name.as_str(); + let singular = Deserializer::singular_candidates(field_name); + if singular.iter().any(|candidate| { + candidate != field_name + && Deserializer::section_base_name(section_name) == candidate + }) { + fields.push((field_name.to_string(), FieldSource::Sequence(vec![index]))); + } else { + fields.push((field_name.to_string(), FieldSource::Section(index))); + } } + } else if let Some(value) = root_section.fields.get(field_name) { + fields.push((field_name.to_string(), FieldSource::Root(value.clone()))); } } @@ -415,10 +507,15 @@ impl<'de> de::MapAccess<'de> for RootStructAccess<'_> { where V: de::DeserializeSeed<'de>, { - let (key, source) = &self.fields[self.index - 1]; + let (_, source) = &self.fields[self.index - 1]; match source { FieldSource::Root(value) => seed.deserialize(ValueDeserializer::new(value)), - FieldSource::Section => seed.deserialize(&mut SectionDeserializer::new(self.de, key)), + FieldSource::Section(index) => { + seed.deserialize(&mut SectionDeserializer::new(self.de, *index)) + } + FieldSource::Sequence(indices) => seed.deserialize( + &mut SequenceValueDeserializer::new(self.de, indices.clone()), + ), } } } @@ -431,7 +528,7 @@ impl<'de> de::MapAccess<'de> for MapAccess<'_> { K: de::DeserializeSeed<'de>, { if self.index < self.sections.len() { - let key = &self.sections[self.index]; + let key = self.de.section(self.sections[self.index]).name.clone(); self.index += 1; seed.deserialize(key.as_str().into_deserializer()).map(Some) } else { @@ -443,7 +540,7 @@ impl<'de> de::MapAccess<'de> for MapAccess<'_> { where V: de::DeserializeSeed<'de>, { - let section = &self.sections[self.index - 1]; + let section = self.sections[self.index - 1]; seed.deserialize(&mut SectionDeserializer::new(self.de, section)) } } @@ -454,35 +551,112 @@ struct StructAccess { } impl StructAccess { - fn new(de: &mut Deserializer, section: &str) -> Self { - let fields = if let Some(section_map) = de.sections.get(section) { - section_map - .iter() - .map(|(k, v)| (k.clone(), v.clone())) - .collect() - } else { - Vec::new() - }; + fn new(de: &mut Deserializer, section_index: usize) -> Self { + let fields = de + .section(section_index) + .fields + .iter() + .map(|(k, v)| (k.clone(), v.clone())) + .collect(); StructAccess { fields, index: 0 } } } -// Section deserializer for nested structs -struct SectionDeserializer<'a> { +struct SequenceDeserializer<'a> { de: &'a mut Deserializer, - section: String, + sections: Vec, + index: usize, } -impl<'a> SectionDeserializer<'a> { - fn new(de: &'a mut Deserializer, section: &str) -> Self { - SectionDeserializer { +impl<'a> SequenceDeserializer<'a> { + fn new(de: &'a mut Deserializer, sections: Vec) -> Self { + SequenceDeserializer { de, - section: section.to_string(), + sections, + index: 0, } } } +impl<'de, 'a> de::SeqAccess<'de> for SequenceDeserializer<'a> { + type Error = Error; + + fn next_element_seed(&mut self, seed: T) -> Result> + where + T: de::DeserializeSeed<'de>, + { + if self.index < self.sections.len() { + let section = self.sections[self.index]; + self.index += 1; + seed.deserialize(&mut SectionDeserializer::new(self.de, section)) + .map(Some) + } else { + Ok(None) + } + } +} + +struct SequenceValueDeserializer<'a> { + inner: SequenceDeserializer<'a>, +} + +impl<'a> SequenceValueDeserializer<'a> { + fn new(de: &'a mut Deserializer, sections: Vec) -> Self { + SequenceValueDeserializer { + inner: SequenceDeserializer::new(de, sections), + } + } +} + +impl<'de, 'a> de::Deserializer<'de> for &'a mut SequenceValueDeserializer<'a> { + type Error = Error; + + fn deserialize_any(self, visitor: V) -> Result + where + V: de::Visitor<'de>, + { + self.deserialize_seq(visitor) + } + + fn deserialize_seq(self, visitor: V) -> Result + where + V: de::Visitor<'de>, + { + visitor.visit_seq(&mut self.inner) + } + + fn deserialize_option(self, visitor: V) -> Result + where + V: de::Visitor<'de>, + { + visitor.visit_some(self) + } + + serde::forward_to_deserialize_any! { + bool i8 i16 i32 i64 u8 u16 u32 u64 f32 f64 char str string + bytes byte_buf unit unit_struct newtype_struct tuple + tuple_struct map struct enum identifier ignored_any + } +} + +// Section deserializer for nested structs and sequence elements +struct SectionDeserializer<'a> { + de: &'a mut Deserializer, + section: usize, +} + +impl<'a> SectionDeserializer<'a> { + fn new(de: &'a mut Deserializer, section: usize) -> Self { + SectionDeserializer { de, section } + } + + fn primitive_value(&self) -> Option<&str> { + let section = self.de.section(self.section); + section.fields.get("value").map(|value| value.as_str()) + } +} + impl<'de, 'a> de::Deserializer<'de> for &'a mut SectionDeserializer<'a> { type Error = Error; @@ -490,7 +664,11 @@ impl<'de, 'a> de::Deserializer<'de> for &'a mut SectionDeserializer<'a> { where V: de::Visitor<'de>, { - self.deserialize_struct("", &[], visitor) + if let Some(value) = self.primitive_value() { + visitor.visit_string(value.to_string()) + } else { + self.deserialize_struct("", &[], visitor) + } } fn deserialize_struct( @@ -502,7 +680,7 @@ impl<'de, 'a> de::Deserializer<'de> for &'a mut SectionDeserializer<'a> { where V: de::Visitor<'de>, { - visitor.visit_map(StructAccess::new(self.de, &self.section)) + visitor.visit_map(StructAccess::new(self.de, self.section)) } fn deserialize_option(self, visitor: V) -> Result @@ -513,11 +691,285 @@ impl<'de, 'a> de::Deserializer<'de> for &'a mut SectionDeserializer<'a> { visitor.visit_some(self) } - // Forward all other deserialize methods to deserialize_any - serde::forward_to_deserialize_any! { - bool i8 i16 i32 i64 u8 u16 u32 u64 f32 f64 char str string - bytes byte_buf unit unit_struct newtype_struct seq tuple - tuple_struct map enum identifier ignored_any + fn deserialize_bool(self, visitor: V) -> Result + where + V: de::Visitor<'de>, + { + if let Some(value) = self.primitive_value() { + ValueDeserializer::new(value).deserialize_bool(visitor) + } else { + Err(Error::UnsupportedFeature( + "booleans in sections".to_string(), + )) + } + } + + fn deserialize_i8(self, visitor: V) -> Result + where + V: de::Visitor<'de>, + { + if let Some(value) = self.primitive_value() { + ValueDeserializer::new(value).deserialize_i8(visitor) + } else { + Err(Error::UnsupportedFeature( + "integers in sections".to_string(), + )) + } + } + + fn deserialize_i16(self, visitor: V) -> Result + where + V: de::Visitor<'de>, + { + if let Some(value) = self.primitive_value() { + ValueDeserializer::new(value).deserialize_i16(visitor) + } else { + Err(Error::UnsupportedFeature( + "integers in sections".to_string(), + )) + } + } + + fn deserialize_i32(self, visitor: V) -> Result + where + V: de::Visitor<'de>, + { + if let Some(value) = self.primitive_value() { + ValueDeserializer::new(value).deserialize_i32(visitor) + } else { + Err(Error::UnsupportedFeature( + "integers in sections".to_string(), + )) + } + } + + fn deserialize_i64(self, visitor: V) -> Result + where + V: de::Visitor<'de>, + { + if let Some(value) = self.primitive_value() { + ValueDeserializer::new(value).deserialize_i64(visitor) + } else { + Err(Error::UnsupportedFeature( + "integers in sections".to_string(), + )) + } + } + + fn deserialize_u8(self, visitor: V) -> Result + where + V: de::Visitor<'de>, + { + if let Some(value) = self.primitive_value() { + ValueDeserializer::new(value).deserialize_u8(visitor) + } else { + Err(Error::UnsupportedFeature( + "integers in sections".to_string(), + )) + } + } + + fn deserialize_u16(self, visitor: V) -> Result + where + V: de::Visitor<'de>, + { + if let Some(value) = self.primitive_value() { + ValueDeserializer::new(value).deserialize_u16(visitor) + } else { + Err(Error::UnsupportedFeature( + "integers in sections".to_string(), + )) + } + } + + fn deserialize_u32(self, visitor: V) -> Result + where + V: de::Visitor<'de>, + { + if let Some(value) = self.primitive_value() { + ValueDeserializer::new(value).deserialize_u32(visitor) + } else { + Err(Error::UnsupportedFeature( + "integers in sections".to_string(), + )) + } + } + + fn deserialize_u64(self, visitor: V) -> Result + where + V: de::Visitor<'de>, + { + if let Some(value) = self.primitive_value() { + ValueDeserializer::new(value).deserialize_u64(visitor) + } else { + Err(Error::UnsupportedFeature( + "integers in sections".to_string(), + )) + } + } + + fn deserialize_f32(self, visitor: V) -> Result + where + V: de::Visitor<'de>, + { + if let Some(value) = self.primitive_value() { + ValueDeserializer::new(value).deserialize_f32(visitor) + } else { + Err(Error::UnsupportedFeature("floats in sections".to_string())) + } + } + + fn deserialize_f64(self, visitor: V) -> Result + where + V: de::Visitor<'de>, + { + if let Some(value) = self.primitive_value() { + ValueDeserializer::new(value).deserialize_f64(visitor) + } else { + Err(Error::UnsupportedFeature("floats in sections".to_string())) + } + } + + fn deserialize_char(self, visitor: V) -> Result + where + V: de::Visitor<'de>, + { + if let Some(value) = self.primitive_value() { + ValueDeserializer::new(value).deserialize_char(visitor) + } else { + Err(Error::UnsupportedFeature("chars in sections".to_string())) + } + } + + fn deserialize_str(self, visitor: V) -> Result + where + V: de::Visitor<'de>, + { + if let Some(value) = self.primitive_value() { + visitor.visit_string(value.to_string()) + } else { + Err(Error::UnsupportedFeature("strings in sections".to_string())) + } + } + + fn deserialize_string(self, visitor: V) -> Result + where + V: de::Visitor<'de>, + { + if let Some(value) = self.primitive_value() { + visitor.visit_string(value.to_string()) + } else { + Err(Error::UnsupportedFeature("strings in sections".to_string())) + } + } + + fn deserialize_bytes(self, visitor: V) -> Result + where + V: de::Visitor<'de>, + { + if let Some(value) = self.primitive_value() { + visitor.visit_bytes(value.as_bytes()) + } else { + Err(Error::UnsupportedFeature("bytes in sections".to_string())) + } + } + + fn deserialize_byte_buf(self, visitor: V) -> Result + where + V: de::Visitor<'de>, + { + if let Some(value) = self.primitive_value() { + visitor.visit_byte_buf(value.as_bytes().to_vec()) + } else { + Err(Error::UnsupportedFeature("bytes in sections".to_string())) + } + } + + fn deserialize_unit(self, visitor: V) -> Result + where + V: de::Visitor<'de>, + { + visitor.visit_unit() + } + + fn deserialize_unit_struct(self, _name: &'static str, visitor: V) -> Result + where + V: de::Visitor<'de>, + { + visitor.visit_unit() + } + + fn deserialize_newtype_struct(self, _name: &'static str, visitor: V) -> Result + where + V: de::Visitor<'de>, + { + visitor.visit_newtype_struct(self) + } + + fn deserialize_seq(self, visitor: V) -> Result + where + V: de::Visitor<'de>, + { + let _ = visitor; + Err(Error::UnsupportedFeature("nested sequences".to_string())) + } + + fn deserialize_tuple(self, _len: usize, _visitor: V) -> Result + where + V: de::Visitor<'de>, + { + Err(Error::UnsupportedFeature("tuples".to_string())) + } + + fn deserialize_tuple_struct( + self, + _name: &'static str, + _len: usize, + _visitor: V, + ) -> Result + where + V: de::Visitor<'de>, + { + Err(Error::UnsupportedFeature("tuple structs".to_string())) + } + + fn deserialize_map(self, visitor: V) -> Result + where + V: de::Visitor<'de>, + { + if self.primitive_value().is_some() { + Err(Error::UnsupportedFeature( + "maps in primitive sections".to_string(), + )) + } else { + visitor.visit_map(StructAccess::new(self.de, self.section)) + } + } + + fn deserialize_enum( + self, + _name: &'static str, + _variants: &'static [&'static str], + _visitor: V, + ) -> Result + where + V: de::Visitor<'de>, + { + Err(Error::UnsupportedFeature("enums".to_string())) + } + + fn deserialize_identifier(self, visitor: V) -> Result + where + V: de::Visitor<'de>, + { + self.deserialize_str(visitor) + } + + fn deserialize_ignored_any(self, visitor: V) -> Result + where + V: de::Visitor<'de>, + { + visitor.visit_unit() } } diff --git a/src/lib.rs b/src/lib.rs index 291b676..11cdef0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,6 +9,7 @@ //! - **Option handling** - [`None`][Option] values are serialized as commented lines //! - **Escape sequences** - Properly handles special characters in values //! - **Section support** - Nested structs are automatically converted to INI sections +//! - **Sequence support** - Sequence elements are written as repeated sections, with optional numbering //! - **Type safety** - Leverages serde's type system for safe conversions //! //! ## Quick Start @@ -238,18 +239,19 @@ //! - **String**: `String`, `&str` //! - **Option**: `Option` where `T` is a supported type //! - **Structs**: Custom structs with named fields +//! - **Sequences**: Each element is serialized as its own INI section //! //! ## Limitations //! //! The following serde types are **not** supported: //! -//! - Sequences (Vec, arrays, etc.) +//! - Deserializing sequences (Vec, arrays, etc.) //! - Tuples and tuple structs //! - Enums with variants //! - Maps (HashMap, BTreeMap, etc.) //! - Unit structs //! -//! Attempting to serialize or deserialize these types will result in an error. +//! Attempting to deserialize these types will result in an error. //! //! ## Error Handling //! @@ -406,7 +408,7 @@ pub mod ser; pub use de::from_str; pub use error::Error; -pub use ser::to_string; +pub use ser::{SerializeOptions, to_string, to_string_with_options}; #[cfg(test)] mod tests { @@ -614,6 +616,119 @@ mod tests { } } + mod sequences { + use super::*; + + #[derive(Debug, Serialize, Deserialize, PartialEq)] + struct Service { + host: String, + port: u16, + } + + #[derive(Debug, Serialize, Deserialize, PartialEq)] + struct Config { + services: Vec, + tags: Vec, + } + + #[test] + fn test_serialize_sequences_as_sections() { + let config = Config { + services: vec![ + Service { + host: "localhost".to_string(), + port: 8080, + }, + Service { + host: "example.com".to_string(), + port: 443, + }, + ], + tags: vec!["alpha".to_string(), "beta".to_string()], + }; + + let ini = to_string(&config).unwrap(); + + println!("{}", ini); + + assert!(ini.contains("[Service]")); + assert!(ini.contains("host = localhost")); + assert!(ini.contains("port = 8080")); + assert!(ini.contains("[Service]")); + assert!(ini.contains("host = example.com")); + assert!(ini.contains("port = 443")); + + assert!(ini.contains("[tags]")); + assert!(ini.contains("value = alpha")); + assert!(ini.contains("[tags]")); + assert!(ini.contains("value = beta")); + + let lines: Vec<&str> = ini.lines().collect(); + let service_sections: Vec = lines + .iter() + .enumerate() + .filter(|&(_, l)| *l == "[Service]") + .map(|(i, _)| i) + .collect(); + let tag_sections: Vec = lines + .iter() + .enumerate() + .filter(|&(_, l)| *l == "[tags]") + .map(|(i, _)| i) + .collect(); + + assert_eq!(service_sections.len(), 2); + assert_eq!(tag_sections.len(), 2); + assert!(service_sections[0] < service_sections[1]); + assert!(service_sections[1] < tag_sections[0]); + assert!(tag_sections[0] < tag_sections[1]); + } + + #[test] + fn test_serialize_sequences_with_numbering() { + let config = Config { + services: vec![Service { + host: "localhost".to_string(), + port: 8080, + }], + tags: vec!["alpha".to_string()], + }; + + let ini = to_string_with_options( + &config, + SerializeOptions { + numbered_sequences: true, + }, + ) + .unwrap(); + + assert!(ini.contains("[Service.0]")); + assert!(ini.contains("[tags.0]")); + } + + #[test] + fn test_roundtrip_sequences() { + let config = Config { + services: vec![ + Service { + host: "localhost".to_string(), + port: 8080, + }, + Service { + host: "example.com".to_string(), + port: 443, + }, + ], + tags: vec!["alpha".to_string(), "beta".to_string()], + }; + + let ini = to_string(&config).unwrap(); + let parsed: Config = from_str(&ini).unwrap(); + + assert_eq!(config, parsed); + } + } + #[test] fn test_escaping() { #[derive(Debug, Serialize, Deserialize, PartialEq)] diff --git a/src/ser.rs b/src/ser.rs index cf58320..835c3f6 100644 --- a/src/ser.rs +++ b/src/ser.rs @@ -4,16 +4,36 @@ use serde::{Serialize, ser}; pub struct Serializer { output: String, current_section: Option, + struct_name: Option, + sequence_prefix: Option, + sequence_index: usize, + numbered_sequences: bool, section_names: Vec, } +#[derive(Clone, Copy, Debug, Default)] +pub struct SerializeOptions { + pub numbered_sequences: bool, +} + pub fn to_string(value: &T) -> Result +where + T: Serialize, +{ + to_string_with_options(value, SerializeOptions::default()) +} + +pub fn to_string_with_options(value: &T, options: SerializeOptions) -> Result where T: Serialize, { let mut serializer = Serializer { output: String::new(), current_section: None, + struct_name: None, + sequence_prefix: None, + sequence_index: 0, + numbered_sequences: options.numbered_sequences, section_names: Vec::new(), }; @@ -178,10 +198,10 @@ impl ser::SerializeStruct for &mut SectionCollector { T: ?Sized + Serialize, { // Check if this field is a struct that will become a section - let mut detector = StructDetector::new(); + let mut detector = ContainerDetector::new(); let _ = value.serialize(&mut detector); - if detector.is_struct { + if detector.kind == Some(ContainerKind::Struct) { self.sections.push(key.to_string()); } @@ -284,18 +304,30 @@ impl ser::SerializeStructVariant for &mut SectionCollector { } } -// Helper struct to detect if a value serializes as a struct -struct StructDetector { - is_struct: bool, +#[derive(Clone, Copy, PartialEq, Eq)] +enum ContainerKind { + Struct, + Seq, +} + +// Helper struct to detect whether a value serializes as a struct or sequence +struct ContainerDetector { + kind: Option, } -impl StructDetector { +impl ContainerDetector { fn new() -> Self { - StructDetector { is_struct: false } + ContainerDetector { kind: None } + } + + fn set_kind(&mut self, kind: ContainerKind) { + if self.kind.is_none() { + self.kind = Some(kind); + } } } -impl ser::Serializer for &mut StructDetector { +impl ser::Serializer for &mut ContainerDetector { type Ok = (); type Error = Error; type SerializeSeq = Self; @@ -307,7 +339,7 @@ impl ser::Serializer for &mut StructDetector { type SerializeStructVariant = Self; fn serialize_struct(self, _name: &'static str, _len: usize) -> Result { - self.is_struct = true; + self.set_kind(ContainerKind::Struct); Ok(self) } @@ -396,6 +428,7 @@ impl ser::Serializer for &mut StructDetector { Ok(()) } fn serialize_seq(self, _len: Option) -> Result { + self.set_kind(ContainerKind::Seq); Ok(self) } fn serialize_tuple(self, _len: usize) -> Result { @@ -431,7 +464,7 @@ impl ser::Serializer for &mut StructDetector { } } -impl ser::SerializeSeq for &mut StructDetector { +impl ser::SerializeSeq for &mut ContainerDetector { type Ok = (); type Error = Error; fn serialize_element(&mut self, _value: &T) -> Result<()> @@ -445,7 +478,7 @@ impl ser::SerializeSeq for &mut StructDetector { } } -impl ser::SerializeTuple for &mut StructDetector { +impl ser::SerializeTuple for &mut ContainerDetector { type Ok = (); type Error = Error; fn serialize_element(&mut self, _value: &T) -> Result<()> @@ -459,7 +492,7 @@ impl ser::SerializeTuple for &mut StructDetector { } } -impl ser::SerializeTupleStruct for &mut StructDetector { +impl ser::SerializeTupleStruct for &mut ContainerDetector { type Ok = (); type Error = Error; fn serialize_field(&mut self, _value: &T) -> Result<()> @@ -473,7 +506,7 @@ impl ser::SerializeTupleStruct for &mut StructDetector { } } -impl ser::SerializeTupleVariant for &mut StructDetector { +impl ser::SerializeTupleVariant for &mut ContainerDetector { type Ok = (); type Error = Error; fn serialize_field(&mut self, _value: &T) -> Result<()> @@ -487,7 +520,7 @@ impl ser::SerializeTupleVariant for &mut StructDetector { } } -impl ser::SerializeMap for &mut StructDetector { +impl ser::SerializeMap for &mut ContainerDetector { type Ok = (); type Error = Error; fn serialize_key(&mut self, _key: &T) -> Result<()> @@ -507,7 +540,7 @@ impl ser::SerializeMap for &mut StructDetector { } } -impl ser::SerializeStruct for &mut StructDetector { +impl ser::SerializeStruct for &mut ContainerDetector { type Ok = (); type Error = Error; fn serialize_field(&mut self, _key: &'static str, _value: &T) -> Result<()> @@ -521,7 +554,7 @@ impl ser::SerializeStruct for &mut StructDetector { } } -impl ser::SerializeStructVariant for &mut StructDetector { +impl ser::SerializeStructVariant for &mut ContainerDetector { type Ok = (); type Error = Error; fn serialize_field(&mut self, _key: &'static str, _value: &T) -> Result<()> @@ -559,6 +592,15 @@ impl Serializer { self.output.push_str(key); self.output.push_str(" = \n"); } + + fn write_section_header(&mut self, name: &str) { + if !self.output.is_empty() && !self.output.ends_with('\n') { + self.output.push('\n'); + } + self.output.push('['); + self.output.push_str(name); + self.output.push_str("]\n"); + } } impl ser::Serializer for &mut Serializer { @@ -683,7 +725,8 @@ impl ser::Serializer for &mut Serializer { } fn serialize_seq(self, _len: Option) -> Result { - Err(Error::UnsupportedFeature("sequences".to_string())) + self.sequence_index = 0; + Ok(self) } fn serialize_tuple(self, _len: usize) -> Result { @@ -712,7 +755,8 @@ impl ser::Serializer for &mut Serializer { Ok(self) } - fn serialize_struct(self, _name: &'static str, _len: usize) -> Result { + fn serialize_struct(self, name: &'static str, _len: usize) -> Result { + self.struct_name = Some(name.to_string()); // Don't create section headers here - they're created in serialize_field if self.current_section.is_none() { self.current_section = Some("".to_string()); @@ -735,11 +779,55 @@ impl ser::SerializeSeq for &mut Serializer { type Ok = (); type Error = Error; - fn serialize_element(&mut self, _value: &T) -> Result<()> + fn serialize_element(&mut self, value: &T) -> Result<()> where T: ?Sized + Serialize, { - Err(Error::UnsupportedFeature("sequences".to_string())) + let mut detector = ContainerDetector::new(); + let _ = value.serialize(&mut detector); + + let mut element_serializer = Serializer { + output: String::new(), + current_section: None, + struct_name: None, + sequence_prefix: None, + sequence_index: 0, + numbered_sequences: self.numbered_sequences, + section_names: self.section_names.clone(), + }; + value.serialize(&mut element_serializer)?; + + let base_name = element_serializer + .struct_name + .clone() + .or_else(|| self.sequence_prefix.clone()) + .unwrap_or_else(|| "item".to_string()); + let section_name = if self.numbered_sequences { + format!("{}.{}", base_name, self.sequence_index) + } else { + base_name + }; + self.sequence_index += 1; + + match detector.kind { + Some(ContainerKind::Struct) => { + self.write_section_header(§ion_name); + self.output.push_str(&element_serializer.output); + } + Some(ContainerKind::Seq) => { + return Err(Error::UnsupportedFeature("nested sequences".to_string())); + } + None => { + self.write_section_header(§ion_name); + if element_serializer.output.is_empty() { + // Empty elements become empty sections. + } else { + self.write_key_value("value", &element_serializer.output); + } + } + } + + Ok(()) } fn end(self) -> Result<()> { @@ -826,34 +914,50 @@ impl ser::SerializeStruct for &mut Serializer { where T: ?Sized + Serialize, { - // First, detect if the value is a struct - let mut detector = StructDetector::new(); + // First, detect if the value is a struct or sequence + let mut detector = ContainerDetector::new(); let _ = value.serialize(&mut detector); - if detector.is_struct { + if detector.kind == Some(ContainerKind::Struct) { // This is a nested struct - serialize it as a section - if !self.output.is_empty() && !self.output.ends_with('\n') { - self.output.push('\n'); - } - self.output.push('['); - self.output.push_str(key); - self.output.push_str("]\n"); + self.write_section_header(key); // Serialize the struct's fields let mut nested_serializer = Serializer { output: String::new(), current_section: Some(key.to_string()), + struct_name: None, + sequence_prefix: None, + sequence_index: 0, + numbered_sequences: self.numbered_sequences, section_names: self.section_names.clone(), }; value.serialize(&mut nested_serializer)?; // Add the fields (the nested serializer won't have section headers) self.output.push_str(&nested_serializer.output); + } else if detector.kind == Some(ContainerKind::Seq) { + // This is a sequence - serialize each element as its own section + let mut nested_serializer = Serializer { + output: String::new(), + current_section: self.current_section.clone(), + struct_name: None, + sequence_prefix: Some(key.to_string()), + sequence_index: 0, + numbered_sequences: self.numbered_sequences, + section_names: self.section_names.clone(), + }; + value.serialize(&mut nested_serializer)?; + self.output.push_str(&nested_serializer.output); } else { // Regular value or Option let mut temp_serializer = Serializer { output: String::new(), current_section: self.current_section.clone(), + struct_name: None, + sequence_prefix: None, + sequence_index: 0, + numbered_sequences: self.numbered_sequences, section_names: self.section_names.clone(), };