aboutsummaryrefslogtreecommitdiffhomepage
path: root/vendor/github.com/hashicorp/hcl2/gohcl/encode.go
blob: 3cbf7e48af499caf025e8f3ee02223b2f5d4e871 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
package gohcl

import (
	"fmt"
	"reflect"
	"sort"

	"github.com/hashicorp/hcl2/hclwrite"
	"github.com/zclconf/go-cty/cty/gocty"
)

// EncodeIntoBody replaces the contents of the given hclwrite Body with
// attributes and blocks derived from the given value, which must be a
// struct value or a pointer to a struct value with the struct tags defined
// in this package.
//
// This function can work only with fully-decoded data. It will ignore any
// fields tagged as "remain", any fields that decode attributes into either
// hcl.Attribute or hcl.Expression values, and any fields that decode blocks
// into hcl.Attributes values. This function does not have enough information
// to complete the decoding of these types.
//
// Any fields tagged as "label" are ignored by this function. Use EncodeAsBlock
// to produce a whole hclwrite.Block including block labels.
//
// As long as a suitable value is given to encode and the destination body
// is non-nil, this function will always complete. It will panic in case of
// any errors in the calling program, such as passing an inappropriate type
// or a nil body.
//
// The layout of the resulting HCL source is derived from the ordering of
// the struct fields, with blank lines around nested blocks of different types.
// Fields representing attributes should usually precede those representing
// blocks so that the attributes can group togather in the result. For more
// control, use the hclwrite API directly.
func EncodeIntoBody(val interface{}, dst *hclwrite.Body) {
	rv := reflect.ValueOf(val)
	ty := rv.Type()
	if ty.Kind() == reflect.Ptr {
		rv = rv.Elem()
		ty = rv.Type()
	}
	if ty.Kind() != reflect.Struct {
		panic(fmt.Sprintf("value is %s, not struct", ty.Kind()))
	}

	tags := getFieldTags(ty)
	populateBody(rv, ty, tags, dst)
}

// EncodeAsBlock creates a new hclwrite.Block populated with the data from
// the given value, which must be a struct or pointer to struct with the
// struct tags defined in this package.
//
// If the given struct type has fields tagged with "label" tags then they
// will be used in order to annotate the created block with labels.
//
// This function has the same constraints as EncodeIntoBody and will panic
// if they are violated.
func EncodeAsBlock(val interface{}, blockType string) *hclwrite.Block {
	rv := reflect.ValueOf(val)
	ty := rv.Type()
	if ty.Kind() == reflect.Ptr {
		rv = rv.Elem()
		ty = rv.Type()
	}
	if ty.Kind() != reflect.Struct {
		panic(fmt.Sprintf("value is %s, not struct", ty.Kind()))
	}

	tags := getFieldTags(ty)
	labels := make([]string, len(tags.Labels))
	for i, lf := range tags.Labels {
		lv := rv.Field(lf.FieldIndex)
		// We just stringify whatever we find. It should always be a string
		// but if not then we'll still do something reasonable.
		labels[i] = fmt.Sprintf("%s", lv.Interface())
	}

	block := hclwrite.NewBlock(blockType, labels)
	populateBody(rv, ty, tags, block.Body())
	return block
}

func populateBody(rv reflect.Value, ty reflect.Type, tags *fieldTags, dst *hclwrite.Body) {
	nameIdxs := make(map[string]int, len(tags.Attributes)+len(tags.Blocks))
	namesOrder := make([]string, 0, len(tags.Attributes)+len(tags.Blocks))
	for n, i := range tags.Attributes {
		nameIdxs[n] = i
		namesOrder = append(namesOrder, n)
	}
	for n, i := range tags.Blocks {
		nameIdxs[n] = i
		namesOrder = append(namesOrder, n)
	}
	sort.SliceStable(namesOrder, func(i, j int) bool {
		ni, nj := namesOrder[i], namesOrder[j]
		return nameIdxs[ni] < nameIdxs[nj]
	})

	dst.Clear()

	prevWasBlock := false
	for _, name := range namesOrder {
		fieldIdx := nameIdxs[name]
		field := ty.Field(fieldIdx)
		fieldTy := field.Type
		fieldVal := rv.Field(fieldIdx)

		if fieldTy.Kind() == reflect.Ptr {
			fieldTy = fieldTy.Elem()
			fieldVal = fieldVal.Elem()
		}

		if _, isAttr := tags.Attributes[name]; isAttr {

			if exprType.AssignableTo(fieldTy) || attrType.AssignableTo(fieldTy) {
				continue // ignore undecoded fields
			}
			if !fieldVal.IsValid() {
				continue // ignore (field value is nil pointer)
			}
			if fieldTy.Kind() == reflect.Ptr && fieldVal.IsNil() {
				continue // ignore
			}
			if prevWasBlock {
				dst.AppendNewline()
				prevWasBlock = false
			}

			valTy, err := gocty.ImpliedType(fieldVal.Interface())
			if err != nil {
				panic(fmt.Sprintf("cannot encode %T as HCL expression: %s", fieldVal.Interface(), err))
			}

			val, err := gocty.ToCtyValue(fieldVal.Interface(), valTy)
			if err != nil {
				// This should never happen, since we should always be able
				// to decode into the implied type.
				panic(fmt.Sprintf("failed to encode %T as %#v: %s", fieldVal.Interface(), valTy, err))
			}

			dst.SetAttributeValue(name, val)

		} else { // must be a block, then
			elemTy := fieldTy
			isSeq := false
			if elemTy.Kind() == reflect.Slice || elemTy.Kind() == reflect.Array {
				isSeq = true
				elemTy = elemTy.Elem()
			}

			if bodyType.AssignableTo(elemTy) || attrsType.AssignableTo(elemTy) {
				continue // ignore undecoded fields
			}
			prevWasBlock = false

			if isSeq {
				l := fieldVal.Len()
				for i := 0; i < l; i++ {
					elemVal := fieldVal.Index(i)
					if !elemVal.IsValid() {
						continue // ignore (elem value is nil pointer)
					}
					if elemTy.Kind() == reflect.Ptr && elemVal.IsNil() {
						continue // ignore
					}
					block := EncodeAsBlock(elemVal.Interface(), name)
					if !prevWasBlock {
						dst.AppendNewline()
						prevWasBlock = true
					}
					dst.AppendBlock(block)
				}
			} else {
				if !fieldVal.IsValid() {
					continue // ignore (field value is nil pointer)
				}
				if elemTy.Kind() == reflect.Ptr && fieldVal.IsNil() {
					continue // ignore
				}
				block := EncodeAsBlock(fieldVal.Interface(), name)
				if !prevWasBlock {
					dst.AppendNewline()
					prevWasBlock = true
				}
				dst.AppendBlock(block)
			}
		}
	}
}