├── .github └── workflows │ ├── codeql-analysis.yml │ └── go.yml ├── LICENSE ├── _tests ├── basic.hcl ├── empty.hcl ├── keyed-nested-structs.hcl ├── label-change.hcl ├── multiple-keys-nested-structs.hcl ├── nested-slices.hcl ├── nested-struct-slice-no-key.hcl ├── nested-struct-slice.hcl ├── nested-structs.hcl └── primitive-lists.hcl ├── example_test.go ├── go.mod ├── go.sum ├── hclencoder.go ├── hclencoder_test.go ├── nodes.go ├── nodes_test.go ├── readme.md └── walker.go /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [master] 9 | schedule: 10 | - cron: '18 17 * * 6' 11 | 12 | jobs: 13 | analyze: 14 | name: Analyze 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | # Override automatic language detection by changing the below list 21 | # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] 22 | language: ['go'] 23 | # Learn more... 24 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection 25 | 26 | steps: 27 | - name: Checkout repository 28 | uses: actions/checkout@v2 29 | with: 30 | # We must fetch at least the immediate parents so that if this is 31 | # a pull request then we can checkout the head. 32 | fetch-depth: 2 33 | 34 | # If this run was triggered by a pull request event, then checkout 35 | # the head of the pull request instead of the merge commit. 36 | - run: git checkout HEAD^2 37 | if: ${{ github.event_name == 'pull_request' }} 38 | 39 | # Initializes the CodeQL tools for scanning. 40 | - name: Initialize CodeQL 41 | uses: github/codeql-action/init@v1 42 | with: 43 | languages: ${{ matrix.language }} 44 | 45 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 46 | # If this step fails, then you should remove it and run the build manually (see below) 47 | - name: Autobuild 48 | uses: github/codeql-action/autobuild@v1 49 | 50 | # ℹ️ Command-line programs to run using the OS shell. 51 | # 📚 https://git.io/JvXDl 52 | 53 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 54 | # and modify them (or add more) to build your code if your project 55 | # uses a compiled language 56 | 57 | #- run: | 58 | # make bootstrap 59 | # make release 60 | 61 | - name: Perform CodeQL Analysis 62 | uses: github/codeql-action/analyze@v1 63 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | 11 | build: 12 | name: Build 13 | runs-on: ubuntu-latest 14 | steps: 15 | 16 | - name: Set up Go 1.x 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: ^1.14 20 | id: go 21 | 22 | - name: Check out code into the Go module directory 23 | uses: actions/checkout@v2 24 | 25 | - name: Get dependencies 26 | run: go get -v -t -d ./... 27 | 28 | - name: Build 29 | run: go build -v ./... 30 | 31 | - name: Test 32 | run: go test -v -race -cover ./... 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Chris Roche 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /_tests/basic.hcl: -------------------------------------------------------------------------------- 1 | String = "bar" 2 | 3 | Int = 123 4 | 5 | Bool = true 6 | 7 | Float = 4.56 8 | -------------------------------------------------------------------------------- /_tests/empty.hcl: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /_tests/keyed-nested-structs.hcl: -------------------------------------------------------------------------------- 1 | Foo "bar" { 2 | Fizz = "buzz" 3 | } 4 | -------------------------------------------------------------------------------- /_tests/label-change.hcl: -------------------------------------------------------------------------------- 1 | foo = "bar" 2 | 3 | baz = 123 4 | -------------------------------------------------------------------------------- /_tests/multiple-keys-nested-structs.hcl: -------------------------------------------------------------------------------- 1 | Foo "bar" "baz" { 2 | Fizz = "buzz" 3 | } 4 | -------------------------------------------------------------------------------- /_tests/nested-slices.hcl: -------------------------------------------------------------------------------- 1 | bar = [ 2 | ["bar"], 3 | ["baz"], 4 | ["buzz"], 5 | ] 6 | 7 | foo = [ 8 | "bar", 9 | "baz", 10 | ] 11 | -------------------------------------------------------------------------------- /_tests/nested-struct-slice-no-key.hcl: -------------------------------------------------------------------------------- 1 | Widget = [ 2 | { 3 | Foo = "bar" 4 | }, 5 | { 6 | Foo = "baz" 7 | }, 8 | ] 9 | -------------------------------------------------------------------------------- /_tests/nested-struct-slice.hcl: -------------------------------------------------------------------------------- 1 | Widget "bar" {} 2 | 3 | Widget "baz" {} 4 | -------------------------------------------------------------------------------- /_tests/nested-structs.hcl: -------------------------------------------------------------------------------- 1 | Foo { 2 | Bar = "baz" 3 | } 4 | 5 | Fizz { 6 | Buzz = 1.23 7 | } 8 | -------------------------------------------------------------------------------- /_tests/primitive-lists.hcl: -------------------------------------------------------------------------------- 1 | Widgets = [ 2 | "foo", 3 | "bar", 4 | "baz", 5 | ] 6 | 7 | Gizmos = [ 8 | 4, 9 | 5, 10 | 6, 11 | ] 12 | 13 | Single = ["foo"] 14 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package hclencoder 2 | 3 | import ( 4 | "log" 5 | 6 | "fmt" 7 | ) 8 | 9 | func Example() { 10 | type Farm struct { 11 | Name string `hcl:"name"` 12 | Owned bool `hcl:"owned"` 13 | Location []float64 `hcl:"location"` 14 | } 15 | 16 | type Farmer struct { 17 | Name string `hcl:"name"` 18 | Age int `hcl:"age"` 19 | SocialSecurityNumber string `hcle:"omit"` 20 | } 21 | 22 | type Animal struct { 23 | Name string `hcl:",key"` 24 | Sound string `hcl:"says" hcle:"omitempty"` 25 | } 26 | 27 | type Pet struct { 28 | Species string `hcl:",key"` 29 | Name string `hcl:",key"` 30 | Sound string `hcl:"says" hcle:"omitempty"` 31 | } 32 | 33 | type Config struct { 34 | Farm `hcl:",squash"` 35 | Farmer Farmer `hcl:"farmer"` 36 | Animals []Animal `hcl:"animal"` 37 | Pets []Pet `hcl:"pet"` 38 | Buildings map[string]string `hcl:"buildings"` 39 | } 40 | 41 | input := Config{ 42 | Farm: Farm{ 43 | Name: "Ol' McDonald's Farm", 44 | Owned: true, 45 | Location: []float64{12.34, -5.67}, 46 | }, 47 | Farmer: Farmer{ 48 | Name: "Robert Beauregard-Michele McDonald, III", 49 | Age: 65, 50 | SocialSecurityNumber: "please-dont-share-me", 51 | }, 52 | Animals: []Animal{ 53 | { 54 | Name: "cow", 55 | Sound: "moo", 56 | }, 57 | { 58 | Name: "pig", 59 | Sound: "oink", 60 | }, 61 | { 62 | Name: "rock", 63 | }, 64 | }, 65 | Pets: []Pet{ 66 | { 67 | Species: "cat", 68 | Name: "whiskers", 69 | Sound: "meow", 70 | }, 71 | }, 72 | Buildings: map[string]string{ 73 | "House": "123 Numbers Lane", 74 | "Barn": "456 Digits Drive", 75 | }, 76 | } 77 | 78 | hcl, err := Encode(input) 79 | if err != nil { 80 | log.Fatal("unable to encode: ", err) 81 | } 82 | 83 | fmt.Print(string(hcl)) 84 | 85 | // Output: 86 | // name = "Ol' McDonald's Farm" 87 | // 88 | // owned = true 89 | // 90 | // location = [ 91 | // 12.34, 92 | // -5.67, 93 | // ] 94 | // 95 | // farmer { 96 | // name = "Robert Beauregard-Michele McDonald, III" 97 | // age = 65 98 | // } 99 | // 100 | // animal "cow" { 101 | // says = "moo" 102 | // } 103 | // 104 | // animal "pig" { 105 | // says = "oink" 106 | // } 107 | // 108 | // animal "rock" {} 109 | // 110 | // pet "cat" "whiskers" { 111 | // says = "meow" 112 | // } 113 | // 114 | // buildings { 115 | // Barn = "456 Digits Drive" 116 | // House = "123 Numbers Lane" 117 | // } 118 | // 119 | } 120 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/rodaine/hclencoder 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/hashicorp/hcl v1.0.0 7 | github.com/stretchr/testify v1.6.1 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 5 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 6 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 7 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 8 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 9 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 10 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 11 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 12 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 13 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 14 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 15 | -------------------------------------------------------------------------------- /hclencoder.go: -------------------------------------------------------------------------------- 1 | package hclencoder 2 | 3 | import ( 4 | "bytes" 5 | "reflect" 6 | 7 | "github.com/hashicorp/hcl/hcl/ast" 8 | "github.com/hashicorp/hcl/hcl/printer" 9 | ) 10 | 11 | // Encode converts any supported type into the corresponding HCL format 12 | func Encode(in interface{}) ([]byte, error) { 13 | node, _, err := encode(reflect.ValueOf(in)) 14 | if err != nil { 15 | return nil, err 16 | } 17 | 18 | file := &ast.File{} 19 | switch node := node.(type) { 20 | case *ast.ObjectType: 21 | file.Node = node.List 22 | default: 23 | file.Node = node 24 | } 25 | 26 | if _, err = positionNodes(file, startingCursor, 2); err != nil { 27 | return nil, err 28 | } 29 | 30 | b := &bytes.Buffer{} 31 | err = printer.Fprint(b, file) 32 | b.WriteString("\n") 33 | 34 | return b.Bytes(), err 35 | } 36 | -------------------------------------------------------------------------------- /hclencoder_test.go: -------------------------------------------------------------------------------- 1 | package hclencoder 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | type encoderTest struct { 12 | ID string 13 | Input interface{} 14 | Output string 15 | Error bool 16 | } 17 | 18 | func TestEncoder(t *testing.T) { 19 | tests := []encoderTest{ 20 | { 21 | ID: "empty struct", 22 | Input: struct{}{}, 23 | Output: "empty", 24 | }, 25 | { 26 | ID: "basic struct", 27 | Input: struct { 28 | String string 29 | Int int 30 | Bool bool 31 | Float float64 32 | }{ 33 | "bar", 34 | 123, 35 | true, 36 | 4.56, 37 | }, 38 | Output: "basic", 39 | }, 40 | { 41 | ID: "labels changed", 42 | Input: struct { 43 | String string `hcl:"foo"` 44 | Int int `hcl:"baz"` 45 | }{ 46 | "bar", 47 | 123, 48 | }, 49 | Output: "label-change", 50 | }, 51 | { 52 | ID: "primitive list", 53 | Input: struct { 54 | Widgets []string 55 | Gizmos []int 56 | Single []string 57 | }{ 58 | []string{"foo", "bar", "baz"}, 59 | []int{4, 5, 6}, 60 | []string{"foo"}, 61 | }, 62 | Output: "primitive-lists", 63 | }, 64 | { 65 | ID: "nested struct", 66 | Input: struct { 67 | Foo struct{ Bar string } 68 | Fizz struct{ Buzz float64 } 69 | }{ 70 | struct{ Bar string }{Bar: "baz"}, 71 | struct{ Buzz float64 }{Buzz: 1.23}, 72 | }, 73 | Output: "nested-structs", 74 | }, 75 | { 76 | ID: "keyed nested struct", 77 | Input: struct { 78 | Foo struct { 79 | Key string `hcl:",key"` 80 | Fizz string 81 | } 82 | }{ 83 | struct { 84 | Key string `hcl:",key"` 85 | Fizz string 86 | }{ 87 | "bar", 88 | "buzz", 89 | }, 90 | }, 91 | Output: "keyed-nested-structs", 92 | }, 93 | { 94 | ID: "multiple keys nested structs", 95 | Input: struct { 96 | Foo struct { 97 | Key string `hcl:",key"` 98 | OtherKey string `hcl:",key"` 99 | Fizz string 100 | } 101 | }{ 102 | struct { 103 | Key string `hcl:",key"` 104 | OtherKey string `hcl:",key"` 105 | Fizz string 106 | }{ 107 | "bar", 108 | "baz", 109 | "buzz", 110 | }, 111 | }, 112 | Output: "multiple-keys-nested-structs", 113 | }, 114 | { 115 | ID: "nested struct slice", 116 | Input: struct { 117 | Widget []struct { 118 | Foo string `hcl:"foo,key"` 119 | } 120 | }{ 121 | []struct { 122 | Foo string `hcl:"foo,key"` 123 | }{ 124 | {"bar"}, 125 | {"baz"}, 126 | }, 127 | }, 128 | Output: "nested-struct-slice", 129 | }, 130 | { 131 | ID: "nested struct slice no key", 132 | Input: struct { 133 | Widget []struct { 134 | Foo string 135 | } 136 | }{ 137 | Widget: []struct { 138 | Foo string 139 | }{ 140 | {"bar"}, 141 | {"baz"}, 142 | }, 143 | }, 144 | Output: "nested-struct-slice-no-key", 145 | }, 146 | { 147 | ID: "nested slices", 148 | Input: map[string]interface{}{ 149 | "foo": []interface{}{ 150 | "bar", "baz", 151 | }, 152 | "bar": []interface{}{ 153 | []interface{}{ 154 | "bar", 155 | }, 156 | []interface{}{ 157 | "baz", 158 | }, 159 | []interface{}{ 160 | "buzz", 161 | }, 162 | }, 163 | }, 164 | Output: "nested-slices", 165 | }, 166 | } 167 | 168 | for _, test := range tests { 169 | actual, err := Encode(test.Input) 170 | 171 | if test.Error { 172 | assert.Error(t, err, test.ID) 173 | } else { 174 | expected, ferr := ioutil.ReadFile(fmt.Sprintf("_tests/%s.hcl", test.Output)) 175 | if ferr != nil { 176 | t.Fatal(test.ID, "- could not read output HCL: ", ferr) 177 | continue 178 | } 179 | 180 | assert.NoError(t, err, test.ID) 181 | assert.EqualValues(t, 182 | string(expected), 183 | string(actual), 184 | fmt.Sprintf("%s\nExpected:\n%s\nActual:\n%s", test.ID, expected, actual), 185 | ) 186 | } 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /nodes.go: -------------------------------------------------------------------------------- 1 | package hclencoder 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "reflect" 7 | "sort" 8 | "strconv" 9 | "strings" 10 | 11 | "github.com/hashicorp/hcl/hcl/ast" 12 | "github.com/hashicorp/hcl/hcl/token" 13 | ) 14 | 15 | const ( 16 | // HCLTagName is the struct field tag used by the HCL decoder. The 17 | // values from this tag are used in the same way as the decoder. 18 | HCLTagName = "hcl" 19 | 20 | // KeyTag indicates that the value of the field should be part of 21 | // the parent object block's key, not a property of that block 22 | KeyTag string = "key" 23 | 24 | // SquashTag is attached to anonymous fields of a struct and indicates 25 | // to the encoder to lift the fields of that value into the parent 26 | // block's scope transparently. Otherwise, the field's type is used as 27 | // the key for the value. 28 | SquashTag string = "squash" 29 | 30 | // UnusedKeysTag is a flag that indicates any unused keys found by the 31 | // decoder are stored in this field of type []string. This has the same 32 | // behavior as the OmitTag and is not encoded. 33 | UnusedKeysTag string = "unusedKeys" 34 | 35 | // DecodedFieldsTag is a flag that indicates all fields decoded are 36 | // stored in this field of type []string. This has the same behavior as 37 | // the OmitTag and is not encoded. 38 | DecodedFieldsTag string = "decodedFields" 39 | 40 | // HCLETagName is the struct field tag used by this package. The 41 | // values from this tag are used in conjunction with HCLTag values. 42 | HCLETagName = "hcle" 43 | 44 | // OmitTag will omit this field from encoding. This is the similar 45 | // behavior to `json:"-"`. 46 | OmitTag string = "omit" 47 | 48 | // OmitEmptyTag will omit this field if it is a zero value. This 49 | // is similar behavior to `json:",omitempty"` 50 | OmitEmptyTag string = "omitempty" 51 | ) 52 | 53 | type fieldMeta struct { 54 | anonymous bool 55 | name string 56 | key bool 57 | squash bool 58 | unusedKeys bool 59 | decodedFields bool 60 | omit bool 61 | omitEmpty bool 62 | } 63 | 64 | // encode converts a reflected valued into an HCL ast.Node in a depth-first manner. 65 | func encode(in reflect.Value) (node ast.Node, key []*ast.ObjectKey, err error) { 66 | in, isNil := deref(in) 67 | if isNil { 68 | return nil, nil, nil 69 | } 70 | 71 | switch in.Kind() { 72 | 73 | case reflect.Bool, reflect.Float64, reflect.String, 74 | reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, 75 | reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 76 | return encodePrimitive(in) 77 | 78 | case reflect.Slice: 79 | return encodeList(in) 80 | 81 | case reflect.Map: 82 | return encodeMap(in) 83 | 84 | case reflect.Struct: 85 | return encodeStruct(in) 86 | 87 | default: 88 | return nil, nil, fmt.Errorf("cannot encode kind %s to HCL", in.Kind()) 89 | } 90 | 91 | } 92 | 93 | // encodePrimitive converts a primitive value into an ast.LiteralType. An 94 | // ast.ObjectKey is never returned. 95 | func encodePrimitive(in reflect.Value) (ast.Node, []*ast.ObjectKey, error) { 96 | tkn, err := tokenize(in, false) 97 | if err != nil { 98 | return nil, nil, err 99 | } 100 | 101 | return &ast.LiteralType{Token: tkn}, nil, nil 102 | } 103 | 104 | // encodeList converts a slice to an appropriate ast.Node type depending on its 105 | // element value type. An ast.ObjectKey is never returned. 106 | func encodeList(in reflect.Value) (ast.Node, []*ast.ObjectKey, error) { 107 | childType := in.Type().Elem() 108 | 109 | childLoop: 110 | for { 111 | switch childType.Kind() { 112 | case reflect.Ptr: 113 | childType = childType.Elem() 114 | default: 115 | break childLoop 116 | } 117 | } 118 | 119 | switch childType.Kind() { 120 | case reflect.Map, reflect.Struct, reflect.Interface: 121 | return encodeBlockList(in) 122 | default: 123 | return encodePrimitiveList(in) 124 | } 125 | } 126 | 127 | // encodePrimitiveList converts a slice of primitive values to an ast.ListType. An 128 | // ast.ObjectKey is never returned. 129 | func encodePrimitiveList(in reflect.Value) (ast.Node, []*ast.ObjectKey, error) { 130 | l := in.Len() 131 | n := &ast.ListType{List: make([]ast.Node, 0, l)} 132 | 133 | for i := 0; i < l; i++ { 134 | child, _, err := encode(in.Index(i)) 135 | if err != nil { 136 | return nil, nil, err 137 | } 138 | if child != nil { 139 | n.Add(child) 140 | } 141 | } 142 | 143 | return n, nil, nil 144 | } 145 | 146 | // encodeBlockList converts a slice of non-primitive types to an ast.ObjectList. An 147 | // ast.ObjectKey is never returned. 148 | func encodeBlockList(in reflect.Value) (ast.Node, []*ast.ObjectKey, error) { 149 | l := in.Len() 150 | n := &ast.ObjectList{Items: make([]*ast.ObjectItem, 0, l)} 151 | 152 | for i := 0; i < l; i++ { 153 | child, childKey, err := encode(in.Index(i)) 154 | if err != nil { 155 | return nil, nil, err 156 | } 157 | if child == nil { 158 | continue 159 | } 160 | if childKey == nil { 161 | return encodePrimitiveList(in) 162 | } 163 | 164 | item := &ast.ObjectItem{Val: child} 165 | item.Keys = childKey 166 | n.Add(item) 167 | } 168 | 169 | return n, nil, nil 170 | } 171 | 172 | // encodeMap converts a map type into an ast.ObjectType. Maps must have string 173 | // key values to be encoded. An ast.ObjectKey is never returned. 174 | func encodeMap(in reflect.Value) (ast.Node, []*ast.ObjectKey, error) { 175 | if keyType := in.Type().Key().Kind(); keyType != reflect.String { 176 | return nil, nil, fmt.Errorf("map keys must be strings, %s given", keyType) 177 | } 178 | 179 | l := make(objectItems, 0, in.Len()) 180 | for _, key := range in.MapKeys() { 181 | tkn, _ := tokenize(key, true) // error impossible since we've already checked key kind 182 | 183 | val, childKey, err := encode(in.MapIndex(key)) 184 | if err != nil { 185 | return nil, nil, err 186 | } 187 | if val == nil { 188 | continue 189 | } 190 | 191 | switch typ := val.(type) { 192 | case *ast.ObjectList: 193 | // If the item is an object list, we need to flatten out the items. 194 | // Child keys are assumed to be added to the above call to encode 195 | itemKey := &ast.ObjectKey{Token: tkn} 196 | for _, obj := range typ.Items { 197 | keys := append([]*ast.ObjectKey{itemKey}, obj.Keys...) 198 | l = append(l, &ast.ObjectItem{ 199 | Keys: keys, 200 | Val: obj.Val, 201 | }) 202 | } 203 | 204 | default: 205 | item := &ast.ObjectItem{ 206 | Keys: []*ast.ObjectKey{{Token: tkn}}, 207 | Val: val, 208 | } 209 | if childKey != nil { 210 | item.Keys = append(item.Keys, childKey...) 211 | } 212 | l = append(l, item) 213 | 214 | } 215 | 216 | } 217 | 218 | sort.Sort(l) 219 | return &ast.ObjectType{List: &ast.ObjectList{Items: []*ast.ObjectItem(l)}}, nil, nil 220 | } 221 | 222 | // encodeStruct converts a struct type into an ast.ObjectType. An ast.ObjectKey 223 | // may be returned if a KeyTag is present that should be used by a parent 224 | // ast.ObjectItem if this node is nested. 225 | func encodeStruct(in reflect.Value) (ast.Node, []*ast.ObjectKey, error) { 226 | l := in.NumField() 227 | list := &ast.ObjectList{Items: make([]*ast.ObjectItem, 0, l)} 228 | keys := make([]*ast.ObjectKey, 0) 229 | 230 | for i := 0; i < l; i++ { 231 | field := in.Type().Field(i) 232 | meta := extractFieldMeta(field) 233 | 234 | // these tags are used for debugging the decoder 235 | // they should not be output 236 | if meta.unusedKeys || meta.decodedFields || meta.omit { 237 | continue 238 | } 239 | 240 | tkn, _ := tokenize(reflect.ValueOf(meta.name), true) // impossible to not be string 241 | 242 | // if the OmitEmptyTag is provided, check if the value is its zero value. 243 | rawVal := in.Field(i) 244 | if meta.omitEmpty { 245 | zeroVal := reflect.Zero(rawVal.Type()).Interface() 246 | if reflect.DeepEqual(rawVal.Interface(), zeroVal) { 247 | continue 248 | } 249 | } 250 | 251 | val, childKeys, err := encode(rawVal) 252 | if err != nil { 253 | return nil, nil, err 254 | } 255 | if val == nil { 256 | continue 257 | } 258 | 259 | // this field is a key and should be bubbled up to the parent node 260 | if meta.key { 261 | if lit, ok := val.(*ast.LiteralType); ok && lit.Token.Type == token.STRING { 262 | keys = append(keys, &ast.ObjectKey{Token: lit.Token}) 263 | continue 264 | } 265 | return nil, nil, errors.New("struct key fields must be string literals") 266 | } 267 | 268 | // this field is anonymous and should be squashed into the parent struct's fields 269 | if meta.anonymous && meta.squash { 270 | switch val := val.(type) { 271 | case *ast.ObjectType: 272 | list.Items = append(list.Items, val.List.Items...) 273 | if childKeys != nil { 274 | keys = childKeys 275 | } 276 | continue 277 | } 278 | } 279 | 280 | itemKey := &ast.ObjectKey{Token: tkn} 281 | 282 | // if the item is an object list, we need to flatten out the items 283 | if objectList, ok := val.(*ast.ObjectList); ok { 284 | for _, obj := range objectList.Items { 285 | objectKeys := append([]*ast.ObjectKey{itemKey}, obj.Keys...) 286 | list.Add(&ast.ObjectItem{ 287 | Keys: objectKeys, 288 | Val: obj.Val, 289 | }) 290 | } 291 | continue 292 | } 293 | 294 | item := &ast.ObjectItem{ 295 | Keys: []*ast.ObjectKey{itemKey}, 296 | Val: val, 297 | } 298 | if childKeys != nil { 299 | item.Keys = append(item.Keys, childKeys...) 300 | } 301 | list.Add(item) 302 | } 303 | if len(keys) == 0 { 304 | return &ast.ObjectType{List: list}, nil, nil 305 | } 306 | return &ast.ObjectType{List: list}, keys, nil 307 | } 308 | 309 | // tokenize converts a primitive type into an token.Token. IDENT tokens (unquoted strings) 310 | // can be optionally triggered for any string types. 311 | func tokenize(in reflect.Value, ident bool) (t token.Token, err error) { 312 | switch in.Kind() { 313 | case reflect.Bool: 314 | return token.Token{ 315 | Type: token.BOOL, 316 | Text: strconv.FormatBool(in.Bool()), 317 | }, nil 318 | 319 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 320 | return token.Token{ 321 | Type: token.NUMBER, 322 | Text: fmt.Sprintf("%d", in.Uint()), 323 | }, nil 324 | 325 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 326 | return token.Token{ 327 | Type: token.NUMBER, 328 | Text: fmt.Sprintf("%d", in.Int()), 329 | }, nil 330 | 331 | case reflect.Float64: 332 | return token.Token{ 333 | Type: token.FLOAT, 334 | Text: strconv.FormatFloat(in.Float(), 'g', -1, 64), 335 | }, nil 336 | 337 | case reflect.String: 338 | if ident { 339 | return token.Token{ 340 | Type: token.IDENT, 341 | Text: in.String(), 342 | }, nil 343 | } 344 | return token.Token{ 345 | Type: token.STRING, 346 | Text: fmt.Sprintf(`"%s"`, in.String()), 347 | }, nil 348 | } 349 | 350 | return t, fmt.Errorf("cannot encode primitive kind %s to token", in.Kind()) 351 | } 352 | 353 | // extractFieldMeta pulls information about struct fields and the optional HCL tags 354 | func extractFieldMeta(f reflect.StructField) (meta fieldMeta) { 355 | if f.Anonymous { 356 | meta.anonymous = true 357 | meta.name = f.Type.Name() 358 | } else { 359 | meta.name = f.Name 360 | } 361 | 362 | tags := strings.Split(f.Tag.Get(HCLTagName), ",") 363 | if len(tags) > 0 { 364 | if tags[0] != "" { 365 | meta.name = tags[0] 366 | } 367 | 368 | for _, tag := range tags[1:] { 369 | switch tag { 370 | case KeyTag: 371 | meta.key = true 372 | case SquashTag: 373 | meta.squash = true 374 | case DecodedFieldsTag: 375 | meta.decodedFields = true 376 | case UnusedKeysTag: 377 | meta.unusedKeys = true 378 | } 379 | } 380 | } 381 | 382 | tags = strings.Split(f.Tag.Get(HCLETagName), ",") 383 | for _, tag := range tags { 384 | switch tag { 385 | case OmitTag: 386 | meta.omit = true 387 | case OmitEmptyTag: 388 | meta.omitEmpty = true 389 | } 390 | } 391 | 392 | return 393 | } 394 | 395 | // deref safely dereferences interface and pointer values to their underlying value types. 396 | // It also detects if that value is invalid or nil. 397 | func deref(in reflect.Value) (val reflect.Value, isNil bool) { 398 | switch in.Kind() { 399 | case reflect.Invalid: 400 | return in, true 401 | case reflect.Interface, reflect.Ptr: 402 | if in.IsNil() { 403 | return in, true 404 | } 405 | // recurse for the elusive double pointer 406 | return deref(in.Elem()) 407 | case reflect.Slice, reflect.Map: 408 | return in, in.IsNil() 409 | default: 410 | return in, false 411 | } 412 | } 413 | 414 | type objectItems []*ast.ObjectItem 415 | 416 | func (ol objectItems) Len() int { return len(ol) } 417 | func (ol objectItems) Swap(i, j int) { ol[i], ol[j] = ol[j], ol[i] } 418 | func (ol objectItems) Less(i, j int) bool { 419 | iKeys := ol[i].Keys 420 | jKeys := ol[j].Keys 421 | for k := 0; k < len(iKeys) && k < len(jKeys); k++ { 422 | if iKeys[k].Token.Text == jKeys[k].Token.Text { 423 | continue 424 | } 425 | return iKeys[k].Token.Text < jKeys[k].Token.Text 426 | } 427 | return len(iKeys) <= len(jKeys) 428 | } 429 | -------------------------------------------------------------------------------- /nodes_test.go: -------------------------------------------------------------------------------- 1 | package hclencoder 2 | 3 | import ( 4 | "reflect" 5 | "sort" 6 | "testing" 7 | 8 | "github.com/hashicorp/hcl/hcl/ast" 9 | "github.com/hashicorp/hcl/hcl/token" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | type encodeFunc func(reflect.Value) (ast.Node, []*ast.ObjectKey, error) 14 | 15 | type encodeTest struct { 16 | ID string 17 | Input reflect.Value 18 | Expected ast.Node 19 | Key []*ast.ObjectKey 20 | Error bool 21 | } 22 | 23 | func (test encodeTest) Test(f encodeFunc, t *testing.T) (node ast.Node, key []*ast.ObjectKey, err error) { 24 | node, key, err = f(test.Input) 25 | 26 | if test.Error { 27 | assert.Error(t, err, test.ID) 28 | return 29 | } 30 | 31 | assert.NoError(t, err, test.ID) 32 | assert.EqualValues(t, test.Key, key, test.ID) 33 | assert.EqualValues(t, test.Expected, node, test.ID) 34 | 35 | return 36 | } 37 | 38 | func RunAll(tests []encodeTest, f encodeFunc, t *testing.T) { 39 | for _, test := range tests { 40 | test.Test(f, t) 41 | } 42 | } 43 | 44 | func TestEncode(t *testing.T) { 45 | tests := []encodeTest{ 46 | { 47 | ID: "primitive int", 48 | Input: reflect.ValueOf(123), 49 | Expected: &ast.LiteralType{Token: token.Token{Type: token.NUMBER, Text: "123"}}, 50 | }, 51 | { 52 | ID: "primitive string", 53 | Input: reflect.ValueOf("foobar"), 54 | Expected: &ast.LiteralType{Token: token.Token{Type: token.STRING, Text: `"foobar"`}}, 55 | }, 56 | { 57 | ID: "primitive bool", 58 | Input: reflect.ValueOf(true), 59 | Expected: &ast.LiteralType{Token: token.Token{Type: token.BOOL, Text: "true"}}, 60 | }, 61 | { 62 | ID: "primitive float", 63 | Input: reflect.ValueOf(float64(1.23)), 64 | Expected: &ast.LiteralType{Token: token.Token{Type: token.FLOAT, Text: "1.23"}}, 65 | }, 66 | { 67 | ID: "list", 68 | Input: reflect.ValueOf([]int{1, 2, 3}), 69 | Expected: &ast.ListType{List: []ast.Node{ 70 | &ast.LiteralType{Token: token.Token{Type: token.NUMBER, Text: "1"}}, 71 | &ast.LiteralType{Token: token.Token{Type: token.NUMBER, Text: "2"}}, 72 | &ast.LiteralType{Token: token.Token{Type: token.NUMBER, Text: "3"}}, 73 | }}, 74 | }, 75 | { 76 | ID: "map", 77 | Input: reflect.ValueOf(map[string]int{"foo": 1, "bar": 2}), 78 | Expected: &ast.ObjectType{List: &ast.ObjectList{Items: []*ast.ObjectItem{ 79 | &ast.ObjectItem{ 80 | Keys: []*ast.ObjectKey{{Token: token.Token{Type: token.IDENT, Text: "bar"}}}, 81 | Val: &ast.LiteralType{Token: token.Token{Type: token.NUMBER, Text: "2"}}, 82 | }, 83 | &ast.ObjectItem{ 84 | Keys: []*ast.ObjectKey{{Token: token.Token{Type: token.IDENT, Text: "foo"}}}, 85 | Val: &ast.LiteralType{Token: token.Token{Type: token.NUMBER, Text: "1"}}, 86 | }, 87 | }}}, 88 | }, 89 | { 90 | ID: "struct", 91 | Input: reflect.ValueOf(TestStruct{Bar: "fizzbuzz"}), 92 | Expected: &ast.ObjectType{List: &ast.ObjectList{Items: []*ast.ObjectItem{ 93 | &ast.ObjectItem{ 94 | Keys: []*ast.ObjectKey{{Token: token.Token{Type: token.IDENT, Text: "Bar"}}}, 95 | Val: &ast.LiteralType{Token: token.Token{Type: token.STRING, Text: `"fizzbuzz"`}}, 96 | }, 97 | }}}, 98 | }, 99 | } 100 | 101 | RunAll(tests, encode, t) 102 | } 103 | 104 | func TestEncodePrimitive(t *testing.T) { 105 | tests := []encodeTest{ 106 | { 107 | ID: "int", 108 | Input: reflect.ValueOf(123), 109 | Expected: &ast.LiteralType{Token: token.Token{Type: token.NUMBER, Text: "123"}}, 110 | }, 111 | { 112 | ID: "string - never ident", 113 | Input: reflect.ValueOf("foobar"), 114 | Expected: &ast.LiteralType{Token: token.Token{Type: token.STRING, Text: `"foobar"`}}, 115 | }, 116 | { 117 | ID: "uint", 118 | Input: reflect.ValueOf(uint(1)), 119 | Expected: &ast.LiteralType{Token: token.Token{Type: token.NUMBER, Text: "1"}}, 120 | }, 121 | } 122 | 123 | RunAll(tests, encodePrimitive, t) 124 | } 125 | 126 | func TestEncodeList(t *testing.T) { 127 | tests := []encodeTest{ 128 | { 129 | ID: "primitive - int", 130 | Input: reflect.ValueOf([]int{1, 2, 3}), 131 | Expected: &ast.ListType{List: []ast.Node{ 132 | &ast.LiteralType{Token: token.Token{Type: token.NUMBER, Text: "1"}}, 133 | &ast.LiteralType{Token: token.Token{Type: token.NUMBER, Text: "2"}}, 134 | &ast.LiteralType{Token: token.Token{Type: token.NUMBER, Text: "3"}}, 135 | }}, 136 | }, 137 | { 138 | ID: "primitive - string - never ident", 139 | Input: reflect.ValueOf([]string{"foo", "bar"}), 140 | Expected: &ast.ListType{List: []ast.Node{ 141 | &ast.LiteralType{Token: token.Token{Type: token.STRING, Text: `"foo"`}}, 142 | &ast.LiteralType{Token: token.Token{Type: token.STRING, Text: `"bar"`}}, 143 | }}, 144 | }, 145 | { 146 | ID: "primitive - nil", 147 | Input: reflect.ValueOf([]string(nil)), 148 | Expected: &ast.ListType{List: []ast.Node{}}, 149 | }, 150 | { 151 | ID: "primitive - nil item", 152 | Input: reflect.ValueOf([]*string{strAddr("fizz"), nil, strAddr("buzz")}), 153 | Expected: &ast.ListType{List: []ast.Node{ 154 | &ast.LiteralType{Token: token.Token{Type: token.STRING, Text: `"fizz"`}}, 155 | &ast.LiteralType{Token: token.Token{Type: token.STRING, Text: `"buzz"`}}, 156 | }}, 157 | }, 158 | { 159 | ID: "primitive - uint", 160 | Input: reflect.ValueOf([]uint{123}), 161 | Expected: &ast.ListType{List: []ast.Node{ 162 | &ast.LiteralType{Token: token.Token{Type: token.NUMBER, Text: "123"}}, 163 | }}, 164 | //Error: true, 165 | }, 166 | { 167 | ID: "block", 168 | Input: reflect.ValueOf([]TestStruct{{}, {Bar: "fizzbuzz"}}), 169 | Expected: &ast.ListType{List: []ast.Node{ 170 | &ast.ObjectType{List: &ast.ObjectList{ 171 | Items: []*ast.ObjectItem{{ 172 | Keys: []*ast.ObjectKey{{Token: token.Token{Type: token.IDENT, Text: "Bar"}}}, 173 | Val: &ast.LiteralType{Token: token.Token{Type: token.STRING, Text: `""`}}, 174 | }}, 175 | }}, 176 | &ast.ObjectType{List: &ast.ObjectList{ 177 | Items: []*ast.ObjectItem{{ 178 | Keys: []*ast.ObjectKey{{Token: token.Token{Type: token.IDENT, Text: "Bar"}}}, 179 | Val: &ast.LiteralType{Token: token.Token{Type: token.STRING, Text: `"fizzbuzz"`}}, 180 | }}, 181 | }}, 182 | }}, 183 | }, 184 | { 185 | ID: "block - nil", 186 | Input: reflect.ValueOf([]TestStruct(nil)), 187 | Expected: &ast.ObjectList{Items: []*ast.ObjectItem{}}, 188 | }, 189 | { 190 | ID: "block - nil item", 191 | Input: reflect.ValueOf([]*TestStruct{&TestStruct{}, nil, &TestStruct{Bar: "fizzbuzz"}}), 192 | Expected: &ast.ListType{List: []ast.Node{ 193 | &ast.ObjectType{List: &ast.ObjectList{ 194 | Items: []*ast.ObjectItem{{ 195 | Keys: []*ast.ObjectKey{{Token: token.Token{Type: token.IDENT, Text: "Bar"}}}, 196 | Val: &ast.LiteralType{Token: token.Token{Type: token.STRING, Text: `""`}}, 197 | }}, 198 | }}, 199 | &ast.ObjectType{List: &ast.ObjectList{ 200 | Items: []*ast.ObjectItem{{ 201 | Keys: []*ast.ObjectKey{{Token: token.Token{Type: token.IDENT, Text: "Bar"}}}, 202 | Val: &ast.LiteralType{Token: token.Token{Type: token.STRING, Text: `"fizzbuzz"`}}, 203 | }}, 204 | }}, 205 | }}, 206 | }, 207 | { 208 | ID: "block - interface", 209 | Input: reflect.ValueOf([]TestInterface{TestStruct{}, TestStruct{Bar: "fizzbuzz"}}), 210 | Expected: &ast.ListType{List: []ast.Node{ 211 | &ast.ObjectType{List: &ast.ObjectList{ 212 | Items: []*ast.ObjectItem{{ 213 | Keys: []*ast.ObjectKey{{Token: token.Token{Type: token.IDENT, Text: "Bar"}}}, 214 | Val: &ast.LiteralType{Token: token.Token{Type: token.STRING, Text: `""`}}, 215 | }}, 216 | }}, 217 | &ast.ObjectType{List: &ast.ObjectList{ 218 | Items: []*ast.ObjectItem{{ 219 | Keys: []*ast.ObjectKey{{Token: token.Token{Type: token.IDENT, Text: "Bar"}}}, 220 | Val: &ast.LiteralType{Token: token.Token{Type: token.STRING, Text: `"fizzbuzz"`}}, 221 | }}, 222 | }}, 223 | }}, 224 | }, 225 | { 226 | ID: "block - key field", 227 | Input: reflect.ValueOf([]KeyStruct{{Bar: "foo"}}), 228 | Expected: &ast.ObjectList{Items: []*ast.ObjectItem{&ast.ObjectItem{ 229 | Keys: []*ast.ObjectKey{&ast.ObjectKey{Token: token.Token{Type: token.STRING, Text: `"foo"`}}}, 230 | Val: &ast.ObjectType{List: &ast.ObjectList{Items: []*ast.ObjectItem{}}}, 231 | }}}, 232 | }, 233 | { 234 | ID: "block - invalid", 235 | Input: reflect.ValueOf([]InvalidStruct{{}}), 236 | Error: true, 237 | }, 238 | } 239 | 240 | RunAll(tests, encodeList, t) 241 | } 242 | 243 | func TestEncodeMap(t *testing.T) { 244 | tests := []encodeTest{ 245 | { 246 | ID: "primitive", 247 | Input: reflect.ValueOf(map[string]int{"foo": 1, "bar": 2}), 248 | Expected: &ast.ObjectType{List: &ast.ObjectList{Items: []*ast.ObjectItem{ 249 | &ast.ObjectItem{ 250 | Keys: []*ast.ObjectKey{{Token: token.Token{Type: token.IDENT, Text: "bar"}}}, 251 | Val: &ast.LiteralType{Token: token.Token{Type: token.NUMBER, Text: "2"}}, 252 | }, 253 | &ast.ObjectItem{ 254 | Keys: []*ast.ObjectKey{{Token: token.Token{Type: token.IDENT, Text: "foo"}}}, 255 | Val: &ast.LiteralType{Token: token.Token{Type: token.NUMBER, Text: "1"}}, 256 | }, 257 | }}}, 258 | }, 259 | { 260 | ID: "invalid key", 261 | Input: reflect.ValueOf(map[int]string{}), 262 | Error: true, 263 | }, 264 | { 265 | ID: "invalid value", 266 | Input: reflect.ValueOf(map[string]InvalidStruct{"foo": InvalidStruct{}}), 267 | Error: true, 268 | }, 269 | { 270 | ID: "nil value", 271 | Input: reflect.ValueOf(map[string]*TestStruct{"fizz": nil}), 272 | Expected: &ast.ObjectType{List: &ast.ObjectList{Items: []*ast.ObjectItem{}}}, 273 | }, 274 | { 275 | ID: "key field", 276 | Input: reflect.ValueOf(map[string]KeyStruct{"fizz": {Bar: "buzz"}}), 277 | Expected: &ast.ObjectType{List: &ast.ObjectList{Items: []*ast.ObjectItem{ 278 | &ast.ObjectItem{ 279 | Keys: []*ast.ObjectKey{ 280 | {Token: token.Token{Type: token.IDENT, Text: "fizz"}}, 281 | {Token: token.Token{Type: token.STRING, Text: `"buzz"`}}, 282 | }, 283 | Val: &ast.ObjectType{List: &ast.ObjectList{Items: []*ast.ObjectItem{}}}, 284 | }, 285 | }}}, 286 | }, 287 | { 288 | ID: "keyed list", 289 | Input: reflect.ValueOf(map[string][]map[string]interface{}{ 290 | "obj1": { 291 | {"foo": "bar"}, 292 | {"boo": "hoo"}, 293 | }, 294 | "obj2": { 295 | {"foo": "bar"}, 296 | {"boo": "hoo"}, 297 | }, 298 | }), 299 | Expected: &ast.ObjectType{List: &ast.ObjectList{Items: []*ast.ObjectItem{ 300 | &ast.ObjectItem{ 301 | Keys: []*ast.ObjectKey{{Token: token.Token{Type: token.IDENT, Text: "obj1"}}}, 302 | Val: &ast.ListType{List: []ast.Node{ 303 | &ast.ObjectType{List: &ast.ObjectList{Items: []*ast.ObjectItem{ 304 | { 305 | Keys: []*ast.ObjectKey{{Token: token.Token{Type: token.IDENT, Text: "foo"}}}, 306 | Val: &ast.LiteralType{Token: token.Token{Type: token.STRING, Text: `"bar"`}}, 307 | }, 308 | }}}, 309 | &ast.ObjectType{List: &ast.ObjectList{Items: []*ast.ObjectItem{ 310 | { 311 | Keys: []*ast.ObjectKey{{Token: token.Token{Type: token.IDENT, Text: "boo"}}}, 312 | Val: &ast.LiteralType{Token: token.Token{Type: token.STRING, Text: `"hoo"`}}, 313 | }, 314 | }}}, 315 | }}, 316 | }, 317 | &ast.ObjectItem{ 318 | Keys: []*ast.ObjectKey{{Token: token.Token{Type: token.IDENT, Text: "obj2"}}}, 319 | Val: &ast.ListType{List: []ast.Node{ 320 | &ast.ObjectType{List: &ast.ObjectList{Items: []*ast.ObjectItem{ 321 | { 322 | Keys: []*ast.ObjectKey{{Token: token.Token{Type: token.IDENT, Text: "foo"}}}, 323 | Val: &ast.LiteralType{Token: token.Token{Type: token.STRING, Text: `"bar"`}}, 324 | }, 325 | }}}, 326 | &ast.ObjectType{List: &ast.ObjectList{Items: []*ast.ObjectItem{ 327 | { 328 | Keys: []*ast.ObjectKey{{Token: token.Token{Type: token.IDENT, Text: "boo"}}}, 329 | Val: &ast.LiteralType{Token: token.Token{Type: token.STRING, Text: `"hoo"`}}, 330 | }, 331 | }}}, 332 | }}, 333 | }, 334 | }}}, 335 | }, 336 | } 337 | 338 | RunAll(tests, encodeMap, t) 339 | } 340 | 341 | func TestEncodeStruct(t *testing.T) { 342 | tests := []encodeTest{ 343 | { 344 | ID: "basic", 345 | Input: reflect.ValueOf(TestStruct{Bar: "fizzbuzz"}), 346 | Expected: &ast.ObjectType{List: &ast.ObjectList{Items: []*ast.ObjectItem{ 347 | &ast.ObjectItem{ 348 | Keys: []*ast.ObjectKey{{Token: token.Token{Type: token.IDENT, Text: "Bar"}}}, 349 | Val: &ast.LiteralType{Token: token.Token{Type: token.STRING, Text: `"fizzbuzz"`}}, 350 | }, 351 | }}}, 352 | }, 353 | { 354 | ID: "debug fields", 355 | Input: reflect.ValueOf(DebugStruct{Decoded: []string{}, Unused: []string{}}), 356 | Expected: &ast.ObjectType{List: &ast.ObjectList{Items: []*ast.ObjectItem{}}}, 357 | }, 358 | { 359 | ID: "omit field", 360 | Input: reflect.ValueOf(OmitStruct{"foo"}), 361 | Expected: &ast.ObjectType{List: &ast.ObjectList{Items: []*ast.ObjectItem{}}}, 362 | }, 363 | { 364 | ID: "omitempty field - empty", 365 | Input: reflect.ValueOf(OmitEmptyStruct{}), 366 | Expected: &ast.ObjectType{List: &ast.ObjectList{Items: []*ast.ObjectItem{}}}, 367 | }, 368 | { 369 | ID: "omitempty field - not empty", 370 | Input: reflect.ValueOf(OmitEmptyStruct{"foo"}), 371 | Expected: &ast.ObjectType{List: &ast.ObjectList{Items: []*ast.ObjectItem{ 372 | &ast.ObjectItem{ 373 | Keys: []*ast.ObjectKey{{Token: token.Token{Type: token.IDENT, Text: "Bar"}}}, 374 | Val: &ast.LiteralType{Token: token.Token{Type: token.STRING, Text: `"foo"`}}, 375 | }, 376 | }}}, 377 | }, 378 | { 379 | ID: "nil field", 380 | Input: reflect.ValueOf(NillableStruct{}), 381 | Expected: &ast.ObjectType{List: &ast.ObjectList{Items: []*ast.ObjectItem{}}}, 382 | }, 383 | { 384 | ID: "invalid key type", 385 | Input: reflect.ValueOf(InvalidKeyStruct{123}), 386 | Error: true, 387 | }, 388 | { 389 | ID: "squash anonymous field", 390 | Input: reflect.ValueOf(SquashStruct{TestStruct: TestStruct{"foo"}}), 391 | Expected: &ast.ObjectType{List: &ast.ObjectList{Items: []*ast.ObjectItem{ 392 | &ast.ObjectItem{ 393 | Keys: []*ast.ObjectKey{{Token: token.Token{Type: token.IDENT, Text: "Bar"}}}, 394 | Val: &ast.LiteralType{Token: token.Token{Type: token.STRING, Text: `"foo"`}}, 395 | }, 396 | }}}, 397 | }, 398 | { 399 | ID: "keyed child struct", 400 | Input: reflect.ValueOf(KeyChildStruct{Foo: KeyStruct{Bar: "baz"}}), 401 | Expected: &ast.ObjectType{List: &ast.ObjectList{Items: []*ast.ObjectItem{ 402 | &ast.ObjectItem{ 403 | Keys: []*ast.ObjectKey{{Token: token.Token{Type: token.IDENT, Text: "Foo"}}, {Token: token.Token{Type: token.STRING, Text: `"baz"`}}}, 404 | Val: &ast.ObjectType{List: &ast.ObjectList{Items: []*ast.ObjectItem{}}}, 405 | }, 406 | }}}, 407 | }, 408 | { 409 | ID: "squash keyed child struct", 410 | Input: reflect.ValueOf(SquashKeyChildStruct{KeyChildStruct: KeyChildStruct{Foo: KeyStruct{Bar: "baz"}}}), 411 | Expected: &ast.ObjectType{List: &ast.ObjectList{Items: []*ast.ObjectItem{ 412 | &ast.ObjectItem{ 413 | Keys: []*ast.ObjectKey{{Token: token.Token{Type: token.IDENT, Text: "Foo"}}, {Token: token.Token{Type: token.STRING, Text: `"baz"`}}}, 414 | Val: &ast.ObjectType{List: &ast.ObjectList{Items: []*ast.ObjectItem{}}}, 415 | }, 416 | }}}, 417 | }, 418 | { 419 | ID: "nested unkeyed struct slice", 420 | Input: reflect.ValueOf(struct{ Foo []TestStruct }{[]TestStruct{{"Test"}}}), 421 | Expected: &ast.ObjectType{List: &ast.ObjectList{Items: []*ast.ObjectItem{ 422 | &ast.ObjectItem{ 423 | Keys: []*ast.ObjectKey{{Token: token.Token{Type: token.IDENT, Text: "Foo"}}}, 424 | Val: &ast.ListType{List: []ast.Node{&ast.ObjectType{ 425 | List: &ast.ObjectList{ 426 | Items: []*ast.ObjectItem{{ 427 | Keys: []*ast.ObjectKey{{Token: token.Token{Type: token.IDENT, Text: "Bar"}}}, 428 | Val: &ast.LiteralType{Token: token.Token{Type: token.STRING, Text: `"Test"`}}, 429 | }}, 430 | }}}, 431 | }, 432 | }, 433 | }}}, 434 | }, 435 | } 436 | 437 | RunAll(tests, encodeStruct, t) 438 | } 439 | 440 | func TestTokenize(t *testing.T) { 441 | is := assert.New(t) 442 | 443 | tests := []struct { 444 | ID string 445 | Input reflect.Value 446 | Ident bool 447 | Expected token.Token 448 | Err bool 449 | }{ 450 | { 451 | "bool", 452 | reflect.ValueOf(true), 453 | false, 454 | token.Token{Type: token.BOOL, Text: "true"}, 455 | false, 456 | }, 457 | { 458 | "int", 459 | reflect.ValueOf(123), 460 | false, 461 | token.Token{Type: token.NUMBER, Text: "123"}, 462 | false, 463 | }, 464 | { 465 | "float", 466 | reflect.ValueOf(float64(4.56)), 467 | false, 468 | token.Token{Type: token.FLOAT, Text: "4.56"}, 469 | false, 470 | }, 471 | { 472 | "float - superfluous", 473 | reflect.ValueOf(float64(78.9000000000)), 474 | false, 475 | token.Token{Type: token.FLOAT, Text: "78.9"}, 476 | false, 477 | }, 478 | { 479 | "float - scientific notation", 480 | reflect.ValueOf(float64(1234567890)), 481 | false, 482 | token.Token{Type: token.FLOAT, Text: "1.23456789e+09"}, 483 | false, 484 | }, 485 | { 486 | "string", 487 | reflect.ValueOf("foobar"), 488 | false, 489 | token.Token{Type: token.STRING, Text: `"foobar"`}, 490 | false, 491 | }, 492 | { 493 | "ident", 494 | reflect.ValueOf("fizzbuzz"), 495 | true, 496 | token.Token{Type: token.IDENT, Text: "fizzbuzz"}, 497 | false, 498 | }, 499 | } 500 | 501 | for _, test := range tests { 502 | tkn, err := tokenize(test.Input, test.Ident) 503 | if test.Err { 504 | is.Error(err, test.ID) 505 | } else { 506 | is.NoError(err, test.ID) 507 | is.EqualValues(test.Expected, tkn, test.ID) 508 | } 509 | } 510 | } 511 | 512 | func TestExtractFieldMeta(t *testing.T) { 513 | is := assert.New(t) 514 | 515 | fieldName := "Foo" 516 | 517 | tests := []struct { 518 | Tag string 519 | Expected fieldMeta 520 | }{ 521 | { 522 | "", 523 | fieldMeta{name: fieldName}, 524 | }, 525 | { 526 | `hcl:"bar"`, 527 | fieldMeta{name: "bar"}, 528 | }, 529 | { 530 | `hcl:"bar,key"`, 531 | fieldMeta{name: "bar", key: true}, 532 | }, 533 | { 534 | `hcl:",squash"`, 535 | fieldMeta{name: fieldName, squash: true}, 536 | }, 537 | { 538 | `hcl:",decodedFields,unusedKeys"`, 539 | fieldMeta{name: fieldName, decodedFields: true, unusedKeys: true}, 540 | }, 541 | { 542 | `hcl:",key" hcle:"omit"`, 543 | fieldMeta{name: fieldName, key: true, omit: true}, 544 | }, 545 | { 546 | `hcle:"omitempty"`, 547 | fieldMeta{name: fieldName, omitEmpty: true}, 548 | }, 549 | } 550 | 551 | for _, test := range tests { 552 | input := reflect.StructField{ 553 | Name: fieldName, 554 | Tag: reflect.StructTag(test.Tag), 555 | } 556 | is.EqualValues(test.Expected, extractFieldMeta(input)) 557 | } 558 | 559 | input := reflect.StructField{ 560 | Anonymous: true, 561 | Type: reflect.TypeOf(TestStruct{}), 562 | } 563 | expected := fieldMeta{ 564 | name: input.Type.Name(), 565 | anonymous: true, 566 | } 567 | is.EqualValues(expected, extractFieldMeta(input)) 568 | } 569 | 570 | func TestDeref(t *testing.T) { 571 | is := assert.New(t) 572 | 573 | var IFace TestInterface 574 | IFace = TestStruct{"baz"} 575 | var nilIFace TestInterface 576 | 577 | var nilPtr *TestStruct 578 | 579 | tests := []struct { 580 | Input interface{} 581 | Expected interface{} 582 | IsNil bool 583 | Message string 584 | }{ 585 | { 586 | IFace, 587 | TestStruct{"baz"}, 588 | false, 589 | "interface", 590 | }, 591 | { 592 | &TestStruct{"fizz"}, 593 | TestStruct{"fizz"}, 594 | false, 595 | "pointer", 596 | }, 597 | { 598 | nil, 599 | nil, 600 | true, 601 | "nil", 602 | }, 603 | { 604 | nilIFace, 605 | nil, 606 | true, 607 | "interface - nil", 608 | }, 609 | { 610 | nilPtr, 611 | nil, 612 | true, 613 | "pointer - nil", 614 | }, 615 | { 616 | []string{"foo", "bar"}, 617 | []string{"foo", "bar"}, 618 | false, 619 | "slice", 620 | }, 621 | { 622 | []string(nil), 623 | nil, 624 | true, 625 | "slice - nil", 626 | }, 627 | } 628 | 629 | for _, test := range tests { 630 | expected := reflect.ValueOf(test.Expected) 631 | val, isNil := deref(reflect.ValueOf(test.Input)) 632 | 633 | if test.IsNil { 634 | is.Equal(test.IsNil, isNil, "%s", test.Message) 635 | } else { 636 | is.EqualValues(expected.Type(), val.Type(), "%s", test.Message) 637 | } 638 | } 639 | } 640 | 641 | func TestObjectItems(t *testing.T) { 642 | noKeys := &ast.ObjectItem{} 643 | bar := &ast.ObjectItem{Keys: []*ast.ObjectKey{{Token: token.Token{Text: "bar"}}}} 644 | foo := &ast.ObjectItem{Keys: []*ast.ObjectKey{{Token: token.Token{Text: "foo"}}}} 645 | foobar := &ast.ObjectItem{Keys: []*ast.ObjectKey{{Token: token.Token{Text: "foo"}}, {Token: token.Token{Text: "bar"}}}} 646 | 647 | oi := objectItems{ 648 | foobar, 649 | foo, 650 | bar, 651 | noKeys, 652 | } 653 | 654 | expected := objectItems{ 655 | noKeys, 656 | bar, 657 | foo, 658 | foobar, 659 | } 660 | 661 | sort.Sort(oi) 662 | assert.EqualValues(t, expected, oi) 663 | } 664 | 665 | type TestInterface interface { 666 | Foo() 667 | } 668 | 669 | type TestStruct struct { 670 | Bar string 671 | } 672 | 673 | func (TestStruct) Foo() {} 674 | 675 | type KeyStruct struct { 676 | Bar string `hcl:",key"` 677 | } 678 | 679 | func (KeyStruct) Foo() {} 680 | 681 | type KeyChildStruct struct { 682 | Foo KeyStruct 683 | } 684 | 685 | type DebugStruct struct { 686 | Decoded []string `hcl:",decodedFields"` 687 | Unused []string `hcl:",unusedKeys"` 688 | } 689 | 690 | type OmitStruct struct { 691 | Bar string `hcle:"omit"` 692 | } 693 | 694 | type OmitEmptyStruct struct { 695 | Bar string `hcle:"omitempty"` 696 | } 697 | 698 | type InvalidStruct struct { 699 | Chan chan struct{} 700 | } 701 | 702 | type InvalidKeyStruct struct { 703 | Bar int `hcl:",key"` 704 | } 705 | 706 | type NillableStruct struct { 707 | Bar *string 708 | } 709 | 710 | type SquashStruct struct { 711 | TestStruct `hcl:",squash"` 712 | } 713 | 714 | type SquashKeyChildStruct struct { 715 | KeyChildStruct `hcl:",squash"` 716 | } 717 | 718 | func strAddr(s string) *string { 719 | return &s 720 | } 721 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # hclencoder
[![Build Status](https://travis-ci.org/rodaine/hclencoder.svg?branch=master)](https://travis-ci.org/rodaine/hclencoder) [![GoDoc](https://godoc.org/github.com/rodaine/hclencoder?status.svg)](https://godoc.org/github.com/rodaine/hclencoder) 2 | 3 | `hclencoder` encodes/marshals/converts Go types into [HCL (Hashicorp Configuration Language)][HCL]. `hclencoder` ensures correctness in the generated HCL, and can be useful for creating programmatic, type-safe config files. 4 | 5 | ```go 6 | type Farm struct { 7 | Name string `hcl:"name"` 8 | Owned bool `hcl:"owned"` 9 | Location []float64 `hcl:"location"` 10 | } 11 | 12 | type Farmer struct { 13 | Name string `hcl:"name"` 14 | Age int `hcl:"age"` 15 | SocialSecurityNumber string `hcle:"omit"` 16 | } 17 | 18 | type Animal struct { 19 | Name string `hcl:",key"` 20 | Sound string `hcl:"says" hcle:"omitempty"` 21 | } 22 | 23 | type Config struct { 24 | Farm `hcl:",squash"` 25 | Farmer Farmer `hcl:"farmer"` 26 | Animals []Animal `hcl:"animal"` 27 | Buildings map[string]string `hcl:"buildings"` 28 | } 29 | 30 | input := Config{ 31 | Farm: Farm{ 32 | Name: "Ol' McDonald's Farm", 33 | Owned: true, 34 | Location: []float64{12.34, -5.67}, 35 | }, 36 | Farmer: Farmer{ 37 | Name: "Robert Beauregard-Michele McDonald, III", 38 | Age: 65, 39 | SocialSecurityNumber: "please-dont-share-me", 40 | }, 41 | Animals: []Animal{ 42 | { 43 | Name: "cow", 44 | Sound: "moo", 45 | }, 46 | { 47 | Name: "pig", 48 | Sound: "oink", 49 | }, 50 | { 51 | Name: "rock", 52 | }, 53 | }, 54 | Buildings: map[string]string{ 55 | "House": "123 Numbers Lane", 56 | "Barn": "456 Digits Drive", 57 | }, 58 | } 59 | 60 | hcl, err := Encode(input) 61 | if err != nil { 62 | log.Fatal("unable to encode: ", err) 63 | } 64 | 65 | fmt.Print(string(hcl)) 66 | 67 | // Output: 68 | // name = "Ol' McDonald's Farm" 69 | // 70 | // owned = true 71 | // 72 | // location = [ 73 | // 12.34, 74 | // -5.67, 75 | // ] 76 | // 77 | // farmer { 78 | // name = "Robert Beauregard-Michele McDonald, III" 79 | // age = 65 80 | // } 81 | // 82 | // animal "cow" { 83 | // says = "moo" 84 | // } 85 | // 86 | // animal "pig" { 87 | // says = "oink" 88 | // } 89 | // 90 | // animal "rock" {} 91 | // 92 | // buildings { 93 | // Barn = "456 Digits Drive" 94 | // House = "123 Numbers Lane" 95 | // } 96 | // 97 | ``` 98 | 99 | ## Features 100 | 101 | - [x] Encodes any `struct` or `map[string]T` type as the input for the generated HCL 102 | - [x] Supports all value, interface, and pointer types supported by the HCL encoder: `bool`, `int`, `float64`, `string`, `struct`, `[]T`, `map[string]T` 103 | - [x] Uses the [HCL Printer][hclprinter] to ensure consistency with the output HCL 104 | - [x] Map types are sorted to ensure ordering 105 | - [ ] Support raw HCL [`ast.Node`][node] types in the struct. 106 | - [ ] Support `HCLMarshaler` interface for types to encode themselves, similar to [`json.Marshaler`][jsonmarshal] 107 | 108 | 109 | ## Struct Tags 110 | 111 | `hclencoder` supports and respects the existing `hcl` [struct tags][tags]: 112 | 113 | - **`hcl:"custom_name"`** - specifies the name of the field as represented in the output HCL to be `custom_name`. The default behavior is to use the unmodified name of the field. If other tag fields are desired but the default name behavior should be used, leave the first comma-delimited value empty (eg, `hcl:",key"`). 114 | 115 | - **`hcl:",key"`** - indicates the field should be used as part of the compound key for the HCL block. This field must be of type `string`. 116 | 117 | - **`hcl:",squash"`** - attached to anonymous fields of a struct, indicates to lift the fields of that value into the parent block's scope transparently. Otherwise, the field's type is used as the key for the value. 118 | 119 | - **`hcl:",unusedKeys"`** - identifies this debug field which stores any unused keys found by the decoder. This field shoudl be of type `[]string`. This has the same behavior as the `hcle:"omit"` tag and is not encoded. 120 | 121 | - **`hcl:",decodedFields"`** - identifies this debug field which stores the names of all fields decoded from HCL. This field should be of type `[]string`. This has the same behavior as the `hcle:"omit"` tag and is not encoded. 122 | 123 | `hclencoder` also supports additional `hcle` struct tags that provide additional capabilities: 124 | 125 | - **`hcle:"omit"`** - omits this field from encoding into HCL. This is similar behavior to [`json:"-"`][json]. 126 | 127 | - **`hcle:"omitempty"`** - omits this field if it is a zero value for its type. This is similar behavior to [`json:",omitempty"`][json]. 128 | 129 | [HCL]: https://github.com/hashicorp/hcl 130 | [hclprinter]: https://godoc.org/github.com/hashicorp/hcl/hcl/printer 131 | [json]: https://golang.org/pkg/encoding/json/#Marshal 132 | [jsonmarshal]: https://golang.org/pkg/encoding/json/#Marshaler 133 | [node]: https://godoc.org/github.com/hashicorp/hcl/hcl/ast#Node 134 | [tags]: https://golang.org/pkg/reflect/#StructTag 135 | 136 | ## License 137 | 138 | The MIT License (MIT) 139 | 140 | Copyright (c) 2016 Chris Roche 141 | 142 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 143 | 144 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 145 | 146 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 147 | -------------------------------------------------------------------------------- /walker.go: -------------------------------------------------------------------------------- 1 | package hclencoder 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "unicode/utf8" 7 | 8 | "github.com/hashicorp/hcl/hcl/ast" 9 | "github.com/hashicorp/hcl/hcl/token" 10 | ) 11 | 12 | type cursor token.Pos 13 | 14 | func (c cursor) pos() token.Pos { 15 | return token.Pos(c) 16 | } 17 | 18 | func (c cursor) crlf() cursor { 19 | c.Line++ 20 | c.Column = 1 21 | return c 22 | } 23 | 24 | func (c cursor) in(step int) cursor { 25 | c.Offset += step 26 | return c 27 | } 28 | 29 | func (c cursor) out(step int) cursor { 30 | c.Offset -= step 31 | return c 32 | } 33 | 34 | var startingCursor = cursor{ 35 | Offset: 0, 36 | Line: 1, 37 | Column: 1, 38 | } 39 | 40 | func positionNodes(node ast.Node, cur cursor, step int) (cursor, error) { 41 | var err error 42 | 43 | switch node := node.(type) { 44 | case *ast.LiteralType: 45 | node.Token.Pos = cur.pos() 46 | cur.Column += utf8.RuneCountInString(node.Token.Text) 47 | return cur, nil 48 | 49 | case *ast.ListType: 50 | node.Lbrack = cur.pos() 51 | if len(node.List) > 1 { 52 | cur = cur.crlf().in(step) 53 | } 54 | for _, item := range node.List { 55 | if cur, err = positionNodes(item, cur, step); err != nil { 56 | return cur, err 57 | } 58 | if len(node.List) > 1 { 59 | cur = cur.crlf() 60 | } 61 | } 62 | cur = cur.out(step) 63 | node.Rbrack = cur.pos() 64 | cur.Column++ 65 | return cur, nil 66 | 67 | case *ast.ObjectItem: 68 | for _, key := range node.Keys { 69 | key.Token.Pos = cur.pos() 70 | cur.Column += 1 + utf8.RuneCountInString(node.Keys[0].Token.Text) 71 | } 72 | 73 | if _, ok := node.Val.(*ast.ObjectType); !ok { 74 | node.Assign = cur.pos() 75 | } 76 | cur.Column += 2 77 | 78 | return positionNodes(node.Val, cur, step) 79 | 80 | case *ast.ObjectList: 81 | for _, item := range node.Items { 82 | cur, err = positionNodes(item, cur, step) 83 | if err != nil { 84 | return cur, err 85 | } 86 | cur = cur.crlf() 87 | } 88 | return cur, nil 89 | 90 | case *ast.ObjectType: 91 | node.Lbrace = cur.pos() 92 | cur = cur.crlf().in(step) 93 | 94 | if cur, err = positionNodes(node.List, cur, step); err != nil { 95 | return cur, err 96 | } 97 | cur = cur.out(step) 98 | node.Rbrace = cur.pos() 99 | cur.Column++ 100 | return cur, nil 101 | 102 | case *ast.File: 103 | return positionNodes(node.Node, cur, step) 104 | 105 | default: 106 | return cur, fmt.Errorf("unknown node kind %s", reflect.ValueOf(node).Kind()) 107 | } 108 | } 109 | --------------------------------------------------------------------------------