Search code examples
rustserdetoml

Serialize toml ArrayOfTables using InlineTables


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 InlineTables 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 Tables 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.


Solution

  • Note: Chayim Friedman pointed out in a comment above that toml_edit::ser::to_string() prints all tables as InlineTables, 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 Tables 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())
        }
    }