Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions pkg/yqlib/candidate_node.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@ type CandidateNode struct {
// For formats like HCL and TOML: indicates that child entries should be emitted as separate blocks/tables
// rather than consolidated into nested mappings (default behaviour)
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
}

func (n *CandidateNode) CreateChild() *CandidateNode {
Expand Down Expand Up @@ -412,6 +415,7 @@ func (n *CandidateNode) doCopy(cloneContent bool) *CandidateNode {
IsMapKey: n.IsMapKey,

EncodeSeparate: n.EncodeSeparate,
TomlInline: n.TomlInline,
}

if cloneContent {
Expand Down Expand Up @@ -467,6 +471,8 @@ func (n *CandidateNode) UpdateAttributesFrom(other *CandidateNode, prefs assignP

// Preserve EncodeSeparate flag for format-specific encoding hints
n.EncodeSeparate = other.EncodeSeparate
// Preserve TomlInline flag for TOML inline table round-trips
n.TomlInline = other.TomlInline

// merge will pickup the style of the new thing
// when autocreating nodes
Expand Down
7 changes: 4 additions & 3 deletions pkg/yqlib/decoder_toml.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,9 +150,10 @@ func (dec *tomlDecoder) createInlineTableMap(tomlNode *toml.Node) (*CandidateNod
}

return &CandidateNode{
Kind: MappingNode,
Tag: "!!map",
Content: content,
Kind: MappingNode,
Tag: "!!map",
TomlInline: true,
Content: content,
}, nil
}

Expand Down
17 changes: 17 additions & 0 deletions pkg/yqlib/doc/usage/toml.md
Original file line number Diff line number Diff line change
Expand Up @@ -384,3 +384,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"
```

40 changes: 23 additions & 17 deletions pkg/yqlib/encoder_toml.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,12 +162,9 @@ 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 for nodes explicitly marked as TOML inline tables
// or YAML flow mappings. All other mappings become readable TOML table sections.
if node.TomlInline || node.Style&FlowStyle != 0 {
return te.writeInlineTableAttribute(w, path[len(path)-1], node)
}
return te.encodeSeparateMapping(w, path, node)
Expand Down Expand Up @@ -429,14 +426,19 @@ 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).
// TomlInline 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]
if v.Kind == ScalarNode {
hasAttrs = true
break
}
if v.Kind == MappingNode && (v.TomlInline || v.Style&FlowStyle != 0) {
hasAttrs = true
break
}
if v.Kind == SequenceNode {
// Check if it's NOT an array of tables
allMaps := true
Expand Down Expand Up @@ -464,18 +466,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:
Expand Down Expand Up @@ -599,13 +597,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: TomlInline or flow-style 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.TomlInline || 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
}
}
}
}
Expand Down
54 changes: 54 additions & 0 deletions pkg/yqlib/toml_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -614,6 +614,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) {
Expand All @@ -629,6 +657,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)
}
}

Expand All @@ -654,6 +684,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))

Expand Down Expand Up @@ -687,6 +739,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))
Expand Down