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