| package schema |
| |
| import ( |
| "fmt" |
| "strconv" |
| "strings" |
| ) |
| |
| // FieldReaders are responsible for decoding fields out of data into |
| // the proper typed representation. ResourceData uses this to query data |
| // out of multiple sources: config, state, diffs, etc. |
| type FieldReader interface { |
| ReadField([]string) (FieldReadResult, error) |
| } |
| |
| // FieldReadResult encapsulates all the resulting data from reading |
| // a field. |
| type FieldReadResult struct { |
| // Value is the actual read value. NegValue is the _negative_ value |
| // or the items that should be removed (if they existed). NegValue |
| // doesn't make sense for primitives but is important for any |
| // container types such as maps, sets, lists. |
| Value interface{} |
| ValueProcessed interface{} |
| |
| // Exists is true if the field was found in the data. False means |
| // it wasn't found if there was no error. |
| Exists bool |
| |
| // Computed is true if the field was found but the value |
| // is computed. |
| Computed bool |
| } |
| |
| // ValueOrZero returns the value of this result or the zero value of the |
| // schema type, ensuring a consistent non-nil return value. |
| func (r *FieldReadResult) ValueOrZero(s *Schema) interface{} { |
| if r.Value != nil { |
| return r.Value |
| } |
| |
| return s.ZeroValue() |
| } |
| |
| // SchemasForFlatmapPath tries its best to find a sequence of schemas that |
| // the given dot-delimited attribute path traverses through. |
| func SchemasForFlatmapPath(path string, schemaMap map[string]*Schema) []*Schema { |
| parts := strings.Split(path, ".") |
| return addrToSchema(parts, schemaMap) |
| } |
| |
| // addrToSchema finds the final element schema for the given address |
| // and the given schema. It returns all the schemas that led to the final |
| // schema. These are in order of the address (out to in). |
| func addrToSchema(addr []string, schemaMap map[string]*Schema) []*Schema { |
| current := &Schema{ |
| Type: typeObject, |
| Elem: schemaMap, |
| } |
| |
| // If we aren't given an address, then the user is requesting the |
| // full object, so we return the special value which is the full object. |
| if len(addr) == 0 { |
| return []*Schema{current} |
| } |
| |
| result := make([]*Schema, 0, len(addr)) |
| for len(addr) > 0 { |
| k := addr[0] |
| addr = addr[1:] |
| |
| REPEAT: |
| // We want to trim off the first "typeObject" since its not a |
| // real lookup that people do. i.e. []string{"foo"} in a structure |
| // isn't {typeObject, typeString}, its just a {typeString}. |
| if len(result) > 0 || current.Type != typeObject { |
| result = append(result, current) |
| } |
| |
| switch t := current.Type; t { |
| case TypeBool, TypeInt, TypeFloat, TypeString: |
| if len(addr) > 0 { |
| return nil |
| } |
| case TypeList, TypeSet: |
| isIndex := len(addr) > 0 && addr[0] == "#" |
| |
| switch v := current.Elem.(type) { |
| case *Resource: |
| current = &Schema{ |
| Type: typeObject, |
| Elem: v.Schema, |
| } |
| case *Schema: |
| current = v |
| case ValueType: |
| current = &Schema{Type: v} |
| default: |
| // we may not know the Elem type and are just looking for the |
| // index |
| if isIndex { |
| break |
| } |
| |
| if len(addr) == 0 { |
| // we've processed the address, so return what we've |
| // collected |
| return result |
| } |
| |
| if len(addr) == 1 { |
| if _, err := strconv.Atoi(addr[0]); err == nil { |
| // we're indexing a value without a schema. This can |
| // happen if the list is nested in another schema type. |
| // Default to a TypeString like we do with a map |
| current = &Schema{Type: TypeString} |
| break |
| } |
| } |
| |
| return nil |
| } |
| |
| // If we only have one more thing and the next thing |
| // is a #, then we're accessing the index which is always |
| // an int. |
| if isIndex { |
| current = &Schema{Type: TypeInt} |
| break |
| } |
| |
| case TypeMap: |
| if len(addr) > 0 { |
| switch v := current.Elem.(type) { |
| case ValueType: |
| current = &Schema{Type: v} |
| case *Schema: |
| current, _ = current.Elem.(*Schema) |
| default: |
| // maps default to string values. This is all we can have |
| // if this is nested in another list or map. |
| current = &Schema{Type: TypeString} |
| } |
| } |
| case typeObject: |
| // If we're already in the object, then we want to handle Sets |
| // and Lists specially. Basically, their next key is the lookup |
| // key (the set value or the list element). For these scenarios, |
| // we just want to skip it and move to the next element if there |
| // is one. |
| if len(result) > 0 { |
| lastType := result[len(result)-2].Type |
| if lastType == TypeSet || lastType == TypeList { |
| if len(addr) == 0 { |
| break |
| } |
| |
| k = addr[0] |
| addr = addr[1:] |
| } |
| } |
| |
| m := current.Elem.(map[string]*Schema) |
| val, ok := m[k] |
| if !ok { |
| return nil |
| } |
| |
| current = val |
| goto REPEAT |
| } |
| } |
| |
| return result |
| } |
| |
| // readListField is a generic method for reading a list field out of a |
| // a FieldReader. It does this based on the assumption that there is a key |
| // "foo.#" for a list "foo" and that the indexes are "foo.0", "foo.1", etc. |
| // after that point. |
| func readListField( |
| r FieldReader, addr []string, schema *Schema) (FieldReadResult, error) { |
| addrPadded := make([]string, len(addr)+1) |
| copy(addrPadded, addr) |
| addrPadded[len(addrPadded)-1] = "#" |
| |
| // Get the number of elements in the list |
| countResult, err := r.ReadField(addrPadded) |
| if err != nil { |
| return FieldReadResult{}, err |
| } |
| if !countResult.Exists { |
| // No count, means we have no list |
| countResult.Value = 0 |
| } |
| |
| // If we have an empty list, then return an empty list |
| if countResult.Computed || countResult.Value.(int) == 0 { |
| return FieldReadResult{ |
| Value: []interface{}{}, |
| Exists: countResult.Exists, |
| Computed: countResult.Computed, |
| }, nil |
| } |
| |
| // Go through each count, and get the item value out of it |
| result := make([]interface{}, countResult.Value.(int)) |
| for i, _ := range result { |
| is := strconv.FormatInt(int64(i), 10) |
| addrPadded[len(addrPadded)-1] = is |
| rawResult, err := r.ReadField(addrPadded) |
| if err != nil { |
| return FieldReadResult{}, err |
| } |
| if !rawResult.Exists { |
| // This should never happen, because by the time the data |
| // gets to the FieldReaders, all the defaults should be set by |
| // Schema. |
| rawResult.Value = nil |
| } |
| |
| result[i] = rawResult.Value |
| } |
| |
| return FieldReadResult{ |
| Value: result, |
| Exists: true, |
| }, nil |
| } |
| |
| // readObjectField is a generic method for reading objects out of FieldReaders |
| // based on the assumption that building an address of []string{k, FIELD} |
| // will result in the proper field data. |
| func readObjectField( |
| r FieldReader, |
| addr []string, |
| schema map[string]*Schema) (FieldReadResult, error) { |
| result := make(map[string]interface{}) |
| exists := false |
| for field, s := range schema { |
| addrRead := make([]string, len(addr), len(addr)+1) |
| copy(addrRead, addr) |
| addrRead = append(addrRead, field) |
| rawResult, err := r.ReadField(addrRead) |
| if err != nil { |
| return FieldReadResult{}, err |
| } |
| if rawResult.Exists { |
| exists = true |
| } |
| |
| result[field] = rawResult.ValueOrZero(s) |
| } |
| |
| return FieldReadResult{ |
| Value: result, |
| Exists: exists, |
| }, nil |
| } |
| |
| // convert map values to the proper primitive type based on schema.Elem |
| func mapValuesToPrimitive(k string, m map[string]interface{}, schema *Schema) error { |
| elemType, err := getValueType(k, schema) |
| if err != nil { |
| return err |
| } |
| |
| switch elemType { |
| case TypeInt, TypeFloat, TypeBool: |
| for k, v := range m { |
| vs, ok := v.(string) |
| if !ok { |
| continue |
| } |
| |
| v, err := stringToPrimitive(vs, false, &Schema{Type: elemType}) |
| if err != nil { |
| return err |
| } |
| |
| m[k] = v |
| } |
| } |
| return nil |
| } |
| |
| func stringToPrimitive( |
| value string, computed bool, schema *Schema) (interface{}, error) { |
| var returnVal interface{} |
| switch schema.Type { |
| case TypeBool: |
| if value == "" { |
| returnVal = false |
| break |
| } |
| if computed { |
| break |
| } |
| |
| v, err := strconv.ParseBool(value) |
| if err != nil { |
| return nil, err |
| } |
| |
| returnVal = v |
| case TypeFloat: |
| if value == "" { |
| returnVal = 0.0 |
| break |
| } |
| if computed { |
| break |
| } |
| |
| v, err := strconv.ParseFloat(value, 64) |
| if err != nil { |
| return nil, err |
| } |
| |
| returnVal = v |
| case TypeInt: |
| if value == "" { |
| returnVal = 0 |
| break |
| } |
| if computed { |
| break |
| } |
| |
| v, err := strconv.ParseInt(value, 0, 0) |
| if err != nil { |
| return nil, err |
| } |
| |
| returnVal = int(v) |
| case TypeString: |
| returnVal = value |
| default: |
| panic(fmt.Sprintf("Unknown type: %s", schema.Type)) |
| } |
| |
| return returnVal, nil |
| } |