I have as struct that looks like this:
#[derive(Serialize, Deserialize)]
pub struct TestItem {
pub a: String,
pub b: String,
}
#[derive(Serialize, Deserialize)]
pub struct Test {
pub items: Vec<TestItem>,
}
I want to use InlineTable
s to serialize this as its much more readable, however the toml
crate defaults to regular tables. For instance I want:
items = [
{ a = "testa1", b = "testb1" },
# etc
]
but toml::to_string(...)
gives
[[items]]
a = "testa1"
b = "testb1"
#etc
This seems to be a fairly common request, see toml#592 for example, and there is an example in the toml_edit
crate which doesn't seem to work in this case as items
is an ArrayOfTables
.
I've tried the following visitor
impl VisitMut for TestVisitor {
fn visit_table_mut(&mut self, node: &mut Table) {
visit_table_mut(self, node);
node.set_implicit(true);
}
fn visit_table_like_kv_mut(&mut self, mut key: KeyMut<'_>, node: &mut Item) {
println!("This is hit: {:?} -> {node:?}", key.get());
if let Item::Table(table) = node {
println!("This is never hit, as its an ArrayOfTables");
let table = std::mem::replace(table, Table::new());
let inline_table = table.into_inline_table();
key.fmt();
*node = Item::Value(Value::InlineTable(inline_table));
}
// Recurse further into the document tree.
visit_table_like_kv_mut(self, key, node);
}
fn visit_array_mut(&mut self, node: &mut Array) {
node.fmt();
}
}
But this seems to skip the Table
s within the ArrayOfTables
, so the if
is never hit. The output is:
This is hit: "items" -> ArrayOfTables(...)
This is hit: "a" -> Value(String(...)
// etc - the tables are never visited
How would I go about constructing a visitor that can convert any table within items
into an InlineTable
?it seems all the other methods in VisitMut
take a Table
and so I can't mutate them to an InlineTable
as the example requires.
Note: Chayim Friedman pointed out in a comment above that toml_edit::ser::to_string()
prints all tables as InlineTable
s, which is different to how toml::to_string
, toml::to_string_pretty
and toml_edit::to_string_pretty
behave. My use case was a bit more complex as I only wanted some tables to print as inline, so I made a custom visitor as described below.
OK, so looking at this the two different TOML formats actually parse in different ways.
[[items]]
a = "testa1"
b = "testb1"
Parses as an Item::ArrayOfTables
which only accepts Table
s as its items, while:
items = [
{ a = "testa1", b = "testb1" },
# etc
]
parses as an Item::Value(Value::Array(...))
where the elements are Value::InlineTable
. This means a visitor like the following seems to do the trick.
struct TestVisitor;
// I could be wrong here, but it seems I have to use the `kv` variant here
// because `visit_table_like_mut` has `node: &mut dyn toml_edit::TableLike`
// as the second argument, which `ArrayOfTables` doesn't seem to impl
impl VisitMut for TestVisitor {
fn visit_table_like_kv_mut(&mut self, mut key: KeyMut<'_>, node: &mut Item) {
if let Item::ArrayOfTables(tables) = node {
let new_tables = tables
.iter_mut()
.map(|t| {
let table = std::mem::replace(t, Table::new());
Value::InlineTable(table.into_inline_table()).decorated("\n", "")
})
.collect::<Vec<_>>();
key.fmt();
*node = Item::Value(Value::Array(Array::from_iter(new_tables.iter())));
}
// Recurse further into the document tree.
visit_table_like_kv_mut(self, key, node);
}
}
Which I can use like this:
impl Test {
pub fn to_toml(&self) -> anyhow::Result<String> {
let mut doc = toml_edit::ser::to_document(self)?;
let mut visitor = TestVisitor;
visitor.visit_document_mut(&mut doc);
Ok(doc.to_string())
}
}