Skip to content

Commit adf84fd

Browse files
committed
encoding/toml: validate the resulting value against toml.Unmarshal
Given some input TOML, we can use our new encoding/toml.Decoder to obtain an ast.Node and then compile that to a cue.Value. Similarly, we can use go-toml's Unmarshal API on the same input TOML to obtain a Go value directly. Those two values should be equivalent, which we check via qt.JSONEquals. Two test cases fail this new check: the one involving duplicate keys, which is a known bug in our decoder, and the pair of tests involving empty TOML input, which we decoded as a CUE null. It seems like all TOML decoders decode empty input as an empty struct, and the TOML spec also hints that way as it allows empty tables which "simply have no key/value pairs within them". Moreover, since we create an ast.File, it seems best to leave it empty, which equals an empty struct, rather than add a "null" embedding. While here, add a TODO to remind myself to revisit literal decoding. Updates #68. Signed-off-by: Daniel Martí <[email protected]> Change-Id: I83e34b939f1c2dd3b7928e14076f69c39e5054e0 Reviewed-on: https://review.gerrithub.io/c/cue-lang/cue/+/1194706 Unity-Result: CUE porcuepine <[email protected]> TryBot-Result: CUEcueckoo <[email protected]> Reviewed-by: Roger Peppe <[email protected]>
1 parent 5472c4b commit adf84fd

File tree

2 files changed

+42
-11
lines changed

2 files changed

+42
-11
lines changed

encoding/toml/decode.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ func (d *Decoder) Decode() (ast.Node, error) {
6868
return nil, err
6969
}
7070
d.parser.Reset(data)
71+
// Note that if the input is empty the result will be the same
72+
// as for an empty table: an empty struct.
73+
// The TOML spec and other decoders also work this way.
7174
file := &ast.File{}
7275
for d.parser.NextExpression() {
7376
if err := d.nextRootNode(d.parser.Expression()); err != nil {
@@ -78,10 +81,6 @@ func (d *Decoder) Decode() (ast.Node, error) {
7881
file.Decls = append(file.Decls, field)
7982
}
8083
d.currentFields = d.currentFields[:0]
81-
if len(file.Decls) == 0 {
82-
// Empty inputs are decoded as null, much like JSON or YAML.
83-
file.Decls = append(file.Decls, ast.NewNull())
84-
}
8584
return file, nil
8685
}
8786

@@ -147,6 +146,8 @@ func (d *Decoder) nextRootNode(tnode *toml.Node) error {
147146

148147
// nextRootNode is called for every top-level expression from the TOML parser.
149148
func (d *Decoder) decodeExpr(tnode *toml.Node) (ast.Expr, error) {
149+
// TODO(mvdan): we currently assume that TOML basic literals (string, int, float)
150+
// are also valid CUE literals; we should double check this, perhaps via fuzzing.
150151
data := string(tnode.Data)
151152
switch tnode.Kind {
152153
case toml.String:

encoding/toml/decode_test.go

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,18 @@
1515
package toml_test
1616

1717
import (
18+
"encoding/json"
1819
"io"
1920
"strings"
2021
"testing"
2122

23+
"github.com/go-quicktest/qt"
24+
gotoml "github.com/pelletier/go-toml/v2"
25+
26+
"cuelang.org/go/cue/ast"
27+
"cuelang.org/go/cue/cuecontext"
2228
"cuelang.org/go/cue/format"
2329
"cuelang.org/go/encoding/toml"
24-
"github.com/go-quicktest/qt"
2530
)
2631

2732
func TestDecoder(t *testing.T) {
@@ -36,13 +41,13 @@ func TestDecoder(t *testing.T) {
3641
}{{
3742
name: "Empty",
3843
input: "",
39-
want: "null",
44+
want: "",
4045
}, {
4146
name: "LoneComment",
4247
input: `
4348
# Just a comment
4449
`,
45-
want: "null",
50+
want: "",
4651
}, {
4752
name: "RootKeysOne",
4853
input: `
@@ -288,10 +293,35 @@ line two.\
288293
qt.Assert(t, qt.IsNil(err))
289294
qt.Assert(t, qt.Equals(string(formatted), string(wantFormatted)))
290295

291-
// TODO(mvdan): validate that the decoded CUE values are equivalent
292-
// to the Go values that a direct TOML unmarshal would produce.
293-
// For example, compare JSON equality between the CUE encoded as JSON
294-
// and the TOML decoded into `any` and encoded as JSON.
296+
// Ensure that the CUE node can be compiled into a cue.Value and validated.
297+
ctx := cuecontext.New()
298+
// TODO(mvdan): cue.Context can only build ast.Expr or ast.File, not ast.Node;
299+
// it's then likely not the right choice for the interface to return ast.Node.
300+
val := ctx.BuildFile(node.(*ast.File))
301+
qt.Assert(t, qt.IsNil(val.Err()))
302+
qt.Assert(t, qt.IsNil(val.Validate()))
303+
304+
// See the TODO above; go-toml rejects duplicate keys per the spec,
305+
// but our decoder does not yet.
306+
if test.name == "RootKeysDuplicate" {
307+
return
308+
}
309+
310+
// Validate that the decoded CUE value is equivalent
311+
// to the Go value that a direct TOML unmarshal produces.
312+
// We use JSON equality as some details such as which integer types are used
313+
// are not actually relevant to an "equal data" check.
314+
var unmarshalTOML any
315+
err = gotoml.Unmarshal([]byte(test.input), &unmarshalTOML)
316+
qt.Assert(t, qt.IsNil(err))
317+
jsonTOML, err := json.Marshal(unmarshalTOML)
318+
qt.Assert(t, qt.IsNil(err))
319+
t.Logf("json.Marshal via go-toml:\t%s\n", jsonTOML)
320+
321+
jsonCUE, err := json.Marshal(val)
322+
qt.Assert(t, qt.IsNil(err))
323+
t.Logf("json.Marshal via CUE:\t%s\n", jsonCUE)
324+
qt.Assert(t, qt.JSONEquals(jsonCUE, unmarshalTOML))
295325
})
296326
}
297327
}

0 commit comments

Comments
 (0)