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

Commit

Permalink
Add support for Nodes
Browse files Browse the repository at this point in the history
  • Loading branch information
DeviaVir committed Jan 31, 2020
1 parent fb9757b commit 5a684fa
Show file tree
Hide file tree
Showing 9 changed files with 81 additions and 30 deletions.
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 ./...

0 comments on commit 5a684fa

Please sign in to comment.