Went over the full TOML AST implementation to give stuff good names.

Also pushed all tests to ast_test to make sure that the public interface
is sufficient for using the package
This commit is contained in:
Maurice Makaay 2019-06-26 15:38:32 +00:00
parent 688894dbf2
commit 54cb75955d
4 changed files with 257 additions and 208 deletions

View File

@ -1,3 +1,4 @@
// Package ast implements an Abstract Syntax Tree that represents a TOML document.
package ast package ast
import ( import (
@ -6,141 +7,183 @@ import (
"strings" "strings"
) )
// item represents a TOML item. // Document is a struct holds the data for a TOML Document.
type item struct { //
Type valueType // Methods on this struct provide functionality to construct a full data
Data []interface{} // structure that represents a TOML document.
type Document struct {
Root Table // the root-level TOML Table (each TOML doc is implicitly a Table)
Current Table // the currently active TOML Table
CurrentKey Key // the key for the currently active TOML Table
} }
// table represents a TOML table. // NewDocument instantiates a new TOML Document.
type table map[string]*item func NewDocument() *Document {
doc := &Document{Root: make(Table)}
// valueType identifies the type of a TOML value. doc.Current = doc.Root
type valueType string return doc
const (
tString valueType = "string" // "various", 'types', """of""", '''strings'''
tInteger valueType = "integer" // 12345, 0xffee12a0, 0o0755, 0b101101011
tFloat valueType = "float" // 10.1234, 143E-12, 43.28377e+4, +inf, -inf, nan
tBoolean valueType = "boolean" // true or false
tOffsetDateTime valueType = "offset datetime" // 2019-06-18 10:32:15.173645362+0200
tLocalDateTime valueType = "datetime" // 2018-12-25 12:12:18.876772533
tLocalDate valueType = "date" // 2017-05-17
tLocalTime valueType = "time" // 23:01:22
tArrayOfTables valueType = "array" // defined using an [[array.of.tables]]
tArray valueType = "static array" // defined using ["an", "inline", "array"]
tTable valueType = "table" // defined using { "inline" = "table" } or [standard.table]
)
// newItem instantiates a new item struct.
func newItem(valueType valueType, data ...interface{}) *item {
return &item{Type: valueType, Data: data}
} }
// newKey instantiates a new key. // Table represents a TOML table: a set of key/value pairs.
func newKey(key ...string) []string { type Table map[string]*Value
// Key represents a TOML table key: one or more strings, where multiple strings
// can be used to represent nested tables.
type Key []string
// NewKey instantiates a new Key.
func NewKey(key ...string) Key {
return key return key
} }
// parser holds the state for the TOML parser. All parsing functions are // Value represents a TOML value.
// methods of this struct. type Value struct {
type parser struct { Type ValueType
Root table // the root-level TOML table (each TOML doc is implicitly a table) Data []interface{}
Current table // the currently active TOML table
CurrentKey []string // the key for the currently active TOML table
} }
func newParser() *parser { // NewValue instantiates a new Value.
p := &parser{Root: make(table)} func NewValue(valueType ValueType, data ...interface{}) *Value {
p.Current = p.Root return &Value{
return p Type: valueType,
Data: data,
}
} }
func (t *parser) setKeyValuePair(key []string, value *item) error { // ValueType identifies the type of a TOML value, as specified by the
// TOML specification. Because these types do not map Go types one-on-one,
// we have to keep track of the TOML type ourselves.
type ValueType string
const (
// TypeString identifies a string value ("various", 'types', """of""", '''strings''').
TypeString ValueType = "string"
// TypeInteger identifies an integer number value (12345, 0xffee12a0, 0o0755, 0b101101011).
TypeInteger ValueType = "integer"
// TypeFloat identifies a floating point number (10.1234, 143E-12, 43.28377e+4, +inf, -inf, nan).
TypeFloat ValueType = "float"
// TypeBoolean identifies a boolean value (true or false).
TypeBoolean ValueType = "boolean"
// TypeOffsetDateTime identifies a date/time value, including timezone info (2019-06-18 10:32:15.173645362+0200).
TypeOffsetDateTime ValueType = "offset datetime"
// TypeLocalDateTime identifies a date/time value, without timezone info (2018-12-25 12:12:18.876772533).
TypeLocalDateTime ValueType = "datetime"
// TypeLocalDate identifies a date value (2017-05-17).
TypeLocalDate ValueType = "date"
// TypeLocalTime identifies a time value (23:01:22).
TypeLocalTime ValueType = "time"
// TypeArrayOfTables identifies an [[array.of.tables]].
TypeArrayOfTables ValueType = "array"
// TypeArray identifies ["an", "inline", "static", "array"].
TypeArray ValueType = "static array"
// TypeTable identifies an { "inline" = "table" } or [standard.table].
TypeTable ValueType = "table"
)
// SetKeyValuePair is used to set a key and an accompanying value in the
// currently active TOML table of the TOML Document.
func (doc *Document) SetKeyValuePair(key Key, value *Value) error {
// First make sure the table structure for storing the value exists. // First make sure the table structure for storing the value exists.
node, lastKeyPart, err := t.makeTablePath(key) node, lastKeyPart, err := doc.makeTablePath(key)
if err != nil { if err != nil {
return fmt.Errorf("invalid key/value pair: %s", err) return fmt.Errorf("invalid key/value pair: %s", err)
} }
// Check if the key is still free for use. // Check if the key is still free for use.
if existing, ok := node[lastKeyPart]; ok { if existing, ok := node[lastKeyPart]; ok {
path := t.formatKeyPath(key, len(key)-1) path := doc.formatKeyPath(key, len(key)-1)
return fmt.Errorf("invalid key/value pair: %s item already exists at key %s", existing.Type, path) return fmt.Errorf("invalid key/value pair: %s value already exists at key %s", existing.Type, path)
} }
// It is, store the value in the table. // It is, store the value in the table.
node[lastKeyPart] = value node[lastKeyPart] = value
return nil return nil
} }
func (t *parser) openTable(key []string) error { // OpenTable creates a new table at the provided Key path and makes this the
t.CurrentKey = nil // active table for the TOML Document. This means that subsequent calls to
t.Current = t.Root // SetKeyValuePair() will add the key/value pairs to this table.
func (doc *Document) OpenTable(key Key) error {
doc.CurrentKey = nil
doc.Current = doc.Root
// Go over all requested levels of the key. For all levels, except the last // Go over all requested levels of the key. For all levels, except the last
// one, it is okay if a table already exists. For at least the last level, // one, it is okay if a Table already exists. For at least the last level,
// no table or value must exist, because that would mean we are overwriting // no table or value must exist, because that would mean we are overwriting
// an existing key/value pair, which is not allowed. // an existing key/value pair, which is not allowed.
node, lastKeyPart, err := t.makeTablePath(key) node, lastKeyPart, err := doc.makeTablePath(key)
if err != nil { if err != nil {
return fmt.Errorf("invalid table: %s", err) return fmt.Errorf("invalid table: %s", err)
} }
// Check if the key is still free for use. // Check if the key is still free for use.
if existing, ok := node[lastKeyPart]; ok { if existing, ok := node[lastKeyPart]; ok {
path := t.formatKeyPath(key, len(key)-1) path := doc.formatKeyPath(key, len(key)-1)
return fmt.Errorf("invalid table: %s item already exists at key %s", existing.Type, path) return fmt.Errorf("invalid table: %s value already exists at key %s", existing.Type, path)
} }
// The subtable does not exist yet. Create the subtable. // The subtable does not exist yet. Create the subtable.
subTable := make(table) subTable := make(Table)
node[lastKeyPart] = newItem(tTable, subTable) node[lastKeyPart] = NewValue(TypeTable, subTable)
node = subTable node = subTable
// From here on, key/value pairs are added to the newly defined table. // From here on, key/value pairs are added to the newly defined table.
t.Current = node doc.Current = node
t.CurrentKey = key doc.CurrentKey = key
return nil return nil
} }
func (t *parser) openArrayOfTables(key []string) error { // OpenArrayOfTables creates a new table and adds it to a (possibly newly
t.CurrentKey = nil // created) table array at the provided Key path. The new table will be
t.Current = t.Root // made the active table for the TOML Document. This means that subsequent
// calls to SetKeyValuePair() will add the key/value pairs to this table.
func (doc *Document) OpenArrayOfTables(key Key) error {
doc.CurrentKey = nil
doc.Current = doc.Root
// Go over all requested levels of the key. For all levels, except the last // Go over all requested levels of the key. For all levels, except the last
// one, it is okay if a table already exists. For the last level, either // one, it is okay if a Table already exists. For the last level, either
// no item must exist (in which case a table array will be created), or a // no value must exist (in which case a table array will be created), or a
// table array must exist. // table array must exist.
// Other cases would mean we are overwriting an existing key/value pair, // Other cases would mean we are overwriting an existing key/value pair,
// which is not allowed. // which is not allowed.
node, lastKeyPart, err := t.makeTablePath(key) node, lastKeyPart, err := doc.makeTablePath(key)
if err != nil { if err != nil {
return fmt.Errorf("invalid table array: %s", err) return fmt.Errorf("invalid table array: %s", err)
} }
// At the last key position, there must be either no value yet, or the // At the last key position, there must be either no value yet, or the
// existing value must be a table array. Other values are invalid. // existing value must be a table array. Other values are invalid.
if existing, ok := node[lastKeyPart]; ok { if existing, ok := node[lastKeyPart]; ok {
if existing.Type != tArrayOfTables { if existing.Type != TypeArrayOfTables {
path := t.formatKeyPath(key, len(key)-1) path := doc.formatKeyPath(key, len(key)-1)
return fmt.Errorf("invalid table array: %s item already exists at key %s", existing.Type, path) return fmt.Errorf("invalid table array: %s value already exists at key %s", existing.Type, path)
} }
// A table array exists. Add a new table to this array. // A table array exists. Add a new table to this array.
array := node[lastKeyPart] array := node[lastKeyPart]
subTable := make(table) subTable := make(Table)
tables := array.Data tables := array.Data
tables = append(tables, newItem(tTable, subTable)) tables = append(tables, NewValue(TypeTable, subTable))
array.Data = tables array.Data = tables
node = subTable node = subTable
} else { } else {
// No value exists at the defined key path. Create a new table array. // No value exists at the defined key path. Create a new table array.
subTable := make(table) subTable := make(Table)
node[lastKeyPart] = newItem(tArrayOfTables, newItem(tTable, subTable)) node[lastKeyPart] = NewValue(TypeArrayOfTables, NewValue(TypeTable, subTable))
node = subTable node = subTable
} }
// From here on, key/value pairs are added to the newly defined table. // From here on, key/value pairs are added to the newly defined table.
t.Current = node doc.Current = node
t.CurrentKey = key doc.CurrentKey = key
return nil return nil
} }
func (t *parser) makeTablePath(key []string) (table, string, error) { func (doc *Document) makeTablePath(key Key) (Table, string, error) {
node := t.Current node := doc.Current
for i, keyPart := range key { for i, keyPart := range key {
// Arrived at the last key part? Then the path towards that key is // Arrived at the last key part? Then the path towards that key is
// setup correctly. Return the last part, so the caller can use it. // setup correctly. Return the last part, so the caller can use it.
@ -151,27 +194,27 @@ func (t *parser) makeTablePath(key []string) (table, string, error) {
if subItem, ok := node[keyPart]; ok { if subItem, ok := node[keyPart]; ok {
// You cannot overwrite an already defined key, regardless its value. // You cannot overwrite an already defined key, regardless its value.
// When a value already exists at the current key, this can only be a table. // When a value already exists at the current key, this can only be a table.
if subItem.Type != tTable { if subItem.Type != TypeTable {
path := t.formatKeyPath(key, i) path := doc.formatKeyPath(key, i)
return nil, "", fmt.Errorf("%s item already exists at key %s", subItem.Type, path) return nil, "", fmt.Errorf("%s value already exists at key %s", subItem.Type, path)
} }
// All is okay, traverse to the subtable. // All is okay, traverse to the subtable.
node = subItem.Data[0].(table) node = subItem.Data[0].(Table)
} else { } else {
// The subtable does not exist yet. Create the subtable. // The subtable does not exist yet. Create the subtable.
subTable := make(table) subTable := make(Table)
node[keyPart] = newItem(tTable, subTable) node[keyPart] = NewValue(TypeTable, subTable)
node = subTable node = subTable
} }
} }
panic("makeTablePath(): empty key provided; a key must have at least one key part") panic("makeTablePath(): empty key provided; a key must have at least one key part")
} }
func (t *parser) formatKeyPath(key []string, end int) string { func (doc *Document) formatKeyPath(key Key, end int) string {
var sb strings.Builder var sb strings.Builder
sb.WriteRune('[') sb.WriteRune('[')
if t.CurrentKey != nil { if doc.CurrentKey != nil {
for i, keyPart := range t.CurrentKey { for i, keyPart := range doc.CurrentKey {
if i > 0 { if i > 0 {
sb.WriteString("->") sb.WriteString("->")
} }
@ -179,7 +222,7 @@ func (t *parser) formatKeyPath(key []string, end int) string {
} }
} }
for i, keyPart := range key { for i, keyPart := range key {
if t.CurrentKey != nil || i > 0 { if doc.CurrentKey != nil || i > 0 {
sb.WriteString("->") sb.WriteString("->")
} }
sb.WriteString(formatKeyName(keyPart)) sb.WriteString(formatKeyName(keyPart))
@ -197,7 +240,3 @@ func formatKeyName(key string) string {
} }
return fmt.Sprintf("%q", key) return fmt.Sprintf("%q", key)
} }
func (t table) String() string {
return newItem(tTable, t).String()
}

View File

@ -1,27 +1,29 @@
package ast package ast_test
import ( import (
"testing" "testing"
"git.makaay.nl/mauricem/go-toml/ast"
) )
func Test_ConstructSlightlyComplexStructure(t *testing.T) { func Test_ConstructSlightlyComplexStructure(t *testing.T) {
testAST(t, func() (error, *parser) { testAST(t, func() (error, *ast.Document) {
p := newParser() p := ast.NewDocument()
p.setKeyValuePair(newKey("ding"), newItem(tInteger, 10)) p.SetKeyValuePair(ast.NewKey("ding"), ast.NewValue(ast.TypeInteger, 10))
p.setKeyValuePair(newKey("dong"), newItem(tString, "not a song")) p.SetKeyValuePair(ast.NewKey("dong"), ast.NewValue(ast.TypeString, "not a song"))
p.openTable(newKey("key1", "key2 a")) p.OpenTable(ast.NewKey("key1", "key2 a"))
p.setKeyValuePair(newKey("dooh"), newItem(tBoolean, true)) p.SetKeyValuePair(ast.NewKey("dooh"), ast.NewValue(ast.TypeBoolean, true))
p.setKeyValuePair(newKey("dah"), newItem(tBoolean, false)) p.SetKeyValuePair(ast.NewKey("dah"), ast.NewValue(ast.TypeBoolean, false))
p.openTable(newKey("key1", "key2 b")) p.OpenTable(ast.NewKey("key1", "key2 b"))
p.setKeyValuePair(newKey("dieh"), newItem(tFloat, 1.111)) p.SetKeyValuePair(ast.NewKey("dieh"), ast.NewValue(ast.TypeFloat, 1.111))
p.setKeyValuePair(newKey("duh"), newItem(tFloat, 1.18e-12)) p.SetKeyValuePair(ast.NewKey("duh"), ast.NewValue(ast.TypeFloat, 1.18e-12))
p.setKeyValuePair(newKey("foo", "bar"), newItem(tArrayOfTables, newItem(tInteger, 1), newItem(tInteger, 2))) p.SetKeyValuePair(ast.NewKey("foo", "bar"), ast.NewValue(ast.TypeArrayOfTables, ast.NewValue(ast.TypeInteger, 1), ast.NewValue(ast.TypeInteger, 2)))
p.openArrayOfTables(newKey("aaah", "table array")) p.OpenArrayOfTables(ast.NewKey("aaah", "table array"))
p.setKeyValuePair(newKey("a"), newItem(tFloat, 1.234)) p.SetKeyValuePair(ast.NewKey("a"), ast.NewValue(ast.TypeFloat, 1.234))
p.openArrayOfTables(newKey("aaah", "table array")) p.OpenArrayOfTables(ast.NewKey("aaah", "table array"))
p.setKeyValuePair(newKey("b"), newItem(tFloat, 2.345)) p.SetKeyValuePair(ast.NewKey("b"), ast.NewValue(ast.TypeFloat, 2.345))
p.setKeyValuePair(newKey("c"), newItem(tString, "bingo!")) p.SetKeyValuePair(ast.NewKey("c"), ast.NewValue(ast.TypeString, "bingo!"))
p.openArrayOfTables(newKey("aaah", "table array")) p.OpenArrayOfTables(ast.NewKey("aaah", "table array"))
return nil, p return nil, p
}, },
"", "",
@ -37,146 +39,146 @@ func Test_EmptyKeyForCreatingTablePath_Panics(t *testing.T) {
t.Fatalf("Did not get the expected panic message") t.Fatalf("Did not get the expected panic message")
} }
}() }()
p := newParser() p := ast.NewDocument()
p.openTable(newKey()) p.OpenTable(ast.NewKey())
} }
func Test_StoreValueInRootTable(t *testing.T) { func Test_StoreValueInRooTypeTable(t *testing.T) {
testAST(t, func() (error, *parser) { testAST(t, func() (error, *ast.Document) {
p := newParser() p := ast.NewDocument()
p.setKeyValuePair(newKey("key1"), newItem(tString, "value1")) p.SetKeyValuePair(ast.NewKey("key1"), ast.NewValue(ast.TypeString, "value1"))
return p.setKeyValuePair(newKey("key2"), newItem(tString, "value2")), p return p.SetKeyValuePair(ast.NewKey("key2"), ast.NewValue(ast.TypeString, "value2")), p
}, },
"", "",
`{"key1": "value1", "key2": "value2"}`) `{"key1": "value1", "key2": "value2"}`)
} }
func Test_StoreValueWithMultipartKey_CreatesTableHierarchy(t *testing.T) { func Test_StoreValueWithMultipartKey_CreatesTableHierarchy(t *testing.T) {
testAST(t, func() (error, *parser) { testAST(t, func() (error, *ast.Document) {
p := newParser() p := ast.NewDocument()
return p.setKeyValuePair(newKey("key1", "key2", "key3"), newItem(tString, "value")), p return p.SetKeyValuePair(ast.NewKey("key1", "key2", "key3"), ast.NewValue(ast.TypeString, "value")), p
}, },
"", "",
`{"key1": {"key2": {"key3": "value"}}}`) `{"key1": {"key2": {"key3": "value"}}}`)
} }
func Test_StoreDuplicateKeyInRootTable_ReturnsError(t *testing.T) { func Test_StoreDuplicateKeyInRooTypeTable_ReturnsError(t *testing.T) {
testAST(t, func() (error, *parser) { testAST(t, func() (error, *ast.Document) {
p := newParser() p := ast.NewDocument()
p.setKeyValuePair(newKey("key"), newItem(tString, "value")) p.SetKeyValuePair(ast.NewKey("key"), ast.NewValue(ast.TypeString, "value"))
return p.setKeyValuePair(newKey("key"), newItem(tInteger, 321)), p return p.SetKeyValuePair(ast.NewKey("key"), ast.NewValue(ast.TypeInteger, 321)), p
}, },
`invalid key/value pair: string item already exists at key [key]`, `invalid key/value pair: string value already exists at key [key]`,
`{"key": "value"}`) `{"key": "value"}`)
} }
func Test_StoreValueWithMultipartKey_UnderSubtable_CreatesTableHierarchy(t *testing.T) { func Test_StoreValueWithMultipartKey_UnderSubtable_CreatesTableHierarchy(t *testing.T) {
testAST(t, func() (error, *parser) { testAST(t, func() (error, *ast.Document) {
p := newParser() p := ast.NewDocument()
p.openTable(newKey("tablekey1", "tablekey2")) p.OpenTable(ast.NewKey("tablekey1", "tablekey2"))
return p.setKeyValuePair(newKey("valuekey1", "valuekey2", "valuekey3"), newItem(tString, "value")), p return p.SetKeyValuePair(ast.NewKey("valuekey1", "valuekey2", "valuekey3"), ast.NewValue(ast.TypeString, "value")), p
}, },
"", "",
`{"tablekey1": {"tablekey2": {"valuekey1": {"valuekey2": {"valuekey3": "value"}}}}}`) `{"tablekey1": {"tablekey2": {"valuekey1": {"valuekey2": {"valuekey3": "value"}}}}}`)
} }
func Test_StoreKeyPathWherePathContainsNonTableAlready_ReturnsError(t *testing.T) { func Test_StoreKeyPathWherePathContainsNonTableAlready_ReturnsError(t *testing.T) {
testAST(t, func() (error, *parser) { testAST(t, func() (error, *ast.Document) {
p := newParser() p := ast.NewDocument()
p.openTable(newKey("key1")) p.OpenTable(ast.NewKey("key1"))
p.setKeyValuePair(newKey("key2"), newItem(tInteger, 0)) p.SetKeyValuePair(ast.NewKey("key2"), ast.NewValue(ast.TypeInteger, 0))
return p.setKeyValuePair(newKey("key2", "key3"), newItem(tString, "value")), p return p.SetKeyValuePair(ast.NewKey("key2", "key3"), ast.NewValue(ast.TypeString, "value")), p
}, },
`invalid key/value pair: integer item already exists at key [key1->key2]`, `invalid key/value pair: integer value already exists at key [key1->key2]`,
`{"key1": {"key2": 0}}`) `{"key1": {"key2": 0}}`)
} }
func Test_GivenExistingTableAtKey_CreatingTableAtSameKey_ReturnsError(t *testing.T) { func Test_GivenExistingTableAtKey_CreatingTableAtSameKey_ReturnsError(t *testing.T) {
testAST(t, func() (error, *parser) { testAST(t, func() (error, *ast.Document) {
p := newParser() p := ast.NewDocument()
p.openTable(newKey("key1", "key2")) p.OpenTable(ast.NewKey("key1", "key2"))
return p.openTable(newKey("key1", "key2")), p return p.OpenTable(ast.NewKey("key1", "key2")), p
}, },
`invalid table: table item already exists at key [key1->key2]`, `invalid table: table value already exists at key [key1->key2]`,
`{"key1": {"key2": {}}}`) `{"key1": {"key2": {}}}`)
} }
func Test_GivenExistingItemAtKey_CreatingTableAtSameKey_ReturnsError(t *testing.T) { func Test_GivenExistingItemAtKey_CreatingTableAtSameKey_ReturnsError(t *testing.T) {
testAST(t, func() (error, *parser) { testAST(t, func() (error, *ast.Document) {
p := newParser() p := ast.NewDocument()
p.setKeyValuePair(newKey("key"), newItem(tString, "value")) p.SetKeyValuePair(ast.NewKey("key"), ast.NewValue(ast.TypeString, "value"))
return p.openTable(newKey("key")), p return p.OpenTable(ast.NewKey("key")), p
}, },
`invalid table: string item already exists at key [key]`, `invalid table: string value already exists at key [key]`,
`{"key": "value"}`) `{"key": "value"}`)
} }
func Test_GivenExistingItemInKeyPath_CreatingTable_ReturnsError(t *testing.T) { func Test_GivenExistingItemInKeyPath_CreatingTable_ReturnsError(t *testing.T) {
testAST(t, func() (error, *parser) { testAST(t, func() (error, *ast.Document) {
p := newParser() p := ast.NewDocument()
p.setKeyValuePair(newKey("key"), newItem(tString, "value")) p.SetKeyValuePair(ast.NewKey("key"), ast.NewValue(ast.TypeString, "value"))
return p.openTable(newKey("key", "subkey")), p return p.OpenTable(ast.NewKey("key", "subkey")), p
}, },
`invalid table: string item already exists at key [key]`, `invalid table: string value already exists at key [key]`,
`{"key": "value"}`) `{"key": "value"}`)
} }
func Test_GivenExistingItemAtDeepKey_CreatingTableAtSameKey_ReturnsError(t *testing.T) { func Test_GivenExistingItemAtDeepKey_CreatingTableAtSameKey_ReturnsError(t *testing.T) {
testAST(t, func() (error, *parser) { testAST(t, func() (error, *ast.Document) {
p := newParser() p := ast.NewDocument()
p.openTable(newKey("deep", "table")) p.OpenTable(ast.NewKey("deep", "table"))
p.setKeyValuePair(newKey("key"), newItem(tInteger, 0)) p.SetKeyValuePair(ast.NewKey("key"), ast.NewValue(ast.TypeInteger, 0))
return p.openTable(newKey("deep", "table", "key")), p return p.OpenTable(ast.NewKey("deep", "table", "key")), p
}, },
`invalid table: integer item already exists at key [deep->table->key]`, `invalid table: integer value already exists at key [deep->table->key]`,
`{"deep": {"table": {"key": 0}}}`) `{"deep": {"table": {"key": 0}}}`)
} }
func Test_GivenExistingItemAtDeepKeyFromSubTable_CreatingTableAtSameKey_ReturnsError(t *testing.T) { func Test_GivenExistingItemAtDeepKeyFromSubTable_CreatingTableAtSameKey_ReturnsError(t *testing.T) {
testAST(t, func() (error, *parser) { testAST(t, func() (error, *ast.Document) {
p := newParser() p := ast.NewDocument()
p.openTable(newKey("deep", "table")) p.OpenTable(ast.NewKey("deep", "table"))
p.setKeyValuePair(newKey("key1", "key2"), newItem(tInteger, 0)) p.SetKeyValuePair(ast.NewKey("key1", "key2"), ast.NewValue(ast.TypeInteger, 0))
return p.setKeyValuePair(newKey("key1", "key2"), newItem(tBoolean, true)), p return p.SetKeyValuePair(ast.NewKey("key1", "key2"), ast.NewValue(ast.TypeBoolean, true)), p
}, },
// This test mainly tests the formatting of [deep->table->key1->key2], being a concatenation // This test mainly tests the formatting of [deep->table->key1->key2], being a concatenation
// of the currently active table plus the multipart key for setKeyValuePair(). // of the currently active table plus the multipart key for SetKeyValuePair().
`invalid key/value pair: integer item already exists at key [deep->table->key1->key2]`, `invalid key/value pair: integer value already exists at key [deep->table->key1->key2]`,
`{"deep": {"table": {"key1": {"key2": 0}}}}`) `{"deep": {"table": {"key1": {"key2": 0}}}}`)
} }
func Test_FormattingOfQuotedPathPartInError(t *testing.T) { func Test_FormattingOfQuotedPathPartInError(t *testing.T) {
testAST(t, func() (error, *parser) { testAST(t, func() (error, *ast.Document) {
p := newParser() p := ast.NewDocument()
p.openTable(newKey("must be quoted")) p.OpenTable(ast.NewKey("must be quoted"))
p.setKeyValuePair(newKey("this one too"), newItem(tInteger, 0)) p.SetKeyValuePair(ast.NewKey("this one too"), ast.NewValue(ast.TypeInteger, 0))
return p.setKeyValuePair(newKey("this one too"), newItem(tInteger, 0)), p return p.SetKeyValuePair(ast.NewKey("this one too"), ast.NewValue(ast.TypeInteger, 0)), p
}, },
`invalid key/value pair: integer item already exists at key ["must be quoted"->"this one too"]`, `invalid key/value pair: integer value already exists at key ["must be quoted"->"this one too"]`,
`{"must be quoted": {"this one too": 0}}`) `{"must be quoted": {"this one too": 0}}`)
} }
func Test_GivenExistingItemAtKey_CreatingArrayOfTablesAtSameKey_ReturnsError(t *testing.T) { func Test_GivenExistingItemAtKey_CreatingArrayOfTablesAtSameKey_ReturnsError(t *testing.T) {
testAST(t, func() (error, *parser) { testAST(t, func() (error, *ast.Document) {
p := newParser() p := ast.NewDocument()
p.Root["key"] = newItem(tString, "value") p.Root["key"] = ast.NewValue(ast.TypeString, "value")
return p.openArrayOfTables(newKey("key")), p return p.OpenArrayOfTables(ast.NewKey("key")), p
}, },
`invalid table array: string item already exists at key [key]`, `invalid table array: string value already exists at key [key]`,
`{"key": "value"}`) `{"key": "value"}`)
} }
func Test_GivenExistingItemInKeyPath_CreatingArrayOfTables_ReturnsError(t *testing.T) { func Test_GivenExistingItemInKeyPath_CreatingArrayOfTables_ReturnsError(t *testing.T) {
testAST(t, func() (error, *parser) { testAST(t, func() (error, *ast.Document) {
p := newParser() p := ast.NewDocument()
p.Root["key"] = newItem(tString, "value") p.Root["key"] = ast.NewValue(ast.TypeString, "value")
return p.openArrayOfTables(newKey("key", "subkey")), p return p.OpenArrayOfTables(ast.NewKey("key", "subkey")), p
}, },
`invalid table array: string item already exists at key [key]`, `invalid table array: string value already exists at key [key]`,
`{"key": "value"}`) `{"key": "value"}`)
} }
func testAST(t *testing.T, code func() (error, *parser), expectedError string, expectedData string) { func testAST(t *testing.T, code func() (error, *ast.Document), expectedError string, expectedData string) {
err, p := code() err, p := code()
if expectedError == "" { if expectedError == "" {
if err != nil { if err != nil {

View File

@ -7,30 +7,36 @@ import (
"time" "time"
) )
// String() produces a JSON-like (but not JSON) string representation of the TOML Document.
// This string version is mainly useful for testing and debugging purposes.
func (t Table) String() string {
return NewValue(TypeTable, t).String()
}
// String() produces a JSON-like (but not JSON) string representation of the value. // String() produces a JSON-like (but not JSON) string representation of the value.
// This string version is useful for testing and debugging purposes. // This string version is mainly useful for testing and debugging purposes.
func (parseItem item) String() string { func (parseItem Value) String() string {
switch parseItem.Type { switch parseItem.Type {
case tString: case TypeString:
return fmt.Sprintf("%q", parseItem.Data[0]) return fmt.Sprintf("%q", parseItem.Data[0])
case tOffsetDateTime: case TypeOffsetDateTime:
return parseItem.Data[0].(time.Time).Format(time.RFC3339Nano) return parseItem.Data[0].(time.Time).Format(time.RFC3339Nano)
case tLocalDateTime: case TypeLocalDateTime:
return parseItem.Data[0].(time.Time).Format("2006-01-02 15:04:05.999999999") return parseItem.Data[0].(time.Time).Format("2006-01-02 15:04:05.999999999")
case tLocalDate: case TypeLocalDate:
return parseItem.Data[0].(time.Time).Format("2006-01-02") return parseItem.Data[0].(time.Time).Format("2006-01-02")
case tLocalTime: case TypeLocalTime:
return parseItem.Data[0].(time.Time).Format("15:04:05.999999999") return parseItem.Data[0].(time.Time).Format("15:04:05.999999999")
case tArrayOfTables: case TypeArrayOfTables:
fallthrough fallthrough
case tArray: case TypeArray:
items := make([]string, len(parseItem.Data)) values := make(Key, len(parseItem.Data))
for i, value := range parseItem.Data { for i, value := range parseItem.Data {
items[i] = value.(*item).String() values[i] = value.(*Value).String()
} }
return fmt.Sprintf("[%s]", strings.Join(items, ", ")) return fmt.Sprintf("[%s]", strings.Join(values, ", "))
case tTable: case TypeTable:
pairs := parseItem.Data[0].(table) pairs := parseItem.Data[0].(Table)
keys := make([]string, len(pairs)) keys := make([]string, len(pairs))
i := 0 i := 0
for k := range pairs { for k := range pairs {
@ -38,11 +44,11 @@ func (parseItem item) String() string {
i++ i++
} }
sort.Strings(keys) sort.Strings(keys)
items := make([]string, len(pairs)) values := make([]string, len(pairs))
for i, k := range keys { for i, k := range keys {
items[i] = fmt.Sprintf("%q: %s", k, pairs[k].String()) values[i] = fmt.Sprintf("%q: %s", k, pairs[k].String())
} }
return fmt.Sprintf("{%s}", strings.Join(items, ", ")) return fmt.Sprintf("{%s}", strings.Join(values, ", "))
default: default:
return fmt.Sprintf("%v", parseItem.Data[0]) return fmt.Sprintf("%v", parseItem.Data[0])
} }

View File

@ -1,28 +1,30 @@
package ast package ast_test
import ( import (
"testing" "testing"
"time" "time"
"git.makaay.nl/mauricem/go-toml/ast"
) )
func Test_StringFormatting(t *testing.T) { func Test_StringFormatting(t *testing.T) {
testAST(t, func() (error, *parser) { testAST(t, func() (error, *ast.Document) {
tableData := make(table) tableData := make(ast.Table)
tableData["x"] = newItem(tBoolean, true) tableData["x"] = ast.NewValue(ast.TypeBoolean, true)
tableData["y"] = newItem(tInteger, 42) tableData["y"] = ast.NewValue(ast.TypeInteger, 42)
dateTime, _ := time.Parse(time.RFC3339Nano, "2003-11-01T01:02:03.999999999+10:00") dateTime, _ := time.Parse(time.RFC3339Nano, "2003-11-01T01:02:03.999999999+10:00")
p := newParser() doc := ast.NewDocument()
p.setKeyValuePair(newKey("a"), newItem(tInteger, 1)) doc.SetKeyValuePair(ast.NewKey("a"), ast.NewValue(ast.TypeInteger, 1))
p.setKeyValuePair(newKey("b"), newItem(tFloat, 2.3)) doc.SetKeyValuePair(ast.NewKey("b"), ast.NewValue(ast.TypeFloat, 2.3))
p.setKeyValuePair(newKey("c"), newItem(tBoolean, true)) doc.SetKeyValuePair(ast.NewKey("c"), ast.NewValue(ast.TypeBoolean, true))
p.setKeyValuePair(newKey("d"), newItem(tString, "foo")) doc.SetKeyValuePair(ast.NewKey("d"), ast.NewValue(ast.TypeString, "foo"))
p.setKeyValuePair(newKey("e"), newItem(tArray, newItem(tInteger, 1), newItem(tInteger, 2))) doc.SetKeyValuePair(ast.NewKey("e"), ast.NewValue(ast.TypeArray, ast.NewValue(ast.TypeInteger, 1), ast.NewValue(ast.TypeInteger, 2)))
p.setKeyValuePair(newKey("f"), newItem(tTable, tableData)) doc.SetKeyValuePair(ast.NewKey("f"), ast.NewValue(ast.TypeTable, tableData))
p.setKeyValuePair(newKey("g"), newItem(tOffsetDateTime, dateTime)) doc.SetKeyValuePair(ast.NewKey("g"), ast.NewValue(ast.TypeOffsetDateTime, dateTime))
p.setKeyValuePair(newKey("h"), newItem(tLocalDateTime, dateTime)) doc.SetKeyValuePair(ast.NewKey("h"), ast.NewValue(ast.TypeLocalDateTime, dateTime))
p.setKeyValuePair(newKey("i"), newItem(tLocalDate, dateTime)) doc.SetKeyValuePair(ast.NewKey("i"), ast.NewValue(ast.TypeLocalDate, dateTime))
p.setKeyValuePair(newKey("j"), newItem(tLocalTime, dateTime)) doc.SetKeyValuePair(ast.NewKey("j"), ast.NewValue(ast.TypeLocalTime, dateTime))
return nil, p return nil, doc
}, "", }, "",
`{"a": 1, `+ `{"a": 1, `+
`"b": 2.3, `+ `"b": 2.3, `+