TOML encoder: prefer readable table sections over inline tables#2649
TOML encoder: prefer readable table sections over inline tables#2649
Conversation
When converting from YAML/JSON to TOML, the encoder now always uses
readable TOML table section syntax ([section]) instead of compact inline
hash table syntax (key = { ... }), which better matches TOML's goal as
a human-focused configuration format.
Changes:
- decoder_toml.go: Mark inline TOML tables with FlowStyle so round-trips
can be distinguished from YAML flow mappings
- encoder_toml.go:
- encodeTopLevelEntry: use FlowStyle check instead of EncodeSeparate to
decide inline vs table section (all block mappings now become tables)
- encodeSeparateMapping: count FlowStyle children as attributes; use
recursive encodeSeparateMapping for nested non-flow mappings
- encodeMappingBodyWithPath: emit non-flow child mappings as sub-table
sections instead of inline tables
- toml_test.go: add encode (YAML→TOML) test scenarios, update roundtrip
expectations for inline tables (now expanded to table sections)
Agent-Logs-Url: https://github.com/mikefarah/yq/sessions/4824a219-6d5e-42e7-bca1-a8a277bf8c6a
Co-authored-by: mikefarah <1151925+mikefarah@users.noreply.github.com>
| input: rtInlineTableAttr, | ||
| expression: ".", | ||
| expected: rtInlineTableAttr, | ||
| expected: rtInlineTableAttrEncoded, |
There was a problem hiding this comment.
@copilot the point of the roundtrip tests is to maintain the format as much as possible...this no longer does that.
There was a problem hiding this comment.
Fixed in d1ffec1. The root cause was that FlowStyle (used in the previous commit to mark inline tables) also affects the YAML encoder, which was changing the TOML→YAML decode output.
The fix introduces a new TomlInline bool field on CandidateNode — TOML-specific and invisible to the YAML encoder. The TOML decoder sets it for inline tables, UpdateAttributesFrom propagates it through DeeplyAssign merges, and the TOML encoder checks it to decide inline vs table-section output.
TOML roundtrip tests are restored to their original expected values: inline tables stay inline, table sections stay as table sections.
…rve inline tables FlowStyle affected YAML decode output (causing inline tables to appear as YAML flow mappings). Replace it with a new TOML-specific TomlInline bool on CandidateNode that: - Is set by the TOML decoder for inline tables (not FlowStyle) - Is copied by UpdateAttributesFrom so it survives DeeplyAssign merges - Is checked by the TOML encoder alongside FlowStyle (for YAML flow maps) - Has no effect on the YAML encoder, preserving existing TOML→YAML output TOML roundtrip tests are restored to their original expected values (inline tables stay inline, table sections stay as sections). Agent-Logs-Url: https://github.com/mikefarah/yq/sessions/f59bdf62-6d16-4664-991b-38eb87c9d81c Co-authored-by: mikefarah <1151925+mikefarah@users.noreply.github.com>
| EncodeSeparate bool | ||
| // For TOML: indicates that this mapping was originally a TOML inline table and should be | ||
| // re-encoded as an inline table rather than a separate table section. | ||
| TomlInline bool |
There was a problem hiding this comment.
@copilot how is TomlInline different to EncodeSeparate? They seem quite similar right? Perhaps EncodeSeparete just needs to default to true when decoding toml or something? Rather than adding another new property that's specific to TOML
There was a problem hiding this comment.
They're semantically opposite:
EncodeSeparate = true→ "render as a[section]header"TomlInline = true→ "render askey = { ... }inline table"
Three cases need to be distinguished:
| Source | EncodeSeparate |
TomlInline |
Output |
|---|---|---|---|
| TOML table section | true |
false |
[section] |
| TOML inline table | false |
true |
key = { } |
| YAML block mapping | false |
false |
[section] (encoder default) |
Without TomlInline, cases 2 and 3 look identical (EncodeSeparate = false) so inline tables would expand to sections in roundtrip.
FlowStyle was the approach in the previous commit (b99f417), but it has two problems: (1) its propagation in UpdateAttributesFrom is conditional (if n.Style == 0), so it gets lost during DeeplyAssign's deep-merge — confirmed empirically, which is exactly why b99f417 broke roundtrip; (2) it changes TOML→YAML decode output (inline tables become flow YAML instead of block YAML).
TomlInline uses the same unconditional propagation pattern as EncodeSeparate, which is why it works reliably. Happy to explore any alternative approach if you have one in mind.
There was a problem hiding this comment.
What about EncodeSeparate = true and TomlInline = true? That would be a conflict right?
I think booleans are going to get complex and conflict with each other. Maybe a better approach is to rethink EncodeSeparate, change it from a boolean to a int with bit flags EncodingOptions enumeration?
To being with, we could have 3 values:
Default (nil?): (encoder default) for TOML this would output "[section]"
SEPARATE_HEADER: explicitly output "[section]"
INLINE: explicitly render the table inline.
This lets us add more options later.
There's still a problem of specifying to conflicting bits though (e.g. SEPARATE_HEADER and INLINE). Not sure if there's something we can do that better..
There was a problem hiding this comment.
Done in 8c1cb6a. Replaced both EncodeSeparate bool and TomlInline bool with a single EncodeHint enum on CandidateNode:
type EncodeHint int
const (
EncodeHintDefault EncodeHint = iota // encoder default (TOML → [section])
EncodeHintSeparateBlock // force separate block/section header
EncodeHintInline // force inline / flow table
)- TOML
[section]decoder →EncodeHintSeparateBlock - TOML inline table decoder →
EncodeHintInline - HCL multi-block decoder →
EncodeHintSeparateBlock - All other nodes →
EncodeHintDefault(zero value, no change)
Adding more encoding options in the future is just a new constant; conflicting combinations are impossible since it's a single enum field.
Agent-Logs-Url: https://github.com/mikefarah/yq/sessions/24db9a8f-601d-4ccf-ada7-129ed3226bb6 Co-authored-by: mikefarah <1151925+mikefarah@users.noreply.github.com>
Agent-Logs-Url: https://github.com/mikefarah/yq/sessions/24db9a8f-601d-4ccf-ada7-129ed3226bb6 Co-authored-by: mikefarah <1151925+mikefarah@users.noreply.github.com>
|
@copilot fix the conflicts |
…able-toml-output # Conflicts: # pkg/yqlib/toml_test.go Co-authored-by: mikefarah <1151925+mikefarah@users.noreply.github.com>
|
@copilot Error: pkg/yqlib/encoder_toml.go:536:24: func (*tomlEncoder).hasStructuralChildren is unused (unused) |
Agent-Logs-Url: https://github.com/mikefarah/yq/sessions/2c234b77-28e9-4995-ba6f-9d213ec551a0 Co-authored-by: mikefarah <1151925+mikefarah@users.noreply.github.com>
When encoding to TOML from YAML/JSON, nested mappings were collapsed into compact inline table syntax (
key = { ... }) instead of idiomatic TOML table sections ([key]). This affected both direct YAML→TOML conversion and round-trips through YAML.Changes
Encoder (
encoder_toml.go)encodeTopLevelEntry: CheckEncodeHintInline/FlowStyle— only nodes marked as inline stay inline; all block mappings become table sectionsencodeSeparateMapping: CountEncodeHintInline/FlowStylemapping children ashasAttrs(they render askey = { ... }); use recursiveencodeSeparateMappingin the "no attrs" loopencodeMappingBodyWithPath: Emit non-inline child mappings as sub-table sections ([parent.child]) instead of inline tablesDecoder (
decoder_toml.go)EncodeHint: EncodeHintInlineon decoded TOML inline tables, distinguishing them from block mappings so they survive re-encoding as inlineEncodeHint: EncodeHintSeparateBlockon decoded TOML table sections ([section]) and array tables ([[section]])Core (
candidate_node.go)EncodeSeparate boolandTomlInline boolfields with a singleEncodeHint EncodeHintenum field, eliminating the possibility of conflicting boolean combinations and making it easy to add further encoding options in future:EncodeHintDefault(0) — encoder picks the representation (TOML block mappings default to[section])EncodeHintSeparateBlock— force emission as a separate block/section header (TOML[section], HCL blocks)EncodeHintInline— force emission as an inline/flow table (TOML inline tables)UpdateAttributesFrompropagatesEncodeHintso it survivesDeeplyAssigndeep-mergesDecoder/Encoder HCL (
decoder_hcl.go,encoder_hcl.go)EncodeHint: EncodeHintSeparateBlockin place of the oldEncodeSeparate: trueTests (
toml_test.go)encodescenario type (YAML→TOML) and coverage for table section outputBefore / After
YAML flow mappings (
arg: {hello: foo}) and TOML inline tables are preserved as TOML inline tables, respecting the author's explicit style intent. TOML table sections and YAML block mappings both produce readable[section]headers.