toml icon indicating copy to clipboard operation
toml copied to clipboard

Feature request: Macros / formatting hooks to control TOML serialization layout in toml_edit

Open ethever opened this issue 5 months ago • 1 comments

Summary

toml_edit focuses on lossless editing, but when serializing Rust types into a DocumentMut (or string), developers often need fine-grained control over the resulting layout—e.g., inline vs. multiline tables, array breaking rules, key ordering, comment injection, and whitespace. Today this requires manual post-processing of the produced DocumentMut, which is brittle and verbose.

I’m proposing first-class formatting controls—either via attribute/derive macros or via a pluggable formatter API—so users can declaratively specify layout while still leveraging serde + toml_edit’s lossless model.

Motivation & use cases

Keep certain small structs as inline tables (e.g., { x = 1, y = 2 }) while keeping others expanded.

  • Force arrays to be inline or multiline (and wrap after N items).

  • Preserve or inject comments at field, table, or document level.

  • Control key ordering/grouping (e.g., “provider.* first, then strategy.*”).

  • Normalize spacing/blank lines between logical sections.

  • Enforce project-wide style without hand-editing the DocumentMut afterwards.

  • These are common needs in configuration tooling and code generators.

Current behavior

Given (simplified):

#[derive(serde::Serialize)]
struct ProviderConfig {
    read_only_rpc_urls: Vec<Vec<String>>,
    write_only_rpc_url: String,
}

#[derive(serde::Serialize)]
struct AppConfig {
    provider_config: ProviderConfig,
    // ...
}

let doc = toml_edit::ser::to_document(&app_config).unwrap();
let s = doc.to_string(); // or to_string_in_original_order()

We get a reasonable TOML, but we cannot declaratively say:

  • “serialize provider_config inline”

  • “format this array multiline with one element per line”

  • “insert a comment above provider_config”

  • “place [dex_price_fetcher_config] before [provider_config]” …without writing custom traversal and mutation code over DocumentMut.

Workarounds (and why they’re not ideal)

  • Serialize to string and re-parse into DocumentMut, then mutate nodes via inline_table, as_array_mut, etc. → Works but is error-prone, verbose, and couples business logic to formatting internals.

  • Custom serde newtypes with manual Serialize impls. → Pushes formatting into ad-hoc serialization code and doesn’t scale for mixed inline/multiline requirements across a large schema.

Proposed solutions

Attribute / derive-based formatting hints Allow users to annotate structs/fields to guide layout. Examples:

#[derive(Serialize)]
struct AppConfig {
    /// Put a comment above this table
    #[toml_edit(comment = "Provider endpoints and options")]
    #[toml_edit(order = 10)]
    provider_config: ProviderConfig,

    /// Emit as an inline table
    #[toml_edit(inline)]
    network: Network,
}

#[derive(Serialize)]
struct ProviderConfig {
    /// Multiline array, one element per line
    #[toml_edit(array = "multiline", per_line = 1, trailing_comma = false)]
    read_only_rpc_urls: Vec<Vec<String>>,

    /// Keep inline (even if long)
    #[toml_edit(inline)]
    write_only_rpc_url: String,
}

ethever avatar Aug 13 '25 02:08 ethever

#[toml_edit] attributes would not work with serde.

What you can do is serialize to a DocumentMut (instead of a String) and then edit the document to suit your needs. VisitMut can help with that.

epage avatar Aug 13 '25 13:08 epage