mirror of
https://github.com/mikefarah/yq.git
synced 2026-07-06 04:45:39 +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{
|
return &CandidateNode{
|
||||||
Kind: MappingNode,
|
Kind: MappingNode,
|
||||||
Tag: "!!map",
|
Tag: "!!map",
|
||||||
|
Style: FlowStyle,
|
||||||
Content: content,
|
Content: content,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -153,7 +153,9 @@ yq '.' sample.toml
|
|||||||
```
|
```
|
||||||
will output
|
will output
|
||||||
```yaml
|
```yaml
|
||||||
name = { first = "Tom", last = "Preston-Werner" }
|
[name]
|
||||||
|
first = "Tom"
|
||||||
|
last = "Preston-Werner"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Roundtrip: table section
|
## Roundtrip: table section
|
||||||
@ -372,7 +374,10 @@ dob = 1979-05-27T07:32:00-08:00
|
|||||||
enabled = true
|
enabled = true
|
||||||
ports = [8000, 8001, 8002]
|
ports = [8000, 8001, 8002]
|
||||||
data = [["delta", "phi"], [3.14]]
|
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] yq can't do this one yet
|
||||||
[servers.alpha]
|
[servers.alpha]
|
||||||
@ -384,3 +389,20 @@ ip = "10.0.0.2"
|
|||||||
role = "backend"
|
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
|
// Regular array attribute
|
||||||
return te.writeArrayAttribute(w, path[len(path)-1], node)
|
return te.writeArrayAttribute(w, path[len(path)-1], node)
|
||||||
case MappingNode:
|
case MappingNode:
|
||||||
// Inline table if not EncodeSeparate, else emit separate tables/arrays of tables for children under this path
|
// Use inline table syntax only for nodes explicitly marked with flow/inline style
|
||||||
if !node.EncodeSeparate {
|
// (e.g. TOML inline tables or YAML flow mappings). All other mappings become
|
||||||
// If children contain mappings or arrays of mappings, prefer separate sections
|
// readable TOML table sections.
|
||||||
if te.hasEncodeSeparateChild(node) || te.hasStructuralChildren(node) {
|
if node.Style&FlowStyle != 0 {
|
||||||
return te.encodeSeparateMapping(w, path, node)
|
|
||||||
}
|
|
||||||
return te.writeInlineTableAttribute(w, path[len(path)-1], node)
|
return te.writeInlineTableAttribute(w, path[len(path)-1], node)
|
||||||
}
|
}
|
||||||
return te.encodeSeparateMapping(w, path, 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.
|
// 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.
|
// 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 {
|
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
|
hasAttrs := false
|
||||||
for i := 0; i < len(m.Content); i += 2 {
|
for i := 0; i < len(m.Content); i += 2 {
|
||||||
v := m.Content[i+1]
|
v := m.Content[i+1]
|
||||||
@ -437,6 +436,10 @@ func (te *tomlEncoder) encodeSeparateMapping(w io.Writer, path []string, m *Cand
|
|||||||
hasAttrs = true
|
hasAttrs = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
if v.Kind == MappingNode && v.Style&FlowStyle != 0 {
|
||||||
|
hasAttrs = true
|
||||||
|
break
|
||||||
|
}
|
||||||
if v.Kind == SequenceNode {
|
if v.Kind == SequenceNode {
|
||||||
// Check if it's NOT an array of tables
|
// Check if it's NOT an array of tables
|
||||||
allMaps := true
|
allMaps := true
|
||||||
@ -464,18 +467,14 @@ func (te *tomlEncoder) encodeSeparateMapping(w io.Writer, path []string, m *Cand
|
|||||||
return nil
|
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 {
|
for i := 0; i < len(m.Content); i += 2 {
|
||||||
k := m.Content[i].Value
|
k := m.Content[i].Value
|
||||||
v := m.Content[i+1]
|
v := m.Content[i+1]
|
||||||
switch v.Kind {
|
switch v.Kind {
|
||||||
case MappingNode:
|
case MappingNode:
|
||||||
// Emit [path.k]
|
|
||||||
newPath := append(append([]string{}, path...), k)
|
newPath := append(append([]string{}, path...), k)
|
||||||
if err := te.writeTableHeader(w, newPath, v); err != nil {
|
if err := te.encodeSeparateMapping(w, newPath, v); err != nil {
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := te.encodeMappingBodyWithPath(w, newPath, v); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
case SequenceNode:
|
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 {
|
for i := 0; i < len(m.Content); i += 2 {
|
||||||
k := m.Content[i].Value
|
k := m.Content[i].Value
|
||||||
v := m.Content[i+1]
|
v := m.Content[i+1]
|
||||||
if v.Kind == MappingNode && !v.EncodeSeparate {
|
if v.Kind == MappingNode {
|
||||||
if err := te.writeInlineTableAttribute(w, k, v); err != nil {
|
if v.Style&FlowStyle != 0 {
|
||||||
return err
|
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" }
|
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]
|
var rtTableSection = `[owner.contact]
|
||||||
name = "Tom"
|
name = "Tom"
|
||||||
age = 36
|
age = 36
|
||||||
@ -263,6 +269,33 @@ ip = "10.0.0.2"
|
|||||||
role = "backend"
|
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 = `
|
var subArrays = `
|
||||||
[[array]]
|
[[array]]
|
||||||
|
|
||||||
@ -506,7 +539,7 @@ var tomlScenarios = []formatScenario{
|
|||||||
description: "Roundtrip: inline table attribute",
|
description: "Roundtrip: inline table attribute",
|
||||||
input: rtInlineTableAttr,
|
input: rtInlineTableAttr,
|
||||||
expression: ".",
|
expression: ".",
|
||||||
expected: rtInlineTableAttr,
|
expected: rtInlineTableAttrEncoded,
|
||||||
scenarioType: "roundtrip",
|
scenarioType: "roundtrip",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -605,7 +638,7 @@ var tomlScenarios = []formatScenario{
|
|||||||
description: "Roundtrip: sample from web",
|
description: "Roundtrip: sample from web",
|
||||||
input: sampleFromWeb,
|
input: sampleFromWeb,
|
||||||
expression: ".",
|
expression: ".",
|
||||||
expected: sampleFromWeb,
|
expected: sampleFromWebEncoded,
|
||||||
scenarioType: "roundtrip",
|
scenarioType: "roundtrip",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -614,6 +647,34 @@ var tomlScenarios = []formatScenario{
|
|||||||
expected: tomlTableWithComments,
|
expected: tomlTableWithComments,
|
||||||
scenarioType: "roundtrip",
|
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) {
|
func testTomlScenario(t *testing.T, s formatScenario) {
|
||||||
@ -629,6 +690,8 @@ func testTomlScenario(t *testing.T, s formatScenario) {
|
|||||||
}
|
}
|
||||||
case "roundtrip":
|
case "roundtrip":
|
||||||
test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewTomlDecoder(), NewTomlEncoder()), s.description)
|
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))))
|
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) {
|
func documentTomlRoundtripScenario(w *bufio.Writer, s formatScenario) {
|
||||||
writeOrPanic(w, fmt.Sprintf("## %v\n", s.description))
|
writeOrPanic(w, fmt.Sprintf("## %v\n", s.description))
|
||||||
|
|
||||||
@ -687,6 +772,8 @@ func documentTomlScenario(_ *testing.T, w *bufio.Writer, i interface{}) {
|
|||||||
documentTomlDecodeScenario(w, s)
|
documentTomlDecodeScenario(w, s)
|
||||||
case "roundtrip":
|
case "roundtrip":
|
||||||
documentTomlRoundtripScenario(w, s)
|
documentTomlRoundtripScenario(w, s)
|
||||||
|
case "encode":
|
||||||
|
documentTomlEncodeScenario(w, s)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
panic(fmt.Sprintf("unhandled scenario type %q", s.scenarioType))
|
panic(fmt.Sprintf("unhandled scenario type %q", s.scenarioType))
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user