Skip to content
This repository has been archived by the owner on Mar 20, 2024. It is now read-only.

Add support for Nodes #19

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions _tests/basic.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ Int = 123
Bool = true

Float = 4.56

Node = baz
3 changes: 3 additions & 0 deletions _tests/nested-struct-slice-no-key.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ Widget = [
{
Foo = "bar"
},
{
Bar = fizz
},
{
Foo = "baz"
},
Expand Down
5 changes: 5 additions & 0 deletions _tests/nested-structs.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,8 @@ Foo {
Fizz {
Buzz = 1.23
}

Bar {
Bar = baz
Fizz = "bar"
}
2 changes: 1 addition & 1 deletion hclencoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (

// Encode converts any supported type into the corresponding HCL format
func Encode(in interface{}) ([]byte, error) {
node, _, err := encode(reflect.ValueOf(in))
node, _, err := encode(reflect.ValueOf(in), false)
if err != nil {
return nil, err
}
Expand Down
21 changes: 17 additions & 4 deletions hclencoder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,13 @@ func TestEncoder(t *testing.T) {
Int int
Bool bool
Float float64
Node string `hcle:"node"`
}{
"bar",
123,
true,
4.56,
"baz",
},
Output: "basic",
},
Expand Down Expand Up @@ -66,9 +68,17 @@ func TestEncoder(t *testing.T) {
Input: struct {
Foo struct{ Bar string }
Fizz struct{ Buzz float64 }
Bar struct {
Bar string `hcle:"node"`
Fizz string
}
}{
struct{ Bar string }{Bar: "baz"},
struct{ Buzz float64 }{Buzz: 1.23},
struct {
Bar string `hcle:"node"`
Fizz string
}{Bar: "baz", Fizz: "bar"},
},
Output: "nested-structs",
},
Expand Down Expand Up @@ -131,14 +141,17 @@ func TestEncoder(t *testing.T) {
ID: "nested struct slice no key",
Input: struct {
Widget []struct {
Foo string
Foo string `hcle:"omitempty"`
Bar string `hcle:"omitempty,node"`
}
}{
Widget: []struct {
Foo string
Foo string `hcle:"omitempty"`
Bar string `hcle:"omitempty,node"`
}{
{"bar"},
{"baz"},
{"bar", ""},
{"", "fizz"},
{"baz", ""},
},
},
Output: "nested-struct-slice-no-key",
Expand Down
44 changes: 25 additions & 19 deletions nodes.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ const (
// OmitEmptyTag will omit this field if it is a zero value. This
// is similar behavior to `json:",omitempty"`
OmitEmptyTag string = "omitempty"

// Node will omit quotes from the output, useful for references.
Node string = "node"
)

type fieldMeta struct {
Expand All @@ -59,10 +62,11 @@ type fieldMeta struct {
decodedFields bool
omit bool
omitEmpty bool
node bool
}

// encode converts a reflected valued into an HCL ast.Node in a depth-first manner.
func encode(in reflect.Value) (node ast.Node, key []*ast.ObjectKey, err error) {
func encode(in reflect.Value, isNode bool) (node ast.Node, key []*ast.ObjectKey, err error) {
in, isNil := deref(in)
if isNil {
return nil, nil, nil
Expand All @@ -73,16 +77,16 @@ func encode(in reflect.Value) (node ast.Node, key []*ast.ObjectKey, err error) {
case reflect.Bool, reflect.Float64, reflect.String,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return encodePrimitive(in)
return encodePrimitive(in, isNode)

case reflect.Slice:
return encodeList(in)
return encodeList(in, isNode)

case reflect.Map:
return encodeMap(in)
return encodeMap(in, isNode)

case reflect.Struct:
return encodeStruct(in)
return encodeStruct(in, isNode)

default:
return nil, nil, fmt.Errorf("cannot encode kind %s to HCL", in.Kind())
Expand All @@ -92,8 +96,8 @@ func encode(in reflect.Value) (node ast.Node, key []*ast.ObjectKey, err error) {

// encodePrimitive converts a primitive value into an ast.LiteralType. An
// ast.ObjectKey is never returned.
func encodePrimitive(in reflect.Value) (ast.Node, []*ast.ObjectKey, error) {
tkn, err := tokenize(in, false)
func encodePrimitive(in reflect.Value, isNode bool) (ast.Node, []*ast.ObjectKey, error) {
tkn, err := tokenize(in, isNode)
if err != nil {
return nil, nil, err
}
Expand All @@ -103,7 +107,7 @@ func encodePrimitive(in reflect.Value) (ast.Node, []*ast.ObjectKey, error) {

// encodeList converts a slice to an appropriate ast.Node type depending on its
// element value type. An ast.ObjectKey is never returned.
func encodeList(in reflect.Value) (ast.Node, []*ast.ObjectKey, error) {
func encodeList(in reflect.Value, isNode bool) (ast.Node, []*ast.ObjectKey, error) {
childType := in.Type().Elem()

childLoop:
Expand All @@ -118,20 +122,20 @@ childLoop:

switch childType.Kind() {
case reflect.Map, reflect.Struct, reflect.Interface:
return encodeBlockList(in)
return encodeBlockList(in, isNode)
default:
return encodePrimitiveList(in)
return encodePrimitiveList(in, isNode)
}
}

// encodePrimitiveList converts a slice of primitive values to an ast.ListType. An
// ast.ObjectKey is never returned.
func encodePrimitiveList(in reflect.Value) (ast.Node, []*ast.ObjectKey, error) {
func encodePrimitiveList(in reflect.Value, isNode bool) (ast.Node, []*ast.ObjectKey, error) {
l := in.Len()
n := &ast.ListType{List: make([]ast.Node, 0, l)}

for i := 0; i < l; i++ {
child, _, err := encode(in.Index(i))
child, _, err := encode(in.Index(i), isNode)
if err != nil {
return nil, nil, err
}
Expand All @@ -145,20 +149,20 @@ func encodePrimitiveList(in reflect.Value) (ast.Node, []*ast.ObjectKey, error) {

// encodeBlockList converts a slice of non-primitive types to an ast.ObjectList. An
// ast.ObjectKey is never returned.
func encodeBlockList(in reflect.Value) (ast.Node, []*ast.ObjectKey, error) {
func encodeBlockList(in reflect.Value, isNode bool) (ast.Node, []*ast.ObjectKey, error) {
l := in.Len()
n := &ast.ObjectList{Items: make([]*ast.ObjectItem, 0, l)}

for i := 0; i < l; i++ {
child, childKey, err := encode(in.Index(i))
child, childKey, err := encode(in.Index(i), isNode)
if err != nil {
return nil, nil, err
}
if child == nil {
continue
}
if childKey == nil {
return encodePrimitiveList(in)
return encodePrimitiveList(in, isNode)
}

item := &ast.ObjectItem{Val: child}
Expand All @@ -171,7 +175,7 @@ func encodeBlockList(in reflect.Value) (ast.Node, []*ast.ObjectKey, error) {

// encodeMap converts a map type into an ast.ObjectType. Maps must have string
// key values to be encoded. An ast.ObjectKey is never returned.
func encodeMap(in reflect.Value) (ast.Node, []*ast.ObjectKey, error) {
func encodeMap(in reflect.Value, isNode bool) (ast.Node, []*ast.ObjectKey, error) {
if keyType := in.Type().Key().Kind(); keyType != reflect.String {
return nil, nil, fmt.Errorf("map keys must be strings, %s given", keyType)
}
Expand All @@ -180,7 +184,7 @@ func encodeMap(in reflect.Value) (ast.Node, []*ast.ObjectKey, error) {
for _, key := range in.MapKeys() {
tkn, _ := tokenize(key, true) // error impossible since we've already checked key kind

val, childKey, err := encode(in.MapIndex(key))
val, childKey, err := encode(in.MapIndex(key), isNode)
if err != nil {
return nil, nil, err
}
Expand Down Expand Up @@ -222,7 +226,7 @@ func encodeMap(in reflect.Value) (ast.Node, []*ast.ObjectKey, error) {
// encodeStruct converts a struct type into an ast.ObjectType. An ast.ObjectKey
// may be returned if a KeyTag is present that should be used by a parent
// ast.ObjectItem if this node is nested.
func encodeStruct(in reflect.Value) (ast.Node, []*ast.ObjectKey, error) {
func encodeStruct(in reflect.Value, isNode bool) (ast.Node, []*ast.ObjectKey, error) {
l := in.NumField()
list := &ast.ObjectList{Items: make([]*ast.ObjectItem, 0, l)}
keys := make([]*ast.ObjectKey, 0)
Expand All @@ -248,7 +252,7 @@ func encodeStruct(in reflect.Value) (ast.Node, []*ast.ObjectKey, error) {
}
}

val, childKeys, err := encode(rawVal)
val, childKeys, err := encode(rawVal, meta.node)
if err != nil {
return nil, nil, err
}
Expand Down Expand Up @@ -386,6 +390,8 @@ func extractFieldMeta(f reflect.StructField) (meta fieldMeta) {
meta.omit = true
case OmitEmptyTag:
meta.omitEmpty = true
case Node:
meta.node = true
}
}

Expand Down
16 changes: 14 additions & 2 deletions nodes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,19 @@ import (
"github.com/stretchr/testify/assert"
)

type encodeFunc func(reflect.Value) (ast.Node, []*ast.ObjectKey, error)
type encodeFunc func(reflect.Value, bool) (ast.Node, []*ast.ObjectKey, error)

type encodeTest struct {
ID string
Input reflect.Value
Node bool
Expected ast.Node
Key []*ast.ObjectKey
Error bool
}

func (test encodeTest) Test(f encodeFunc, t *testing.T) (node ast.Node, key []*ast.ObjectKey, err error) {
node, key, err = f(test.Input)
node, key, err = f(test.Input, test.Node)

if test.Error {
assert.Error(t, err, test.ID)
Expand Down Expand Up @@ -113,6 +114,12 @@ func TestEncodePrimitive(t *testing.T) {
Input: reflect.ValueOf("foobar"),
Expected: &ast.LiteralType{Token: token.Token{Type: token.STRING, Text: `"foobar"`}},
},
{
ID: "string - always ident",
Input: reflect.ValueOf("foobar"),
Node: true,
Expected: &ast.LiteralType{Token: token.Token{Type: token.IDENT, Text: `foobar`}},
},
{
ID: "uint",
Input: reflect.ValueOf(uint(1)),
Expand Down Expand Up @@ -350,6 +357,7 @@ func TestEncodeStruct(t *testing.T) {
},
}}},
},

{
ID: "debug fields",
Input: reflect.ValueOf(DebugStruct{Decoded: []string{}, Unused: []string{}}),
Expand Down Expand Up @@ -546,6 +554,10 @@ func TestExtractFieldMeta(t *testing.T) {
`hcle:"omitempty"`,
fieldMeta{name: fieldName, omitEmpty: true},
},
{
`hcle:"node"`,
fieldMeta{name: fieldName, node: true},
},
}

for _, test := range tests {
Expand Down
16 changes: 13 additions & 3 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ type Farmer struct {
}

type Animal struct {
Name string `hcl:",key"`
Sound string `hcl:"says" hcle:"omitempty"`
Name string `hcl:",key"`
Sound string `hcl:"says" hcle:"omitempty"`
Category string `hcle:"node"`
}

type Config struct {
Expand All @@ -42,13 +43,16 @@ input := Config{
{
Name: "cow",
Sound: "moo",
Category: "data.animal_categories.cow"
},
{
Name: "pig",
Sound: "oink",
Category: "data.animal_categories.pig"
},
{
Name: "rock",
Category: "data.animal_categories.rock"
},
},
Buildings: map[string]string{
Expand Down Expand Up @@ -81,13 +85,17 @@ fmt.Print(string(hcl))
//
// animal "cow" {
// says = "moo"
// category = data.animal_categories.cow
// }
//
// animal "pig" {
// says = "oink"
// category = data.animal_categories.pig
// }
//
// animal "rock" {}
// animal "rock" {
// category = data.animal_categories.rock
// }
//
// buildings {
// Barn = "456 Digits Drive"
Expand Down Expand Up @@ -126,6 +134,8 @@ fmt.Print(string(hcl))

- **`hcle:"omitempty"`** - omits this field if it is a zero value for its type. This is similar behavior to [`json:",omitempty"`][json].

- **`hcle:"node"`** - node will omit quotes from the output, useful for references.

[HCL]: https://github.com/hashicorp/hcl
[hclprinter]: https://godoc.org/github.com/hashicorp/hcl/hcl/printer
[json]: https://golang.org/pkg/encoding/json/#Marshal
Expand Down
2 changes: 1 addition & 1 deletion script/test
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ echo "go lint..."
test -z "$(golint ./... | tee /dev/stderr)"

echo "go vet..."
test -z "$(go tool vet -all -shadow . 2>&1 | tee /dev/stderr)"
test -z "$(go vet -all . 2>&1 | tee /dev/stderr)"

echo "go test..."
go test -race -cover ./...