Skip to content

Commit 7e1533e

Browse files
Merge pull request #2 from multycloud/cty_value_update_docs
Implement cty.value encoding and update docs.
2 parents df8734c + 9e5f51c commit 7e1533e

File tree

8 files changed

+326
-235
lines changed

8 files changed

+326
-235
lines changed

_tests/cty-value.hcl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
String = "test"
2+
Map = { "outer" = { "inner" = 5 } }
3+
Slice = [["foo"], "bar", null]
4+
TemplateExpression = "${func("str")}\n"

hclencoder.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ func Encode(in interface{}) ([]byte, error) {
3030
func addRootBlock(block *hclwrite.Block, f *hclwrite.File) {
3131
// root blocks without types are squashed by default
3232
if block.Type() == "" {
33-
SquashBlock(block, f.Body())
33+
squashBlock(block, f.Body())
3434
} else {
3535
f.Body().AppendBlock(block)
3636
}

hclencoder_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package hclencoder
22

33
import (
44
"fmt"
5+
"github.com/zclconf/go-cty/cty"
56
"io/ioutil"
67
"testing"
78

@@ -40,6 +41,29 @@ func TestEncoder(t *testing.T) {
4041
},
4142
Output: "basic",
4243
},
44+
{
45+
ID: "cty value",
46+
Input: struct {
47+
String cty.Value
48+
Map cty.Value
49+
Slice cty.Value
50+
TemplateExpression cty.Value
51+
}{
52+
String: cty.StringVal("test"),
53+
Map: cty.ObjectVal(map[string]cty.Value{
54+
"outer": cty.MapVal(map[string]cty.Value{
55+
"inner": cty.NumberIntVal(5),
56+
}),
57+
}),
58+
Slice: cty.TupleVal([]cty.Value{
59+
cty.ListVal([]cty.Value{cty.StringVal("foo")}),
60+
cty.StringVal("bar"),
61+
cty.NullVal(cty.String),
62+
}),
63+
TemplateExpression: cty.StringVal("${func(\"str\")}\n"),
64+
},
65+
Output: "cty-value",
66+
},
4367
{
4468
ID: "escaped strings",
4569
Input: struct {

nodes.go

Lines changed: 43 additions & 205 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,9 @@ package hclencoder
33
import (
44
"errors"
55
"fmt"
6-
"github.com/hashicorp/hcl/v2"
7-
"github.com/hashicorp/hcl/v2/hclsyntax"
86
"github.com/hashicorp/hcl/v2/hclwrite"
97
"github.com/zclconf/go-cty/cty"
108
"reflect"
11-
"sort"
129
"strings"
1310
)
1411

@@ -70,12 +67,35 @@ type fieldMeta struct {
7067
omitEmpty bool
7168
}
7269

73-
func encode(in reflect.Value) (node *Node, err error) {
70+
type node struct {
71+
Block *hclwrite.Block
72+
BlockList []*hclwrite.Block
73+
Value *cty.Value
74+
Tokens hclwrite.Tokens
75+
}
76+
77+
func (n node) isValue() bool {
78+
return n.Value != nil
79+
}
80+
81+
func (n node) isBlock() bool {
82+
return n.Block != nil
83+
}
84+
85+
func (n node) isBlockList() bool {
86+
return n.BlockList != nil
87+
}
88+
89+
func (n node) isTokens() bool {
90+
return n.Tokens != nil
91+
}
92+
93+
func encode(in reflect.Value) (node *node, err error) {
7494
return encodeField(in, fieldMeta{})
7595
}
7696

77-
// encode converts a reflected valued into an HCL ast.Node in a depth-first manner.
78-
func encodeField(in reflect.Value, meta fieldMeta) (node *Node, err error) {
97+
// encode converts a reflected valued into an HCL ast.node in a depth-first manner.
98+
func encodeField(in reflect.Value, meta fieldMeta) (node *node, err error) {
7999
in, isNil := deref(in)
80100
if isNil {
81101
return nil, nil
@@ -95,29 +115,34 @@ func encodeField(in reflect.Value, meta fieldMeta) (node *Node, err error) {
95115
return encodePrimitive(in, meta)
96116

97117
case reflect.Struct:
118+
if in.Type().AssignableTo(reflect.TypeOf(cty.Value{})) {
119+
meta.expression = true
120+
str, _ := ValueToString(in.Interface().(cty.Value))
121+
return encodePrimitive(reflect.ValueOf(str), meta)
122+
}
98123
return encodeStruct(in, meta)
99124
default:
100125
return nil, fmt.Errorf("cannot encode kind %s to HCL", in.Kind())
101126
}
102127
}
103128

104-
// encodePrimitive converts a primitive value into a Node contains its tokens
105-
func encodePrimitive(in reflect.Value, meta fieldMeta) (*Node, error) {
129+
// encodePrimitive converts a primitive value into a node contains its tokens
130+
func encodePrimitive(in reflect.Value, meta fieldMeta) (*node, error) {
106131
// Keys must be literals, so we don't tokenize.
107132
if meta.key {
108133
k := cty.StringVal(in.String())
109-
return &Node{Value: &k}, nil
134+
return &node{Value: &k}, nil
110135
}
111136
tkn, err := tokenize(in, meta)
112137
if err != nil {
113138
return nil, err
114139
}
115140

116-
return &Node{Tokens: tkn}, nil
141+
return &node{Tokens: tkn}, nil
117142
}
118143

119144
// encodeList converts a slice into either a block list or a primitive list depending on its element type
120-
func encodeList(in reflect.Value, meta fieldMeta) (*Node, error) {
145+
func encodeList(in reflect.Value, meta fieldMeta) (*node, error) {
121146
childType := in.Type().Elem()
122147

123148
childLoop:
@@ -140,13 +165,13 @@ childLoop:
140165

141166
// encodePrimitiveList converts a slice of primitive values to an ast.ListType. An
142167
// ast.ObjectKey is never returned.
143-
func encodePrimitiveList(in reflect.Value, meta fieldMeta) (*Node, error) {
168+
func encodePrimitiveList(in reflect.Value, meta fieldMeta) (*node, error) {
144169
return encodePrimitive(in, meta)
145170
}
146171

147172
// encodeBlockList converts a slice of non-primitive types to an ast.ObjectList. An
148173
// ast.ObjectKey is never returned.
149-
func encodeBlockList(in reflect.Value, meta fieldMeta) (*Node, error) {
174+
func encodeBlockList(in reflect.Value, meta fieldMeta) (*node, error) {
150175
var blocks []*hclwrite.Block
151176

152177
if !meta.repeatBlock {
@@ -164,34 +189,11 @@ func encodeBlockList(in reflect.Value, meta fieldMeta) (*Node, error) {
164189
blocks = append(blocks, node.Block)
165190
}
166191

167-
return &Node{BlockList: blocks}, nil
168-
}
169-
170-
type Node struct {
171-
Block *hclwrite.Block
172-
BlockList []*hclwrite.Block
173-
Value *cty.Value
174-
Tokens hclwrite.Tokens
175-
}
176-
177-
func (n Node) isValue() bool {
178-
return n.Value != nil
179-
}
180-
181-
func (n Node) isBlock() bool {
182-
return n.Block != nil
183-
}
184-
185-
func (n Node) isBlockList() bool {
186-
return n.BlockList != nil
187-
}
188-
189-
func (n Node) isTokens() bool {
190-
return n.Tokens != nil
192+
return &node{BlockList: blocks}, nil
191193
}
192194

193195
// encodeStruct converts a struct type into a block
194-
func encodeStruct(in reflect.Value, parentMeta fieldMeta) (*Node, error) {
196+
func encodeStruct(in reflect.Value, parentMeta fieldMeta) (*node, error) {
195197
l := in.NumField()
196198
block := hclwrite.NewBlock(parentMeta.name, nil)
197199

@@ -238,7 +240,7 @@ func encodeStruct(in reflect.Value, parentMeta fieldMeta) (*Node, error) {
238240

239241
if val.isBlock() {
240242
if meta.squash {
241-
SquashBlock(val.Block, block.Body())
243+
squashBlock(val.Block, block.Body())
242244
for _, label := range val.Block.Labels() {
243245
block.SetLabels(append(block.Labels(), label))
244246
}
@@ -260,179 +262,15 @@ func encodeStruct(in reflect.Value, parentMeta fieldMeta) (*Node, error) {
260262

261263
}
262264

263-
return &Node{Block: block}, nil
265+
return &node{Block: block}, nil
264266
}
265267

266-
func SquashBlock(innerBlock *hclwrite.Block, block *hclwrite.Body) {
268+
func squashBlock(innerBlock *hclwrite.Block, block *hclwrite.Body) {
267269
tkns := innerBlock.Body().BuildTokens(nil)
268270
block.AppendUnstructuredTokens(tkns)
269271

270272
}
271273

272-
func convertTokens(tokens hclsyntax.Tokens) hclwrite.Tokens {
273-
var result []*hclwrite.Token
274-
for _, token := range tokens {
275-
result = append(result, &hclwrite.Token{
276-
Type: token.Type,
277-
Bytes: token.Bytes,
278-
SpacesBefore: 0,
279-
})
280-
}
281-
return result
282-
}
283-
284-
// tokenize converts a primitive type into tokens. structs and maps are converted into objects and slices are converted
285-
// into tuples.
286-
func tokenize(in reflect.Value, meta fieldMeta) (tkns hclwrite.Tokens, err error) {
287-
288-
tokenEqual := hclwrite.Token{
289-
Type: hclsyntax.TokenEqual,
290-
Bytes: []byte("="),
291-
SpacesBefore: 0,
292-
}
293-
tokenComma := hclwrite.Token{
294-
Type: hclsyntax.TokenComma,
295-
Bytes: []byte(","),
296-
SpacesBefore: 0,
297-
}
298-
tokenOCurlyBrace := hclwrite.Token{
299-
Type: hclsyntax.TokenOBrace,
300-
Bytes: []byte("{"),
301-
SpacesBefore: 0,
302-
}
303-
tokenCCurlyBrace := hclwrite.Token{
304-
Type: hclsyntax.TokenCBrace,
305-
Bytes: []byte("}"),
306-
SpacesBefore: 0,
307-
}
308-
309-
switch in.Kind() {
310-
case reflect.Bool:
311-
return hclwrite.TokensForValue(cty.BoolVal(in.Bool())), nil
312-
313-
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
314-
return hclwrite.TokensForValue(cty.NumberUIntVal(in.Uint())), nil
315-
316-
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
317-
return hclwrite.TokensForValue(cty.NumberIntVal(in.Int())), nil
318-
319-
case reflect.Float64:
320-
return hclwrite.TokensForValue(cty.NumberFloatVal(in.Float())), nil
321-
322-
case reflect.String:
323-
val := in.String()
324-
if !meta.expression {
325-
val = fmt.Sprintf(`"%s"`, EscapeString(val))
326-
}
327-
// Unfortunately hcl escapes template expressions (${...}) when using hclwrite.TokensForValue. So we escape
328-
// everything but template expressions and then parse the expression into tokens.
329-
tokens, diags := hclsyntax.LexExpression([]byte(val), meta.name, hcl.Pos{
330-
Line: 0,
331-
Column: 0,
332-
Byte: 0,
333-
})
334-
335-
if diags != nil {
336-
return nil, fmt.Errorf("error when parsing string %s: %v", val, diags.Error())
337-
}
338-
return convertTokens(tokens), nil
339-
case reflect.Pointer, reflect.Interface:
340-
val, isNil := deref(in)
341-
if isNil {
342-
return nil, nil
343-
}
344-
return tokenize(val, meta)
345-
case reflect.Struct:
346-
var tokens []*hclwrite.Token
347-
tokens = append(tokens, &tokenOCurlyBrace)
348-
for i := 0; i < in.NumField(); i++ {
349-
field := in.Type().Field(i)
350-
meta := extractFieldMeta(field)
351-
352-
rawVal := in.Field(i)
353-
if meta.omitEmpty {
354-
zeroVal := reflect.Zero(rawVal.Type()).Interface()
355-
if reflect.DeepEqual(rawVal.Interface(), zeroVal) {
356-
continue
357-
}
358-
}
359-
val, err := tokenize(rawVal, meta)
360-
if err != nil {
361-
return nil, err
362-
}
363-
for _, tkn := range hclwrite.TokensForValue(cty.StringVal(meta.name)) {
364-
tokens = append(tokens, tkn)
365-
}
366-
tokens = append(tokens, &tokenEqual)
367-
for _, tkn := range val {
368-
tokens = append(tokens, tkn)
369-
}
370-
if i < in.NumField()-1 {
371-
tokens = append(tokens, &tokenComma)
372-
}
373-
}
374-
tokens = append(tokens, &tokenCCurlyBrace)
375-
return tokens, nil
376-
case reflect.Slice:
377-
var tokens []*hclwrite.Token
378-
tokens = append(tokens, &hclwrite.Token{
379-
Type: hclsyntax.TokenOBrace,
380-
Bytes: []byte("["),
381-
SpacesBefore: 0,
382-
})
383-
for i := 0; i < in.Len(); i++ {
384-
value, err := tokenize(in.Index(i), meta)
385-
if err != nil {
386-
return nil, err
387-
}
388-
for _, tkn := range value {
389-
tokens = append(tokens, tkn)
390-
}
391-
if i < in.Len()-1 {
392-
tokens = append(tokens, &tokenComma)
393-
}
394-
}
395-
tokens = append(tokens, &hclwrite.Token{
396-
Type: hclsyntax.TokenCBrace,
397-
Bytes: []byte("]"),
398-
SpacesBefore: 0,
399-
})
400-
return tokens, nil
401-
case reflect.Map:
402-
if keyType := in.Type().Key().Kind(); keyType != reflect.String {
403-
return nil, fmt.Errorf("map keys must be strings, %s given", keyType)
404-
}
405-
var tokens []*hclwrite.Token
406-
tokens = append(tokens, &tokenOCurlyBrace)
407-
408-
var keys []string
409-
for _, k := range in.MapKeys() {
410-
keys = append(keys, k.String())
411-
}
412-
sort.Strings(keys)
413-
for i, k := range keys {
414-
val, err := tokenize(in.MapIndex(reflect.ValueOf(k)), meta)
415-
if err != nil {
416-
return nil, err
417-
}
418-
for _, tkn := range hclwrite.TokensForValue(cty.StringVal(k)) {
419-
tokens = append(tokens, tkn)
420-
}
421-
tokens = append(tokens, &tokenEqual)
422-
for _, tkn := range val {
423-
tokens = append(tokens, tkn)
424-
}
425-
if i < len(keys)-1 {
426-
tokens = append(tokens, &tokenComma)
427-
}
428-
}
429-
tokens = append(tokens, &tokenCCurlyBrace)
430-
return tokens, nil
431-
}
432-
433-
return nil, fmt.Errorf("cannot encode primitive kind %s to token", in.Kind())
434-
}
435-
436274
// extractFieldMeta pulls information about struct fields and the optional HCL tags
437275
func extractFieldMeta(f reflect.StructField) (meta fieldMeta) {
438276
if f.Anonymous {

0 commit comments

Comments
 (0)