Support dotted-keys (#260)

Implement dotted keys as sequence of bare and quoted keys. Introduced in
TOML 0.5.0.
Fixes #230
This commit is contained in:
Thomas Pelletier
2019-03-04 22:35:03 -08:00
committed by GitHub
parent d9a27b8052
commit e1803f96f6
6 changed files with 155 additions and 68 deletions
+82 -54
View File
@@ -3,79 +3,107 @@
package toml package toml
import ( import (
"bytes"
"errors" "errors"
"fmt" "fmt"
"unicode" "unicode"
) )
// Convert the bare key group string to an array. // Convert the bare key group string to an array.
// The input supports double quotation to allow "." inside the key name, // The input supports double quotation and single quotation,
// but escape sequences are not supported. Lexers must unescape them beforehand. // but escape sequences are not supported. Lexers must unescape them beforehand.
func parseKey(key string) ([]string, error) { func parseKey(key string) ([]string, error) {
groups := []string{} runes := []rune(key)
var buffer bytes.Buffer var groups []string
inQuotes := false
wasInQuotes := false
ignoreSpace := true
expectDot := false
for _, char := range key { if len(key) == 0 {
if ignoreSpace { return nil, errors.New("empty key")
if char == ' ' { }
continue
} idx := 0
ignoreSpace = false for idx < len(runes) {
for ; idx < len(runes) && isSpace(runes[idx]); idx++ {
// skip leading whitespace
} }
switch char { if idx >= len(runes) {
case '"': break
if inQuotes { }
groups = append(groups, buffer.String()) r := runes[idx]
buffer.Reset() if isValidBareChar(r) {
wasInQuotes = true // parse bare key
} startIdx := idx
inQuotes = !inQuotes endIdx := -1
expectDot = false idx++
case '.': for idx < len(runes) {
if inQuotes { r = runes[idx]
buffer.WriteRune(char) if isValidBareChar(r) {
} else { idx++
if !wasInQuotes { } else if r == '.' {
if buffer.Len() == 0 { endIdx = idx
return nil, errors.New("empty table key") break
} else if isSpace(r) {
endIdx = idx
for ; idx < len(runes) && isSpace(runes[idx]); idx++ {
// skip trailing whitespace
} }
groups = append(groups, buffer.String()) if idx < len(runes) && runes[idx] != '.' {
buffer.Reset() return nil, fmt.Errorf("invalid key character after whitespace: %c", runes[idx])
}
break
} else {
return nil, fmt.Errorf("invalid bare key character: %c", r)
} }
ignoreSpace = true
expectDot = false
wasInQuotes = false
} }
case ' ': if endIdx == -1 {
if inQuotes { endIdx = idx
buffer.WriteRune(char)
} else {
expectDot = true
} }
default: groups = append(groups, string(runes[startIdx:endIdx]))
if !inQuotes && !isValidBareChar(char) { } else if r == '\'' {
return nil, fmt.Errorf("invalid bare character: %c", char) // parse single quoted key
idx++
startIdx := idx
for {
if idx >= len(runes) {
return nil, fmt.Errorf("unclosed single-quoted key")
}
r = runes[idx]
if r == '\'' {
groups = append(groups, string(runes[startIdx:idx]))
idx++
break
}
idx++
} }
if !inQuotes && expectDot { } else if r == '"' {
return nil, errors.New("what?") // parse double quoted key
idx++
startIdx := idx
for {
if idx >= len(runes) {
return nil, fmt.Errorf("unclosed double-quoted key")
}
r = runes[idx]
if r == '"' {
groups = append(groups, string(runes[startIdx:idx]))
idx++
break
}
idx++
} }
buffer.WriteRune(char) } else if r == '.' {
expectDot = false idx++
if idx >= len(runes) {
return nil, fmt.Errorf("unexpected end of key")
}
r = runes[idx]
if !isValidBareChar(r) && r != '\'' && r != '"' && r != ' ' {
return nil, fmt.Errorf("expecting key part after dot")
}
} else {
return nil, fmt.Errorf("invalid key character: %c", r)
} }
} }
if inQuotes {
return nil, errors.New("mismatched quotes")
}
if buffer.Len() > 0 {
groups = append(groups, buffer.String())
}
if len(groups) == 0 { if len(groups) == 0 {
return nil, errors.New("empty key") return nil, fmt.Errorf("empty key")
} }
return groups, nil return groups, nil
} }
+19 -3
View File
@@ -44,7 +44,23 @@ func TestDottedKeyBasic(t *testing.T) {
} }
func TestBaseKeyPound(t *testing.T) { func TestBaseKeyPound(t *testing.T) {
testError(t, "hello#world", "invalid bare character: #") testError(t, "hello#world", "invalid bare key character: #")
}
func TestUnclosedSingleQuotedKey(t *testing.T) {
testError(t, "'", "unclosed single-quoted key")
}
func TestUnclosedDoubleQuotedKey(t *testing.T) {
testError(t, "\"", "unclosed double-quoted key")
}
func TestInvalidStartKeyCharacter(t *testing.T) {
testError(t, "/", "invalid key character: /")
}
func TestInvalidSpaceInKey(t *testing.T) {
testError(t, "invalid key", "invalid key character after whitespace: k")
} }
func TestQuotedKeys(t *testing.T) { func TestQuotedKeys(t *testing.T) {
@@ -57,7 +73,7 @@ func TestQuotedKeys(t *testing.T) {
} }
func TestEmptyKey(t *testing.T) { func TestEmptyKey(t *testing.T) {
testError(t, "", "empty key") testError(t, ``, "empty key")
testError(t, " ", "empty key") testError(t, ` `, "empty key")
testResult(t, `""`, []string{""}) testResult(t, `""`, []string{""})
} }
+4 -2
View File
@@ -309,7 +309,7 @@ func (l *tomlLexer) lexKey() tomlLexStateFn {
if err != nil { if err != nil {
return l.errorf(err.Error()) return l.errorf(err.Error())
} }
growingString += str growingString += "\"" + str + "\""
l.next() l.next()
continue continue
} else if r == '\'' { } else if r == '\'' {
@@ -318,13 +318,15 @@ func (l *tomlLexer) lexKey() tomlLexStateFn {
if err != nil { if err != nil {
return l.errorf(err.Error()) return l.errorf(err.Error())
} }
growingString += str growingString += "'" + str + "'"
l.next() l.next()
continue continue
} else if r == '\n' { } else if r == '\n' {
return l.errorf("keys cannot contain new lines") return l.errorf("keys cannot contain new lines")
} else if isSpace(r) { } else if isSpace(r) {
break break
} else if r == '.' {
// skip
} else if !isValidBareChar(r) { } else if !isValidBareChar(r) {
return l.errorf("keys cannot contain %c character", r) return l.errorf("keys cannot contain %c character", r)
} }
+1 -1
View File
@@ -690,7 +690,7 @@ func TestKeyGroupArray(t *testing.T) {
func TestQuotedKey(t *testing.T) { func TestQuotedKey(t *testing.T) {
testFlow(t, "\"a b\" = 42", []token{ testFlow(t, "\"a b\" = 42", []token{
{Position{1, 1}, tokenKey, "a b"}, {Position{1, 1}, tokenKey, "\"a b\""},
{Position{1, 7}, tokenEqual, "="}, {Position{1, 7}, tokenEqual, "="},
{Position{1, 9}, tokenInteger, "42"}, {Position{1, 9}, tokenInteger, "42"},
{Position{1, 11}, tokenEOF, ""}, {Position{1, 11}, tokenEOF, ""},
+18 -6
View File
@@ -77,8 +77,10 @@ func (p *tomlParser) parseStart() tomlParserStateFn {
return p.parseAssign return p.parseAssign
case tokenEOF: case tokenEOF:
return nil return nil
case tokenError:
p.raiseError(tok, "parsing error: %s", tok.String())
default: default:
p.raiseError(tok, "unexpected token") p.raiseError(tok, "unexpected token %s", tok.typ)
} }
return nil return nil
} }
@@ -165,6 +167,11 @@ func (p *tomlParser) parseAssign() tomlParserStateFn {
key := p.getToken() key := p.getToken()
p.assume(tokenEqual) p.assume(tokenEqual)
parsedKey, err := parseKey(key.val)
if err != nil {
p.raiseError(key, "invalid key: %s", err.Error())
}
value := p.parseRvalue() value := p.parseRvalue()
var tableKey []string var tableKey []string
if len(p.currentTable) > 0 { if len(p.currentTable) > 0 {
@@ -173,6 +180,9 @@ func (p *tomlParser) parseAssign() tomlParserStateFn {
tableKey = []string{} tableKey = []string{}
} }
prefixKey := parsedKey[0 : len(parsedKey)-1]
tableKey = append(tableKey, prefixKey...)
// find the table to assign, looking out for arrays of tables // find the table to assign, looking out for arrays of tables
var targetNode *Tree var targetNode *Tree
switch node := p.tree.GetPath(tableKey).(type) { switch node := p.tree.GetPath(tableKey).(type) {
@@ -180,17 +190,19 @@ func (p *tomlParser) parseAssign() tomlParserStateFn {
targetNode = node[len(node)-1] targetNode = node[len(node)-1]
case *Tree: case *Tree:
targetNode = node targetNode = node
case nil:
// create intermediate
if err := p.tree.createSubTree(tableKey, key.Position); err != nil {
p.raiseError(key, "could not create intermediate group: %s", err)
}
targetNode = p.tree.GetPath(tableKey).(*Tree)
default: default:
p.raiseError(key, "Unknown table type for path: %s", p.raiseError(key, "Unknown table type for path: %s",
strings.Join(tableKey, ".")) strings.Join(tableKey, "."))
} }
// assign value to the found table // assign value to the found table
keyVals := []string{key.val} keyVal := parsedKey[len(parsedKey)-1]
if len(keyVals) != 1 {
p.raiseError(key, "Invalid key")
}
keyVal := keyVals[0]
localKey := []string{keyVal} localKey := []string{keyVal}
finalKey := append(tableKey, keyVal) finalKey := append(tableKey, keyVal)
if targetNode.GetPath(localKey) != nil { if targetNode.GetPath(localKey) != nil {
+31 -2
View File
@@ -79,7 +79,7 @@ zyx = 42`)
if err == nil { if err == nil {
t.Error("Error should have been returned.") t.Error("Error should have been returned.")
} }
if err.Error() != "(1, 4): unexpected token" { if err.Error() != "(1, 4): parsing error: keys cannot contain ] character" {
t.Error("Bad error message:", err.Error()) t.Error("Bad error message:", err.Error())
} }
} }
@@ -581,7 +581,7 @@ func TestDuplicateKeys(t *testing.T) {
func TestEmptyIntermediateTable(t *testing.T) { func TestEmptyIntermediateTable(t *testing.T) {
_, err := Load("[foo..bar]") _, err := Load("[foo..bar]")
if err.Error() != "(1, 2): invalid table array key: empty table key" { if err.Error() != "(1, 2): invalid table array key: expecting key part after dot" {
t.Error("Bad error message:", err.Error()) t.Error("Bad error message:", err.Error())
} }
} }
@@ -908,3 +908,32 @@ func TestMapKeyIsNum(t *testing.T) {
t.Error("should be passed") t.Error("should be passed")
} }
} }
func TestDottedKeys(t *testing.T) {
tree, err := Load(`
name = "Orange"
physical.color = "orange"
physical.shape = "round"
site."google.com" = true`)
assertTree(t, tree, err, map[string]interface{}{
"name": "Orange",
"physical": map[string]interface{}{
"color": "orange",
"shape": "round",
},
"site": map[string]interface{}{
"google.com": true,
},
})
}
func TestInvalidDottedKeyEmptyGroup(t *testing.T) {
_, err := Load(`a..b = true`)
if err == nil {
t.Fatal("should return an error")
}
if err.Error() != "(1, 1): invalid key: expecting key part after dot" {
t.Fatalf("invalid error message: %s", err)
}
}