mirror of
https://github.com/mikefarah/yq.git
synced 2026-06-30 17:21:42 +00:00
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>
This commit is contained in:
parent
a2f8c900bf
commit
b99f4174ec
@ -152,6 +152,7 @@ func (dec *tomlDecoder) createInlineTableMap(tomlNode *toml.Node) (*CandidateNod
|
||||
return &CandidateNode{
|
||||
Kind: MappingNode,
|
||||
Tag: "!!map",
|
||||
Style: FlowStyle,
|
||||
Content: content,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@ -153,7 +153,9 @@ yq '.' sample.toml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
name = { first = "Tom", last = "Preston-Werner" }
|
||||
[name]
|
||||
first = "Tom"
|
||||
last = "Preston-Werner"
|
||||
```
|
||||
|
||||
## Roundtrip: table section
|
||||
@ -372,7 +374,10 @@ dob = 1979-05-27T07:32:00-08:00
|
||||
enabled = true
|
||||
ports = [8000, 8001, 8002]
|
||||
data = [["delta", "phi"], [3.14]]
|
||||
temp_targets = { cpu = 79.5, case = 72.0 }
|
||||
|
||||
[database.temp_targets]
|
||||
cpu = 79.5
|
||||
case = 72.0
|
||||
|
||||
# [servers] yq can't do this one yet
|
||||
[servers.alpha]
|
||||
@ -384,3 +389,20 @@ ip = "10.0.0.2"
|
||||
role = "backend"
|
||||
```
|
||||
|
||||
## Encode: Simple mapping produces table section
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
arg:
|
||||
hello: foo
|
||||
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq -o toml '.' sample.yml
|
||||
```
|
||||
will output
|
||||
```toml
|
||||
[arg]
|
||||
hello = "foo"
|
||||
```
|
||||
|
||||
|
||||
@ -162,12 +162,10 @@ func (te *tomlEncoder) encodeTopLevelEntry(w io.Writer, path []string, node *Can
|
||||
// Regular array attribute
|
||||
return te.writeArrayAttribute(w, path[len(path)-1], node)
|
||||
case MappingNode:
|
||||
// Inline table if not EncodeSeparate, else emit separate tables/arrays of tables for children under this path
|
||||
if !node.EncodeSeparate {
|
||||
// If children contain mappings or arrays of mappings, prefer separate sections
|
||||
if te.hasEncodeSeparateChild(node) || te.hasStructuralChildren(node) {
|
||||
return te.encodeSeparateMapping(w, path, node)
|
||||
}
|
||||
// Use inline table syntax only for nodes explicitly marked with flow/inline style
|
||||
// (e.g. TOML inline tables or YAML flow mappings). All other mappings become
|
||||
// readable TOML table sections.
|
||||
if node.Style&FlowStyle != 0 {
|
||||
return te.writeInlineTableAttribute(w, path[len(path)-1], node)
|
||||
}
|
||||
return te.encodeSeparateMapping(w, path, node)
|
||||
@ -429,7 +427,8 @@ func (te *tomlEncoder) writeTableHeader(w io.Writer, path []string, m *Candidate
|
||||
// encodeSeparateMapping handles a mapping that should be encoded as table sections.
|
||||
// It emits the table header for this mapping if it has any content, then processes children.
|
||||
func (te *tomlEncoder) encodeSeparateMapping(w io.Writer, path []string, m *CandidateNode) error {
|
||||
// Check if this mapping has any non-mapping, non-array-of-tables children (i.e., attributes)
|
||||
// Check if this mapping has any non-mapping, non-array-of-tables children (i.e., attributes).
|
||||
// Flow-style (inline) mapping children also count as attributes since they render as key = { ... }.
|
||||
hasAttrs := false
|
||||
for i := 0; i < len(m.Content); i += 2 {
|
||||
v := m.Content[i+1]
|
||||
@ -437,6 +436,10 @@ func (te *tomlEncoder) encodeSeparateMapping(w io.Writer, path []string, m *Cand
|
||||
hasAttrs = true
|
||||
break
|
||||
}
|
||||
if v.Kind == MappingNode && v.Style&FlowStyle != 0 {
|
||||
hasAttrs = true
|
||||
break
|
||||
}
|
||||
if v.Kind == SequenceNode {
|
||||
// Check if it's NOT an array of tables
|
||||
allMaps := true
|
||||
@ -464,18 +467,14 @@ func (te *tomlEncoder) encodeSeparateMapping(w io.Writer, path []string, m *Cand
|
||||
return nil
|
||||
}
|
||||
|
||||
// No attributes, just nested structures - process children
|
||||
// No attributes, just nested table structures - process children recursively
|
||||
for i := 0; i < len(m.Content); i += 2 {
|
||||
k := m.Content[i].Value
|
||||
v := m.Content[i+1]
|
||||
switch v.Kind {
|
||||
case MappingNode:
|
||||
// Emit [path.k]
|
||||
newPath := append(append([]string{}, path...), k)
|
||||
if err := te.writeTableHeader(w, newPath, v); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := te.encodeMappingBodyWithPath(w, newPath, v); err != nil {
|
||||
if err := te.encodeSeparateMapping(w, newPath, v); err != nil {
|
||||
return err
|
||||
}
|
||||
case SequenceNode:
|
||||
@ -599,13 +598,21 @@ func (te *tomlEncoder) encodeMappingBodyWithPath(w io.Writer, path []string, m *
|
||||
}
|
||||
}
|
||||
|
||||
// Finally, child mappings that are not marked EncodeSeparate get inlined as attributes
|
||||
// Finally, child mappings: flow-style (inline) ones become inline table attributes,
|
||||
// while all others are emitted as separate sub-table sections.
|
||||
for i := 0; i < len(m.Content); i += 2 {
|
||||
k := m.Content[i].Value
|
||||
v := m.Content[i+1]
|
||||
if v.Kind == MappingNode && !v.EncodeSeparate {
|
||||
if err := te.writeInlineTableAttribute(w, k, v); err != nil {
|
||||
return err
|
||||
if v.Kind == MappingNode {
|
||||
if v.Style&FlowStyle != 0 {
|
||||
if err := te.writeInlineTableAttribute(w, k, v); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
subPath := append(append([]string{}, path...), k)
|
||||
if err := te.encodeSeparateMapping(w, subPath, v); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -182,6 +182,12 @@ var expectedSampleWithHeader = `servers:
|
||||
var rtInlineTableAttr = `name = { first = "Tom", last = "Preston-Werner" }
|
||||
`
|
||||
|
||||
// Inline tables are converted to readable table sections on encode
|
||||
var rtInlineTableAttrEncoded = `[name]
|
||||
first = "Tom"
|
||||
last = "Preston-Werner"
|
||||
`
|
||||
|
||||
var rtTableSection = `[owner.contact]
|
||||
name = "Tom"
|
||||
age = 36
|
||||
@ -263,6 +269,33 @@ ip = "10.0.0.2"
|
||||
role = "backend"
|
||||
`
|
||||
|
||||
// Inline table temp_targets is expanded to a readable sub-table when re-encoding
|
||||
var sampleFromWebEncoded = `# This is a TOML document
|
||||
title = "TOML Example"
|
||||
|
||||
[owner]
|
||||
name = "Tom Preston-Werner"
|
||||
dob = 1979-05-27T07:32:00-08:00
|
||||
|
||||
[database]
|
||||
enabled = true
|
||||
ports = [8000, 8001, 8002]
|
||||
data = [["delta", "phi"], [3.14]]
|
||||
|
||||
[database.temp_targets]
|
||||
cpu = 79.5
|
||||
case = 72.0
|
||||
|
||||
# [servers] yq can't do this one yet
|
||||
[servers.alpha]
|
||||
ip = "10.0.0.1"
|
||||
role = "frontend"
|
||||
|
||||
[servers.beta]
|
||||
ip = "10.0.0.2"
|
||||
role = "backend"
|
||||
`
|
||||
|
||||
var subArrays = `
|
||||
[[array]]
|
||||
|
||||
@ -506,7 +539,7 @@ var tomlScenarios = []formatScenario{
|
||||
description: "Roundtrip: inline table attribute",
|
||||
input: rtInlineTableAttr,
|
||||
expression: ".",
|
||||
expected: rtInlineTableAttr,
|
||||
expected: rtInlineTableAttrEncoded,
|
||||
scenarioType: "roundtrip",
|
||||
},
|
||||
{
|
||||
@ -605,7 +638,7 @@ var tomlScenarios = []formatScenario{
|
||||
description: "Roundtrip: sample from web",
|
||||
input: sampleFromWeb,
|
||||
expression: ".",
|
||||
expected: sampleFromWeb,
|
||||
expected: sampleFromWebEncoded,
|
||||
scenarioType: "roundtrip",
|
||||
},
|
||||
{
|
||||
@ -614,6 +647,34 @@ var tomlScenarios = []formatScenario{
|
||||
expected: tomlTableWithComments,
|
||||
scenarioType: "roundtrip",
|
||||
},
|
||||
// Encode (YAML → TOML) scenarios - verify readable table sections are produced
|
||||
{
|
||||
description: "Encode: Simple mapping produces table section",
|
||||
input: "arg:\n hello: foo\n",
|
||||
expected: "[arg]\nhello = \"foo\"\n",
|
||||
scenarioType: "encode",
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
description: "Encode: Nested mappings produce nested table sections",
|
||||
input: "a:\n b:\n c: val\n",
|
||||
expected: "[a.b]\nc = \"val\"\n",
|
||||
scenarioType: "encode",
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
description: "Encode: Mixed scalars and nested mapping",
|
||||
input: "a:\n hello: foo\n nested:\n key: val\n",
|
||||
expected: "[a]\nhello = \"foo\"\n\n[a.nested]\nkey = \"val\"\n",
|
||||
scenarioType: "encode",
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
description: "Encode: YAML flow mapping stays inline",
|
||||
input: "arg: {hello: foo}\n",
|
||||
expected: "arg = { hello = \"foo\" }\n",
|
||||
scenarioType: "encode",
|
||||
},
|
||||
}
|
||||
|
||||
func testTomlScenario(t *testing.T, s formatScenario) {
|
||||
@ -629,6 +690,8 @@ func testTomlScenario(t *testing.T, s formatScenario) {
|
||||
}
|
||||
case "roundtrip":
|
||||
test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewTomlDecoder(), NewTomlEncoder()), s.description)
|
||||
case "encode":
|
||||
test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewTomlEncoder()), s.description)
|
||||
}
|
||||
}
|
||||
|
||||
@ -654,6 +717,28 @@ func documentTomlDecodeScenario(w *bufio.Writer, s formatScenario) {
|
||||
writeOrPanic(w, fmt.Sprintf("```yaml\n%v```\n\n", mustProcessFormatScenario(s, NewTomlDecoder(), NewYamlEncoder(ConfiguredYamlPreferences))))
|
||||
}
|
||||
|
||||
func documentTomlEncodeScenario(w *bufio.Writer, s formatScenario) {
|
||||
writeOrPanic(w, fmt.Sprintf("## %v\n", s.description))
|
||||
|
||||
if s.subdescription != "" {
|
||||
writeOrPanic(w, s.subdescription)
|
||||
writeOrPanic(w, "\n\n")
|
||||
}
|
||||
|
||||
writeOrPanic(w, "Given a sample.yml file of:\n")
|
||||
writeOrPanic(w, fmt.Sprintf("```yaml\n%v\n```\n", s.input))
|
||||
|
||||
writeOrPanic(w, "then\n")
|
||||
expression := s.expression
|
||||
if expression == "" {
|
||||
expression = "."
|
||||
}
|
||||
writeOrPanic(w, fmt.Sprintf("```bash\nyq -o toml '%v' sample.yml\n```\n", expression))
|
||||
writeOrPanic(w, "will output\n")
|
||||
|
||||
writeOrPanic(w, fmt.Sprintf("```toml\n%v```\n\n", mustProcessFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewTomlEncoder())))
|
||||
}
|
||||
|
||||
func documentTomlRoundtripScenario(w *bufio.Writer, s formatScenario) {
|
||||
writeOrPanic(w, fmt.Sprintf("## %v\n", s.description))
|
||||
|
||||
@ -687,6 +772,8 @@ func documentTomlScenario(_ *testing.T, w *bufio.Writer, i interface{}) {
|
||||
documentTomlDecodeScenario(w, s)
|
||||
case "roundtrip":
|
||||
documentTomlRoundtripScenario(w, s)
|
||||
case "encode":
|
||||
documentTomlEncodeScenario(w, s)
|
||||
|
||||
default:
|
||||
panic(fmt.Sprintf("unhandled scenario type %q", s.scenarioType))
|
||||
|
||||
Loading…
Reference in New Issue
Block a user