From fd9365b842b05626203cc1a363781cfee2a6fe55 Mon Sep 17 00:00:00 2001 From: Maurice Makaay Date: Wed, 26 Jun 2019 12:54:04 +0000 Subject: [PATCH] Implemented inline tables + tests. --- keyvaluepair.go | 6 +- keyvaluepair_test.go | 9 ++ value.go | 3 + value_datetime.go | 5 +- value_table.go | 239 ++++++++++++++++++++++++++----------------- value_table_test.go | 32 ++++++ value_test.go | 83 +++++++-------- 7 files changed, 238 insertions(+), 139 deletions(-) diff --git a/keyvaluepair.go b/keyvaluepair.go index 26550f1..070f019 100644 --- a/keyvaluepair.go +++ b/keyvaluepair.go @@ -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: diff --git a/keyvaluepair_test.go b/keyvaluepair_test.go index bc32bd1..f0ca46e 100644 --- a/keyvaluepair_test.go +++ b/keyvaluepair_test.go @@ -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) diff --git a/value.go b/value.go index 00b325e..86d955c 100644 --- a/value.go +++ b/value.go @@ -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 diff --git a/value_datetime.go b/value_datetime.go index 4666a25..48a4e0f 100644 --- a/value_datetime.go +++ b/value_datetime.go @@ -83,10 +83,9 @@ 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 } + 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. diff --git a/value_table.go b/value_table.go index 92017fb..b9dce02 100644 --- a/value_table.go +++ b/value_table.go @@ -5,107 +5,18 @@ 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)) - // 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 - // table without any key/value pairs will be considered an empty table. - // - // [[products]] - // name = "Hammer" - // sku = 738594937 - // - // [[products]] - // - // [[products]] - // name = "Nail" - // sku = 284758393 - // color = "gray" - // - // You can create nested arrays of tables as well. Just use the same double - // bracket syntax on sub-tables. Each double-bracketed sub-table will belong - // to the most recently defined table element above it. - // - // [[fruit]] - // name = "apple" - // - // [fruit.physical] - // color = "red" - // shape = "round" - // - // [[fruit.variety]] - // name = "red delicious" - // - // [[fruit.variety]] - // name = "granny smith" - // - // [[fruit]] - // name = "banana" - // - // [[fruit.variety]] - // name = "plantain" + // 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)) - // Inline tables provide a more compact syntax for expressing tables. - // They are especially useful for grouped data that can otherwise quickly - // become verbose. Inline tables are enclosed in curly braces { and }. - // Within the braces, zero or more comma separated key/value pairs may appear. - // Key/value pairs take the same form as key/value pairs in standard tables. - // All value types are allowed, including inline tables. - // - // Inline tables are intended to appear on a single line. No newlines are - // allowed between the curly braces unless they are valid within a value. - // Even so, it is strongly discouraged to break an inline table onto multiple - // lines. If you find yourself gripped with this desire, it means you should - // be using standard tables. - // - // 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)) + // 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) { @@ -119,6 +30,44 @@ func (t *parser) startTable(p *parse.API) { } } +// 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 +// table without any key/value pairs will be considered an empty table. +// +// [[products]] +// name = "Hammer" +// sku = 738594937 +// +// [[products]] +// +// [[products]] +// name = "Nail" +// sku = 284758393 +// color = "gray" +// +// You can create nested arrays of tables as well. Just use the same double +// bracket syntax on sub-tables. Each double-bracketed sub-table will belong +// to the most recently defined table element above it. +// +// [[fruit]] +// name = "apple" +// +// [fruit.physical] +// color = "red" +// shape = "round" +// +// [[fruit.variety]] +// name = "red delicious" +// +// [[fruit.variety]] +// name = "granny smith" +// +// [[fruit]] +// name = "banana" +// +// [[fruit.variety]] +// name = "plantain" func (t *parser) startArrayOfTables(p *parse.API) { if key, ok := t.parseKey(p, []string{}); ok { if !p.Accept(tableArrayClose) { @@ -133,6 +82,44 @@ func (t *parser) startArrayOfTables(p *parse.API) { } } +// 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) { @@ -146,3 +133,65 @@ func (t *parser) startPlainTable(p *parse.API) { 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 +// become verbose. Inline tables are enclosed in curly braces { and }. +// Within the braces, zero or more comma separated key/value pairs may appear. +// Key/value pairs take the same form as key/value pairs in standard tables. +// All value types are allowed, including inline tables. +// +// Inline tables are intended to appear on a single line. No newlines are +// allowed between the curly braces unless they are valid within a value. +// Even so, it is strongly discouraged to break an inline table onto multiple +// lines. If you find yourself gripped with this desire, it means you should +// be using standard tables. +// +// name = { first = "Tom", last = "Preston-Werner" } +// point = { x = 1, y = 2 } +// animal = { type.name = "pug" } +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 + } + + subt := newParser() + + // Check for an empty inline table. + if p.Accept(inlineTableClose) { + return newItem(tTable, subt.Root), true + } + + // 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 nil, false + } + + // 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 + } + } +} diff --git a/value_table_test.go b/value_table_test.go index f9f0c6c..1865624 100644 --- a/value_table_test.go +++ b/value_table_test.go @@ -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) + } +} diff --git a/value_test.go b/value_test.go index f141074..f086d8a 100644 --- a/value_test.go +++ b/value_test.go @@ -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) + } +}