From f416e0edf02226a5c91c939048e59f2129d1136e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 19 Dec 2014 05:56:46 -0500 Subject: [PATCH] helper/schema: FieldReader and MapFieldReader --- helper/schema/field_reader.go | 240 +++++++++++++++++++++++++++++ helper/schema/field_reader_test.go | 189 +++++++++++++++++++++++ helper/schema/schema.go | 1 + 3 files changed, 430 insertions(+) create mode 100644 helper/schema/field_reader.go create mode 100644 helper/schema/field_reader_test.go diff --git a/helper/schema/field_reader.go b/helper/schema/field_reader.go new file mode 100644 index 000000000..ba94280bd --- /dev/null +++ b/helper/schema/field_reader.go @@ -0,0 +1,240 @@ +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, *Schema) (interface{}, bool, error) +} + +// MapFieldReader reads fields out of an untyped map[string]string to +// the best of its ability. +type MapFieldReader struct { + Map map[string]string +} + +func (r *MapFieldReader) ReadField( + address []string, schema *Schema) (interface{}, bool, error) { + k := strings.Join(address, ".") + + switch schema.Type { + case TypeBool: + fallthrough + case TypeInt: + fallthrough + case TypeString: + return r.readPrimitive(k, schema) + case TypeList: + return r.readList(k, schema) + case TypeMap: + return r.readMap(k) + case TypeSet: + return r.readSet(k, schema) + case typeObject: + return r.readObject(k, schema.Elem.(map[string]*Schema)) + default: + panic(fmt.Sprintf("Unknown type: %#v", schema.Type)) + } +} + +func (r *MapFieldReader) readObject( + k string, schema map[string]*Schema) (interface{}, bool, error) { + result := make(map[string]interface{}) + for field, schema := range schema { + v, ok, err := r.ReadField([]string{k, field}, schema) + if err != nil { + return nil, false, err + } + if !ok { + continue + } + + result[field] = v + } + + return result, true, nil +} + +func (r *MapFieldReader) readList( + k string, schema *Schema) (interface{}, bool, error) { + // Get the number of elements in the list + countRaw, countOk, err := r.readPrimitive( + k+".#", &Schema{Type: TypeInt}) + if err != nil { + return nil, false, err + } + if !countOk { + // No count, means we have no list + countRaw = 0 + } + + // If we have an empty list, then return an empty list + if countRaw.(int) == 0 { + return []interface{}{}, true, nil + } + + // Get the schema for the elements + var elemSchema *Schema + switch t := schema.Elem.(type) { + case *Resource: + elemSchema = &Schema{ + Type: typeObject, + Elem: t.Schema, + } + case *Schema: + elemSchema = t + } + + // Go through each count, and get the item value out of it + result := make([]interface{}, countRaw.(int)) + for i, _ := range result { + is := strconv.FormatInt(int64(i), 10) + raw, ok, err := r.ReadField([]string{k, is}, elemSchema) + if err != nil { + return nil, false, err + } + if !ok { + // This should never happen, because by the time the data + // gets to the FieldReaders, all the defaults should be set by + // Schema. + raw = nil + } + + result[i] = raw + } + + return result, true, nil +} + +func (r *MapFieldReader) readMap(k string) (interface{}, bool, error) { + result := make(map[string]interface{}) + resultSet := false + + prefix := k + "." + for k, v := range r.Map { + if !strings.HasPrefix(k, prefix) { + continue + } + + result[k[len(prefix):]] = v + resultSet = true + } + + var resultVal interface{} + if resultSet { + resultVal = result + } + + return resultVal, resultSet, nil +} + +func (r *MapFieldReader) readPrimitive( + k string, schema *Schema) (interface{}, bool, error) { + result, ok := r.Map[k] + if !ok { + return nil, false, nil + } + + var returnVal interface{} + switch schema.Type { + case TypeBool: + if result == "" { + returnVal = false + break + } + + v, err := strconv.ParseBool(result) + if err != nil { + return nil, false, err + } + + returnVal = v + case TypeInt: + if result == "" { + returnVal = 0 + break + } + + v, err := strconv.ParseInt(result, 0, 0) + if err != nil { + return nil, false, err + } + + returnVal = int(v) + case TypeString: + returnVal = result + default: + panic(fmt.Sprintf("Unknown type: %#v", schema.Type)) + } + + return returnVal, true, nil +} + +func (r *MapFieldReader) readSet( + k string, schema *Schema) (interface{}, bool, error) { + // Get the number of elements in the list + countRaw, countOk, err := r.readPrimitive( + k+".#", &Schema{Type: TypeInt}) + if err != nil { + return nil, false, err + } + if !countOk { + // No count, means we have no list + countRaw = 0 + } + + // Create the set that will be our result + set := &Set{F: schema.Set} + + // If we have an empty list, then return an empty list + if countRaw.(int) == 0 { + return set, true, nil + } + + // Get the schema for the elements + var elemSchema *Schema + switch t := schema.Elem.(type) { + case *Resource: + elemSchema = &Schema{ + Type: typeObject, + Elem: t.Schema, + } + case *Schema: + elemSchema = t + } + + // Go through the map and find all the set items + prefix := k + "." + for k, _ := range r.Map { + if !strings.HasPrefix(k, prefix) { + continue + } + if strings.HasPrefix(k, prefix+"#") { + // Ignore the count field + continue + } + + // Split the key, since it might be a sub-object like "idx.field" + parts := strings.Split(k[len(prefix):], ".") + idx := parts[0] + + v, ok, err := r.ReadField([]string{prefix + idx}, elemSchema) + if err != nil { + return nil, false, err + } + if !ok { + // This shouldn't happen because we just verified it does exist + panic("missing field in set: " + k + "." + idx) + } + + set.Add(v) + } + + return set, true, nil +} diff --git a/helper/schema/field_reader_test.go b/helper/schema/field_reader_test.go new file mode 100644 index 000000000..642d7e10e --- /dev/null +++ b/helper/schema/field_reader_test.go @@ -0,0 +1,189 @@ +package schema + +import ( + "reflect" + "testing" +) + +func TestMapFieldReader_impl(t *testing.T) { + var _ FieldReader = new(MapFieldReader) +} + +func TestMapFieldReader(t *testing.T) { + r := &MapFieldReader{ + Map: map[string]string{ + "bool": "true", + "int": "42", + "string": "string", + + "list.#": "2", + "list.0": "foo", + "list.1": "bar", + + "listInt.#": "2", + "listInt.0": "21", + "listInt.1": "42", + + "map.foo": "bar", + "map.bar": "baz", + + "set.#": "2", + "set.10": "10", + "set.50": "50", + + "setDeep.#": "2", + "setDeep.10.index": "10", + "setDeep.10.value": "foo", + "setDeep.50.index": "50", + "setDeep.50.value": "bar", + }, + } + + cases := map[string]struct { + Addr []string + Schema *Schema + Out interface{} + OutOk bool + OutErr bool + }{ + "noexist": { + []string{"boolNOPE"}, + &Schema{Type: TypeBool}, + nil, + false, + false, + }, + + "bool": { + []string{"bool"}, + &Schema{Type: TypeBool}, + true, + true, + false, + }, + + "int": { + []string{"int"}, + &Schema{Type: TypeInt}, + 42, + true, + false, + }, + + "string": { + []string{"string"}, + &Schema{Type: TypeString}, + "string", + true, + false, + }, + + "list": { + []string{"list"}, + &Schema{ + Type: TypeList, + Elem: &Schema{Type: TypeString}, + }, + []interface{}{ + "foo", + "bar", + }, + true, + false, + }, + + "listInt": { + []string{"listInt"}, + &Schema{ + Type: TypeList, + Elem: &Schema{Type: TypeInt}, + }, + []interface{}{ + 21, + 42, + }, + true, + false, + }, + + "map": { + []string{"map"}, + &Schema{Type: TypeMap}, + map[string]interface{}{ + "foo": "bar", + "bar": "baz", + }, + true, + false, + }, + + "mapelem": { + []string{"map", "foo"}, + &Schema{Type: TypeString}, + "bar", + true, + false, + }, + + "set": { + []string{"set"}, + &Schema{ + Type: TypeSet, + Elem: &Schema{Type: TypeInt}, + Set: func(a interface{}) int { + return a.(int) + }, + }, + []interface{}{10, 50}, + true, + false, + }, + + "setDeep": { + []string{"setDeep"}, + &Schema{ + Type: TypeSet, + Elem: &Resource{ + Schema: map[string]*Schema{ + "index": &Schema{Type: TypeInt}, + "value": &Schema{Type: TypeString}, + }, + }, + Set: func(a interface{}) int { + return a.(map[string]interface{})["index"].(int) + }, + }, + []interface{}{ + map[string]interface{}{ + "index": 10, + "value": "foo", + }, + map[string]interface{}{ + "index": 50, + "value": "bar", + }, + }, + true, + false, + }, + } + + for name, tc := range cases { + out, outOk, outErr := r.ReadField(tc.Addr, tc.Schema) + if (outErr != nil) != tc.OutErr { + t.Fatalf("%s: err: %s", name, outErr) + } + + if s, ok := out.(*Set); ok { + // If it is a set, convert to a list so its more easily checked. + out = s.List() + } + + if !reflect.DeepEqual(out, tc.Out) { + t.Fatalf("%s: out: %#v", name, out) + } + if outOk != tc.OutOk { + t.Fatalf("%s: outOk: %#v", name, outOk) + } + } +} diff --git a/helper/schema/schema.go b/helper/schema/schema.go index 8c2c20674..e875028bd 100644 --- a/helper/schema/schema.go +++ b/helper/schema/schema.go @@ -33,6 +33,7 @@ const ( TypeList TypeMap TypeSet + typeObject ) // Schema is used to describe the structure of a value.