diff --git a/parser_test.go b/parser_test.go index 0d7e68f..61a3a7c 100644 --- a/parser_test.go +++ b/parser_test.go @@ -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 { diff --git a/toml.go b/toml.go index 8596091..1ba56a1 100644 --- a/toml.go +++ b/toml.go @@ -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. diff --git a/toml_test.go b/toml_test.go index 9d85ccb..7c7f9ef 100644 --- a/toml_test.go +++ b/toml_test.go @@ -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")) } } diff --git a/tomltree_create.go b/tomltree_create.go new file mode 100644 index 0000000..d447755 --- /dev/null +++ b/tomltree_create.go @@ -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 +} diff --git a/tomltree_create_test.go b/tomltree_create_test.go new file mode 100644 index 0000000..8523f9e --- /dev/null +++ b/tomltree_create_test.go @@ -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()) + } +} diff --git a/tomltree_write.go b/tomltree_write.go index a042a1c..b50b549 100644 --- a/tomltree_write.go +++ b/tomltree_write.go @@ -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 } diff --git a/tomltree_write_test.go b/tomltree_write_test.go index 2cf3577..6344b1c 100644 --- a/tomltree_write_test.go +++ b/tomltree_write_test.go @@ -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) {