Skip to content

Commit b99f417

Browse files
Copilotmikefarah
andauthored
Fix TOML encoder to prefer readable table sections over inline tables
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>
1 parent a2f8c90 commit b99f417

File tree

4 files changed

+138
-21
lines changed

4 files changed

+138
-21
lines changed

pkg/yqlib/decoder_toml.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ func (dec *tomlDecoder) createInlineTableMap(tomlNode *toml.Node) (*CandidateNod
152152
return &CandidateNode{
153153
Kind: MappingNode,
154154
Tag: "!!map",
155+
Style: FlowStyle,
155156
Content: content,
156157
}, nil
157158
}

pkg/yqlib/doc/usage/toml.md

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,9 @@ yq '.' sample.toml
153153
```
154154
will output
155155
```yaml
156-
name = { first = "Tom", last = "Preston-Werner" }
156+
[name]
157+
first = "Tom"
158+
last = "Preston-Werner"
157159
```
158160

159161
## Roundtrip: table section
@@ -372,7 +374,10 @@ dob = 1979-05-27T07:32:00-08:00
372374
enabled = true
373375
ports = [8000, 8001, 8002]
374376
data = [["delta", "phi"], [3.14]]
375-
temp_targets = { cpu = 79.5, case = 72.0 }
377+
378+
[database.temp_targets]
379+
cpu = 79.5
380+
case = 72.0
376381

377382
# [servers] yq can't do this one yet
378383
[servers.alpha]
@@ -384,3 +389,20 @@ ip = "10.0.0.2"
384389
role = "backend"
385390
```
386391

392+
## Encode: Simple mapping produces table section
393+
Given a sample.yml file of:
394+
```yaml
395+
arg:
396+
hello: foo
397+
398+
```
399+
then
400+
```bash
401+
yq -o toml '.' sample.yml
402+
```
403+
will output
404+
```toml
405+
[arg]
406+
hello = "foo"
407+
```
408+

