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 }