diff --git a/src/ser.rs b/src/ser.rs index 7ca19f8..9460704 100644 --- a/src/ser.rs +++ b/src/ser.rs @@ -3,10 +3,33 @@ //! This module contains all the Serde support for serializing Rust structures //! into TOML documents (as strings). Note that some top-level functions here //! are also provided at the top of the crate. +//! +//! Note that the TOML format has a restriction that if a table itself contains +//! tables, all keys with non-table values must be emitted first. This is +//! typically easy to ensure happens when you're defining a `struct` as you can +//! reorder the fields manually, but when working with maps (such as `BTreeMap` +//! or `HashMap`) this can lead to serialization errors. In those situations you +//! may use the `tables_last` function in this module like so: +//! +//! ```rust +//! # #[macro_use] extern crate serde_derive; +//! # extern crate toml; +//! # use std::collections::HashMap; +//! #[derive(Serialize)] +//! struct Manifest { +//! package: Package, +//! #[serde(serialize_with = "toml::ser::tables_last")] +//! dependencies: HashMap, +//! } +//! # type Package = String; +//! # type Dependency = String; +//! # fn main() {} +//! ``` -use std::fmt::{self, Write}; -use std::error; use std::cell::Cell; +use std::error; +use std::fmt::{self, Write}; +use std::marker; use serde::ser; use datetime::{SERDE_STRUCT_FIELD_NAME, SERDE_STRUCT_NAME}; @@ -1004,3 +1027,244 @@ impl ser::Error for Error { Error::Custom(msg.to_string()) } } + +enum Category { + Primitive, + Array, + Table, +} + +/// Convenience function to serialize items in a map in an order valid with +/// TOML. +/// +/// TOML carries the restriction that keys in a table must be serialized last if +/// their value is a table itself. This isn't always easy to guarantee, so this +/// helper can be used like so: +/// +/// ```rust +/// # #[macro_use] extern crate serde_derive; +/// # extern crate toml; +/// # use std::collections::HashMap; +/// #[derive(Serialize)] +/// struct Manifest { +/// package: Package, +/// #[serde(serialize_with = "toml::ser::tables_last")] +/// dependencies: HashMap, +/// } +/// # type Package = String; +/// # type Dependency = String; +/// # fn main() {} +/// ``` +pub fn tables_last<'a, I, K, V, S>(data: &'a I, serializer: S) + -> Result + where &'a I: IntoIterator, + K: ser::Serialize, + V: ser::Serialize, + S: ser::Serializer +{ + use serde::ser::SerializeMap; + + let mut map = serializer.serialize_map(None)?; + for (k, v) in data { + if let Category::Primitive = v.serialize(Categorize::new())? { + map.serialize_entry(&k, &v)?; + } + } + for (k, v) in data { + if let Category::Array = v.serialize(Categorize::new())? { + map.serialize_entry(&k, &v)?; + } + } + for (k, v) in data { + if let Category::Table = v.serialize(Categorize::new())? { + map.serialize_entry(&k, &v)?; + } + } + map.end() +} + +struct Categorize(marker::PhantomData); + +impl Categorize { + fn new() -> Self { + Categorize(marker::PhantomData) + } +} + +impl ser::Serializer for Categorize { + type Ok = Category; + type Error = E; + type SerializeSeq = Self; + type SerializeTuple = ser::Impossible; + type SerializeTupleStruct = ser::Impossible; + type SerializeTupleVariant = ser::Impossible; + type SerializeMap = Self; + type SerializeStruct = Self; + type SerializeStructVariant = ser::Impossible; + + fn serialize_bool(self, _: bool) -> Result { + Ok(Category::Primitive) + } + + fn serialize_i8(self, _: i8) -> Result { + Ok(Category::Primitive) + } + + fn serialize_i16(self, _: i16) -> Result { + Ok(Category::Primitive) + } + + fn serialize_i32(self, _: i32) -> Result { + Ok(Category::Primitive) + } + + fn serialize_i64(self, _: i64) -> Result { + Ok(Category::Primitive) + } + + fn serialize_u8(self, _: u8) -> Result { + Ok(Category::Primitive) + } + + fn serialize_u16(self, _: u16) -> Result { + Ok(Category::Primitive) + } + + fn serialize_u32(self, _: u32) -> Result { + Ok(Category::Primitive) + } + + fn serialize_u64(self, _: u64) -> Result { + Ok(Category::Primitive) + } + + fn serialize_f32(self, _: f32) -> Result { + Ok(Category::Primitive) + } + + fn serialize_f64(self, _: f64) -> Result { + Ok(Category::Primitive) + } + + fn serialize_char(self, _: char) -> Result { + Ok(Category::Primitive) + } + + fn serialize_str(self, _: &str) -> Result { + Ok(Category::Primitive) + } + + fn serialize_bytes(self, _: &[u8]) -> Result { + Err(ser::Error::custom("unsupported")) + } + + fn serialize_none(self) -> Result { + Err(ser::Error::custom("unsupported")) + } + + fn serialize_some(self, v: &T) -> Result { + v.serialize(self) + } + + fn serialize_unit(self) -> Result { + Err(ser::Error::custom("unsupported")) + } + + fn serialize_unit_struct(self, _: &'static str) -> Result { + Err(ser::Error::custom("unsupported")) + } + + fn serialize_unit_variant(self, _: &'static str, _: usize, _: &'static str) -> Result { + Err(ser::Error::custom("unsupported")) + } + + fn serialize_newtype_struct(self, _: &'static str, v: &T) -> Result { + v.serialize(self) + } + + fn serialize_newtype_variant(self, _: &'static str, _: usize, _: &'static str, _: &T) -> Result { + Err(ser::Error::custom("unsupported")) + } + + fn serialize_seq(self, _: Option) -> Result { + Ok(self) + } + + fn serialize_seq_fixed_size(self, _: usize) -> Result { + Ok(self) + } + + fn serialize_tuple(self, _: usize) -> Result { + Err(ser::Error::custom("unsupported")) + } + + fn serialize_tuple_struct(self, _: &'static str, _: usize) -> Result { + Err(ser::Error::custom("unsupported")) + } + + fn serialize_tuple_variant(self, _: &'static str, _: usize, _: &'static str, _: usize) -> Result { + Err(ser::Error::custom("unsupported")) + } + + fn serialize_map(self, _: Option) -> Result { + Ok(self) + } + + fn serialize_struct(self, _: &'static str, _: usize) -> Result { + Ok(self) + } + + fn serialize_struct_variant(self, _: &'static str, _: usize, _: &'static str, _: usize) -> Result { + Err(ser::Error::custom("unsupported")) + } +} + +impl ser::SerializeSeq for Categorize { + type Ok = Category; + type Error = E; + + fn serialize_element(&mut self, _: &T) + -> Result<(), Self::Error> { + Ok(()) + } + + fn end(self) -> Result { + Ok(Category::Array) + } +} + +impl ser::SerializeMap for Categorize { + type Ok = Category; + type Error = E; + + fn serialize_key(&mut self, _: &T) + -> Result<(), Self::Error> { + Ok(()) + } + + fn serialize_value(&mut self, _: &T) + -> Result<(), Self::Error> { + Ok(()) + } + + fn end(self) -> Result { + Ok(Category::Table) + } +} + +impl ser::SerializeStruct for Categorize { + type Ok = Category; + type Error = E; + + fn serialize_field(&mut self, + _: &'static str, + _: &T) -> Result<(), Self::Error> + where T: ser::Serialize, + { + Ok(()) + } + + fn end(self) -> Result { + Ok(Category::Table) + } +} diff --git a/tests/tables-last.rs b/tests/tables-last.rs new file mode 100644 index 0000000..d05c8f0 --- /dev/null +++ b/tests/tables-last.rs @@ -0,0 +1,30 @@ +#[macro_use] +extern crate serde_derive; +extern crate toml; + +use std::collections::HashMap; + +#[derive(Serialize)] +struct A { + #[serde(serialize_with = "toml::ser::tables_last")] + vals: HashMap<&'static str, Value>, +} + +#[derive(Serialize)] +#[serde(untagged)] +enum Value { + Map(HashMap<&'static str, &'static str>), + Int(i32), +} + +#[test] +fn always_works() { + let mut a = A { vals: HashMap::new() }; + a.vals.insert("foo", Value::Int(0)); + + let mut sub = HashMap::new(); + sub.insert("foo", "bar"); + a.vals.insert("bar", Value::Map(sub)); + + toml::to_string(&a).unwrap(); +}