Implemented inline tables + tests.

This commit is contained in:
Maurice Makaay 2019-06-26 12:54:04 +00:00
parent 97153fc806
commit fd9365b842
7 changed files with 238 additions and 139 deletions

View File

@ -49,7 +49,11 @@ func (t *parser) startKeyValuePair(p *parse.API) {
key, ok := t.parseKey(p, []string{})
if ok && p.Handle(t.startAssignment) {
if value, ok := t.parseValue(p); ok {
t.setKeyValuePair(key, value)
err := t.setKeyValuePair(key, value)
if err != nil {
p.Error("%s", err)
return
}
}
}
default:

View File

@ -48,6 +48,7 @@ func TestKeyValuePair(t *testing.T) {
{`another = "# This is not a comment"`, `{"another": "# This is not a comment"}`, ``},
{"key1=\"value1\"key2=\"value2\"\r\nkey3a.key3b=\"value3\"", `{"key1": "value1", "key2": "value2", "key3a": {"key3b": "value3"}}`, ``},
{"with=\"comments\"# boring \nanother.cool =\"one\" \t # to the end\r\n", `{"another": {"cool": "one"}, "with": "comments"}`, ``},
{"key='value'\nkey='another value'", `{"key": "value"}`, `invalid key/value pair: string item already exists at key [key] at line 2, column 20`},
} {
p := newParser()
testParse(t, p, p.startKeyValuePair, test)
@ -129,6 +130,14 @@ func TestKeyValuePair_ExamplesFromSpecification(t *testing.T) {
{`arr6 = [ 1, 2.0 ] # INVALID`, `{}`, `type mismatch in array of integers: found an item of type float at line 1, column 16`},
{"arr7 = [\n 1, 2, 3\n]", `{"arr7": [1, 2, 3]}`, ``},
{"arr8 = [\n 1,\n 2, # this is ok\n]", `{"arr8": [1, 2]}`, ``},
{`name = { first = "Tom", last = "Preston-Werner" }`,
`{"name": {"first": "Tom", "last": "Preston-Werner"}}`, ``},
{`point = { x = 1, y = 2 }`,
`{"point": {"x": 1, "y": 2}}`, ``},
{`animal = { type.name = "pug" }`,
`{"animal": {"type": {"name": "pug"}}}`, ``},
{"points = [ { x = 1, y = 2, z = 3 },\n { x = 7, y = 8, z = 9 },\n { x = 2, y = 4, z = 8 } ]",
`{"points": [{"x": 1, "y": 2, "z": 3}, {"x": 7, "y": 8, "z": 9}, {"x": 2, "y": 4, "z": 8}]}`, ``},
} {
p := newParser()
testParse(t, p, p.startKeyValuePair, test)

View File

@ -11,6 +11,7 @@ var (
detectDateTime = a.Digits.Then(a.Minus.Or(a.Colon))
detectNumber = a.Digit
detectArray = a.SquareOpen
detectInlineTable = a.CurlyOpen
)
// Values must be of the following types: String, Integer, Float, Boolean,
@ -29,6 +30,8 @@ func (t *parser) parseValue(p *parse.API) (*item, bool) {
return t.parseNumber(p)
case p.Peek(detectArray):
return t.parseArray(p)
case p.Peek(detectInlineTable):
return t.parseInlineTable(p)
default:
p.Expected("a value")
return nil, false

View File

@ -83,11 +83,10 @@ func (t *parser) parseDateTime(p *parse.API) (*item, bool) {
input, value, err := getDateTimeValue(&tokens)
if err == nil {
return newItem(valueType, value), true
} else {
}
p.Error("invalid date/time value %s: %s", input, err)
return nil, false
}
}
// The first token is a token that wraps the complete date/time input.
// Its type denotes the type of date/time value that it wraps.

View File

@ -5,48 +5,31 @@ import (
)
var (
// Tables (also known as hash tables or dictionaries) are collections of
// key/value pairs. They appear in square brackets on a line by themselves.
// You can tell them apart from arrays because arrays are only ever values.
//
// Under that, and until the next table or EOF are the key/values of that
// table. Key/value pairs within tables are not guaranteed to be in any
// specific order.
//
// [table-1]
// key1 = "some string"
// key2 = 123
//
// [table-2]
// key1 = "another string"
// key2 = 456
//
// Naming rules for tables are the same as for keys.
//
// [dog."tater.man"]
// type.name = "pug"
//
// Whitespace around the key is ignored, however, best practice is to not
// use any extraneous whitespace.
//
// [a.b.c] # this is best practice
// [ d.e.f ] # same as [d.e.f]
// [ g . h . i ] # same as [g.h.i]
// [ j . "ʞ" . 'l' ] # same as [j."ʞ".'l']
//
// You don't need to specify all the super-tables if you don't want to.
// TOML knows how to do it for you.
//
// # [x] you
// # [x.y] don't
// # [x.y.z] need these
// [x.y.z.w] # for this to work
//
// Empty tables are allowed and simply have no key/value pairs within them.
//
// Opener and closer for [table].
tableOpen = c.Seq(dropBlanks, a.SquareOpen, dropBlanks)
tableClose = c.Seq(dropBlanks, a.SquareClose, dropBlanks, a.EndOfLine.Or(comment))
// Opener and closer for [[array.of.tables]].
tableArrayOpen = c.Seq(dropBlanks, a.SquareOpen, a.SquareOpen, dropBlanks)
tableArrayClose = c.Seq(dropBlanks, a.SquareClose, a.SquareClose, dropBlanks, a.EndOfLine.Or(comment))
// Opener, separator and closer for { inline: "tables" }.
inlineTableOpen = c.Seq(dropBlanks, a.CurlyOpen, dropBlanks)
inlineTableSeparator = c.Seq(dropBlanks, a.Comma, dropBlanks)
inlineTableClose = c.Seq(dropBlanks, a.CurlyClose, dropBlanks)
)
func (t *parser) startTable(p *parse.API) {
switch {
case p.Accept(tableArrayOpen):
p.Handle(t.startArrayOfTables)
case p.Accept(tableOpen):
p.Handle(t.startPlainTable)
default:
p.Expected("a table")
}
}
// Arrays of tables can be expressed by using a table name in double brackets.
// Each table with the same double bracketed name will be an element in the
// array. The tables are inserted in the order encountered. A double bracketed
@ -85,8 +68,71 @@ var (
//
// [[fruit.variety]]
// name = "plantain"
tableArrayOpen = c.Seq(dropBlanks, a.SquareOpen, a.SquareOpen, dropBlanks)
tableArrayClose = c.Seq(dropBlanks, a.SquareClose, a.SquareClose, dropBlanks, a.EndOfLine.Or(comment))
func (t *parser) startArrayOfTables(p *parse.API) {
if key, ok := t.parseKey(p, []string{}); ok {
if !p.Accept(tableArrayClose) {
p.Expected("closing ']]' for array of tables name")
return
}
if err := t.openArrayOfTables(key); err != nil {
p.Error("%s", err)
return
}
p.Handle(t.startKeyValuePair)
}
}
// Tables (also known as hash tables or dictionaries) are collections of
// key/value pairs. They appear in square brackets on a line by themselves.
// You can tell them apart from arrays because arrays are only ever values.
//
// Under that, and until the next table or EOF are the key/values of that
// table. Key/value pairs within tables are not guaranteed to be in any
// specific order.
//
// [table-1]
// key1 = "some string"
// key2 = 123
//
// [table-2]
// key1 = "another string"
// key2 = 456
//
// Naming rules for tables are the same as for keys.
//
// [dog."tater.man"]
// type.name = "pug"
//
// Whitespace around the key is ignored, however, best practice is to not
// use any extraneous whitespace.
//
// [a.b.c] # this is best practice
// [ d.e.f ] # same as [d.e.f]
// [ g . h . i ] # same as [g.h.i]
// [ j . "ʞ" . 'l' ] # same as [j."ʞ".'l']
//
// You don't need to specify all the super-tables if you don't want to.
// TOML knows how to do it for you.
//
// # [x] you
// # [x.y] don't
// # [x.y.z] need these
// [x.y.z.w] # for this to work
//
// Empty tables are allowed and simply have no key/value pairs within them.
func (t *parser) startPlainTable(p *parse.API) {
if key, ok := t.parseKey(p, []string{}); ok {
if !p.Accept(tableClose) {
p.Expected("closing ']' for table name")
return
}
if err := t.openTable(key); err != nil {
p.Error("%s", err)
return
}
p.Handle(t.startKeyValuePair)
}
}
// Inline tables provide a more compact syntax for expressing tables.
// They are especially useful for grouped data that can otherwise quickly
@ -104,45 +150,48 @@ var (
// name = { first = "Tom", last = "Preston-Werner" }
// point = { x = 1, y = 2 }
// animal = { type.name = "pug" }
inlineTableOpen = c.Seq(dropBlanks, a.CurlyOpen, dropBlanks)
inlineTableClose = c.Seq(dropBlanks, a.CurlyClose, dropBlanks, a.EndOfLine.Or(comment))
)
func (t *parser) startTable(p *parse.API) {
switch {
case p.Accept(tableArrayOpen):
p.Handle(t.startArrayOfTables)
case p.Accept(tableOpen):
p.Handle(t.startPlainTable)
default:
p.Expected("a table")
}
func (t *parser) parseInlineTable(p *parse.API) (*item, bool) {
// Check for the start of the array.
if !p.Accept(inlineTableOpen) {
p.Expected("an inline table")
return nil, false
}
func (t *parser) startArrayOfTables(p *parse.API) {
if key, ok := t.parseKey(p, []string{}); ok {
if !p.Accept(tableArrayClose) {
p.Expected("closing ']]' for array of tables name")
return
subt := newParser()
// Check for an empty inline table.
if p.Accept(inlineTableClose) {
return newItem(tTable, subt.Root), true
}
if err := t.openArrayOfTables(key); err != nil {
// Not an empty table, parse the table data.
for {
key, ok := subt.parseKey(p, []string{})
if !ok {
return nil, false
}
if !p.Handle(subt.startAssignment) {
return nil, false
}
value, ok := subt.parseValue(p)
if !ok {
return nil, false
}
err := subt.setKeyValuePair(key, value)
if err != nil {
p.Error("%s", err)
return
}
p.Handle(t.startKeyValuePair)
}
return nil, false
}
func (t *parser) startPlainTable(p *parse.API) {
if key, ok := t.parseKey(p, []string{}); ok {
if !p.Accept(tableClose) {
p.Expected("closing ']' for table name")
return
// Check for the end of the inline table.
if p.Accept(inlineTableClose) {
return newItem(tTable, subt.Root), true
}
// Not the end of the inline table? Then we should find a key/value pair separator.
if !p.Accept(inlineTableSeparator) {
p.Expected("an array separator")
return nil, false
}
if err := t.openTable(key); err != nil {
p.Error("%s", err)
return
}
p.Handle(t.startKeyValuePair)
}
}

View File

@ -81,3 +81,35 @@ func TestArrayOfTables(t *testing.T) {
testParse(t, p, p.startTable, test)
}
}
func TestStartInlineTable(t *testing.T) {
parser := newParser()
wrapper := func(p *parse.API) { parser.parseInlineTable(p) }
testParse(t, parser, wrapper, parseTest{"(not an inline table)", "{}", "unexpected input (expected an inline table) at start of file"})
}
func TestInlineTable(t *testing.T) {
for _, test := range []parseTest{
{"x={}", `{"x": {}}`, ``},
{"x={} # comments", `{"x": {}}`, ``},
{"x={ # comments }", `{}`, `unexpected input (expected a key name) at line 1, column 5`},
{"x={a = 1, b\t=2}", `{"x": {"a": 1, "b": 2}}`, ``},
{"x={a='string', b=\"values\"}", `{"x": {"a": "string", "b": "values"}}`, ``},
{"x={a={}}", `{"x": {"a": {}}}`, ``},
{"x={a=[{}]}", `{"x": {"a": [{}]}}`, ``},
{"x=[{b=1},{b=2},{b=3}]", `{"x": [{"b": 1}, {"b": 2}, {"b": 3}]}`, ``},
{"x={a=[{b=1},{b=2},{b=3}]}", `{"x": {"a": [{"b": 1}, {"b": 2}, {"b": 3}]}}`, ``},
{"x={", `{}`, `unexpected end of file (expected a key name) at line 1, column 4`},
{"x={a", `{}`, `unexpected end of file (expected a value assignment) at line 1, column 5`},
{"x={a=", `{}`, `unexpected end of file (expected a value) at line 1, column 6`},
{"x={a=,", `{}`, `unexpected input (expected a value) at line 1, column 6`},
{"x={a=1", `{}`, `unexpected end of file (expected an array separator) at line 1, column 7`},
{"x={a=1,", `{}`, `unexpected end of file (expected a key name) at line 1, column 8`},
{"x={a=1,}", `{}`, `unexpected input (expected a key name) at line 1, column 8`},
{"x={a=1,a=2}", `{}`, `invalid key/value pair: integer item already exists at key [a] at line 1, column 11`},
{"x={a={b=1,b=2}}", `{}`, `invalid key/value pair: integer item already exists at key [b] at line 1, column 14`},
} {
p := newParser()
testParse(t, p, p.startKeyValuePair, test)
}
}

View File

@ -1,42 +1,45 @@
package toml
// func TestValue(t *testing.T) {
// for _, test := range []parseTest{
// {``, []string{`Error: unexpected end of file (expected a value) at start of file`}},
// {`"basic s\tring value"`, []string{`"basic s\tring value"`}},
// {`'literal s\tring value'`, []string{`"literal s\\tring value"`}},
// {"\"\"\"basic multi-line\nstring value\"\"\"", []string{`"basic multi-line\nstring value"`}},
// {"'''literal multi-line\nstring value'''", []string{`"literal multi-line\nstring value"`}},
// {"true", []string{`true`}},
// {"false", []string{`false`}},
// {"0", []string{`0`}},
// {"+0", []string{`0`}},
// {"-0", []string{`0`}},
// {"0.0", []string{`0`}},
// {"+0.0", []string{`0`}},
// {"-0.0", []string{`-0`}},
// {"1234", []string{`1234`}},
// {"-1234", []string{`-1234`}},
// {"+9_8_7.6_5_4e-321", []string{`9.8765e-319`}},
// {"-1_234.5678e-33", []string{`-1.2345678e-30`}},
// {"inf", []string{`+Inf`}},
// {"+inf", []string{`+Inf`}},
// {"-inf", []string{`-Inf`}},
// {"nan", []string{`NaN`}},
// {"+nan", []string{`NaN`}},
// {"-nan", []string{`NaN`}},
// {"2019-06-19", []string{`2019-06-19`}},
// {"08:38:54", []string{`08:38:54`}},
// {"08:38:54.8765487654876", []string{`08:38:54.876548765`}},
// {"2019-06-19 08:38:54", []string{`2019-06-19 08:38:54`}},
// {"2019-06-19T08:38:54", []string{`2019-06-19 08:38:54`}},
// {"2019-06-19T08:38:54.88888", []string{`2019-06-19 08:38:54.88888`}},
// {"1979-05-27T07:32:00Z", []string{`1979-05-27T07:32:00Z`}},
// {"1979-05-27T00:32:00-07:00", []string{`1979-05-27T00:32:00-07:00`}},
// {"1979-05-27T00:32:00.999999-07:00", []string{`1979-05-27T00:32:00.999999-07:00`}},
// {"[1,2,3]", []string{`[1, 2, 3]`}},
// } {
// p := newParser()
// testParseHandler(t, p, p.startValue, test)
// }
// }
import "testing"
func TestValue(t *testing.T) {
for _, test := range []parseTest{
{`x=`, `{}`, `unexpected end of file (expected a value) at line 1, column 3`},
{`x="basic s\tring value"`, `{"x": "basic s\tring value"}`, ``},
{`x='literal s\tring value'`, `{"x": "literal s\\tring value"}`, ``},
{"x=\"\"\"basic multi-line\nstring value\"\"\"", `{"x": "basic multi-line\nstring value"}`, ``},
{"x='''literal multi-line\nstring value'''", `{"x": "literal multi-line\nstring value"}`, ``},
{"x=true", `{"x": true}`, ``},
{"x=false", `{"x": false}`, ``},
{"x=0", `{"x": 0}`, ``},
{"x=+0", `{"x": 0}`, ``},
{"x=-0", `{"x": 0}`, ``},
{"x=0.0", `{"x": 0}`, ``},
{"x=+0.0", `{"x": 0}`, ``},
{"x=-0.0", `{"x": -0}`, ``},
{"x=1234", `{"x": 1234}`, ``},
{"x=-1234", `{"x": -1234}`, ``},
{"x=+9_8_7.6_5_4e-321", `{"x": 9.8765e-319}`, ``},
{"x=-1_234.5678e-33", `{"x": -1.2345678e-30}`, ``},
{"x=inf", `{"x": +Inf}`, ``},
{"x=+inf", `{"x": +Inf}`, ``},
{"x=-inf", `{"x": -Inf}`, ``},
{"x=nan", `{"x": NaN}`, ``},
{"x=+nan", `{"x": NaN}`, ``},
{"x=-nan", `{"x": NaN}`, ``},
{"x=2019-06-19", `{"x": 2019-06-19}`, ``},
{"x=08:38:54", `{"x": 08:38:54}`, ``},
{"x=08:38:54.8765487654876", `{"x": 08:38:54.876548765}`, ``},
{"x=2019-06-19 08:38:54", `{"x": 2019-06-19 08:38:54}`, ``},
{"x=2019-06-19T08:38:54", `{"x": 2019-06-19 08:38:54}`, ``},
{"x=2019-06-19T08:38:54.88888", `{"x": 2019-06-19 08:38:54.88888}`, ``},
{"x=1979-05-27T07:32:00Z", `{"x": 1979-05-27T07:32:00Z}`, ``},
{"x=1979-05-27T00:32:00-07:00", `{"x": 1979-05-27T00:32:00-07:00}`, ``},
{"x=1979-05-27T00:32:00.999999-07:00", `{"x": 1979-05-27T00:32:00.999999-07:00}`, ``},
{"x=[1,2,3]", `{"x": [1, 2, 3]}`, ``},
{"x={1=1,2=2,3=3}", `{"x": {"1": 1, "2": 2, "3": 3}}`, ``},
} {
p := newParser()
testParse(t, p, p.startKeyValuePair, test)
}
}