Group array support; ToString() support
diff --git a/lexer.go b/lexer.go index 67f9ebc..41f0d02 100644 --- a/lexer.go +++ b/lexer.go
@@ -34,8 +34,11 @@ tokenFloat tokenLeftBracket tokenRightBracket + tokenDoubleLeftBracket + tokenDoubleRightBracket tokenDate tokenKeyGroup + tokenKeyGroupArray tokenComma tokenEOL ) @@ -386,9 +389,43 @@ func lexKeyGroup(l *lexer) stateFn { l.ignore() - l.pos += 1 - l.emit(tokenLeftBracket) - return lexInsideKeyGroup + l.pos += 1 + + if l.peek() == '[' { + // token '[[' signifies an array of anonymous key groups + l.ignore() + l.pos += 1 + l.emit(tokenDoubleLeftBracket) + return lexInsideKeyGroupArray + } else { + // vanilla key group + l.emit(tokenLeftBracket) + return lexInsideKeyGroup + } +} + +func lexInsideKeyGroupArray(l *lexer) stateFn { + for { + if l.peek() == ']' { + if l.pos > l.start { + l.emit(tokenKeyGroupArray) + } + l.ignore() + l.pos += 1 + if l.peek() != ']' { + break // error + } + l.ignore() + l.pos += 1 + l.emit(tokenDoubleRightBracket) + return lexVoid + } + + if l.next() == eof { + break + } + } + return l.errorf("unclosed key group array") } func lexInsideKeyGroup(l *lexer) stateFn {
diff --git a/parser.go b/parser.go index 672bb35..e12fb8e 100644 --- a/parser.go +++ b/parser.go
@@ -71,6 +71,8 @@ } switch tok.typ { + case tokenDoubleLeftBracket: + return parseGroupArray case tokenLeftBracket: return parseGroup case tokenKey: @@ -83,6 +85,38 @@ return nil } +func parseGroupArray(p *parser) parserStateFn { + p.getToken() // discard the [[ + key := p.getToken() + if key.typ != tokenKeyGroupArray { + panic(fmt.Sprintf("unexpected token %s, was expecting a key group array", key)) + } + + // get or create group array element at the indicated part in the path + p.currentGroup = strings.Split(key.val, ".") + dest_tree := p.tree.GetPath(p.currentGroup) + var array []*TomlTree + if dest_tree == nil { + array = make([]*TomlTree, 0) + } else if dest_tree.([]*TomlTree) != nil { + array = dest_tree.([]*TomlTree) + } else { + panic(fmt.Sprintf("key %s is already assigned and not of type group array", key)) + } + + // add a new tree to the end of the group array + new_tree := make(TomlTree) + array = append(array, &new_tree) + p.tree.SetPath(p.currentGroup, array) + + // keep this key name from use by other kinds of assignments + p.seenGroupKeys = append(p.seenGroupKeys, key.val) + + // move to next parser state + p.assume(tokenDoubleRightBracket) + return parseStart(p) +} + func parseGroup(p *parser) parserStateFn { p.getToken() // discard the [ key := p.getToken() @@ -105,17 +139,31 @@ key := p.getToken() p.assume(tokenEqual) value := parseRvalue(p) - var final_key []string + var group_key []string if len(p.currentGroup) > 0 { - final_key = p.currentGroup + group_key = p.currentGroup } else { - final_key = make([]string, 0) + group_key = make([]string, 0) } - final_key = append(final_key, key.val) - if p.tree.GetPath(final_key) != nil { - panic(fmt.Sprintf("the following key was defined twice: %s", strings.Join(final_key, "."))) - } - p.tree.SetPath(final_key, value) + + // find the group to assign, looking out for arrays of groups + var target_node *TomlTree + switch node := p.tree.GetPath(group_key).(type) { + case []*TomlTree: + target_node = node[len(node)-1] + case *TomlTree: + target_node = node + default: + panic(fmt.Sprintf("Unknown group type for path %v", group_key)) + } + + // assign value to the found group + local_key := []string{ key.val } + final_key := append(group_key, key.val) + if target_node.GetPath(local_key) != nil { + panic(fmt.Sprintf("the following key was defined twice: %s", strings.Join(final_key, "."))) + } + target_node.SetPath(local_key, value) return parseStart(p) }
diff --git a/parser_test.go b/parser_test.go index 238c3c5..72d09e3 100644 --- a/parser_test.go +++ b/parser_test.go
@@ -13,13 +13,16 @@ } for k, v := range ref { node := tree.Get(k) - switch node.(type) { + switch cast_node := node.(type) { + case []*TomlTree: + for idx, item := range cast_node { + assertTree(t, item, err, v.([]map[string]interface{})[idx]) + } case *TomlTree: - assertTree(t, node.(*TomlTree), err, v.(map[string]interface{})) + assertTree(t, cast_node, err, v.(map[string]interface{})) default: if fmt.Sprintf("%v", node) != fmt.Sprintf("%v", v) { - t.Log("was expecting", v, "at", k, "but got", node) - t.Error() + t.Errorf("was expecting %v at %v but got %v", v, k, node) } } } @@ -284,3 +287,54 @@ "clients.data": []interface{}{[]string{"gamma", "delta"}, []int64{1, 2}}, }) } + +func TestParseKeyGroupArray(t *testing.T) { + tree, err := Load("[[foo.bar]] a = 42\n[[foo.bar]] a = 69") + assertTree(t, tree, err, map[string]interface{}{ + "foo": map[string]interface{} { + "bar": []map[string]interface{} { + { "a": int64(42), }, + { "a": int64(69), }, + }, + }, + }) +} + +func TestToTomlValue(t *testing.T) { + for idx, item := range []struct{ + Value interface{} + Expect string + }{ + { int64(12345), "12345", }, + { float64(123.45), "123.45", }, + { bool(true), "true", }, + { "hello world", "\"hello world\"", }, + { "\b\t\n\f\r\"\\", "\"\\b\\t\\n\\f\\r\\\"\\\\\"", }, + { "\x05", "\"\\u0005\"", }, + { time.Date(1979, time.May, 27, 7, 32, 0, 0, time.UTC), + "1979-05-27T07:32:00Z", }, + { []interface{}{"gamma", "delta"}, + "[\n \"gamma\",\n \"delta\",\n]", }, + }{ + result := toTomlValue(item.Value, 0) + if result != item.Expect { + t.Errorf("Test %d - got '%s', expected '%s'", idx, result, item.Expect) + } + } +} + +func TestToString(t *testing.T) { + tree := &TomlTree{ + "foo": &TomlTree{ + "bar": []*TomlTree { + { "a": int64(42), }, + { "a": int64(69), }, + }, + }, + } + result := tree.ToString() + expected := "\n[foo]\n\n[[foo.bar]]\na = 42\n\n[[foo.bar]]\na = 69\n" + if result != expected { + t.Errorf("Expected got '%s', expected '%s'", result, expected) + } +}
diff --git a/toml.go b/toml.go index dbf8ae8..ac6ae10 100644 --- a/toml.go +++ b/toml.go
@@ -1,7 +1,7 @@ // TOML markup language parser. // // This version supports the specification as described in -// https://github.com/mojombo/toml/tree/e3656ad493400895f4460f1244a25f8f8e31a32a +// https://github.com/toml-lang/toml/blob/master/versions/toml-v0.2.0.md package toml import ( @@ -9,6 +9,9 @@ "io/ioutil" "runtime" "strings" + "strconv" + "time" + "fmt" ) // Definition of a TomlTree. @@ -41,11 +44,16 @@ // Get the value at key in the TomlTree. // Key is a dot-separated path (e.g. a.b.c). // Returns nil if the path does not exist in the tree. +// If keys is of length zero, the current tree is returned. func (t *TomlTree) Get(key string) interface{} { + if key == "" { return t } return t.GetPath(strings.Split(key, ".")) } +// Returns the element in the tree indicated by 'keys'. +// If keys is of length zero, the current tree is returned. func (t *TomlTree) GetPath(keys []string) interface{} { + if len(keys) == 0 { return t } subtree := t for _, intermediate_key := range keys[:len(keys)-1] { _, exists := (*subtree)[intermediate_key] @@ -106,6 +114,93 @@ } } +// encodes a string to a TOML-compliant string value +func encodeTomlString(value string) string { + result := "" + for _, rr := range value { + int_rr := 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 int_rr < 0x001F { + result += fmt.Sprintf("\\u%0.4X", int_rr) + } 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" } else { 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(keyspace string) string { + result := "" + for k, v := range (map[string]interface{})(*t) { + // figure out the keyspace + combined_key := k + if keyspace != "" { + combined_key = keyspace + "." + combined_key + } + // output based on type + switch node := v.(type) { + case []*TomlTree: + for _, item := range node { + if len(item.Keys()) > 0 { + result += fmt.Sprintf("\n[[%s]]\n", combined_key) + } + result += item.toToml(combined_key) + } + case *TomlTree: + if len(node.Keys()) > 0 { + result += fmt.Sprintf("\n[%s]\n", combined_key) + } + result += node.toToml(combined_key) + default: + result += fmt.Sprintf("%s = %s\n", k, toTomlValue(node,0)) + } + } + return result +} + +// 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("") +} + // Create a TomlTree from a string. func Load(content string) (tree *TomlTree, err error) { defer func() {
diff --git a/toml_test.go b/toml_test.go index f9fa173..2626902 100644 --- a/toml_test.go +++ b/toml_test.go
@@ -1 +1,25 @@ package toml + +import ( + "testing" +) + +func TestTomlGetPath(t *testing.T) { + node := make(TomlTree) + //TODO: set other node data + + for idx, item := range []struct { + Path []string + Expected interface{} + } { + { // empty path test + []string{}, + &node, + }, + } { + result := node.GetPath(item.Path) + if result != item.Expected { + t.Errorf("GetPath[%d] %v - expected %v, got %v instead.", idx, item.Path, item.Expected, result) + } + } +}