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:
parent
688894dbf2
commit
54cb75955d
209
ast/ast.go
209
ast/ast.go
|
@ -1,3 +1,4 @@
|
|||
// Package ast implements an Abstract Syntax Tree that represents a TOML document.
|
||||
package ast
|
||||
|
||||
import (
|
||||
|
@ -6,141 +7,183 @@ import (
|
|||
"strings"
|
||||
)
|
||||
|
||||
// item represents a TOML item.
|
||||
type item struct {
|
||||
Type valueType
|
||||
Data []interface{}
|
||||
// Document is a struct holds the data for a TOML Document.
|
||||
//
|
||||
// Methods on this struct provide functionality to construct a full data
|
||||
// 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.
|
||||
type table map[string]*item
|
||||
|
||||
// valueType identifies the type of a TOML value.
|
||||
type valueType string
|
||||
|
||||
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}
|
||||
// NewDocument instantiates a new TOML Document.
|
||||
func NewDocument() *Document {
|
||||
doc := &Document{Root: make(Table)}
|
||||
doc.Current = doc.Root
|
||||
return doc
|
||||
}
|
||||
|
||||
// newKey instantiates a new key.
|
||||
func newKey(key ...string) []string {
|
||||
// Table represents a TOML table: a set of key/value pairs.
|
||||
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
|
||||
}
|
||||
|
||||
// parser holds the state for the TOML parser. All parsing functions are
|
||||
// methods of this struct.
|
||||
type parser struct {
|
||||
Root table // the root-level TOML table (each TOML doc is implicitly a table)
|
||||
Current table // the currently active TOML table
|
||||
CurrentKey []string // the key for the currently active TOML table
|
||||
// Value represents a TOML value.
|
||||
type Value struct {
|
||||
Type ValueType
|
||||
Data []interface{}
|
||||
}
|
||||
|
||||
func newParser() *parser {
|
||||
p := &parser{Root: make(table)}
|
||||
p.Current = p.Root
|
||||
return p
|
||||
// NewValue instantiates a new Value.
|
||||
func NewValue(valueType ValueType, data ...interface{}) *Value {
|
||||
return &Value{
|
||||
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.
|
||||
node, lastKeyPart, err := t.makeTablePath(key)
|
||||
node, lastKeyPart, err := doc.makeTablePath(key)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid key/value pair: %s", err)
|
||||
}
|
||||
// Check if the key is still free for use.
|
||||
if existing, ok := node[lastKeyPart]; ok {
|
||||
path := t.formatKeyPath(key, len(key)-1)
|
||||
return fmt.Errorf("invalid key/value pair: %s item already exists at key %s", existing.Type, path)
|
||||
path := doc.formatKeyPath(key, len(key)-1)
|
||||
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.
|
||||
node[lastKeyPart] = value
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *parser) openTable(key []string) error {
|
||||
t.CurrentKey = nil
|
||||
t.Current = t.Root
|
||||
// OpenTable creates a new table at the provided Key path and makes this 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) 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
|
||||
// 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
|
||||
// an existing key/value pair, which is not allowed.
|
||||
node, lastKeyPart, err := t.makeTablePath(key)
|
||||
node, lastKeyPart, err := doc.makeTablePath(key)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid table: %s", err)
|
||||
}
|
||||
// Check if the key is still free for use.
|
||||
if existing, ok := node[lastKeyPart]; ok {
|
||||
path := t.formatKeyPath(key, len(key)-1)
|
||||
return fmt.Errorf("invalid table: %s item already exists at key %s", existing.Type, path)
|
||||
path := doc.formatKeyPath(key, len(key)-1)
|
||||
return fmt.Errorf("invalid table: %s value already exists at key %s", existing.Type, path)
|
||||
}
|
||||
// The subtable does not exist yet. Create the subtable.
|
||||
subTable := make(table)
|
||||
node[lastKeyPart] = newItem(tTable, subTable)
|
||||
subTable := make(Table)
|
||||
node[lastKeyPart] = NewValue(TypeTable, subTable)
|
||||
node = subTable
|
||||
|
||||
// From here on, key/value pairs are added to the newly defined table.
|
||||
t.Current = node
|
||||
t.CurrentKey = key
|
||||
doc.Current = node
|
||||
doc.CurrentKey = key
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *parser) openArrayOfTables(key []string) error {
|
||||
t.CurrentKey = nil
|
||||
t.Current = t.Root
|
||||
// OpenArrayOfTables creates a new table and adds it to a (possibly newly
|
||||
// created) table array at the provided Key path. The new table will be
|
||||
// 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
|
||||
// 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
|
||||
// one, it is okay if a Table already exists. For the last level, either
|
||||
// no value must exist (in which case a table array will be created), or a
|
||||
// table array must exist.
|
||||
// Other cases would mean we are overwriting an existing key/value pair,
|
||||
// which is not allowed.
|
||||
node, lastKeyPart, err := t.makeTablePath(key)
|
||||
node, lastKeyPart, err := doc.makeTablePath(key)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid table array: %s", err)
|
||||
}
|
||||
// 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.
|
||||
if existing, ok := node[lastKeyPart]; ok {
|
||||
if existing.Type != tArrayOfTables {
|
||||
path := t.formatKeyPath(key, len(key)-1)
|
||||
return fmt.Errorf("invalid table array: %s item already exists at key %s", existing.Type, path)
|
||||
if existing.Type != TypeArrayOfTables {
|
||||
path := doc.formatKeyPath(key, len(key)-1)
|
||||
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.
|
||||
array := node[lastKeyPart]
|
||||
subTable := make(table)
|
||||
subTable := make(Table)
|
||||
tables := array.Data
|
||||
tables = append(tables, newItem(tTable, subTable))
|
||||
tables = append(tables, NewValue(TypeTable, subTable))
|
||||
array.Data = tables
|
||||
node = subTable
|
||||
} else {
|
||||
// No value exists at the defined key path. Create a new table array.
|
||||
subTable := make(table)
|
||||
node[lastKeyPart] = newItem(tArrayOfTables, newItem(tTable, subTable))
|
||||
subTable := make(Table)
|
||||
node[lastKeyPart] = NewValue(TypeArrayOfTables, NewValue(TypeTable, subTable))
|
||||
node = subTable
|
||||
}
|
||||
|
||||
// From here on, key/value pairs are added to the newly defined table.
|
||||
t.Current = node
|
||||
t.CurrentKey = key
|
||||
doc.Current = node
|
||||
doc.CurrentKey = key
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *parser) makeTablePath(key []string) (table, string, error) {
|
||||
node := t.Current
|
||||
func (doc *Document) makeTablePath(key Key) (Table, string, error) {
|
||||
node := doc.Current
|
||||
for i, keyPart := range key {
|
||||
// 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.
|
||||
|
@ -151,27 +194,27 @@ func (t *parser) makeTablePath(key []string) (table, string, error) {
|
|||
if subItem, ok := node[keyPart]; ok {
|
||||
// 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.
|
||||
if subItem.Type != tTable {
|
||||
path := t.formatKeyPath(key, i)
|
||||
return nil, "", fmt.Errorf("%s item already exists at key %s", subItem.Type, path)
|
||||
if subItem.Type != TypeTable {
|
||||
path := doc.formatKeyPath(key, i)
|
||||
return nil, "", fmt.Errorf("%s value already exists at key %s", subItem.Type, path)
|
||||
}
|
||||
// All is okay, traverse to the subtable.
|
||||
node = subItem.Data[0].(table)
|
||||
node = subItem.Data[0].(Table)
|
||||
} else {
|
||||
// The subtable does not exist yet. Create the subtable.
|
||||
subTable := make(table)
|
||||
node[keyPart] = newItem(tTable, subTable)
|
||||
subTable := make(Table)
|
||||
node[keyPart] = NewValue(TypeTable, subTable)
|
||||
node = subTable
|
||||
}
|
||||
}
|
||||
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
|
||||
sb.WriteRune('[')
|
||||
if t.CurrentKey != nil {
|
||||
for i, keyPart := range t.CurrentKey {
|
||||
if doc.CurrentKey != nil {
|
||||
for i, keyPart := range doc.CurrentKey {
|
||||
if i > 0 {
|
||||
sb.WriteString("->")
|
||||
}
|
||||
|
@ -179,7 +222,7 @@ func (t *parser) formatKeyPath(key []string, end int) string {
|
|||
}
|
||||
}
|
||||
for i, keyPart := range key {
|
||||
if t.CurrentKey != nil || i > 0 {
|
||||
if doc.CurrentKey != nil || i > 0 {
|
||||
sb.WriteString("->")
|
||||
}
|
||||
sb.WriteString(formatKeyName(keyPart))
|
||||
|
@ -197,7 +240,3 @@ func formatKeyName(key string) string {
|
|||
}
|
||||
return fmt.Sprintf("%q", key)
|
||||
}
|
||||
|
||||
func (t table) String() string {
|
||||
return newItem(tTable, t).String()
|
||||
}
|
||||
|
|
180
ast/ast_test.go
180
ast/ast_test.go
|
@ -1,27 +1,29 @@
|
|||
package ast
|
||||
package ast_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.makaay.nl/mauricem/go-toml/ast"
|
||||
)
|
||||
|
||||
func Test_ConstructSlightlyComplexStructure(t *testing.T) {
|
||||
testAST(t, func() (error, *parser) {
|
||||
p := newParser()
|
||||
p.setKeyValuePair(newKey("ding"), newItem(tInteger, 10))
|
||||
p.setKeyValuePair(newKey("dong"), newItem(tString, "not a song"))
|
||||
p.openTable(newKey("key1", "key2 a"))
|
||||
p.setKeyValuePair(newKey("dooh"), newItem(tBoolean, true))
|
||||
p.setKeyValuePair(newKey("dah"), newItem(tBoolean, false))
|
||||
p.openTable(newKey("key1", "key2 b"))
|
||||
p.setKeyValuePair(newKey("dieh"), newItem(tFloat, 1.111))
|
||||
p.setKeyValuePair(newKey("duh"), newItem(tFloat, 1.18e-12))
|
||||
p.setKeyValuePair(newKey("foo", "bar"), newItem(tArrayOfTables, newItem(tInteger, 1), newItem(tInteger, 2)))
|
||||
p.openArrayOfTables(newKey("aaah", "table array"))
|
||||
p.setKeyValuePair(newKey("a"), newItem(tFloat, 1.234))
|
||||
p.openArrayOfTables(newKey("aaah", "table array"))
|
||||
p.setKeyValuePair(newKey("b"), newItem(tFloat, 2.345))
|
||||
p.setKeyValuePair(newKey("c"), newItem(tString, "bingo!"))
|
||||
p.openArrayOfTables(newKey("aaah", "table array"))
|
||||
testAST(t, func() (error, *ast.Document) {
|
||||
p := ast.NewDocument()
|
||||
p.SetKeyValuePair(ast.NewKey("ding"), ast.NewValue(ast.TypeInteger, 10))
|
||||
p.SetKeyValuePair(ast.NewKey("dong"), ast.NewValue(ast.TypeString, "not a song"))
|
||||
p.OpenTable(ast.NewKey("key1", "key2 a"))
|
||||
p.SetKeyValuePair(ast.NewKey("dooh"), ast.NewValue(ast.TypeBoolean, true))
|
||||
p.SetKeyValuePair(ast.NewKey("dah"), ast.NewValue(ast.TypeBoolean, false))
|
||||
p.OpenTable(ast.NewKey("key1", "key2 b"))
|
||||
p.SetKeyValuePair(ast.NewKey("dieh"), ast.NewValue(ast.TypeFloat, 1.111))
|
||||
p.SetKeyValuePair(ast.NewKey("duh"), ast.NewValue(ast.TypeFloat, 1.18e-12))
|
||||
p.SetKeyValuePair(ast.NewKey("foo", "bar"), ast.NewValue(ast.TypeArrayOfTables, ast.NewValue(ast.TypeInteger, 1), ast.NewValue(ast.TypeInteger, 2)))
|
||||
p.OpenArrayOfTables(ast.NewKey("aaah", "table array"))
|
||||
p.SetKeyValuePair(ast.NewKey("a"), ast.NewValue(ast.TypeFloat, 1.234))
|
||||
p.OpenArrayOfTables(ast.NewKey("aaah", "table array"))
|
||||
p.SetKeyValuePair(ast.NewKey("b"), ast.NewValue(ast.TypeFloat, 2.345))
|
||||
p.SetKeyValuePair(ast.NewKey("c"), ast.NewValue(ast.TypeString, "bingo!"))
|
||||
p.OpenArrayOfTables(ast.NewKey("aaah", "table array"))
|
||||
return nil, p
|
||||
},
|
||||
"",
|
||||
|
@ -37,146 +39,146 @@ func Test_EmptyKeyForCreatingTablePath_Panics(t *testing.T) {
|
|||
t.Fatalf("Did not get the expected panic message")
|
||||
}
|
||||
}()
|
||||
p := newParser()
|
||||
p.openTable(newKey())
|
||||
p := ast.NewDocument()
|
||||
p.OpenTable(ast.NewKey())
|
||||
}
|
||||
|
||||
func Test_StoreValueInRootTable(t *testing.T) {
|
||||
testAST(t, func() (error, *parser) {
|
||||
p := newParser()
|
||||
p.setKeyValuePair(newKey("key1"), newItem(tString, "value1"))
|
||||
return p.setKeyValuePair(newKey("key2"), newItem(tString, "value2")), p
|
||||
func Test_StoreValueInRooTypeTable(t *testing.T) {
|
||||
testAST(t, func() (error, *ast.Document) {
|
||||
p := ast.NewDocument()
|
||||
p.SetKeyValuePair(ast.NewKey("key1"), ast.NewValue(ast.TypeString, "value1"))
|
||||
return p.SetKeyValuePair(ast.NewKey("key2"), ast.NewValue(ast.TypeString, "value2")), p
|
||||
},
|
||||
"",
|
||||
`{"key1": "value1", "key2": "value2"}`)
|
||||
}
|
||||
|
||||
func Test_StoreValueWithMultipartKey_CreatesTableHierarchy(t *testing.T) {
|
||||
testAST(t, func() (error, *parser) {
|
||||
p := newParser()
|
||||
return p.setKeyValuePair(newKey("key1", "key2", "key3"), newItem(tString, "value")), p
|
||||
testAST(t, func() (error, *ast.Document) {
|
||||
p := ast.NewDocument()
|
||||
return p.SetKeyValuePair(ast.NewKey("key1", "key2", "key3"), ast.NewValue(ast.TypeString, "value")), p
|
||||
},
|
||||
"",
|
||||
`{"key1": {"key2": {"key3": "value"}}}`)
|
||||
}
|
||||
|
||||
func Test_StoreDuplicateKeyInRootTable_ReturnsError(t *testing.T) {
|
||||
testAST(t, func() (error, *parser) {
|
||||
p := newParser()
|
||||
p.setKeyValuePair(newKey("key"), newItem(tString, "value"))
|
||||
return p.setKeyValuePair(newKey("key"), newItem(tInteger, 321)), p
|
||||
func Test_StoreDuplicateKeyInRooTypeTable_ReturnsError(t *testing.T) {
|
||||
testAST(t, func() (error, *ast.Document) {
|
||||
p := ast.NewDocument()
|
||||
p.SetKeyValuePair(ast.NewKey("key"), ast.NewValue(ast.TypeString, "value"))
|
||||
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"}`)
|
||||
}
|
||||
|
||||
func Test_StoreValueWithMultipartKey_UnderSubtable_CreatesTableHierarchy(t *testing.T) {
|
||||
testAST(t, func() (error, *parser) {
|
||||
p := newParser()
|
||||
p.openTable(newKey("tablekey1", "tablekey2"))
|
||||
return p.setKeyValuePair(newKey("valuekey1", "valuekey2", "valuekey3"), newItem(tString, "value")), p
|
||||
testAST(t, func() (error, *ast.Document) {
|
||||
p := ast.NewDocument()
|
||||
p.OpenTable(ast.NewKey("tablekey1", "tablekey2"))
|
||||
return p.SetKeyValuePair(ast.NewKey("valuekey1", "valuekey2", "valuekey3"), ast.NewValue(ast.TypeString, "value")), p
|
||||
},
|
||||
"",
|
||||
`{"tablekey1": {"tablekey2": {"valuekey1": {"valuekey2": {"valuekey3": "value"}}}}}`)
|
||||
}
|
||||
|
||||
func Test_StoreKeyPathWherePathContainsNonTableAlready_ReturnsError(t *testing.T) {
|
||||
testAST(t, func() (error, *parser) {
|
||||
p := newParser()
|
||||
p.openTable(newKey("key1"))
|
||||
p.setKeyValuePair(newKey("key2"), newItem(tInteger, 0))
|
||||
return p.setKeyValuePair(newKey("key2", "key3"), newItem(tString, "value")), p
|
||||
testAST(t, func() (error, *ast.Document) {
|
||||
p := ast.NewDocument()
|
||||
p.OpenTable(ast.NewKey("key1"))
|
||||
p.SetKeyValuePair(ast.NewKey("key2"), ast.NewValue(ast.TypeInteger, 0))
|
||||
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}}`)
|
||||
}
|
||||
|
||||
func Test_GivenExistingTableAtKey_CreatingTableAtSameKey_ReturnsError(t *testing.T) {
|
||||
testAST(t, func() (error, *parser) {
|
||||
p := newParser()
|
||||
p.openTable(newKey("key1", "key2"))
|
||||
return p.openTable(newKey("key1", "key2")), p
|
||||
testAST(t, func() (error, *ast.Document) {
|
||||
p := ast.NewDocument()
|
||||
p.OpenTable(ast.NewKey("key1", "key2"))
|
||||
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": {}}}`)
|
||||
}
|
||||
|
||||
func Test_GivenExistingItemAtKey_CreatingTableAtSameKey_ReturnsError(t *testing.T) {
|
||||
testAST(t, func() (error, *parser) {
|
||||
p := newParser()
|
||||
p.setKeyValuePair(newKey("key"), newItem(tString, "value"))
|
||||
return p.openTable(newKey("key")), p
|
||||
testAST(t, func() (error, *ast.Document) {
|
||||
p := ast.NewDocument()
|
||||
p.SetKeyValuePair(ast.NewKey("key"), ast.NewValue(ast.TypeString, "value"))
|
||||
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"}`)
|
||||
}
|
||||
|
||||
func Test_GivenExistingItemInKeyPath_CreatingTable_ReturnsError(t *testing.T) {
|
||||
testAST(t, func() (error, *parser) {
|
||||
p := newParser()
|
||||
p.setKeyValuePair(newKey("key"), newItem(tString, "value"))
|
||||
return p.openTable(newKey("key", "subkey")), p
|
||||
testAST(t, func() (error, *ast.Document) {
|
||||
p := ast.NewDocument()
|
||||
p.SetKeyValuePair(ast.NewKey("key"), ast.NewValue(ast.TypeString, "value"))
|
||||
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"}`)
|
||||
}
|
||||
|
||||
func Test_GivenExistingItemAtDeepKey_CreatingTableAtSameKey_ReturnsError(t *testing.T) {
|
||||
testAST(t, func() (error, *parser) {
|
||||
p := newParser()
|
||||
p.openTable(newKey("deep", "table"))
|
||||
p.setKeyValuePair(newKey("key"), newItem(tInteger, 0))
|
||||
return p.openTable(newKey("deep", "table", "key")), p
|
||||
testAST(t, func() (error, *ast.Document) {
|
||||
p := ast.NewDocument()
|
||||
p.OpenTable(ast.NewKey("deep", "table"))
|
||||
p.SetKeyValuePair(ast.NewKey("key"), ast.NewValue(ast.TypeInteger, 0))
|
||||
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}}}`)
|
||||
}
|
||||
|
||||
func Test_GivenExistingItemAtDeepKeyFromSubTable_CreatingTableAtSameKey_ReturnsError(t *testing.T) {
|
||||
testAST(t, func() (error, *parser) {
|
||||
p := newParser()
|
||||
p.openTable(newKey("deep", "table"))
|
||||
p.setKeyValuePair(newKey("key1", "key2"), newItem(tInteger, 0))
|
||||
return p.setKeyValuePair(newKey("key1", "key2"), newItem(tBoolean, true)), p
|
||||
testAST(t, func() (error, *ast.Document) {
|
||||
p := ast.NewDocument()
|
||||
p.OpenTable(ast.NewKey("deep", "table"))
|
||||
p.SetKeyValuePair(ast.NewKey("key1", "key2"), ast.NewValue(ast.TypeInteger, 0))
|
||||
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
|
||||
// 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]`,
|
||||
// of the currently active table plus the multipart key for SetKeyValuePair().
|
||||
`invalid key/value pair: integer value already exists at key [deep->table->key1->key2]`,
|
||||
`{"deep": {"table": {"key1": {"key2": 0}}}}`)
|
||||
}
|
||||
|
||||
func Test_FormattingOfQuotedPathPartInError(t *testing.T) {
|
||||
testAST(t, func() (error, *parser) {
|
||||
p := newParser()
|
||||
p.openTable(newKey("must be quoted"))
|
||||
p.setKeyValuePair(newKey("this one too"), newItem(tInteger, 0))
|
||||
return p.setKeyValuePair(newKey("this one too"), newItem(tInteger, 0)), p
|
||||
testAST(t, func() (error, *ast.Document) {
|
||||
p := ast.NewDocument()
|
||||
p.OpenTable(ast.NewKey("must be quoted"))
|
||||
p.SetKeyValuePair(ast.NewKey("this one too"), ast.NewValue(ast.TypeInteger, 0))
|
||||
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}}`)
|
||||
}
|
||||
|
||||
func Test_GivenExistingItemAtKey_CreatingArrayOfTablesAtSameKey_ReturnsError(t *testing.T) {
|
||||
testAST(t, func() (error, *parser) {
|
||||
p := newParser()
|
||||
p.Root["key"] = newItem(tString, "value")
|
||||
return p.openArrayOfTables(newKey("key")), p
|
||||
testAST(t, func() (error, *ast.Document) {
|
||||
p := ast.NewDocument()
|
||||
p.Root["key"] = ast.NewValue(ast.TypeString, "value")
|
||||
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"}`)
|
||||
}
|
||||
|
||||
func Test_GivenExistingItemInKeyPath_CreatingArrayOfTables_ReturnsError(t *testing.T) {
|
||||
testAST(t, func() (error, *parser) {
|
||||
p := newParser()
|
||||
p.Root["key"] = newItem(tString, "value")
|
||||
return p.openArrayOfTables(newKey("key", "subkey")), p
|
||||
testAST(t, func() (error, *ast.Document) {
|
||||
p := ast.NewDocument()
|
||||
p.Root["key"] = ast.NewValue(ast.TypeString, "value")
|
||||
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"}`)
|
||||
}
|
||||
|
||||
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()
|
||||
if expectedError == "" {
|
||||
if err != nil {
|
||||
|
|
|
@ -7,30 +7,36 @@ import (
|
|||
"time"
|
||||
)
|
||||
|
||||
// String() produces a JSON-like (but not JSON) string representation of the value.
|
||||
// This string version is useful for testing and debugging purposes.
|
||||
func (parseItem item) String() string {
|
||||
switch parseItem.Type {
|
||||
case tString:
|
||||
return fmt.Sprintf("%q", parseItem.Data[0])
|
||||
case tOffsetDateTime:
|
||||
return parseItem.Data[0].(time.Time).Format(time.RFC3339Nano)
|
||||
case tLocalDateTime:
|
||||
return parseItem.Data[0].(time.Time).Format("2006-01-02 15:04:05.999999999")
|
||||
case tLocalDate:
|
||||
return parseItem.Data[0].(time.Time).Format("2006-01-02")
|
||||
case tLocalTime:
|
||||
return parseItem.Data[0].(time.Time).Format("15:04:05.999999999")
|
||||
case tArrayOfTables:
|
||||
fallthrough
|
||||
case tArray:
|
||||
items := make([]string, len(parseItem.Data))
|
||||
for i, value := range parseItem.Data {
|
||||
items[i] = value.(*item).String()
|
||||
// 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()
|
||||
}
|
||||
return fmt.Sprintf("[%s]", strings.Join(items, ", "))
|
||||
case tTable:
|
||||
pairs := parseItem.Data[0].(table)
|
||||
|
||||
// String() produces a JSON-like (but not JSON) string representation of the value.
|
||||
// This string version is mainly useful for testing and debugging purposes.
|
||||
func (parseItem Value) String() string {
|
||||
switch parseItem.Type {
|
||||
case TypeString:
|
||||
return fmt.Sprintf("%q", parseItem.Data[0])
|
||||
case TypeOffsetDateTime:
|
||||
return parseItem.Data[0].(time.Time).Format(time.RFC3339Nano)
|
||||
case TypeLocalDateTime:
|
||||
return parseItem.Data[0].(time.Time).Format("2006-01-02 15:04:05.999999999")
|
||||
case TypeLocalDate:
|
||||
return parseItem.Data[0].(time.Time).Format("2006-01-02")
|
||||
case TypeLocalTime:
|
||||
return parseItem.Data[0].(time.Time).Format("15:04:05.999999999")
|
||||
case TypeArrayOfTables:
|
||||
fallthrough
|
||||
case TypeArray:
|
||||
values := make(Key, len(parseItem.Data))
|
||||
for i, value := range parseItem.Data {
|
||||
values[i] = value.(*Value).String()
|
||||
}
|
||||
return fmt.Sprintf("[%s]", strings.Join(values, ", "))
|
||||
case TypeTable:
|
||||
pairs := parseItem.Data[0].(Table)
|
||||
keys := make([]string, len(pairs))
|
||||
i := 0
|
||||
for k := range pairs {
|
||||
|
@ -38,11 +44,11 @@ func (parseItem item) String() string {
|
|||
i++
|
||||
}
|
||||
sort.Strings(keys)
|
||||
items := make([]string, len(pairs))
|
||||
values := make([]string, len(pairs))
|
||||
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:
|
||||
return fmt.Sprintf("%v", parseItem.Data[0])
|
||||
}
|
||||
|
|
|
@ -1,28 +1,30 @@
|
|||
package ast
|
||||
package ast_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.makaay.nl/mauricem/go-toml/ast"
|
||||
)
|
||||
|
||||
func Test_StringFormatting(t *testing.T) {
|
||||
testAST(t, func() (error, *parser) {
|
||||
tableData := make(table)
|
||||
tableData["x"] = newItem(tBoolean, true)
|
||||
tableData["y"] = newItem(tInteger, 42)
|
||||
testAST(t, func() (error, *ast.Document) {
|
||||
tableData := make(ast.Table)
|
||||
tableData["x"] = ast.NewValue(ast.TypeBoolean, true)
|
||||
tableData["y"] = ast.NewValue(ast.TypeInteger, 42)
|
||||
dateTime, _ := time.Parse(time.RFC3339Nano, "2003-11-01T01:02:03.999999999+10:00")
|
||||
p := newParser()
|
||||
p.setKeyValuePair(newKey("a"), newItem(tInteger, 1))
|
||||
p.setKeyValuePair(newKey("b"), newItem(tFloat, 2.3))
|
||||
p.setKeyValuePair(newKey("c"), newItem(tBoolean, true))
|
||||
p.setKeyValuePair(newKey("d"), newItem(tString, "foo"))
|
||||
p.setKeyValuePair(newKey("e"), newItem(tArray, newItem(tInteger, 1), newItem(tInteger, 2)))
|
||||
p.setKeyValuePair(newKey("f"), newItem(tTable, tableData))
|
||||
p.setKeyValuePair(newKey("g"), newItem(tOffsetDateTime, dateTime))
|
||||
p.setKeyValuePair(newKey("h"), newItem(tLocalDateTime, dateTime))
|
||||
p.setKeyValuePair(newKey("i"), newItem(tLocalDate, dateTime))
|
||||
p.setKeyValuePair(newKey("j"), newItem(tLocalTime, dateTime))
|
||||
return nil, p
|
||||
doc := ast.NewDocument()
|
||||
doc.SetKeyValuePair(ast.NewKey("a"), ast.NewValue(ast.TypeInteger, 1))
|
||||
doc.SetKeyValuePair(ast.NewKey("b"), ast.NewValue(ast.TypeFloat, 2.3))
|
||||
doc.SetKeyValuePair(ast.NewKey("c"), ast.NewValue(ast.TypeBoolean, true))
|
||||
doc.SetKeyValuePair(ast.NewKey("d"), ast.NewValue(ast.TypeString, "foo"))
|
||||
doc.SetKeyValuePair(ast.NewKey("e"), ast.NewValue(ast.TypeArray, ast.NewValue(ast.TypeInteger, 1), ast.NewValue(ast.TypeInteger, 2)))
|
||||
doc.SetKeyValuePair(ast.NewKey("f"), ast.NewValue(ast.TypeTable, tableData))
|
||||
doc.SetKeyValuePair(ast.NewKey("g"), ast.NewValue(ast.TypeOffsetDateTime, dateTime))
|
||||
doc.SetKeyValuePair(ast.NewKey("h"), ast.NewValue(ast.TypeLocalDateTime, dateTime))
|
||||
doc.SetKeyValuePair(ast.NewKey("i"), ast.NewValue(ast.TypeLocalDate, dateTime))
|
||||
doc.SetKeyValuePair(ast.NewKey("j"), ast.NewValue(ast.TypeLocalTime, dateTime))
|
||||
return nil, doc
|
||||
}, "",
|
||||
`{"a": 1, `+
|
||||
`"b": 2.3, `+
|
||||
|
|
Loading…
Reference in New Issue