pkg/yqlib/encoder_toml.go

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -162,12 +162,10 @@ func (te *tomlEncoder) encodeTopLevelEntry(w io.Writer, path []string, node *Can
162162
// Regular array attribute
163163
return te.writeArrayAttribute(w, path[len(path)-1], node)
164164
case MappingNode:
165-
// Inline table if not EncodeSeparate, else emit separate tables/arrays of tables for children under this path
166-
if !node.EncodeSeparate {
167-
// If children contain mappings or arrays of mappings, prefer separate sections
168-
if te.hasEncodeSeparateChild(node) || te.hasStructuralChildren(node) {
169-
return te.encodeSeparateMapping(w, path, node)
170-
}
165+
// Use inline table syntax only for nodes explicitly marked with flow/inline style
166+
// (e.g. TOML inline tables or YAML flow mappings). All other mappings become
167+
// readable TOML table sections.
168+
if node.Style&FlowStyle != 0 {
171169
return te.writeInlineTableAttribute(w, path[len(path)-1], node)
172170
}
173171
return te.encodeSeparateMapping(w, path, node)
@@ -429,14 +427,19 @@ func (te *tomlEncoder) writeTableHeader(w io.Writer, path []string, m *Candidate
429427
// encodeSeparateMapping handles a mapping that should be encoded as table sections.
430428
// It emits the table header for this mapping if it has any content, then processes children.
431429
func (te *tomlEncoder) encodeSeparateMapping(w io.Writer, path []string, m *CandidateNode) error {
432-
// Check if this mapping has any non-mapping, non-array-of-tables children (i.e., attributes)
430+
// Check if this mapping has any non-mapping, non-array-of-tables children (i.e., attributes).
431+
// Flow-style (inline) mapping children also count as attributes since they render as key = { ... }.
433432
hasAttrs := false
434433
for i := 0; i < len(m.Content); i += 2 {
435434
v := m.Content[i+1]
436435
if v.Kind == ScalarNode {
437436
hasAttrs = true
438437
break
439438
}
439+
if v.Kind == MappingNode && v.Style&FlowStyle != 0 {
440+
hasAttrs = true
441+
break
442+
}
440443
if v.Kind == SequenceNode {
441444
// Check if it's NOT an array of tables
442445
allMaps := true
@@ -464,18 +467,14 @@ func (te *tomlEncoder) encodeSeparateMapping(w io.Writer, path []string, m *Cand
464467
return nil
465468
}
466469

467-
// No attributes, just nested structures - process children
470+
// No attributes, just nested table structures - process children recursively
468471
for i := 0; i < len(m.Content); i += 2 {
469472
k := m.Content[i].Value
470473
v := m.Content[i+1]
471474
switch v.Kind {
472475
case MappingNode:
473-
// Emit [path.k]
474476
newPath := append(append([]string{}, path...), k)
475-
if err := te.writeTableHeader(w, newPath, v); err != nil {
476-
return err
477-
}
478-
if err := te.encodeMappingBodyWithPath(w, newPath, v); err != nil {
477+
if err := te.encodeSeparateMapping(w, newPath, v); err != nil {
479478
return err
480479
}
481480
case SequenceNode:
@@ -599,13 +598,21 @@ func (te *tomlEncoder) encodeMappingBodyWithPath(w io.Writer, path []string, m *
599598
}
600599
}
601600

602-
// Finally, child mappings that are not marked EncodeSeparate get inlined as attributes
601+
// Finally, child mappings: flow-style (inline) ones become inline table attributes,
602+
// while all others are emitted as separate sub-table sections.
603603
for i := 0; i < len(m.Content); i += 2 {
604604
k := m.Content[i].Value
605605
v := m.Content[i+1]
606-
if v.Kind == MappingNode && !v.EncodeSeparate {
607-
if err := te.writeInlineTableAttribute(w, k, v); err != nil {
608-
return err
606+
if v.Kind == MappingNode {
607+
if v.Style&FlowStyle != 0 {
608+
if err := te.writeInlineTableAttribute(w, k, v); err != nil {
609+
return err
610+
}
611+
} else {
612+
subPath := append(append([]string{}, path...), k)
613+
if err := te.encodeSeparateMapping(w, subPath, v); err != nil {
614+
return err
615+
}
609616
}
610617
}
611618
}

pkg/yqlib/toml_test.go

Lines changed: 89 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,12 @@ var expectedSampleWithHeader = `servers:
182182
var rtInlineTableAttr = `name = { first = "Tom", last = "Preston-Werner" }
183183
`
184184

185+
// Inline tables are converted to readable table sections on encode
186+
var rtInlineTableAttrEncoded = `[name]
187+
first = "Tom"
188+
last = "Preston-Werner"
189+
`
190+
185191
var rtTableSection = `[owner.contact]
186192
name = "Tom"
187193
age = 36
@@ -263,6 +269,33 @@ ip = "10.0.0.2"
263269
role = "backend"
264270
`
265271

272+
// Inline table temp_targets is expanded to a readable sub-table when re-encoding
273+
var sampleFromWebEncoded = `# This is a TOML document
274+
title = "TOML Example"
275+
276+
[owner]
277+
name = "Tom Preston-Werner"
278+
dob = 1979-05-27T07:32:00-08:00
279+
280+
[database]
281+
enabled = true
282+
ports = [8000, 8001, 8002]
283+
data = [["delta", "phi"], [3.14]]
284+
285+
[database.temp_targets]
286+
cpu = 79.5
287+
case = 72.0
288+
289+
# [servers] yq can't do this one yet
290+
[servers.alpha]
291+
ip = "10.0.0.1"
292+
role = "frontend"
293+
294+
[servers.beta]
295+
ip = "10.0.0.2"
296+
role = "backend"
297+
`
298+
266299
var subArrays = `
267300
[[array]]
268301
@@ -506,7 +539,7 @@ var tomlScenarios = []formatScenario{
506539
description: "Roundtrip: inline table attribute",
507540
input: rtInlineTableAttr,
508541
expression: ".",
509-
expected: rtInlineTableAttr,
542+
expected: rtInlineTableAttrEncoded,
510543
scenarioType: "roundtrip",
511544
},
512545
{
@@ -605,7 +638,7 @@ var tomlScenarios = []formatScenario{
605638
description: "Roundtrip: sample from web",
606639
input: sampleFromWeb,
607640
expression: ".",
608-
expected: sampleFromWeb,
641+
expected: sampleFromWebEncoded,
609642
scenarioType: "roundtrip",
610643
},
611644
{
@@ -614,6 +647,34 @@ var tomlScenarios = []formatScenario{
614647
expected: tomlTableWithComments,
615648
scenarioType: "roundtrip",
616649
},
650+
// Encode (YAML → TOML) scenarios - verify readable table sections are produced
651+
{
652+
description: "Encode: Simple mapping produces table section",
653+
input: "arg:\n hello: foo\n",
654+
expected: "[arg]\nhello = \"foo\"\n",
655+
scenarioType: "encode",
656+
},
657+
{
658+
skipDoc: true,
659+
description: "Encode: Nested mappings produce nested table sections",
660+
input: "a:\n b:\n c: val\n",
661+
expected: "[a.b]\nc = \"val\"\n",
662+
scenarioType: "encode",
663+
},
664+
{
665+
skipDoc: true,
666+
description: "Encode: Mixed scalars and nested mapping",
667+
input: "a:\n hello: foo\n nested:\n key: val\n",
668+
expected: "[a]\nhello = \"foo\"\n\n[a.nested]\nkey = \"val\"\n",
669+
scenarioType: "encode",
670+
},
671+
{
672+
skipDoc: true,
673+
description: "Encode: YAML flow mapping stays inline",
674+
input: "arg: {hello: foo}\n",
675+
expected: "arg = { hello = \"foo\" }\n",
676+
scenarioType: "encode",
677+
},
617678
}
618679

619680
func testTomlScenario(t *testing.T, s formatScenario) {
@@ -629,6 +690,8 @@ func testTomlScenario(t *testing.T, s formatScenario) {
629690
}
630691
case "roundtrip":
631692
test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewTomlDecoder(), NewTomlEncoder()), s.description)
693+
case "encode":
694+
test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewTomlEncoder()), s.description)
632695
}
633696
}
634697

@@ -654,6 +717,28 @@ func documentTomlDecodeScenario(w *bufio.Writer, s formatScenario) {
654717
writeOrPanic(w, fmt.Sprintf("```yaml\n%v```\n\n", mustProcessFormatScenario(s, NewTomlDecoder(), NewYamlEncoder(ConfiguredYamlPreferences))))
655718
}
656719

720+
func documentTomlEncodeScenario(w *bufio.Writer, s formatScenario) {
721+
writeOrPanic(w, fmt.Sprintf("## %v\n", s.description))
722+
723+
if s.subdescription != "" {
724+
writeOrPanic(w, s.subdescription)
725+
writeOrPanic(w, "\n\n")
726+
}
727+
728+
writeOrPanic(w, "Given a sample.yml file of:\n")
729+
writeOrPanic(w, fmt.Sprintf("```yaml\n%v\n```\n", s.input))
730+
731+
writeOrPanic(w, "then\n")
732+
expression := s.expression
733+
if expression == "" {
734+
expression = "."
735+
}
736+
writeOrPanic(w, fmt.Sprintf("```bash\nyq -o toml '%v' sample.yml\n```\n", expression))
737+
writeOrPanic(w, "will output\n")
738+
739+
writeOrPanic(w, fmt.Sprintf("```toml\n%v```\n\n", mustProcessFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewTomlEncoder())))
740+
}
741+
657742
func documentTomlRoundtripScenario(w *bufio.Writer, s formatScenario) {
658743
writeOrPanic(w, fmt.Sprintf("## %v\n", s.description))
659744

@@ -687,6 +772,8 @@ func documentTomlScenario(_ *testing.T, w *bufio.Writer, i interface{}) {
687772
documentTomlDecodeScenario(w, s)
688773
case "roundtrip":
689774
documentTomlRoundtripScenario(w, s)
775+
case "encode":
776+
documentTomlEncodeScenario(w, s)
690777

691778
default:
692779
panic(fmt.Sprintf("unhandled scenario type %q", s.scenarioType))

0 commit comments

Comments
 (0)