github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/legacy/helper/schema/field_reader.go (about)

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