TomlTree.ToMap (#59)

* Extract TomlTree conversion to its own file

* Implement ToMap

* Reorder imports in tomltree_conversions
diff --git a/toml.go b/toml.go
index f3de359..3975d01 100644
--- a/toml.go
+++ b/toml.go
@@ -6,9 +6,7 @@
 	"io"
 	"os"
 	"runtime"
-	"strconv"
 	"strings"
-	"time"
 )
 
 type tomlValue struct {
@@ -248,106 +246,6 @@
 	return nil
 }
 
-// encodes a string to a TOML-compliant string value
-func encodeTomlString(value string) string {
-	result := ""
-	for _, rr := range value {
-		intRr := uint16(rr)
-		switch rr {
-		case '\b':
-			result += "\\b"
-		case '\t':
-			result += "\\t"
-		case '\n':
-			result += "\\n"
-		case '\f':
-			result += "\\f"
-		case '\r':
-			result += "\\r"
-		case '"':
-			result += "\\\""
-		case '\\':
-			result += "\\\\"
-		default:
-			if intRr < 0x001F {
-				result += fmt.Sprintf("\\u%0.4X", intRr)
-			} else {
-				result += string(rr)
-			}
-		}
-	}
-	return result
-}
-
-// Value print support function for ToString()
-// Outputs the TOML compliant string representation of a value
-func toTomlValue(item interface{}, indent int) string {
-	tab := strings.Repeat(" ", indent)
-	switch value := item.(type) {
-	case int64:
-		return tab + strconv.FormatInt(value, 10)
-	case float64:
-		return tab + strconv.FormatFloat(value, 'f', -1, 64)
-	case string:
-		return tab + "\"" + encodeTomlString(value) + "\""
-	case bool:
-		if value {
-			return "true"
-		}
-		return "false"
-	case time.Time:
-		return tab + value.Format(time.RFC3339)
-	case []interface{}:
-		result := tab + "[\n"
-		for _, item := range value {
-			result += toTomlValue(item, indent+2) + ",\n"
-		}
-		return result + tab + "]"
-	default:
-		panic(fmt.Sprintf("unsupported value type: %v", value))
-	}
-}
-
-// Recursive support function for ToString()
-// Outputs a tree, using the provided keyspace to prefix group names
-func (t *TomlTree) toToml(indent, keyspace string) string {
-	result := ""
-	for k, v := range t.values {
-		// figure out the keyspace
-		combinedKey := k
-		if keyspace != "" {
-			combinedKey = keyspace + "." + combinedKey
-		}
-		// output based on type
-		switch node := v.(type) {
-		case []*TomlTree:
-			for _, item := range node {
-				if len(item.Keys()) > 0 {
-					result += fmt.Sprintf("\n%s[[%s]]\n", indent, combinedKey)
-				}
-				result += item.toToml(indent+"  ", combinedKey)
-			}
-		case *TomlTree:
-			if len(node.Keys()) > 0 {
-				result += fmt.Sprintf("\n%s[%s]\n", indent, combinedKey)
-			}
-			result += node.toToml(indent+"  ", combinedKey)
-		case map[string]interface{}:
-			sub := TreeFromMap(node)
-
-			if len(sub.Keys()) > 0 {
-				result += fmt.Sprintf("\n%s[%s]\n", indent, combinedKey)
-			}
-			result += sub.toToml(indent+"  ", combinedKey)
-		case *tomlValue:
-			result += fmt.Sprintf("%s%s = %s\n", indent, k, toTomlValue(node.value, 0))
-		default:
-			result += fmt.Sprintf("%s%s = %s\n", indent, k, toTomlValue(v, 0))
-		}
-	}
-	return result
-}
-
 // Query compiles and executes a query on a tree and returns the query result.
 func (t *TomlTree) Query(query string) (*QueryResult, error) {
 	q, err := CompileQuery(query)
@@ -357,12 +255,6 @@
 	return q.Execute(t), nil
 }
 
