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 }