Skip to content

Commit

Permalink
Implement first draft of HCL writer
Browse files Browse the repository at this point in the history
  • Loading branch information
TomWright committed Nov 7, 2024
1 parent 38ec57f commit 7e48c95
Show file tree
Hide file tree
Showing 3 changed files with 235 additions and 3 deletions.
3 changes: 1 addition & 2 deletions parsing/hcl/hcl.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,5 @@ var _ parsing.Writer = (*hclWriter)(nil)

func init() {
parsing.RegisterReader(HCL, newHCLReader)
// HCL writer is not implemented yet
//parsing.RegisterWriter(HCL, newHCLWriter)
parsing.RegisterWriter(HCL, newHCLWriter)
}
170 changes: 169 additions & 1 deletion parsing/hcl/writer.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package hcl

import (
"bytes"
"fmt"
"github.com/hashicorp/hcl/v2/hclwrite"
"github.com/tomwright/dasel/v3/model"
"github.com/tomwright/dasel/v3/parsing"
"github.com/zclconf/go-cty/cty"
)

func newHCLWriter(options parsing.WriterOptions) (parsing.Writer, error) {
Expand All @@ -15,5 +19,169 @@ type hclWriter struct {

// Write writes a value to a byte slice.
func (j *hclWriter) Write(value *model.Value) ([]byte, error) {
return nil, nil
f, err := j.valueToFile(value)
if err != nil {
return nil, err
}

buf := new(bytes.Buffer)
if _, err := f.WriteTo(buf); err != nil {
return nil, err
}
return buf.Bytes(), nil
}

func (j *hclWriter) valueToFile(v *model.Value) (*hclwrite.File, error) {
f := hclwrite.NewEmptyFile()

body := f.Body()

if err := j.addValueToBody(nil, v, body); err != nil {
return nil, err
}

return f, nil
}

func (j *hclWriter) addValueToBody(previousLabels []string, v *model.Value, body *hclwrite.Body) error {
if !v.IsMap() {
return fmt.Errorf("hcl body is expected to be a map, got %s", v.Type())
}

kvs, err := v.MapKeyValues()
if err != nil {
return err
}

blocks := make([]*hclwrite.Block, 0)
for _, kv := range kvs {
switch kv.Value.Type() {
case model.TypeMap:
block, err := j.valueToBlock(kv.Key, previousLabels, kv.Value)
if err != nil {
return fmt.Errorf("failed to encode %q to hcl block: %w", kv.Key, err)
}
blocks = append(blocks, block)
case model.TypeSlice:
vals := make([]cty.Value, 0)

allMaps := true

if err := kv.Value.RangeSlice(func(_ int, value *model.Value) error {
ctyVal, err := j.valueToCty(value)
if err != nil {
return err
}
vals = append(vals, ctyVal)

if !value.IsMap() {
allMaps = false
}
return nil
}); err != nil {
return err
}

if allMaps {
if err := kv.Value.RangeSlice(func(_ int, value *model.Value) error {
block, err := j.valueToBlock(kv.Key, previousLabels, value)
if err != nil {
return fmt.Errorf("failed to encode %q to hcl block: %w", kv.Key, err)
}
blocks = append(blocks, block)
return nil
}); err != nil {
return err
}
} else {
body.SetAttributeValue(kv.Key, cty.TupleVal(vals))
}

default:
ctyVal, err := j.valueToCty(kv.Value)
if err != nil {
return fmt.Errorf("failed to encode attribute %q: %w", kv.Key, err)
}
body.SetAttributeValue(kv.Key, ctyVal)
}
}

for _, block := range blocks {
body.AppendBlock(block)
}

return nil
}

func (j *hclWriter) valueToCty(v *model.Value) (cty.Value, error) {
switch v.Type() {
case model.TypeString:
val, err := v.StringValue()
if err != nil {
return cty.Value{}, err
}
return cty.StringVal(val), nil
case model.TypeBool:
val, err := v.BoolValue()
if err != nil {
return cty.Value{}, err
}
return cty.BoolVal(val), nil
case model.TypeInt:
val, err := v.IntValue()
if err != nil {
return cty.Value{}, err
}
return cty.NumberIntVal(val), nil
case model.TypeFloat:
val, err := v.FloatValue()
if err != nil {
return cty.Value{}, err
}
return cty.NumberFloatVal(val), nil
case model.TypeNull:
return cty.NullVal(cty.NilType), nil
case model.TypeSlice:
var vals []cty.Value
if err := v.RangeSlice(func(_ int, value *model.Value) error {
ctyVal, err := j.valueToCty(value)
if err != nil {
return err
}
vals = append(vals, ctyVal)
return nil
}); err != nil {
return cty.Value{}, err
}
return cty.TupleVal(vals), nil
case model.TypeMap:
mapV := map[string]cty.Value{}
if err := v.RangeMap(func(s string, value *model.Value) error {
ctyVal, err := j.valueToCty(value)
if err != nil {
return err
}
mapV[s] = ctyVal
return nil
}); err != nil {
return cty.Value{}, err
}
return cty.ObjectVal(mapV), nil
default:
return cty.Value{}, fmt.Errorf("unhandled type when converting to cty value %q", v.Type())
}
}

func (j *hclWriter) valueToBlock(key string, labels []string, v *model.Value) (*hclwrite.Block, error) {
if !v.IsMap() {
return nil, fmt.Errorf("must be map")
}

b := hclwrite.NewBlock(key, labels)

if err := j.addValueToBody(labels, v, b.Body()); err != nil {
return nil, err
}

return b, nil
}
65 changes: 65 additions & 0 deletions parsing/hcl/writer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package hcl_test

import (
"github.com/google/go-cmp/cmp"
"testing"

"github.com/tomwright/dasel/v3/parsing"
"github.com/tomwright/dasel/v3/parsing/hcl"
)

type readWriteTestCase struct {
in string
}

func (tc readWriteTestCase) run(t *testing.T) {
r, err := hcl.HCL.NewReader(parsing.DefaultReaderOptions())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
w, err := hcl.HCL.NewWriter(parsing.DefaultWriterOptions())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

in := []byte(tc.in)

data, err := r.Read(in)
if err != nil {
t.Errorf("unexpected error: %v", err)
return
}

got, err := w.Write(data)
if err != nil {
t.Errorf("unexpected error: %v", err)
return
}
gotStr := string(got)

if !cmp.Equal(tc.in, gotStr) {
t.Errorf("unexpected output: %s", cmp.Diff(tc.in, gotStr))
}
}

func TestHclReader_ReadWrite(t *testing.T) {
t.Run("document a", readWriteTestCase{
in: `io_mode = "async"
service "http" "web_proxy" {
listen_addr = "127.0.0.1:8080"
process "main" {
command = ["/usr/local/bin/awesome-app", "server"]
}
process "mgmt" {
command = ["/usr/local/bin/awesome-app", "mgmt"]
}
process "mgmt" {
command = ["/usr/local/bin/awesome-app", "mgmt2"]
}
}`,
}.run)
}

0 comments on commit 7e48c95

Please sign in to comment.