diff --git a/CHANGELOG.md b/CHANGELOG.md index 542fc6b43..edb40a240 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Breaking: Enforce that ron always writes valid UTF-8 ([#488](https://github.com/ron-rs/ron/pull/488)) - Add convenient `Value::from` impls ([#498](https://github.com/ron-rs/ron/pull/498)) - Add new extension `explicit_struct_names` which requires that struct names are included during deserialization ([#522](https://github.com/ron-rs/ron/pull/522)) +- Add new path-based field metadata serialization support via `PrettyConfig` ([#544](https://github.com/ron-rs/ron/pull/544)) - Breaking: Change `PrettyConfig` so that `new_line`, `indentor` and `separator` are all `Cow<'static, str>` instead of `String` ([#546](https://github.com/ron-rs/ron/pull/546)) ### Format Changes diff --git a/src/ser/mod.rs b/src/ser/mod.rs index 8c05070d2..1d3b6c111 100644 --- a/src/ser/mod.rs +++ b/src/ser/mod.rs @@ -11,6 +11,8 @@ use crate::{ parse::{is_ident_first_char, is_ident_raw_char, is_whitespace_char, LargeSInt, LargeUInt}, }; +pub mod path_meta; + mod raw; #[cfg(test)] mod tests; @@ -109,6 +111,8 @@ pub struct PrettyConfig { pub compact_maps: bool, /// Enable explicit number type suffixes like `1u16` pub number_suffixes: bool, + /// Additional path-based field metadata to serialize + pub path_meta: Option, } impl PrettyConfig { @@ -359,6 +363,7 @@ impl Default for PrettyConfig { compact_structs: false, compact_maps: false, number_suffixes: false, + path_meta: None, } } } @@ -378,6 +383,15 @@ pub struct Serializer { implicit_some_depth: usize, } +fn indent(output: &mut W, config: &PrettyConfig, pretty: &Pretty) -> fmt::Result { + if pretty.indent <= config.depth_limit { + for _ in 0..pretty.indent { + output.write_str(&config.indentor)?; + } + } + Ok(()) +} + impl Serializer { /// Creates a new [`Serializer`]. /// @@ -491,11 +505,7 @@ impl Serializer { fn indent(&mut self) -> fmt::Result { if let Some((ref config, ref pretty)) = self.pretty { - if pretty.indent <= config.depth_limit { - for _ in 0..pretty.indent { - self.output.write_str(&config.indentor)?; - } - } + indent(&mut self.output, config, pretty)?; } Ok(()) } @@ -1313,6 +1323,15 @@ impl<'a, W: fmt::Write> ser::SerializeStruct for Compound<'a, W> { where T: ?Sized + Serialize, { + let mut restore_field = self.ser.pretty.as_mut().and_then(|(config, _)| { + config.path_meta.take().map(|mut field| { + if let Some(fields) = field.fields_mut() { + config.path_meta = fields.remove(key); + } + field + }) + }); + if let State::First = self.state { self.state = State::Rest; } else { @@ -1328,7 +1347,18 @@ impl<'a, W: fmt::Write> ser::SerializeStruct for Compound<'a, W> { } if !self.ser.compact_structs() { - self.ser.indent()?; + if let Some((ref config, ref pretty)) = self.ser.pretty { + indent(&mut self.ser.output, config, pretty)?; + + if let Some(ref field) = config.path_meta { + for doc_line in field.doc().lines() { + self.ser.output.write_str("/// ")?; + self.ser.output.write_str(doc_line)?; + self.ser.output.write_char('\n')?; + indent(&mut self.ser.output, config, pretty)?; + } + } + } } self.ser.write_identifier(key)?; @@ -1340,6 +1370,18 @@ impl<'a, W: fmt::Write> ser::SerializeStruct for Compound<'a, W> { guard_recursion! { self.ser => value.serialize(&mut *self.ser)? }; + if let Some((ref mut config, _)) = self.ser.pretty { + std::mem::swap(&mut config.path_meta, &mut restore_field); + + if let Some(ref mut field) = config.path_meta { + if let Some(fields) = field.fields_mut() { + if let Some(restore_field) = restore_field { + fields.insert(key, restore_field); + } + } + } + }; + Ok(()) } @@ -1360,6 +1402,7 @@ impl<'a, W: fmt::Write> ser::SerializeStruct for Compound<'a, W> { if !self.newtype_variant { self.ser.output.write_char(')')?; } + Ok(()) } } diff --git a/src/ser/path_meta.rs b/src/ser/path_meta.rs new file mode 100644 index 000000000..8a870523e --- /dev/null +++ b/src/ser/path_meta.rs @@ -0,0 +1,360 @@ +//! Path-based metadata to serialize with a value. +//! +//! Path-based in this context means that the metadata is linked +//! to the data in a relative and hierarchical fashion by tracking +//! the current absolute path of the field being serialized. +//! +//! # Example +//! +//! ``` +//! # use ron::ser::{PrettyConfig, path_meta::Field}; +//! +//! #[derive(serde::Serialize)] +//! struct Creature { +//! seconds_since_existing: usize, +//! linked: Option>, +//! } +//! +//! let mut config = PrettyConfig::default(); +//! +//! config +//! .path_meta +//! // The path meta defaults to no root structure, +//! // so we either provide a prebuilt one or initialize +//! // an empty one to build. +//! .get_or_insert_with(Field::empty) +//! .build_fields(|fields| { +//! fields +//! // Get or insert the named field +//! .field("seconds_since_existing") +//! .with_doc("Outer seconds_since_existing"); +//! fields +//! .field("linked") +//! // Doc metadata is serialized preceded by three forward slashes and a space for each line +//! .with_doc("Optional.\nProvide another creature to be wrapped.") +//! // Even though it's another Creature, the fields have different paths, so they are addressed separately. +//! .build_fields(|fields| { +//! fields +//! .field("seconds_since_existing") +//! .with_doc("Inner seconds_since_existing"); +//! }); +//! }); +//! +//! let value = Creature { +//! seconds_since_existing: 0, +//! linked: Some(Box::new(Creature { +//! seconds_since_existing: 0, +//! linked: None, +//! })), +//! }; +//! +//! let s = ron::ser::to_string_pretty(&value, config).unwrap(); +//! +//! assert_eq!(s, r#"( +//! /// Outer seconds_since_existing +//! seconds_since_existing: 0, +//! /// Optional. +//! /// Provide another creature to be wrapped. +//! linked: Some(( +//! /// Inner seconds_since_existing +//! seconds_since_existing: 0, +//! linked: None, +//! )), +//! )"#); +//! ``` +//! +//! # Identical paths +//! +//! Especially in enums and tuples it's possible for fields +//! to share a path, thus being unable to be addressed separately. +//! +//! ```no_run +//! enum Kind { +//! A { +//! field: (), +//! }, // ^ +//! // cannot be addressed separately because they have the same path +//! B { // v +//! field: (), +//! }, +//! } +//! ``` +//! +//! ```no_run +//! struct A { +//! field: (), +//! } +//! +//! struct B { +//! field: (), +//! } +//! +//! type Value = ( +//! A, +//! // ^ +//! // These are different types, but they share the path `field` +//! // v +//! B, +//! ); +//! ``` + +use std::collections::HashMap; + +use serde_derive::{Deserialize, Serialize}; + +/// The metadata and inner [`Fields`] of a field. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)] +pub struct Field { + doc: String, + fields: Option, +} + +impl Field { + /// Create a new empty field metadata. + #[must_use] + pub const fn empty() -> Self { + Self { + doc: String::new(), + fields: None, + } + } + + /// Create a new field metadata from parts. + pub fn new(doc: impl Into, fields: Option) -> Self { + Self { + doc: doc.into(), + fields, + } + } + + /// Get a shared reference to the documentation metadata of this field. + #[inline] + #[must_use] + pub fn doc(&self) -> &str { + self.doc.as_str() + } + + /// Get a mutable reference to the documentation metadata of this field. + #[inline] + #[must_use] + pub fn doc_mut(&mut self) -> &mut String { + &mut self.doc + } + + /// Set the documentation metadata of this field. + /// + /// ``` + /// # use ron::ser::path_meta::Field; + /// + /// let mut field = Field::empty(); + /// + /// assert_eq!(field.doc(), ""); + /// + /// field.with_doc("some meta"); + /// + /// assert_eq!(field.doc(), "some meta"); + /// ``` + pub fn with_doc(&mut self, doc: impl Into) -> &mut Self { + self.doc = doc.into(); + self + } + + /// Get a shared reference to the inner fields of this field, if it has any. + #[must_use] + pub fn fields(&self) -> Option<&Fields> { + self.fields.as_ref() + } + + /// Get a mutable reference to the inner fields of this field, if it has any. + pub fn fields_mut(&mut self) -> Option<&mut Fields> { + self.fields.as_mut() + } + + /// Return whether this field has inner fields. + /// + /// ``` + /// # use ron::ser::path_meta::{Field, Fields}; + /// + /// let mut field = Field::empty(); + /// + /// assert!(!field.has_fields()); + /// + /// field.with_fields(Some(Fields::default())); + /// + /// assert!(field.has_fields()); + /// ``` + #[must_use] + pub fn has_fields(&self) -> bool { + self.fields.is_some() + } + + /// Set the inner fields of this field. + /// + /// ``` + /// # use ron::ser::path_meta::{Field, Fields}; + /// + /// let mut field = Field::empty(); + /// + /// assert!(!field.has_fields()); + /// + /// field.with_fields(Some(Fields::default())); + /// + /// assert!(field.has_fields()); + /// + /// field.with_fields(None); + /// + /// assert!(!field.has_fields()); + /// ``` + pub fn with_fields(&mut self, fields: Option) -> &mut Self { + self.fields = fields; + self + } + + /// Ergonomic shortcut for building some inner fields. + /// + /// ``` + /// # use ron::ser::path_meta::Field; + /// + /// let mut field = Field::empty(); + /// + /// field.build_fields(|fields| { + /// fields.field("inner field"); + /// }); + /// + /// assert_eq!(field.fields().map(|fields| fields.contains("inner field")), Some(true)); + /// ``` + pub fn build_fields(&mut self, builder: impl FnOnce(&mut Fields)) -> &mut Self { + let mut fields = Fields::default(); + builder(&mut fields); + self.with_fields(Some(fields)); + self + } +} + +/// Mapping of names to [`Field`]s. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)] +pub struct Fields { + fields: HashMap, +} + +impl Fields { + /// Return a new, empty metadata field map. + #[must_use] + pub fn new() -> Self { + Self::default() + } + + /// Return whether this field map contains no fields. + /// + /// ``` + /// # use ron::ser::path_meta::{Fields, Field}; + /// + /// let mut fields = Fields::default(); + /// + /// assert!(fields.is_empty()); + /// + /// fields.insert("", Field::empty()); + /// + /// assert!(!fields.is_empty()); + /// ``` + #[must_use] + pub fn is_empty(&self) -> bool { + self.fields.is_empty() + } + + /// Return whether this field map contains a field with the given name. + /// + /// ``` + /// # use ron::ser::path_meta::{Fields, Field}; + /// + /// let fields: Fields = [("a thing", Field::empty())].into_iter().collect(); + /// + /// assert!(fields.contains("a thing")); + /// assert!(!fields.contains("not a thing")); + /// ``` + pub fn contains(&self, name: impl AsRef) -> bool { + self.fields.contains_key(name.as_ref()) + } + + /// Get a reference to the field with the provided `name`, if it exists. + /// + /// ``` + /// # use ron::ser::path_meta::{Fields, Field}; + /// + /// let fields: Fields = [("a thing", Field::empty())].into_iter().collect(); + /// + /// assert!(fields.get("a thing").is_some()); + /// assert!(fields.get("not a thing").is_none()); + /// ``` + pub fn get(&self, name: impl AsRef) -> Option<&Field> { + self.fields.get(name.as_ref()) + } + + /// Get a mutable reference to the field with the provided `name`, if it exists. + /// + /// ``` + /// # use ron::ser::path_meta::{Fields, Field}; + /// + /// let mut fields: Fields = [("a thing", Field::empty())].into_iter().collect(); + /// + /// assert!(fields.get_mut("a thing").is_some()); + /// assert!(fields.get_mut("not a thing").is_none()); + /// ``` + pub fn get_mut(&mut self, name: impl AsRef) -> Option<&mut Field> { + self.fields.get_mut(name.as_ref()) + } + + /// Insert a field with the given name into the map. + /// + /// ``` + /// # use ron::ser::path_meta::{Fields, Field}; + /// + /// let mut fields = Fields::default(); + /// + /// assert!(fields.insert("field", Field::empty()).is_none()); + /// assert!(fields.insert("field", Field::empty()).is_some()); + /// ``` + pub fn insert(&mut self, name: impl Into, field: Field) -> Option { + self.fields.insert(name.into(), field) + } + + /// Remove a field with the given name from the map. + /// + /// ``` + /// # use ron::ser::path_meta::{Fields, Field}; + /// + /// let mut fields: Fields = [("a", Field::empty())].into_iter().collect(); + /// + /// assert_eq!(fields.remove("a"), Some(Field::empty())); + /// assert_eq!(fields.remove("a"), None); + /// ``` + pub fn remove(&mut self, name: impl AsRef) -> Option { + self.fields.remove(name.as_ref()) + } + + /// Get a mutable reference to the field with the provided `name`, + /// inserting an empty [`Field`] if it didn't exist. + /// + /// ``` + /// # use ron::ser::path_meta::Fields; + /// + /// let mut fields = Fields::default(); + /// + /// assert!(!fields.contains("thing")); + /// + /// fields.field("thing"); + /// + /// assert!(fields.contains("thing")); + /// ``` + pub fn field(&mut self, name: impl Into) -> &mut Field { + self.fields.entry(name.into()).or_insert_with(Field::empty) + } +} + +impl> FromIterator<(K, Field)> for Fields { + fn from_iter>(iter: T) -> Self { + Self { + fields: iter.into_iter().map(|(k, v)| (k.into(), v)).collect(), + } + } +} diff --git a/tests/544_path_meta.rs b/tests/544_path_meta.rs new file mode 100644 index 000000000..a165bcf73 --- /dev/null +++ b/tests/544_path_meta.rs @@ -0,0 +1,129 @@ +use ron::ser::{path_meta::Field, PrettyConfig}; + +#[test] +fn serialize_field() { + #[derive(serde::Serialize)] + enum PetKind { + Isopod, + } + + #[derive(serde::Serialize)] + struct Pet { + name: &'static str, + age: u8, + kind: PetKind, + } + + #[derive(serde::Serialize)] + struct Person { + name: &'static str, + age: u8, + knows: Vec, + pet: Option, + } + + let value = ( + Person { + name: "Walter", + age: 43, + knows: vec![0, 1], + pet: None, + }, + vec![ + Person { + name: "Alice", + age: 29, + knows: vec![1], + pet: Some(Pet { + name: "Herbert", + age: 7, + kind: PetKind::Isopod, + }), + }, + Person { + name: "Bob", + age: 29, + knows: vec![0], + pet: None, + }, + ], + ); + + let mut config = PrettyConfig::default(); + + config + .path_meta + .get_or_insert_with(Field::empty) + .build_fields(|fields| { + fields + .field("age") + .with_doc("0@age (person)\nmust be within range 0..256"); + fields + .field("knows") + .with_doc("0@knows (person)\nmust be list of valid person indices"); + fields.field("pet").build_fields(|fields| { + fields + .field("age") + .with_doc("1@age (pet)\nmust be valid range 0..256"); + fields + .field("kind") + .with_doc("1@kind (pet)\nmust be `Isopod`"); + }); + + // provide meta for a field that doesn't exist; + // this should not end up anywhere in the final string + fields.field("0").with_doc("unreachable"); + }); + + let s = ron::ser::to_string_pretty(&value, config).unwrap(); + + assert_eq!( + s, + r#"(( + name: "Walter", + /// 0@age (person) + /// must be within range 0..256 + age: 43, + /// 0@knows (person) + /// must be list of valid person indices + knows: [ + 0, + 1, + ], + pet: None, +), [ + ( + name: "Alice", + /// 0@age (person) + /// must be within range 0..256 + age: 29, + /// 0@knows (person) + /// must be list of valid person indices + knows: [ + 1, + ], + pet: Some(( + name: "Herbert", + /// 1@age (pet) + /// must be valid range 0..256 + age: 7, + /// 1@kind (pet) + /// must be `Isopod` + kind: Isopod, + )), + ), + ( + name: "Bob", + /// 0@age (person) + /// must be within range 0..256 + age: 29, + /// 0@knows (person) + /// must be list of valid person indices + knows: [ + 0, + ], + pet: None, + ), +])"# + ); +}