This commit is contained in:
Mike Farah 2025-12-07 21:44:03 +11:00
parent 5f3dcb1ccf
commit 8af768a015
4 changed files with 173 additions and 11 deletions

View File

@ -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"]
}

View File

@ -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 {

View File

@ -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 {

View File

@ -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" }`,