mirror of
https://github.com/opentffoundation/opentf.git
synced 2026-01-07 01:00:44 -05:00
Fixes #7715 If a bool field was computed and the raw value was not convertable to a boolean, helper/schema would crash. The correct behavior is to try not to read the raw value when the value is computed and to simply mark that it is computed. This does that (and matches the behavior of the other primitives).
274 lines
6.5 KiB
Go
274 lines
6.5 KiB
Go
package schema
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
)
|
|
|
|
// FieldReaders are responsible for decoding fields out of data into
|
|
// the proper typed representation. ResourceData uses this to query data
|
|
// out of multiple sources: config, state, diffs, etc.
|
|
type FieldReader interface {
|
|
ReadField([]string) (FieldReadResult, error)
|
|
}
|
|
|
|
// FieldReadResult encapsulates all the resulting data from reading
|
|
// a field.
|
|
type FieldReadResult struct {
|
|
// Value is the actual read value. NegValue is the _negative_ value
|
|
// or the items that should be removed (if they existed). NegValue
|
|
// doesn't make sense for primitives but is important for any
|
|
// container types such as maps, sets, lists.
|
|
Value interface{}
|
|
ValueProcessed interface{}
|
|
|
|
// Exists is true if the field was found in the data. False means
|
|
// it wasn't found if there was no error.
|
|
Exists bool
|
|
|
|
// Computed is true if the field was found but the value
|
|
// is computed.
|
|
Computed bool
|
|
}
|
|
|
|
// ValueOrZero returns the value of this result or the zero value of the
|
|
// schema type, ensuring a consistent non-nil return value.
|
|
func (r *FieldReadResult) ValueOrZero(s *Schema) interface{} {
|
|
if r.Value != nil {
|
|
return r.Value
|
|
}
|
|
|
|
return s.ZeroValue()
|
|
}
|
|
|
|
// addrToSchema finds the final element schema for the given address
|
|
// and the given schema. It returns all the schemas that led to the final
|
|
// schema. These are in order of the address (out to in).
|
|
func addrToSchema(addr []string, schemaMap map[string]*Schema) []*Schema {
|
|
current := &Schema{
|
|
Type: typeObject,
|
|
Elem: schemaMap,
|
|
}
|
|
|
|
// If we aren't given an address, then the user is requesting the
|
|
// full object, so we return the special value which is the full object.
|
|
if len(addr) == 0 {
|
|
return []*Schema{current}
|
|
}
|
|
|
|
result := make([]*Schema, 0, len(addr))
|
|
for len(addr) > 0 {
|
|
k := addr[0]
|
|
addr = addr[1:]
|
|
|
|
REPEAT:
|
|
// We want to trim off the first "typeObject" since its not a
|
|
// real lookup that people do. i.e. []string{"foo"} in a structure
|
|
// isn't {typeObject, typeString}, its just a {typeString}.
|
|
if len(result) > 0 || current.Type != typeObject {
|
|
result = append(result, current)
|
|
}
|
|
|
|
switch t := current.Type; t {
|
|
case TypeBool, TypeInt, TypeFloat, TypeString:
|
|
if len(addr) > 0 {
|
|
return nil
|
|
}
|
|
case TypeList, TypeSet:
|
|
switch v := current.Elem.(type) {
|
|
case *Resource:
|
|
current = &Schema{
|
|
Type: typeObject,
|
|
Elem: v.Schema,
|
|
}
|
|
case *Schema:
|
|
current = v
|
|
default:
|
|
return nil
|
|
}
|
|
|
|
// If we only have one more thing and the next thing
|
|
// is a #, then we're accessing the index which is always
|
|
// an int.
|
|
if len(addr) > 0 && addr[0] == "#" {
|
|
current = &Schema{Type: TypeInt}
|
|
break
|
|
}
|
|
case TypeMap:
|
|
if len(addr) > 0 {
|
|
current = &Schema{Type: TypeString}
|
|
}
|
|
case typeObject:
|
|
// If we're already in the object, then we want to handle Sets
|
|
// and Lists specially. Basically, their next key is the lookup
|
|
// key (the set value or the list element). For these scenarios,
|
|
// we just want to skip it and move to the next element if there
|
|
// is one.
|
|
if len(result) > 0 {
|
|
lastType := result[len(result)-2].Type
|
|
if lastType == TypeSet || lastType == TypeList {
|
|
if len(addr) == 0 {
|
|
break
|
|
}
|
|
|
|
k = addr[0]
|
|
addr = addr[1:]
|
|
}
|
|
}
|
|
|
|
m := current.Elem.(map[string]*Schema)
|
|
val, ok := m[k]
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
current = val
|
|
goto REPEAT
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// readListField is a generic method for reading a list field out of a
|
|
// a FieldReader. It does this based on the assumption that there is a key
|
|
// "foo.#" for a list "foo" and that the indexes are "foo.0", "foo.1", etc.
|
|
// after that point.
|
|
func readListField(
|
|
r FieldReader, addr []string, schema *Schema) (FieldReadResult, error) {
|
|
addrPadded := make([]string, len(addr)+1)
|
|
copy(addrPadded, addr)
|
|
addrPadded[len(addrPadded)-1] = "#"
|
|
|
|
// Get the number of elements in the list
|
|
countResult, err := r.ReadField(addrPadded)
|
|
if err != nil {
|
|
return FieldReadResult{}, err
|
|
}
|
|
if !countResult.Exists {
|
|
// No count, means we have no list
|
|
countResult.Value = 0
|
|
}
|
|
|
|
// If we have an empty list, then return an empty list
|
|
if countResult.Computed || countResult.Value.(int) == 0 {
|
|
return FieldReadResult{
|
|
Value: []interface{}{},
|
|
Exists: countResult.Exists,
|
|
Computed: countResult.Computed,
|
|
}, nil
|
|
}
|
|
|
|
// Go through each count, and get the item value out of it
|
|
result := make([]interface{}, countResult.Value.(int))
|
|
for i, _ := range result {
|
|
is := strconv.FormatInt(int64(i), 10)
|
|
addrPadded[len(addrPadded)-1] = is
|
|
rawResult, err := r.ReadField(addrPadded)
|
|
if err != nil {
|
|
return FieldReadResult{}, err
|
|
}
|
|
if !rawResult.Exists {
|
|
// This should never happen, because by the time the data
|
|
// gets to the FieldReaders, all the defaults should be set by
|
|
// Schema.
|
|
rawResult.Value = nil
|
|
}
|
|
|
|
result[i] = rawResult.Value
|
|
}
|
|
|
|
return FieldReadResult{
|
|
Value: result,
|
|
Exists: true,
|
|
}, nil
|
|
}
|
|
|
|
// readObjectField is a generic method for reading objects out of FieldReaders
|
|
// based on the assumption that building an address of []string{k, FIELD}
|
|
// will result in the proper field data.
|
|
func readObjectField(
|
|
r FieldReader,
|
|
addr []string,
|
|
schema map[string]*Schema) (FieldReadResult, error) {
|
|
result := make(map[string]interface{})
|
|
exists := false
|
|
for field, s := range schema {
|
|
addrRead := make([]string, len(addr), len(addr)+1)
|
|
copy(addrRead, addr)
|
|
addrRead = append(addrRead, field)
|
|
rawResult, err := r.ReadField(addrRead)
|
|
if err != nil {
|
|
return FieldReadResult{}, err
|
|
}
|
|
if rawResult.Exists {
|
|
exists = true
|
|
}
|
|
|
|
result[field] = rawResult.ValueOrZero(s)
|
|
}
|
|
|
|
return FieldReadResult{
|
|
Value: result,
|
|
Exists: exists,
|
|
}, nil
|
|
}
|
|
|
|
func stringToPrimitive(
|
|
value string, computed bool, schema *Schema) (interface{}, error) {
|
|
var returnVal interface{}
|
|
switch schema.Type {
|
|
case TypeBool:
|
|
if value == "" {
|
|
returnVal = false
|
|
break
|
|
}
|
|
if computed {
|
|
break
|
|
}
|
|
|
|
v, err := strconv.ParseBool(value)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
returnVal = v
|
|
case TypeFloat:
|
|
if value == "" {
|
|
returnVal = 0.0
|
|
break
|
|
}
|
|
if computed {
|
|
break
|
|
}
|
|
|
|
v, err := strconv.ParseFloat(value, 64)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
returnVal = v
|
|
case TypeInt:
|
|
if value == "" {
|
|
returnVal = 0
|
|
break
|
|
}
|
|
if computed {
|
|
break
|
|
}
|
|
|
|
v, err := strconv.ParseInt(value, 0, 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
returnVal = int(v)
|
|
case TypeString:
|
|
returnVal = value
|
|
default:
|
|
panic(fmt.Sprintf("Unknown type: %s", schema.Type))
|
|
}
|
|
|
|
return returnVal, nil
|
|
}
|