this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

encoding/toml: add support for inline tables

Decoding a field is moved to a method, as now it is used for both
top-level key-values as well as key-values inside an inline table.
We will soon use this method for tables with headers too.

While here, add more edge cases for duplicate keys as well as
test cases where the keys are different but in subtle ways.

And also ensure that go-toml's Unmarshal errors on all the cases
where our decoder errors, primarily to ensure that we are identical
in terms of failing on duplicate keys.
This test validation with go-toml might need to be tweaked
if or when our decoder ever starts giving any CUE-specific errors.

Signed-off-by: Daniel Martí <mvdan@mvdan.cc>
Change-Id: Idfb92615c2bbdfd2cdc7fe299990438bdf4eb009
Reviewed-on: https://review.gerrithub.io/c/cue-lang/cue/+/1194758
Reviewed-by: Roger Peppe <rogpeppe@gmail.com>
Unity-Result: CUE porcuepine <cue.porcuepine@gmail.com>
TryBot-Result: CUEcueckoo <cueckoo@cuelang.org>

+156 -48
+72 -37
encoding/toml/decode.go
··· 21 21 import ( 22 22 "fmt" 23 23 "io" 24 + "strconv" 24 25 25 26 toml "github.com/pelletier/go-toml/v2/unstable" 26 27 ··· 118 119 // bar: baz: "value" 119 120 // } 120 121 case toml.KeyValue: 121 - keys := tnode.Key() 122 - curName := string(keys.Node().Data) 123 - curField := &ast.Field{ 124 - Label: &ast.Ident{ 125 - NamePos: token.NoPos.WithRel(token.Newline), 126 - Name: curName, 127 - }, 128 - } 129 - 130 - topField := curField 131 - rootKey := quoteLabelIfNeeded(curName) 132 - 133 - keys.Next() // TODO(mvdan): for some reason the first Next call doesn't count? 134 - for keys.Next() { 135 - nextName := string(keys.Node().Data) 136 - nextField := &ast.Field{ 137 - Label: &ast.Ident{ 138 - NamePos: token.NoPos.WithRel(token.Blank), 139 - Name: nextName, 140 - }, 141 - } 142 - 143 - curField.Value = &ast.StructLit{Elts: []ast.Decl{nextField}} 144 - curField = nextField 145 - // TODO(mvdan): use an append-like API once we have benchmarks 146 - rootKey += "." + quoteLabelIfNeeded(nextName) 147 - } 148 - if d.seenKeys[rootKey] { 149 - return fmt.Errorf("duplicate key: %s", rootKey) 150 - } 151 - d.seenKeys[rootKey] = true 152 - value, err := d.decodeExpr(tnode.Value()) 122 + field, err := d.decodeField("", tnode) 153 123 if err != nil { 154 124 return err 155 125 } 156 - curField.Value = value 157 - d.currentFields = append(d.currentFields, topField) 126 + d.currentFields = append(d.currentFields, field) 158 127 // TODO(mvdan): tables 159 128 // TODO(mvdan): array tables 160 129 default: ··· 171 140 } 172 141 173 142 // nextRootNode is called for every top-level expression from the TOML parser. 174 - func (d *Decoder) decodeExpr(tnode *toml.Node) (ast.Expr, error) { 143 + func (d *Decoder) decodeExpr(rootKey string, tnode *toml.Node) (ast.Expr, error) { 175 144 // TODO(mvdan): we currently assume that TOML basic literals (string, int, float) 176 145 // are also valid CUE literals; we should double check this, perhaps via fuzzing. 177 146 data := string(tnode.Data) ··· 188 157 list := &ast.ListLit{} 189 158 elems := tnode.Children() 190 159 for elems.Next() { 191 - elem, err := d.decodeExpr(elems.Node()) 160 + // A path into an array element is like "arr.3", 161 + // which looks very similar to a table's "tbl.key", 162 + // particularly since a table key can be any string. 163 + // However, we just need these keys to detect duplicates, 164 + // and a path cannot be both an array and table, so it's OK. 165 + rootKey := rootKey + "." + strconv.Itoa(len(list.Elts)) 166 + elem, err := d.decodeExpr(rootKey, elems.Node()) 192 167 if err != nil { 193 168 return nil, err 194 169 } 195 170 list.Elts = append(list.Elts, elem) 196 171 } 197 172 return list, nil 173 + case toml.InlineTable: 174 + strct := &ast.StructLit{ 175 + // We want a single-line struct, just like TOML's inline tables are on a single line. 176 + Lbrace: token.NoPos.WithRel(token.Blank), 177 + Rbrace: token.NoPos.WithRel(token.Blank), 178 + } 179 + elems := tnode.Children() 180 + for elems.Next() { 181 + field, err := d.decodeField(rootKey, elems.Node()) 182 + if err != nil { 183 + return nil, err 184 + } 185 + strct.Elts = append(strct.Elts, field) 186 + } 187 + return strct, nil 198 188 // TODO(mvdan): dates and times 199 - // TODO(mvdan): inline tables 200 189 default: 201 190 return nil, fmt.Errorf("encoding/toml.Decoder.decodeExpr: unknown %s %#v\n", tnode.Kind, tnode) 202 191 } 203 192 } 193 + 194 + func (d *Decoder) decodeField(rootKey string, tnode *toml.Node) (*ast.Field, error) { 195 + keys := tnode.Key() 196 + curName := string(keys.Node().Data) 197 + 198 + // If we are decoding a top-level field, it follows a newline. 199 + // Otherwise, we are in an inline table, and it goes on the same line. 200 + relPos := token.Newline 201 + if rootKey != "" { 202 + rootKey += "." 203 + relPos = token.Blank 204 + } 205 + rootKey += quoteLabelIfNeeded(curName) 206 + curField := &ast.Field{ 207 + Label: &ast.Ident{ 208 + NamePos: token.NoPos.WithRel(relPos), 209 + Name: curName, 210 + }, 211 + } 212 + 213 + topField := curField 214 + keys.Next() // TODO(mvdan): for some reason the first Next call doesn't count? 215 + for keys.Next() { 216 + nextName := string(keys.Node().Data) 217 + nextField := &ast.Field{ 218 + Label: &ast.Ident{ 219 + NamePos: token.NoPos.WithRel(token.Blank), 220 + Name: nextName, 221 + }, 222 + } 223 + curField.Value = &ast.StructLit{Elts: []ast.Decl{nextField}} 224 + curField = nextField 225 + // TODO(mvdan): use an append-like API once we have benchmarks 226 + rootKey += "." + quoteLabelIfNeeded(nextName) 227 + } 228 + if d.seenKeys[rootKey] { 229 + return nil, fmt.Errorf("duplicate key: %s", rootKey) 230 + } 231 + d.seenKeys[rootKey] = true 232 + value, err := d.decodeExpr(rootKey, tnode.Value()) 233 + if err != nil { 234 + return nil, err 235 + } 236 + curField.Value = value 237 + return topField, nil 238 + }
+84 -11
encoding/toml/decode_test.go
··· 120 120 site: "foo.com": title: "foo bar" 121 121 `, 122 122 }, { 123 - name: "RootKeysDuplicate", 123 + name: "KeysDuplicateSimple", 124 124 input: ` 125 - foo = "same value" 126 - foo = "same value" 125 + foo = "same key" 126 + foo = "same key" 127 + `, 128 + wantErr: `duplicate key: foo`, 129 + }, { 130 + name: "KeysDuplicateQuoted", 131 + input: ` 132 + "foo" = "same key" 133 + foo = "same key" 127 134 `, 128 135 wantErr: `duplicate key: foo`, 136 + }, { 137 + name: "KeysDuplicateWhitespace", 138 + input: ` 139 + foo . bar = "same key" 140 + foo.bar = "same key" 141 + `, 142 + wantErr: `duplicate key: foo\.bar`, 143 + }, { 144 + name: "KeysDuplicateDots", 145 + input: ` 146 + foo."bar.baz".zzz = "same key" 147 + foo."bar.baz".zzz = "same key" 148 + `, 149 + wantErr: `duplicate key: foo\."bar\.baz"\.zzz`, 150 + }, { 151 + name: "KeysNotDuplicateDots", 152 + input: ` 153 + foo."bar.baz" = "different key" 154 + "foo.bar".baz = "different key" 155 + `, 156 + wantCUE: ` 157 + foo: "bar.baz": "different key" 158 + "foo.bar": baz: "different key" 159 + `, 129 160 }, { 130 161 name: "BasicStrings", 131 162 input: ` ··· 258 289 }, { 259 290 name: "Arrays", 260 291 input: ` 261 - integers = [ 1, 2, 3 ] 262 - colors = [ "red", "yellow", "green" ] 263 - nested_ints = [ [ 1, 2 ], [3, 4, 5] ] 264 - nested_mixed = [ [ 1, 2 ], ["a", "b", "c"] ] 265 - strings = [ "all", 'strings', """are the same""", '''type''' ] 266 - mixed_numbers = [ 0.1, 0.2, 0.5, 1, 2, 5 ] 292 + integers = [1, 2, 3] 293 + colors = ["red", "yellow", "green"] 294 + nested_ints = [[1, 2], [3, 4, 5]] 295 + nested_mixed = [[1, 2], ["a", "b", "c"], {extra = "keys"}] 296 + strings = ["all", 'strings', """are the same""", '''type'''] 297 + mixed_numbers = [0.1, 0.2, 0.5, 1, 2, 5] 267 298 `, 268 299 wantCUE: ` 269 300 integers: [1, 2, 3] 270 301 colors: ["red", "yellow", "green"] 271 302 nested_ints: [[1, 2], [3, 4, 5]] 272 - nested_mixed: [[1, 2], ["a", "b", "c"]] 303 + nested_mixed: [[1, 2], ["a", "b", "c"], {extra: "keys"}] 273 304 strings: ["all", "strings", "are the same", "type"] 274 305 mixed_numbers: [0.1, 0.2, 0.5, 1, 2, 5] 275 306 `, 307 + }, { 308 + name: "InlineTables", 309 + input: ` 310 + point = {x = 1, y = 2} 311 + animal = {type.name = "pug"} 312 + deep = {l1 = {l2 = {l3 = "leaf"}}} 313 + `, 314 + wantCUE: ` 315 + point: {x: 1, y: 2} 316 + animal: {type: name: "pug"} 317 + deep: {l1: {l2: {l3: "leaf"}}} 318 + `, 319 + }, { 320 + name: "InlineTablesDuplicate", 321 + input: ` 322 + point = {x = "same key", x = "same key"} 323 + `, 324 + wantErr: `duplicate key: point\.x`, 325 + }, { 326 + name: "ArrayInlineTablesDuplicate", 327 + input: ` 328 + point = [{}, {}, {x = "same key", x = "same key"}] 329 + `, 330 + wantErr: `duplicate key: point\.2\.x`, 331 + }, { 332 + name: "InlineTablesNotDuplicateScoping", 333 + input: ` 334 + repeat = {repeat = {repeat = "leaf"}} 335 + struct1 = {sibling = "leaf"} 336 + struct2 = {sibling = "leaf"} 337 + arrays = [{sibling = "leaf"}, {sibling = "leaf"}] 338 + `, 339 + wantCUE: ` 340 + repeat: {repeat: {repeat: "leaf"}} 341 + struct1: {sibling: "leaf"} 342 + struct2: {sibling: "leaf"} 343 + arrays: [{sibling: "leaf"}, {sibling: "leaf"}] 344 + `, 276 345 }} 277 346 for _, test := range tests { 278 347 test := test ··· 287 356 qt.Assert(t, qt.IsNil(node)) 288 357 // We don't continue, so we can't expect any decoded CUE. 289 358 qt.Assert(t, qt.Equals(test.wantCUE, "")) 359 + 360 + // Validate that go-toml's Unmarshal also rejects this input. 361 + err = gotoml.Unmarshal([]byte(test.input), new(any)) 362 + qt.Assert(t, qt.IsNotNil(err)) 290 363 return 291 364 } 292 365 qt.Assert(t, qt.IsNil(err)) ··· 311 384 qt.Assert(t, qt.IsNil(val.Validate())) 312 385 313 386 // Validate that the decoded CUE value is equivalent 314 - // to the Go value that a direct TOML unmarshal produces. 387 + // to the Go value that go-toml's Unmarshal produces. 315 388 // We use JSON equality as some details such as which integer types are used 316 389 // are not actually relevant to an "equal data" check. 317 390 var unmarshalTOML any