diff --git a/_tests/basic.hcl b/_tests/basic.hcl index 17296f4..a1da848 100644 --- a/_tests/basic.hcl +++ b/_tests/basic.hcl @@ -5,3 +5,5 @@ Int = 123 Bool = true Float = 4.56 + +Node = baz diff --git a/_tests/nested-struct-slice-no-key.hcl b/_tests/nested-struct-slice-no-key.hcl index eaf7601..6b47cb0 100644 --- a/_tests/nested-struct-slice-no-key.hcl +++ b/_tests/nested-struct-slice-no-key.hcl @@ -2,6 +2,9 @@ Widget = [ { Foo = "bar" }, + { + Bar = fizz + }, { Foo = "baz" }, diff --git a/_tests/nested-structs.hcl b/_tests/nested-structs.hcl index e813bf0..5ef0b58 100644 --- a/_tests/nested-structs.hcl +++ b/_tests/nested-structs.hcl @@ -5,3 +5,8 @@ Foo { Fizz { Buzz = 1.23 } + +Bar { + Bar = baz + Fizz = "bar" +} diff --git a/hclencoder.go b/hclencoder.go index 3a5d589..82a77b0 100644 --- a/hclencoder.go +++ b/hclencoder.go @@ -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 } diff --git a/hclencoder_test.go b/hclencoder_test.go index 84ffb18..343b73b 100644 --- a/hclencoder_test.go +++ b/hclencoder_test.go @@ -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", }, @@ -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", }, @@ -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", diff --git a/nodes.go b/nodes.go index 47a3aee..69c706b 100644 --- a/nodes.go +++ b/nodes.go @@ -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 { @@ -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 @@ -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()) @@ -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 } @@ -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: @@ -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 } @@ -145,12 +149,12 @@ 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 } @@ -158,7 +162,7 @@ func encodeBlockList(in reflect.Value) (ast.Node, []*ast.ObjectKey, error) { continue } if childKey == nil { - return encodePrimitiveList(in) + return encodePrimitiveList(in, isNode) } item := &ast.ObjectItem{Val: child} @@ -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) } @@ -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 } @@ -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) @@ -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 } @@ -386,6 +390,8 @@ func extractFieldMeta(f reflect.StructField) (meta fieldMeta) { meta.omit = true case OmitEmptyTag: meta.omitEmpty = true + case Node: + meta.node = true } } diff --git a/nodes_test.go b/nodes_test.go index 4c90f52..5ffbe8d 100644 --- a/nodes_test.go +++ b/nodes_test.go @@ -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) @@ -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)), @@ -350,6 +357,7 @@ func TestEncodeStruct(t *testing.T) { }, }}}, }, + { ID: "debug fields", Input: reflect.ValueOf(DebugStruct{Decoded: []string{}, Unused: []string{}}), @@ -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 { diff --git a/readme.md b/readme.md index c655601..892737e 100644 --- a/readme.md +++ b/readme.md @@ -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 { @@ -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{ @@ -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" @@ -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 diff --git a/script/test b/script/test index 6242a1f..80ad249 100755 --- a/script/test +++ b/script/test @@ -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 ./...