Rework tree from map (#139)

* Make TreeFromMap reflect to construct tree
* Fix wording of invalid value type in writeTo

Fixes #138, #139, #134 

⚠️ TreeFromMap signature changed to `TreeFromMap(map[string]interface{}) (*TomlTree, error)`
This commit is contained in:
Thomas Pelletier
2017-03-14 13:16:40 -07:00
committed by GitHub
parent 3b00596b2e
commit fee7787d3f
7 changed files with 240 additions and 13 deletions
+4 -3
View File
@@ -662,10 +662,11 @@ func TestTomlValueStringRepresentation(t *testing.T) {
}
func TestToStringMapStringString(t *testing.T) {
in := map[string]interface{}{"m": TreeFromMap(map[string]interface{}{
"v": &tomlValue{"abc", Position{0, 0}}})}
tree, err := TreeFromMap(map[string]interface{}{"m": map[string]interface{}{"v": "abc"}})
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
want := "\n[m]\n v = \"abc\"\n"
tree := TreeFromMap(in)
got := tree.String()
if got != want {
+5 -3
View File
@@ -28,10 +28,12 @@ func newTomlTree() *TomlTree {
}
// TreeFromMap initializes a new TomlTree object using the given map.
func TreeFromMap(m map[string]interface{}) *TomlTree {
return &TomlTree{
values: m,
func TreeFromMap(m map[string]interface{}) (*TomlTree, error) {
result, err := toTree(m)
if err != nil {
return nil, err
}
return result.(*TomlTree), nil
}
// Has returns a boolean indicating if the given key exists.
+5 -2
View File
@@ -121,8 +121,11 @@ func TestTomlQuery(t *testing.T) {
func TestTomlFromMap(t *testing.T) {
simpleMap := map[string]interface{}{"hello": 42}
tree := TreeFromMap(simpleMap)
if tree.Get("hello") != 42 {
tree, err := TreeFromMap(simpleMap)
if err != nil {
t.Fatal("unexpected error:", err)
}
if tree.Get("hello") != int64(42) {
t.Fatal("hello should be 42, not", tree.Get("hello"))
}
}
+129
View File
@@ -0,0 +1,129 @@
package toml
import (
"fmt"
"reflect"
"time"
)
// supported values:
// string, bool, int64, uint64, float64, time.Time, int, int8, int16, int32, uint, uint8, uint16, uint32, float32
var kindToTypeMapping = map[reflect.Kind]reflect.Type{
reflect.Bool: reflect.TypeOf(true),
reflect.String: reflect.TypeOf(""),
reflect.Float32: reflect.TypeOf(float64(1)),
reflect.Float64: reflect.TypeOf(float64(1)),
reflect.Int: reflect.TypeOf(int64(1)),
reflect.Int8: reflect.TypeOf(int64(1)),
reflect.Int16: reflect.TypeOf(int64(1)),
reflect.Int32: reflect.TypeOf(int64(1)),
reflect.Int64: reflect.TypeOf(int64(1)),
reflect.Uint: reflect.TypeOf(uint64(1)),
reflect.Uint8: reflect.TypeOf(uint64(1)),
reflect.Uint16: reflect.TypeOf(uint64(1)),
reflect.Uint32: reflect.TypeOf(uint64(1)),
reflect.Uint64: reflect.TypeOf(uint64(1)),
}
func simpleValueCoercion(object interface{}) (interface{}, error) {
switch original := object.(type) {
case string, bool, int64, uint64, float64, time.Time:
return original, nil
case int:
return int64(original), nil
case int8:
return int64(original), nil
case int16:
return int64(original), nil
case int32:
return int64(original), nil
case uint:
return uint64(original), nil
case uint8:
return uint64(original), nil
case uint16:
return uint64(original), nil
case uint32:
return uint64(original), nil
case float32:
return float64(original), nil
default:
return nil, fmt.Errorf("cannot convert type %T to TomlTree", object)
}
}
func sliceToTree(object interface{}) (interface{}, error) {
// arrays are a bit tricky, since they can represent either a
// collection of simple values, which is represented by one
// *tomlValue, or an array of tables, which is represented by an
// array of *TomlTree.
// holding the assumption that this function is called from toTree only when value.Kind() is Array or Slice
value := reflect.ValueOf(object)
insideType := value.Type().Elem()
length := value.Len()
if insideType.Kind() == reflect.Map {
// this is considered as an array of tables
tablesArray := make([]*TomlTree, 0, length)
for i := 0; i < length; i++ {
table := value.Index(i)
tree, err := toTree(table.Interface())
if err != nil {
return nil, err
}
tablesArray = append(tablesArray, tree.(*TomlTree))
}
return tablesArray, nil
}
sliceType := kindToTypeMapping[insideType.Kind()]
if sliceType == nil {
sliceType = insideType
}
arrayValue := reflect.MakeSlice(reflect.SliceOf(sliceType), 0, length)
for i := 0; i < length; i++ {
val := value.Index(i).Interface()
simpleValue, err := simpleValueCoercion(val)
if err != nil {
return nil, err
}
arrayValue = reflect.Append(arrayValue, reflect.ValueOf(simpleValue))
}
return &tomlValue{arrayValue.Interface(), Position{}}, nil
}
func toTree(object interface{}) (interface{}, error) {
value := reflect.ValueOf(object)
if value.Kind() == reflect.Map {
values := map[string]interface{}{}
keys := value.MapKeys()
for _, key := range keys {
k, ok := key.Interface().(string)
if !ok {
return nil, fmt.Errorf("map key needs to be a string, not %T", key.Interface())
}
v := value.MapIndex(key)
newValue, err := toTree(v.Interface())
if err != nil {
return nil, err
}
values[k] = newValue
}
return &TomlTree{values, Position{}}, nil
}
if value.Kind() == reflect.Array || value.Kind() == reflect.Slice {
return sliceToTree(object)
}
simpleValue, err := simpleValueCoercion(object)
if err != nil {
return nil, err
}
return &tomlValue{simpleValue, Position{}}, nil
}
+95
View File
@@ -0,0 +1,95 @@
package toml
import (
"testing"
"time"
)
func validate(t *testing.T, path string, object interface{}) {
switch o := object.(type) {
case *TomlTree:
for key, tree := range o.values {
validate(t, path+"."+key, tree)
}
case []*TomlTree:
for index, tree := range o {
validate(t, path+"."+string(index), tree)
}
case *tomlValue:
switch o.value.(type) {
case int64, uint64, bool, string, float64, time.Time,
[]int64, []uint64, []bool, []string, []float64, []time.Time:
return // ok
default:
t.Fatalf("tomlValue at key %s containing incorrect type %T", path, o.value)
}
default:
t.Fatalf("value at key %s is of incorrect type %T", path, object)
}
t.Log("validation ok", path)
}
func validateTree(t *testing.T, tree *TomlTree) {
validate(t, "", tree)
}
func TestTomlTreeCreateToTree(t *testing.T) {
data := map[string]interface{}{
"a_string": "bar",
"an_int": 42,
"time": time.Now(),
"int8": int8(2),
"int16": int16(2),
"int32": int32(2),
"uint8": uint8(2),
"uint16": uint16(2),
"uint32": uint32(2),
"float32": float32(2),
"a_bool": false,
"nested": map[string]interface{}{
"foo": "bar",
},
"array": []string{"a", "b", "c"},
"array_uint": []uint{uint(1), uint(2)},
"array_table": []map[string]interface{}{map[string]interface{}{"sub_map": 52}},
"array_times": []time.Time{time.Now(), time.Now()},
"map_times": map[string]time.Time{"now": time.Now()},
}
tree, err := TreeFromMap(data)
if err != nil {
t.Fatal("unexpected error:", err)
}
validateTree(t, tree)
}
func TestTomlTreeCreateToTreeInvalidLeafType(t *testing.T) {
_, err := TreeFromMap(map[string]interface{}{"foo": t})
expected := "cannot convert type *testing.T to TomlTree"
if err.Error() != expected {
t.Fatalf("expected error %s, got %s", expected, err.Error())
}
}
func TestTomlTreeCreateToTreeInvalidMapKeyType(t *testing.T) {
_, err := TreeFromMap(map[string]interface{}{"foo": map[int]interface{}{2: 1}})
expected := "map key needs to be a string, not int"
if err.Error() != expected {
t.Fatalf("expected error %s, got %s", expected, err.Error())
}
}
func TestTomlTreeCreateToTreeInvalidArrayMemberType(t *testing.T) {
_, err := TreeFromMap(map[string]interface{}{"foo": []*testing.T{t}})
expected := "cannot convert type *testing.T to TomlTree"
if err.Error() != expected {
t.Fatalf("expected error %s, got %s", expected, err.Error())
}
}
func TestTomlTreeCreateToTreeInvalidTableGroupType(t *testing.T) {
_, err := TreeFromMap(map[string]interface{}{"foo": []map[string]interface{}{map[string]interface{}{"hello": t}}})
expected := "cannot convert type *testing.T to TomlTree"
if err.Error() != expected {
t.Fatalf("expected error %s, got %s", expected, err.Error())
}
}
+1 -4
View File
@@ -95,7 +95,7 @@ func (t *TomlTree) writeTo(w io.Writer, indent, keyspace string, bytesCount int6
for _, k := range simpleValuesKeys {
v, ok := t.values[k].(*tomlValue)
if !ok {
return bytesCount, fmt.Errorf("invalid key type at %s: %T", k, t.values[k])
return bytesCount, fmt.Errorf("invalid value type at %s: %T", k, t.values[k])
}
repr, err := tomlValueStringRepresentation(v.value)
@@ -201,9 +201,6 @@ func (t *TomlTree) ToMap() map[string]interface{} {
result[k] = array
case *TomlTree:
result[k] = node.ToMap()
case map[string]interface{}:
sub := TreeFromMap(node)
result[k] = sub.ToMap()
case *tomlValue:
result[k] = node.value
}
+1 -1
View File
@@ -146,7 +146,7 @@ func TestTomlTreeWriteToMapSimple(t *testing.T) {
func TestTomlTreeWriteToInvalidTreeSimpleValue(t *testing.T) {
tree := TomlTree{values: map[string]interface{}{"foo": int8(1)}}
_, err := tree.ToTomlString()
assertErrorString(t, "invalid key type at foo: int8", err)
assertErrorString(t, "invalid value type at foo: int8", err)
}
func TestTomlTreeWriteToInvalidTreeTomlValue(t *testing.T) {