-// ToString generates a human-readable representation of the current tree.
-// Output spans multiple lines, and is suitable for ingest by a TOML parser
-func (t *TomlTree) ToString() string {
-	return t.toToml("", "")
-}
-
 // LoadReader creates a TomlTree from any io.Reader.
 func LoadReader(reader io.Reader) (tree *TomlTree, err error) {
 	defer func() {
diff --git a/tomltree_conversions.go b/tomltree_conversions.go
new file mode 100644
index 0000000..caa2b4a
--- /dev/null
+++ b/tomltree_conversions.go
@@ -0,0 +1,139 @@
+// Tools to convert a TomlTree to different representations
+package toml
+
+import (
+	"fmt"
+	"strconv"
+	"strings"
+	"time"
+)
+
+// encodes a string to a TOML-compliant string value
+func encodeTomlString(value string) string {
+	result := ""
+	for _, rr := range value {
+		intRr := uint16(rr)
+		switch rr {
+		case '\b':
+			result += "\\b"
+		case '\t':
+			result += "\\t"
+		case '\n':
+			result += "\\n"
+		case '\f':
+			result += "\\f"
+		case '\r':
+			result += "\\r"
+		case '"':
+			result += "\\\""
+		case '\\':
+			result += "\\\\"
+		default:
+			if intRr < 0x001F {
+				result += fmt.Sprintf("\\u%0.4X", intRr)
+			} else {
+				result += string(rr)
+			}
+		}
+	}
+	return result
+}
+
+// Value print support function for ToString()
+// Outputs the TOML compliant string representation of a value
+func toTomlValue(item interface{}, indent int) string {
+	tab := strings.Repeat(" ", indent)
+	switch value := item.(type) {
+	case int64:
+		return tab + strconv.FormatInt(value, 10)
+	case float64:
+		return tab + strconv.FormatFloat(value, 'f', -1, 64)
+	case string:
+		return tab + "\"" + encodeTomlString(value) + "\""
+	case bool:
+		if value {
+			return "true"
+		}
+		return "false"
+	case time.Time:
+		return tab + value.Format(time.RFC3339)
+	case []interface{}:
+		result := tab + "[\n"
+		for _, item := range value {
+			result += toTomlValue(item, indent+2) + ",\n"
+		}
+		return result + tab + "]"
+	default:
+		panic(fmt.Sprintf("unsupported value type: %v", value))
+	}
+}
+
+// Recursive support function for ToString()
+// Outputs a tree, using the provided keyspace to prefix group names
+func (t *TomlTree) toToml(indent, keyspace string) string {
+	result := ""
+	for k, v := range t.values {
+		// figure out the keyspace
+		combinedKey := k
+		if keyspace != "" {
+			combinedKey = keyspace + "." + combinedKey
+		}
+		// output based on type
+		switch node := v.(type) {
+		case []*TomlTree:
+			for _, item := range node {
+				if len(item.Keys()) > 0 {
+					result += fmt.Sprintf("\n%s[[%s]]\n", indent, combinedKey)
+				}
+				result += item.toToml(indent+"  ", combinedKey)
+			}
+		case *TomlTree:
+			if len(node.Keys()) > 0 {
+				result += fmt.Sprintf("\n%s[%s]\n", indent, combinedKey)
+			}
+			result += node.toToml(indent+"  ", combinedKey)
+		case map[string]interface{}:
+			sub := TreeFromMap(node)
+
+			if len(sub.Keys()) > 0 {
+				result += fmt.Sprintf("\n%s[%s]\n", indent, combinedKey)
+			}
+			result += sub.toToml(indent+"  ", combinedKey)
+		case *tomlValue:
+			result += fmt.Sprintf("%s%s = %s\n", indent, k, toTomlValue(node.value, 0))
+		default:
+			result += fmt.Sprintf("%s%s = %s\n", indent, k, toTomlValue(v, 0))
+		}
+	}
+	return result
+}
+
+// ToString generates a human-readable representation of the current tree.
+// Output spans multiple lines, and is suitable for ingest by a TOML parser
+func (t *TomlTree) ToString() string {
+	return t.toToml("", "")
+}
+
+// ToMap recursively generates a representation of the current tree using map[string]interface{}.
+func (t *TomlTree) ToMap() map[string]interface{} {
+	result := map[string]interface{}{}
+
+	for k, v := range t.values {
+		switch node := v.(type) {
+		case []*TomlTree:
+			result[k] = make([]interface{}, 0)
+			for _, item := range node {
+				result[k] = item.ToMap()
+			}
+		case *TomlTree:
+			result[k] = node.ToMap()
+		case map[string]interface{}:
+			sub := TreeFromMap(node)
+			result[k] = sub.ToMap()
+		case *tomlValue:
+			result[k] = node.value
+		}
+	}
+
+	return result
+}
diff --git a/tomltree_conversions_test.go b/tomltree_conversions_test.go
new file mode 100644
index 0000000..a420c12
--- /dev/null
+++ b/tomltree_conversions_test.go
@@ -0,0 +1,60 @@
+package toml
+
+import (
+	"reflect"
+	"testing"
+	"time"
+)
+
+func testMaps(t *testing.T, actual, expected map[string]interface{}) {
+	if !reflect.DeepEqual(actual, expected) {
+		t.Fatal("trees aren't equal.\n", "Expected:\n", expected, "\nActual:\n", actual)
+	}
+}
+
+func TestTomlTreeConversionToMapSimple(t *testing.T) {
+	tree, _ := Load("a = 42\nb = 17")
+
+	expected := map[string]interface{}{
+		"a": int64(42),
+		"b": int64(17),
+	}
+
+	testMaps(t, tree.ToMap(), expected)
+}
+
+func TestTomlTreeConversionToMapExampleFile(t *testing.T) {
+	tree, _ := LoadFile("example.toml")
+	expected := map[string]interface{}{
+		"title": "TOML Example",
+		"owner": map[string]interface{}{
+			"name":         "Tom Preston-Werner",
+			"organization": "GitHub",
+			"bio":          "GitHub Cofounder & CEO\nLikes tater tots and beer.",
+			"dob":          time.Date(1979, time.May, 27, 7, 32, 0, 0, time.UTC),
+		},
+		"database": map[string]interface{}{
+			"server":         "192.168.1.1",
+			"ports":          []interface{}{int64(8001), int64(8001), int64(8002)},
+			"connection_max": int64(5000),
+			"enabled":        true,
+		},
+		"servers": map[string]interface{}{
+			"alpha": map[string]interface{}{
+				"ip": "10.0.0.1",
+				"dc": "eqdc10",
+			},
+			"beta": map[string]interface{}{
+				"ip": "10.0.0.2",
+				"dc": "eqdc10",
+			},
+		},
+		"clients": map[string]interface{}{
+			"data": []interface{}{
+				[]interface{}{"gamma", "delta"},
+				[]interface{}{int64(1), int64(2)},
+			},
+		},
+	}
+	testMaps(t, tree.ToMap(), expected)
+}