terraform/internal/legacy/helper/schema/field_reader.go

344 lines
8.2 KiB
Go
Raw Normal View History

package schema
import (
"fmt"
"strconv"
"strings"
)
// 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()
}
// SchemasForFlatmapPath tries its best to find a sequence of schemas that
// the given dot-delimited attribute path traverses through.
func SchemasForFlatmapPath(path string, schemaMap map[string]*Schema) []*Schema {
parts := strings.Split(path, ".")
return addrToSchema(parts, schemaMap)
}
// 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:
isIndex := len(addr) > 0 && addr[0] == "#"
switch v := current.Elem.(type) {
case *Resource:
current = &Schema{
Type: typeObject,
Elem: v.Schema,
}
case *Schema:
current = v
case ValueType:
current = &Schema{Type: v}
default:
// we may not know the Elem type and are just looking for the
// index
if isIndex {
break
}
if len(addr) == 0 {
// we've processed the address, so return what we've
// collected
return result
}
if len(addr) == 1 {
if _, err := strconv.Atoi(addr[0]); err == nil {
// we're indexing a value without a schema. This can
// happen if the list is nested in another schema type.
// Default to a TypeString like we do with a map
current = &Schema{Type: TypeString}
break
}
}
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 isIndex {
current = &Schema{Type: TypeInt}
break
}
case TypeMap:
if len(addr) > 0 {
switch v := current.Elem.(type) {
case ValueType:
current = &Schema{Type: v}
case *Schema:
current, _ = current.Elem.(*Schema)
default:
// maps default to string values. This is all we can have
// if this is nested in another list or map.
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
}
// convert map values to the proper primitive type based on schema.Elem
func mapValuesToPrimitive(k string, m map[string]interface{}, schema *Schema) error {
elemType, err := getValueType(k, schema)
if err != nil {
return err
}
switch elemType {
case TypeInt, TypeFloat, TypeBool:
for k, v := range m {
vs, ok := v.(string)
if !ok {
continue
}
v, err := stringToPrimitive(vs, false, &Schema{Type: elemType})
if err != nil {
return err
}
m[k] = v
}
}
return 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
}