diff --git a/pkg/yqlib/decoder_toml.go b/pkg/yqlib/decoder_toml.go index cf3a9d31..0d583836 100644 --- a/pkg/yqlib/decoder_toml.go +++ b/pkg/yqlib/decoder_toml.go @@ -152,6 +152,7 @@ func (dec *tomlDecoder) createInlineTableMap(tomlNode *toml.Node) (*CandidateNod return &CandidateNode{ Kind: MappingNode, Tag: "!!map", + Style: FlowStyle, Content: content, }, nil } diff --git a/pkg/yqlib/doc/usage/toml.md b/pkg/yqlib/doc/usage/toml.md index 45402b47..4c418589 100644 --- a/pkg/yqlib/doc/usage/toml.md +++ b/pkg/yqlib/doc/usage/toml.md @@ -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" +``` + diff --git a/pkg/yqlib/encoder_toml.go b/pkg/yqlib/encoder_toml.go index b4ccc287..de4096fe 100644 --- a/pkg/yqlib/encoder_toml.go +++ b/pkg/yqlib/encoder_toml.go @@ -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 + } } } } diff --git a/pkg/yqlib/toml_test.go b/pkg/yqlib/toml_test.go index fe542a01..c917f8fb 100644 --- a/pkg/yqlib/toml_test.go +++ b/pkg/yqlib/toml_test.go @@ -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))