github.com/opentofu/opentofu@v1.7.1/internal/legacy/helper/schema/field_reader.go (about)

     1  // Copyright (c) The OpenTofu Authors
     2  // SPDX-License-Identifier: MPL-2.0
     3  // Copyright (c) 2023 HashiCorp, Inc.
     4  // SPDX-License-Identifier: MPL-2.0
     5  
     6  package schema
     7  
     8  import (
     9  	"fmt"
    10  	"strconv"
    11  	"strings"
    12  )
    13  
    14  // FieldReaders are responsible for decoding fields out of data into
    15  // the proper typed representation. ResourceData uses this to query data
    16  // out of multiple sources: config, state, diffs, etc.
    17  type FieldReader interface {
    18  	ReadField([]string) (FieldReadResult, error)
    19  }
    20  
    21  // FieldReadResult encapsulates all the resulting data from reading
    22  // a field.
    23  type FieldReadResult struct {
    24  	// Value is the actual read value. NegValue is the _negative_ value
    25  	// or the items that should be removed (if they existed). NegValue
    26  	// doesn't make sense for primitives but is important for any
    27  	// container types such as maps, sets, lists.
    28  	Value          interface{}
    29  	ValueProcessed interface{}
    30  
    31  	// Exists is true if the field was found in the data. False means
    32  	// it wasn't found if there was no error.
    33  	Exists bool
    34  
    35  	// Computed is true if the field was found but the value
    36  	// is computed.
    37  	Computed bool
    38  }
    39  
    40  // ValueOrZero returns the value of this result or the zero value of the
    41  // schema type, ensuring a consistent non-nil return value.
    42  func (r *FieldReadResult) ValueOrZero(s *Schema) interface{} {
    43  	if r.Value != nil {
    44  		return r.Value
    45  	}
    46  
    47  	return s.ZeroValue()
    48  }
    49  
    50  // SchemasForFlatmapPath tries its best to find a sequence of schemas that
    51  // the given dot-delimited attribute path traverses through.
    52  func SchemasForFlatmapPath(path string, schemaMap map[string]*Schema) []*Schema {
    53  	parts := strings.Split(path, ".")
    54  	return addrToSchema(parts, schemaMap)
    55  }
    56  
    57  // addrToSchema finds the final element schema for the given address
    58  // and the given schema. It returns all the schemas that led to the final
    59  // schema. These are in order of the address (out to in).
    60  func addrToSchema(addr []string, schemaMap map[string]*Schema) []*Schema {
    61  	current := &Schema{
    62  		Type: typeObject,
    63  		Elem: schemaMap,
    64  	}
    65  
    66  	// If we aren't given an address, then the user is requesting the
    67  	// full object, so we return the special value which is the full object.
    68  	if len(addr) == 0 {
    69  		return []*Schema{current}
    70  	}
    71  
    72  	result := make([]*Schema, 0, len(addr))
    73  	for len(addr) > 0 {
    74  		k := addr[0]
    75  		addr = addr[1:]
    76  
    77  	REPEAT:
    78  		// We want to trim off the first "typeObject" since its not a
    79  		// real lookup that people do. i.e. []string{"foo"} in a structure
    80  		// isn't {typeObject, typeString}, its just a {typeString}.
    81  		if len(result) > 0 || current.Type != typeObject {
    82  			result = append(result, current)
    83  		}
    84  
    85  		switch t := current.Type; t {
    86  		case TypeBool, TypeInt, TypeFloat, TypeString:
    87  			if len(addr) > 0 {
    88  				return nil
    89  			}
    90  		case TypeList, TypeSet:
    91  			isIndex := len(addr) > 0 && addr[0] == "#"
    92  
    93  			switch v := current.Elem.(type) {
    94  			case *Resource:
    95  				current = &Schema{
    96  					Type: typeObject,
    97  					Elem: v.Schema,
    98  				}
    99  			case *Schema:
   100  				current = v
   101  			case ValueType:
   102  				current = &Schema{Type: v}
   103  			default:
   104  				// we may not know the Elem type and are just looking for the
   105  				// index
   106  				if isIndex {
   107  					break
   108  				}
   109  
   110  				if len(addr) == 0 {
   111  					// we've processed the address, so return what we've
   112  					// collected
   113  					return result
   114  				}
   115  
   116  				if len(addr) == 1 {
   117  					if _, err := strconv.Atoi(addr[0]); err == nil {
   118  						// we're indexing a value without a schema. This can
   119  						// happen if the list is nested in another schema type.
   120  						// Default to a TypeString like we do with a map
   121  						current = &Schema{Type: TypeString}
   122  						break
   123  					}
   124  				}
   125  
   126  				return nil
   127  			}
   128  
   129  			// If we only have one more thing and the next thing
   130  			// is a #, then we're accessing the index which is always
   131  			// an int.
   132  			if isIndex {
   133  				current = &Schema{Type: TypeInt}
   134  				break
   135  			}
   136  
   137  		case TypeMap:
   138  			if len(addr) > 0 {
   139  				switch v := current.Elem.(type) {
   140  				case ValueType:
   141  					current = &Schema{Type: v}
   142  				case *Schema:
   143  					current, _ = current.Elem.(*Schema)
   144  				default:
   145  					// maps default to string values. This is all we can have
   146  					// if this is nested in another list or map.
   147  					current = &Schema{Type: TypeString}
   148  				}
   149  			}
   150  		case typeObject:
   151  			// If we're already in the object, then we want to handle Sets
   152  			// and Lists specially. Basically, their next key is the lookup
   153  			// key (the set value or the list element). For these scenarios,
   154  			// we just want to skip it and move to the next element if there
   155  			// is one.
   156  			if len(result) > 0 {
   157  				lastType := result[len(result)-2].Type
   158  				if lastType == TypeSet || lastType == TypeList {
   159  					if len(addr) == 0 {
   160  						break
   161  					}
   162  
   163  					k = addr[0]
   164  					addr = addr[1:]
   165  				}
   166  			}
   167  
   168  			m := current.Elem.(map[string]*Schema)
   169  			val, ok := m[k]
   170  			if !ok {
   171  				return nil
   172  			}
   173  
   174  			current = val
   175  			goto REPEAT
   176  		}
   177  	}
   178  
   179  	return result
   180  }
   181  
   182  // readListField is a generic method for reading a list field out of a
   183  // a FieldReader. It does this based on the assumption that there is a key
   184  // "foo.#" for a list "foo" and that the indexes are "foo.0", "foo.1", etc.
   185  // after that point.
   186  func readListField(
   187  	r FieldReader, addr []string, schema *Schema) (FieldReadResult, error) {
   188  	addrPadded := make([]string, len(addr)+1)
   189  	copy(addrPadded, addr)
   190  	addrPadded[len(addrPadded)-1] = "#"
   191  
   192  	// Get the number of elements in the list
   193  	countResult, err := r.ReadField(addrPadded)
   194  	if err != nil {
   195  		return FieldReadResult{}, err
   196  	}
   197  	if !countResult.Exists {
   198  		// No count, means we have no list
   199  		countResult.Value = 0
   200  	}
   201  
   202  	// If we have an empty list, then return an empty list
   203  	if countResult.Computed || countResult.Value.(int) == 0 {
   204  		return FieldReadResult{
   205  			Value:    []interface{}{},
   206  			Exists:   countResult.Exists,
   207  			Computed: countResult.Computed,
   208  		}, nil
   209  	}
   210  
   211  	// Go through each count, and get the item value out of it
   212  	result := make([]interface{}, countResult.Value.(int))
   213  	for i, _ := range result {
   214  		is := strconv.FormatInt(int64(i), 10)
   215  		addrPadded[len(addrPadded)-1] = is
   216  		rawResult, err := r.ReadField(addrPadded)
   217  		if err != nil {
   218  			return FieldReadResult{}, err
   219  		}
   220  		if !rawResult.Exists {
   221  			// This should never happen, because by the time the data
   222  			// gets to the FieldReaders, all the defaults should be set by
   223  			// Schema.
   224  			rawResult.Value = nil
   225  		}
   226  
   227  		result[i] = rawResult.Value
   228  	}
   229  
   230  	return FieldReadResult{
   231  		Value:  result,
   232  		Exists: true,
   233  	}, nil
   234  }
   235  
   236  // readObjectField is a generic method for reading objects out of FieldReaders
   237  // based on the assumption that building an address of []string{k, FIELD}
   238  // will result in the proper field data.
   239  func readObjectField(
   240  	r FieldReader,
   241  	addr []string,
   242  	schema map[string]*Schema) (FieldReadResult, error) {
   243  	result := make(map[string]interface{})
   244  	exists := false
   245  	for field, s := range schema {
   246  		addrRead := make([]string, len(addr), len(addr)+1)
   247  		copy(addrRead, addr)
   248  		addrRead = append(addrRead, field)
   249  		rawResult, err := r.ReadField(addrRead)
   250  		if err != nil {
   251  			return FieldReadResult{}, err
   252  		}
   253  		if rawResult.Exists {
   254  			exists = true
   255  		}
   256  
   257  		result[field] = rawResult.ValueOrZero(s)
   258  	}
   259  
   260  	return FieldReadResult{
   261  		Value:  result,
   262  		Exists: exists,
   263  	}, nil
   264  }
   265  
   266  // convert map values to the proper primitive type based on schema.Elem
   267  func mapValuesToPrimitive(k string, m map[string]interface{}, schema *Schema) error {
   268  	elemType, err := getValueType(k, schema)
   269  	if err != nil {
   270  		return err
   271  	}
   272  
   273  	switch elemType {
   274  	case TypeInt, TypeFloat, TypeBool:
   275  		for k, v := range m {
   276  			vs, ok := v.(string)
   277  			if !ok {
   278  				continue
   279  			}
   280  
   281  			v, err := stringToPrimitive(vs, false, &Schema{Type: elemType})
   282  			if err != nil {
   283  				return err
   284  			}
   285  
   286  			m[k] = v
   287  		}
   288  	}
   289  	return nil
   290  }
   291  
   292  func stringToPrimitive(
   293  	value string, computed bool, schema *Schema) (interface{}, error) {
   294  	var returnVal interface{}
   295  	switch schema.Type {
   296  	case TypeBool:
   297  		if value == "" {
   298  			returnVal = false
   299  			break
   300  		}
   301  		if computed {
   302  			break
   303  		}
   304  
   305  		v, err := strconv.ParseBool(value)
   306  		if err != nil {
   307  			return nil, err
   308  		}
   309  
   310  		returnVal = v
   311  	case TypeFloat:
   312  		if value == "" {
   313  			returnVal = 0.0
   314  			break
   315  		}
   316  		if computed {
   317  			break
   318  		}
   319  
   320  		v, err := strconv.ParseFloat(value, 64)
   321  		if err != nil {
   322  			return nil, err
   323  		}
   324  
   325  		returnVal = v
   326  	case TypeInt:
   327  		if value == "" {
   328  			returnVal = 0
   329  			break
   330  		}
   331  		if computed {
   332  			break
   333  		}
   334  
   335  		v, err := strconv.ParseInt(value, 0, 0)
   336  		if err != nil {
   337  			return nil, err
   338  		}
   339  
   340  		returnVal = int(v)
   341  	case TypeString:
   342  		returnVal = value
   343  	default:
   344  		panic(fmt.Sprintf("Unknown type: %s", schema.Type))
   345  	}
   346  
   347  	return returnVal, nil
   348  }