8 "github.com/zclconf/go-cty/cty/convert"
10 "github.com/zclconf/go-cty/cty"
13 // FlatmapValueFromHCL2 converts a value from HCL2 (really, from the cty dynamic
14 // types library that HCL2 uses) to a map compatible with what would be
15 // produced by the "flatmap" package.
17 // The type of the given value informs the structure of the resulting map.
18 // The value must be of an object type or this function will panic.
20 // Flatmap values can only represent maps when they are of primitive types,
21 // so the given value must not have any maps of complex types or the result
23 func FlatmapValueFromHCL2(v cty.Value) map[string]string {
28 if !v.Type().IsObjectType() {
29 panic(fmt.Sprintf("HCL2ValueFromFlatmap called on %#v", v.Type()))
32 m := make(map[string]string)
33 flatmapValueFromHCL2Map(m, "", v)
37 func flatmapValueFromHCL2Value(m map[string]string, key string, val cty.Value) {
40 case ty.IsPrimitiveType() || ty == cty.DynamicPseudoType:
41 flatmapValueFromHCL2Primitive(m, key, val)
42 case ty.IsObjectType() || ty.IsMapType():
43 flatmapValueFromHCL2Map(m, key+".", val)
44 case ty.IsTupleType() || ty.IsListType() || ty.IsSetType():
45 flatmapValueFromHCL2Seq(m, key+".", val)
47 panic(fmt.Sprintf("cannot encode %s to flatmap", ty.FriendlyName()))
51 func flatmapValueFromHCL2Primitive(m map[string]string, key string, val cty.Value) {
53 m[key] = UnknownVariableValue
62 val, err = convert.Convert(val, cty.String)
64 // Should not be possible, since all primitive types can convert to string.
65 panic(fmt.Sprintf("invalid primitive encoding to flatmap: %s", err))
67 m[key] = val.AsString()
70 func flatmapValueFromHCL2Map(m map[string]string, prefix string, val cty.Value) {
77 case val.Type().IsObjectType():
78 // Whole objects can't be unknown in flatmap, so instead we'll
79 // just write all of the attribute values out as unknown.
80 for name, aty := range val.Type().AttributeTypes() {
81 flatmapValueFromHCL2Value(m, prefix+name, cty.UnknownVal(aty))
84 m[prefix+"%"] = UnknownVariableValue
90 for it := val.ElementIterator(); it.Next(); {
91 ak, av := it.Element()
93 flatmapValueFromHCL2Value(m, prefix+name, av)
96 if !val.Type().IsObjectType() { // objects don't have an explicit count included, since their attribute count is fixed
97 m[prefix+"%"] = strconv.Itoa(len)
101 func flatmapValueFromHCL2Seq(m map[string]string, prefix string, val cty.Value) {
107 m[prefix+"#"] = UnknownVariableValue
111 // For sets this won't actually generate exactly what helper/schema would've
112 // generated, because we don't have access to the set key function it
113 // would've used. However, in practice it doesn't actually matter what the
114 // keys are as long as they are unique, so we'll just generate sequential
115 // indexes for them as if it were a list.
117 // An important implication of this, however, is that the set ordering will
118 // not be consistent across mutations and so different keys may be assigned
119 // to the same value when round-tripping. Since this shim is intended to
120 // be short-lived and not used for round-tripping, we accept this.
122 for it := val.ElementIterator(); it.Next(); {
123 _, av := it.Element()
124 key := prefix + strconv.Itoa(i)
125 flatmapValueFromHCL2Value(m, key, av)
128 m[prefix+"#"] = strconv.Itoa(i)
131 // HCL2ValueFromFlatmap converts a map compatible with what would be produced
132 // by the "flatmap" package to a HCL2 (really, the cty dynamic types library
133 // that HCL2 uses) object type.
135 // The intended result type must be provided in order to guide how the
136 // map contents are decoded. This must be an object type or this function
139 // Flatmap values can only represent maps when they are of primitive types,
140 // so the given type must not have any maps of complex types or the result
143 // The result may contain null values if the given map does not contain keys
144 // for all of the different key paths implied by the given type.
145 func HCL2ValueFromFlatmap(m map[string]string, ty cty.Type) (cty.Value, error) {
147 return cty.NullVal(ty), nil
149 if !ty.IsObjectType() {
150 panic(fmt.Sprintf("HCL2ValueFromFlatmap called on %#v", ty))
153 return hcl2ValueFromFlatmapObject(m, "", ty.AttributeTypes())
156 func hcl2ValueFromFlatmapValue(m map[string]string, key string, ty cty.Type) (cty.Value, error) {
160 case ty.IsPrimitiveType():
161 val, err = hcl2ValueFromFlatmapPrimitive(m, key, ty)
162 case ty.IsObjectType():
163 val, err = hcl2ValueFromFlatmapObject(m, key+".", ty.AttributeTypes())
164 case ty.IsTupleType():
165 val, err = hcl2ValueFromFlatmapTuple(m, key+".", ty.TupleElementTypes())
167 val, err = hcl2ValueFromFlatmapMap(m, key+".", ty)
168 case ty.IsListType():
169 val, err = hcl2ValueFromFlatmapList(m, key+".", ty)
171 val, err = hcl2ValueFromFlatmapSet(m, key+".", ty)
173 err = fmt.Errorf("cannot decode %s from flatmap", ty.FriendlyName())
177 return cty.DynamicVal, err
182 func hcl2ValueFromFlatmapPrimitive(m map[string]string, key string, ty cty.Type) (cty.Value, error) {
183 rawVal, exists := m[key]
185 return cty.NullVal(ty), nil
187 if rawVal == UnknownVariableValue {
188 return cty.UnknownVal(ty), nil
192 val := cty.StringVal(rawVal)
193 val, err = convert.Convert(val, ty)
195 // This should never happen for _valid_ input, but flatmap data might
196 // be tampered with by the user and become invalid.
197 return cty.DynamicVal, fmt.Errorf("invalid value for %q in state: %s", key, err)
203 func hcl2ValueFromFlatmapObject(m map[string]string, prefix string, atys map[string]cty.Type) (cty.Value, error) {
204 vals := make(map[string]cty.Value)
205 for name, aty := range atys {
206 val, err := hcl2ValueFromFlatmapValue(m, prefix+name, aty)
208 return cty.DynamicVal, err
212 return cty.ObjectVal(vals), nil
215 func hcl2ValueFromFlatmapTuple(m map[string]string, prefix string, etys []cty.Type) (cty.Value, error) {
218 // if the container is unknown, there is no count string
219 listName := strings.TrimRight(prefix, ".")
220 if m[listName] == UnknownVariableValue {
221 return cty.UnknownVal(cty.Tuple(etys)), nil
224 countStr, exists := m[prefix+"#"]
226 return cty.NullVal(cty.Tuple(etys)), nil
228 if countStr == UnknownVariableValue {
229 return cty.UnknownVal(cty.Tuple(etys)), nil
232 count, err := strconv.Atoi(countStr)
234 return cty.DynamicVal, fmt.Errorf("invalid count value for %q in state: %s", prefix, err)
236 if count != len(etys) {
237 return cty.DynamicVal, fmt.Errorf("wrong number of values for %q in state: got %d, but need %d", prefix, count, len(etys))
240 vals = make([]cty.Value, len(etys))
241 for i, ety := range etys {
242 key := prefix + strconv.Itoa(i)
243 val, err := hcl2ValueFromFlatmapValue(m, key, ety)
245 return cty.DynamicVal, err
249 return cty.TupleVal(vals), nil
252 func hcl2ValueFromFlatmapMap(m map[string]string, prefix string, ty cty.Type) (cty.Value, error) {
253 vals := make(map[string]cty.Value)
254 ety := ty.ElementType()
256 // if the container is unknown, there is no count string
257 listName := strings.TrimRight(prefix, ".")
258 if m[listName] == UnknownVariableValue {
259 return cty.UnknownVal(ty), nil
262 // We actually don't really care about the "count" of a map for our
263 // purposes here, but we do need to check if it _exists_ in order to
264 // recognize the difference between null (not set at all) and empty.
265 if strCount, exists := m[prefix+"%"]; !exists {
266 return cty.NullVal(ty), nil
267 } else if strCount == UnknownVariableValue {
268 return cty.UnknownVal(ty), nil
271 for fullKey := range m {
272 if !strings.HasPrefix(fullKey, prefix) {
276 // The flatmap format doesn't allow us to distinguish between keys
277 // that contain periods and nested objects, so by convention a
278 // map is only ever of primitive type in flatmap, and we just assume
279 // that the remainder of the raw key (dots and all) is the key we
280 // want in the result value.
281 key := fullKey[len(prefix):]
283 // Ignore the "count" key
287 val, err := hcl2ValueFromFlatmapValue(m, fullKey, ety)
289 return cty.DynamicVal, err
295 return cty.MapValEmpty(ety), nil
297 return cty.MapVal(vals), nil
300 func hcl2ValueFromFlatmapList(m map[string]string, prefix string, ty cty.Type) (cty.Value, error) {
303 // if the container is unknown, there is no count string
304 listName := strings.TrimRight(prefix, ".")
305 if m[listName] == UnknownVariableValue {
306 return cty.UnknownVal(ty), nil
309 countStr, exists := m[prefix+"#"]
311 return cty.NullVal(ty), nil
313 if countStr == UnknownVariableValue {
314 return cty.UnknownVal(ty), nil
317 count, err := strconv.Atoi(countStr)
319 return cty.DynamicVal, fmt.Errorf("invalid count value for %q in state: %s", prefix, err)
322 ety := ty.ElementType()
324 return cty.ListValEmpty(ety), nil
327 vals = make([]cty.Value, count)
328 for i := 0; i < count; i++ {
329 key := prefix + strconv.Itoa(i)
330 val, err := hcl2ValueFromFlatmapValue(m, key, ety)
332 return cty.DynamicVal, err
337 return cty.ListVal(vals), nil
340 func hcl2ValueFromFlatmapSet(m map[string]string, prefix string, ty cty.Type) (cty.Value, error) {
342 ety := ty.ElementType()
344 // if the container is unknown, there is no count string
345 listName := strings.TrimRight(prefix, ".")
346 if m[listName] == UnknownVariableValue {
347 return cty.UnknownVal(ty), nil
350 strCount, exists := m[prefix+"#"]
352 return cty.NullVal(ty), nil
353 } else if strCount == UnknownVariableValue {
354 return cty.UnknownVal(ty), nil
357 // Keep track of keys we've seen, se we don't add the same set value
358 // multiple times. The cty.Set will normally de-duplicate values, but we may
359 // have unknown values that would not show as equivalent.
360 seen := map[string]bool{}
362 for fullKey := range m {
363 if !strings.HasPrefix(fullKey, prefix) {
366 subKey := fullKey[len(prefix):]
368 // Ignore the "count" key
372 if dot := strings.IndexByte(subKey, '.'); dot != -1 {
373 key = fullKey[:dot+len(prefix)]
382 // The flatmap format doesn't allow us to distinguish between keys
383 // that contain periods and nested objects, so by convention a
384 // map is only ever of primitive type in flatmap, and we just assume
385 // that the remainder of the raw key (dots and all) is the key we
386 // want in the result value.
388 val, err := hcl2ValueFromFlatmapValue(m, key, ety)
390 return cty.DynamicVal, err
392 vals = append(vals, val)
395 if len(vals) == 0 && strCount == "1" {
396 // An empty set wouldn't be represented in the flatmap, so this must be
397 // a single empty object since the count is actually 1.
398 // Add an appropriately typed null value to the set.
401 case ety.IsMapType():
402 val = cty.MapValEmpty(ety)
403 case ety.IsListType():
404 val = cty.ListValEmpty(ety)
405 case ety.IsSetType():
406 val = cty.SetValEmpty(ety)
407 case ety.IsObjectType():
408 // TODO: cty.ObjectValEmpty
409 objectMap := map[string]cty.Value{}
410 for attr, ty := range ety.AttributeTypes() {
411 objectMap[attr] = cty.NullVal(ty)
413 val = cty.ObjectVal(objectMap)
415 val = cty.NullVal(ety)
417 vals = append(vals, val)
419 } else if len(vals) == 0 {
420 return cty.SetValEmpty(ety), nil
423 return cty.SetVal(vals), nil