mirror of
https://github.com/mikefarah/yq.git
synced 2026-03-10 15:54:26 +00:00
wip
This commit is contained in:
parent
1852073f29
commit
656f07d0c2
@ -181,6 +181,7 @@ func convertHclExprToNode(expr hclsyntax.Expression, src []byte) *CandidateNode
|
||||
case *hclsyntax.ObjectConsExpr:
|
||||
// parse object into YAML mapping
|
||||
m := &CandidateNode{Kind: MappingNode}
|
||||
m.Style = FlowStyle // Mark as inline object (flow style) for encoder
|
||||
for _, item := range e.Items {
|
||||
// evaluate key expression to get the key string
|
||||
keyVal, keyDiags := item.KeyExpr.Value(nil)
|
||||
|
||||
240
pkg/yqlib/encoder_hcl.go
Normal file
240
pkg/yqlib/encoder_hcl.go
Normal file
@ -0,0 +1,240 @@
|
||||
package yqlib
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type hclEncoder struct {
|
||||
indentString string
|
||||
}
|
||||
|
||||
// NewHclEncoder creates a new HCL encoder
|
||||
func NewHclEncoder() Encoder {
|
||||
return &hclEncoder{
|
||||
indentString: " ", // 2 spaces for HCL indentation
|
||||
}
|
||||
}
|
||||
|
||||
func (he *hclEncoder) CanHandleAliases() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (he *hclEncoder) PrintDocumentSeparator(_ io.Writer) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (he *hclEncoder) PrintLeadingContent(_ io.Writer, _ string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (he *hclEncoder) Encode(writer io.Writer, node *CandidateNode) error {
|
||||
log.Debugf("I need to encode %v", NodeToString(node))
|
||||
|
||||
return he.encodeNodeInContext(writer, node, "", false)
|
||||
}
|
||||
|
||||
func (he *hclEncoder) encodeNodeInContext(writer io.Writer, node *CandidateNode, indent string, isInAttribute bool) error {
|
||||
switch node.Kind {
|
||||
case ScalarNode:
|
||||
return writeString(writer, he.formatScalarValue(node.Value))
|
||||
case MappingNode:
|
||||
return he.encodeMappingInContext(writer, node, indent, isInAttribute)
|
||||
case SequenceNode:
|
||||
return he.encodeSequence(writer, node, indent)
|
||||
case AliasNode:
|
||||
return fmt.Errorf("HCL encoder does not support aliases")
|
||||
default:
|
||||
return fmt.Errorf("unsupported node kind: %v", node.Kind)
|
||||
}
|
||||
}
|
||||
|
||||
func (he *hclEncoder) encodeMappingInContext(writer io.Writer, node *CandidateNode, indent string, isInAttribute bool) error {
|
||||
if len(node.Content) == 0 {
|
||||
return writeString(writer, "{}")
|
||||
}
|
||||
|
||||
// If this mapping is an attribute value or flow-styled, render as inline object: { a = 1, b = "two" }
|
||||
if isInAttribute || node.Style == FlowStyle {
|
||||
return he.encodeInlineMapping(writer, node, indent)
|
||||
} // If we're at the top level (indent == "") AND all values are scalars OR mappings,
|
||||
// render as attributes (key = value) or blocks (key { ... })
|
||||
if indent == "" {
|
||||
for i := 0; i < len(node.Content); i += 2 {
|
||||
keyNode := node.Content[i]
|
||||
valueNode := node.Content[i+1]
|
||||
key := keyNode.Value
|
||||
|
||||
// Block-style for nested mappings (unless they're inline objects), attribute-style for scalars/sequences
|
||||
if valueNode.Kind == MappingNode && valueNode.Style != FlowStyle {
|
||||
// Block: key { ... }
|
||||
if err := writeString(writer, key); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := writeString(writer, " {\n"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
nextIndent := he.indentString
|
||||
for j := 0; j < len(valueNode.Content); j += 2 {
|
||||
nestedKeyNode := valueNode.Content[j]
|
||||
nestedValueNode := valueNode.Content[j+1]
|
||||
nestedKey := nestedKeyNode.Value
|
||||
|
||||
if err := writeString(writer, nextIndent); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := writeString(writer, nestedKey); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := writeString(writer, " = "); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := he.encodeNodeInContext(writer, nestedValueNode, nextIndent, true); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := writeString(writer, "\n"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := writeString(writer, "}\n"); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// Attribute: key = value
|
||||
if err := writeString(writer, key); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := writeString(writer, " = "); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := he.encodeNodeInContext(writer, valueNode, "", true); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := writeString(writer, "\n"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Otherwise, this shouldn't happen at nested levels in top-level syntax
|
||||
return writeString(writer, "{}")
|
||||
}
|
||||
|
||||
func (he *hclEncoder) encodeInlineMapping(writer io.Writer, node *CandidateNode, indent string) error {
|
||||
if len(node.Content) == 0 {
|
||||
return writeString(writer, "{}")
|
||||
}
|
||||
|
||||
if err := writeString(writer, "{\n"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
nextIndent := indent + he.indentString
|
||||
for i := 0; i < len(node.Content); i += 2 {
|
||||
keyNode := node.Content[i]
|
||||
valueNode := node.Content[i+1]
|
||||
key := keyNode.Value
|
||||
|
||||
if err := writeString(writer, nextIndent); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := writeString(writer, key); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := writeString(writer, " = "); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := he.encodeNodeInContext(writer, valueNode, nextIndent, true); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := writeString(writer, "\n"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := writeString(writer, indent+"}"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (he *hclEncoder) encodeSequence(writer io.Writer, node *CandidateNode, indent string) error {
|
||||
if len(node.Content) == 0 {
|
||||
return writeString(writer, "[]")
|
||||
}
|
||||
|
||||
// Check if we should use inline format (simple values only)
|
||||
useInline := true
|
||||
for _, item := range node.Content {
|
||||
if item.Kind != ScalarNode {
|
||||
useInline = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if useInline {
|
||||
// Inline format: ["a", "b", "c"]
|
||||
if err := writeString(writer, "["); err != nil {
|
||||
return err
|
||||
}
|
||||
for i, item := range node.Content {
|
||||
if i > 0 {
|
||||
if err := writeString(writer, ", "); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := he.encodeNodeInContext(writer, item, indent, true); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := writeString(writer, "]"); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Multi-line format for complex items
|
||||
if err := writeString(writer, "[\n"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
nextIndent := indent + he.indentString
|
||||
for _, item := range node.Content {
|
||||
if err := writeString(writer, nextIndent); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := he.encodeNodeInContext(writer, item, nextIndent, true); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := writeString(writer, ",\n"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := writeString(writer, indent+"]"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (he *hclEncoder) formatScalarValue(value string) string {
|
||||
// Check if value is a boolean
|
||||
if value == "true" || value == "false" {
|
||||
return value
|
||||
}
|
||||
|
||||
// Check if value is a number
|
||||
if _, err := strconv.ParseFloat(value, 64); err == nil {
|
||||
return value
|
||||
}
|
||||
|
||||
// Treat as string, quote it
|
||||
return strconv.Quote(value)
|
||||
}
|
||||
@ -68,7 +68,7 @@ var TomlFormat = &Format{"toml", []string{},
|
||||
}
|
||||
|
||||
var HclFormat = &Format{"hcl", []string{"h", "hcl"},
|
||||
nil,
|
||||
func() Encoder { return NewHclEncoder() },
|
||||
func() Decoder { return NewHclDecoder() },
|
||||
}
|
||||
|
||||
|
||||
@ -40,7 +40,7 @@ var hclFormatScenarios = []formatScenario{
|
||||
{
|
||||
description: "object/map attribute",
|
||||
input: `obj = { a = 1, b = "two" }`,
|
||||
expected: "obj:\n a: 1\n b: two\n",
|
||||
expected: "obj: {a: 1, b: two}\n",
|
||||
scenarioType: "decode",
|
||||
},
|
||||
{
|
||||
@ -76,7 +76,7 @@ var hclFormatScenarios = []formatScenario{
|
||||
{
|
||||
description: "nested object",
|
||||
input: `config = { db = { host = "localhost", port = 5432 } }`,
|
||||
expected: "config:\n db:\n host: localhost\n port: 5432\n",
|
||||
expected: "config: {db: {host: localhost, port: 5432}}\n",
|
||||
scenarioType: "decode",
|
||||
},
|
||||
{
|
||||
@ -91,11 +91,64 @@ var hclFormatScenarios = []formatScenario{
|
||||
expected: "resource aws_instance example:\n ami: ami-12345\n",
|
||||
scenarioType: "decode",
|
||||
},
|
||||
{
|
||||
description: "roundtrip simple attribute",
|
||||
input: `io_mode = "async"`,
|
||||
expected: `io_mode = "async"` + "\n",
|
||||
scenarioType: "roundtrip",
|
||||
},
|
||||
{
|
||||
description: "roundtrip number attribute",
|
||||
input: `port = 8080`,
|
||||
expected: "port = 8080\n",
|
||||
scenarioType: "roundtrip",
|
||||
},
|
||||
{
|
||||
description: "roundtrip float attribute",
|
||||
input: `pi = 3.14`,
|
||||
expected: "pi = 3.14\n",
|
||||
scenarioType: "roundtrip",
|
||||
},
|
||||
{
|
||||
description: "roundtrip boolean attribute",
|
||||
input: `enabled = true`,
|
||||
expected: "enabled = true\n",
|
||||
scenarioType: "roundtrip",
|
||||
},
|
||||
{
|
||||
description: "roundtrip list of strings",
|
||||
input: `tags = ["a", "b"]`,
|
||||
expected: "tags = [\"a\", \"b\"]\n",
|
||||
scenarioType: "roundtrip",
|
||||
},
|
||||
{
|
||||
description: "roundtrip object/map attribute",
|
||||
input: `obj = { a = 1, b = "two" }`,
|
||||
expected: "obj = {\n a = 1\n b = \"two\"\n}\n",
|
||||
scenarioType: "roundtrip",
|
||||
},
|
||||
{
|
||||
description: "roundtrip nested block",
|
||||
input: `server { port = 8080 }`,
|
||||
expected: "server {\n port = 8080\n}\n",
|
||||
scenarioType: "roundtrip",
|
||||
},
|
||||
{
|
||||
description: "roundtrip multiple attributes",
|
||||
input: "name = \"app\"\nversion = 1\nenabled = true",
|
||||
expected: "name = \"app\"\nversion = 1\nenabled = true\n",
|
||||
scenarioType: "roundtrip",
|
||||
},
|
||||
}
|
||||
|
||||
func testHclScenario(t *testing.T, s formatScenario) {
|
||||
if s.scenarioType == "decode" {
|
||||
test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewHclDecoder(), NewYamlEncoder(ConfiguredYamlPreferences)), s.description)
|
||||
switch s.scenarioType {
|
||||
case "decode":
|
||||
// Decode to YAML, which means we need to clear HCL-specific tags
|
||||
result := mustProcessFormatScenario(s, NewHclDecoder(), NewYamlEncoder(ConfiguredYamlPreferences))
|
||||
test.AssertResultWithContext(t, s.expected, result, s.description)
|
||||
case "roundtrip":
|
||||
test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewHclDecoder(), NewHclEncoder()), s.description)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user