From 8af768a015645d4d0cdacaf782b8b692eb1acfa6 Mon Sep 17 00:00:00 2001 From: Mike Farah Date: Sun, 7 Dec 2025 21:44:03 +1100 Subject: [PATCH] wip --- examples/sample2.hcl | 6 +-- pkg/yqlib/decoder_hcl.go | 39 ++++++++++++++--- pkg/yqlib/encoder_hcl.go | 94 ++++++++++++++++++++++++++++++++++++++++ pkg/yqlib/hcl_test.go | 45 +++++++++++++++++++ 4 files changed, 173 insertions(+), 11 deletions(-) diff --git a/examples/sample2.hcl b/examples/sample2.hcl index 8a9fe227..0ca07a91 100644 --- a/examples/sample2.hcl +++ b/examples/sample2.hcl @@ -1,8 +1,4 @@ -io_mode = "async" - -service "http" "web_proxy" { - listen_addr = "127.0.0.1:8080" - +service "cat" { process "main" { command = ["/usr/local/bin/awesome-app", "server"] } diff --git a/pkg/yqlib/decoder_hcl.go b/pkg/yqlib/decoder_hcl.go index 597eb5c9..d14b5180 100644 --- a/pkg/yqlib/decoder_hcl.go +++ b/pkg/yqlib/decoder_hcl.go @@ -105,13 +105,40 @@ func hclBodyToNode(body *hclsyntax.Body, src []byte) *CandidateNode { // addBlockToMapping nests block type and labels into the parent mapping, merging children. func addBlockToMapping(parent *CandidateNode, block *hclsyntax.Block, src []byte) { bodyNode := hclBodyToNode(block.Body, src) - chain := bodyNode - for i := len(block.Labels) - 1; i >= 0; i-- { - wrap := &CandidateNode{Kind: MappingNode} - wrap.AddKeyValueChild(createStringScalarNode(block.Labels[i]), chain) - chain = wrap + current := parent + + // ensure block type mapping exists + var typeNode *CandidateNode + for i := 0; i < len(current.Content); i += 2 { + if current.Content[i].Value == block.Type { + typeNode = current.Content[i+1] + break + } + } + if typeNode == nil { + _, typeNode = current.AddKeyValueChild(createStringScalarNode(block.Type), &CandidateNode{Kind: MappingNode}) + } + current = typeNode + + // walk labels, creating/merging mappings + for _, label := range block.Labels { + var next *CandidateNode + for i := 0; i < len(current.Content); i += 2 { + if current.Content[i].Value == label { + next = current.Content[i+1] + break + } + } + if next == nil { + _, next = current.AddKeyValueChild(createStringScalarNode(label), &CandidateNode{Kind: MappingNode}) + } + current = next + } + + // merge body attributes/blocks into the final mapping + for i := 0; i < len(bodyNode.Content); i += 2 { + current.AddKeyValueChild(bodyNode.Content[i], bodyNode.Content[i+1]) } - parent.AddKeyValueChild(createStringScalarNode(block.Type), chain) } func convertHclExprToNode(expr hclsyntax.Expression, src []byte) *CandidateNode { diff --git a/pkg/yqlib/encoder_hcl.go b/pkg/yqlib/encoder_hcl.go index c367d37d..6c2216a6 100644 --- a/pkg/yqlib/encoder_hcl.go +++ b/pkg/yqlib/encoder_hcl.go @@ -153,17 +153,37 @@ func (he *hclEncoder) encodeNode(body *hclwrite.Body, node *CandidateNode) error if valueNode.Kind == MappingNode && valueNode.Style != FlowStyle { // Try to extract block labels from a single-entry mapping chain if labels, bodyNode, ok := extractBlockLabels(valueNode); ok { + if len(labels) > 1 && mappingChildrenAllMappings(bodyNode) { + primaryLabels := labels[:len(labels)-1] + nestedType := labels[len(labels)-1] + block := body.AppendNewBlock(key, primaryLabels) + if handled, err := he.encodeMappingChildrenAsBlocks(block.Body(), nestedType, bodyNode); err != nil { + return err + } else if !handled { + if err := he.encodeNodeAttributes(block.Body(), bodyNode); err != nil { + return err + } + } + continue + } block := body.AppendNewBlock(key, labels) if err := he.encodeNodeAttributes(block.Body(), bodyNode); err != nil { return err } continue } + // If all child values are mappings, treat each child key as a labeled instance of this block type + if handled, err := he.encodeMappingChildrenAsBlocks(body, key, valueNode); err != nil { + return err + } else if handled { + continue + } // No labels detected, render as unlabeled block block := body.AppendNewBlock(key, nil) if err := he.encodeNodeAttributes(block.Body(), valueNode); err != nil { return err } + continue } else { // Render as attribute: key = value // Check the style to determine how to encode strings @@ -252,6 +272,47 @@ func (he *hclEncoder) encodeNode(body *hclwrite.Body, node *CandidateNode) error return nil } +// mappingChildrenAllMappings reports whether all values in a mapping node are non-flow mappings. +func mappingChildrenAllMappings(node *CandidateNode) bool { + if node == nil || node.Kind != MappingNode || node.Style == FlowStyle { + return false + } + if len(node.Content) == 0 { + return false + } + for i := 0; i < len(node.Content); i += 2 { + childVal := node.Content[i+1] + if childVal.Kind != MappingNode || childVal.Style == FlowStyle { + return false + } + } + return true +} + +// encodeMappingChildrenAsBlocks emits a block for each mapping child, treating the child key as a label. +// Returns handled=true when it emitted blocks. +func (he *hclEncoder) encodeMappingChildrenAsBlocks(body *hclwrite.Body, blockType string, valueNode *CandidateNode) (bool, error) { + if !mappingChildrenAllMappings(valueNode) { + return false, nil + } + + for i := 0; i < len(valueNode.Content); i += 2 { + childKey := valueNode.Content[i].Value + childVal := valueNode.Content[i+1] + labels := []string{childKey} + if extraLabels, bodyNode, ok := extractBlockLabels(childVal); ok { + labels = append(labels, extraLabels...) + childVal = bodyNode + } + block := body.AppendNewBlock(blockType, labels) + if err := he.encodeNodeAttributes(block.Body(), childVal); err != nil { + return true, err + } + } + + return true, nil +} + // encodeNodeAttributes encodes the attributes of a mapping node (used for blocks) func (he *hclEncoder) encodeNodeAttributes(body *hclwrite.Body, node *CandidateNode) error { if node.Kind != MappingNode { @@ -263,6 +324,39 @@ func (he *hclEncoder) encodeNodeAttributes(body *hclwrite.Body, node *CandidateN valueNode := node.Content[i+1] key := keyNode.Value + if valueNode.Kind == MappingNode && valueNode.Style != FlowStyle { + if labels, bodyNode, ok := extractBlockLabels(valueNode); ok { + if len(labels) > 1 && mappingChildrenAllMappings(bodyNode) { + primaryLabels := labels[:len(labels)-1] + nestedType := labels[len(labels)-1] + block := body.AppendNewBlock(key, primaryLabels) + if handled, err := he.encodeMappingChildrenAsBlocks(block.Body(), nestedType, bodyNode); err != nil { + return err + } else if !handled { + if err := he.encodeNodeAttributes(block.Body(), bodyNode); err != nil { + return err + } + } + continue + } + block := body.AppendNewBlock(key, labels) + if err := he.encodeNodeAttributes(block.Body(), bodyNode); err != nil { + return err + } + continue + } + if handled, err := he.encodeMappingChildrenAsBlocks(body, key, valueNode); err != nil { + return err + } else if handled { + continue + } + block := body.AppendNewBlock(key, nil) + if err := he.encodeNodeAttributes(block.Body(), valueNode); err != nil { + return err + } + continue + } + // Check if this is an unquoted identifier (no DoubleQuotedStyle) if valueNode.Kind == ScalarNode && valueNode.Tag == "!!str" && valueNode.Style&DoubleQuotedStyle == 0 { if valueNode.Style&LiteralStyle != 0 { diff --git a/pkg/yqlib/hcl_test.go b/pkg/yqlib/hcl_test.go index 7bb302ff..194e550e 100644 --- a/pkg/yqlib/hcl_test.go +++ b/pkg/yqlib/hcl_test.go @@ -12,6 +12,39 @@ var nestedExample = `service "http" "web_proxy" { var nestedExampleYaml = "service:\n http:\n web_proxy:\n listen_addr: \"127.0.0.1:8080\"\n" +var multipleBlockLabelKeys = `service "cat" { + process "main" { + command = ["/usr/local/bin/awesome-app", "server"] + } + + process "management" { + command = ["/usr/local/bin/awesome-app", "management"] + } +} +` +var multipleBlockLabelKeysExpected = `service "cat" { + process "main" { + command = ["/usr/local/bin/awesome-app", "server"] + } + process "management" { + command = ["/usr/local/bin/awesome-app", "management"] + } +} +` + +var multipleBlockLabelKeysExpectedYaml = `service: + cat: + process: + main: + command: + - "/usr/local/bin/awesome-app" + - "server" + management: + command: + - "/usr/local/bin/awesome-app" + - "management" +` + var hclFormatScenarios = []formatScenario{ { description: "Simple decode", @@ -133,6 +166,18 @@ var hclFormatScenarios = []formatScenario{ expected: "values:\n - 1\n - \"two\"\n - true\n", scenarioType: "decode", }, + { + description: "multiple block label keys roundtrip", + input: multipleBlockLabelKeys, + expected: multipleBlockLabelKeysExpected, + scenarioType: "roundtrip", + }, + { + description: "multiple block label keys decode", + input: multipleBlockLabelKeys, + expected: multipleBlockLabelKeysExpectedYaml, + scenarioType: "decode", + }, { description: "block with labels", input: `resource "aws_instance" "example" { ami = "ami-12345" }`,