Skip to content

Commit

Permalink
decoder: strict mode (#512)
Browse files Browse the repository at this point in the history
  • Loading branch information
pelletier committed Apr 21, 2021
1 parent dca2103 commit 9b67e40
Show file tree
Hide file tree
Showing 11 changed files with 715 additions and 259 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Development branch. Use at your own risk.
- [x] Abstract AST.
- [x] Original go-toml testgen tests pass.
- [x] Track file position (line, column) for errors.
- [ ] Strict mode.
- [x] Strict mode.
- [ ] Document Unmarshal / Decode

### Marshal
Expand Down
39 changes: 38 additions & 1 deletion errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,46 @@ type DecodeError struct {
message string
line int
column int
key Key

human string
}

// StrictMissingError occurs in a TOML document that does not have a
// corresponding field in the target value. It contains all the missing fields
// in Errors.
//
// Emitted by Decoder when SetStrict(true) was called.
type StrictMissingError struct {
// One error per field that could not be found.
Errors []DecodeError
}

// Error returns the cannonical string for this error.
func (s *StrictMissingError) Error() string {
return "strict mode: fields in the document are missing in the target struct"
}

// String returns a human readable description of all errors.
func (s *StrictMissingError) String() string {
var buf strings.Builder
for i, e := range s.Errors {
if i > 0 {
buf.WriteString("\n---\n")
}
buf.WriteString(e.String())
}
return buf.String()
}

type Key []string

// internal version of DecodeError that is used as the base to create a
// DecodeError with full context.
type decodeError struct {
highlight []byte
message string
key Key // optional
}

func (de *decodeError) Error() string {
Expand Down Expand Up @@ -56,6 +87,11 @@ func (e *DecodeError) Position() (row int, column int) {
return e.line, e.column
}

// Key that was being processed when the error occured.
func (e *DecodeError) Key() Key {
return e.key
}

// decodeErrorFromHighlight creates a DecodeError referencing to a highlighted
// range of bytes from document.
//
Expand All @@ -64,7 +100,7 @@ func (e *DecodeError) Position() (row int, column int) {
// The function copies all bytes used in DecodeError, so that document and
// highlight can be freely deallocated.
//nolint:funlen
func wrapDecodeError(document []byte, de *decodeError) error {
func wrapDecodeError(document []byte, de *decodeError) *DecodeError {
if de == nil {
return nil
}
Expand Down Expand Up @@ -137,6 +173,7 @@ func wrapDecodeError(document []byte, de *decodeError) error {
message: errMessage,
line: errLine,
column: errColumn,
key: de.key,
human: buf.String(),
}
}
Expand Down
127 changes: 71 additions & 56 deletions internal/imported_tests/unmarshal_imported_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package imported_tests
// marked as skipped until we figure out if that's something we want in v2.

import (
"bytes"
"errors"
"fmt"
"reflect"
Expand Down Expand Up @@ -1955,66 +1956,80 @@ String2="2"`
assert.Error(t, err)
}

func decoder(doc string) *toml.Decoder {
return toml.NewDecoder(bytes.NewReader([]byte(doc)))
}

func strictDecoder(doc string) *toml.Decoder {
d := decoder(doc)
d.SetStrict(true)
return d
}

func TestDecoderStrict(t *testing.T) {
t.Skip()
// input := `
//[decoded]
// key = ""
//
//[undecoded]
// key = ""
//
// [undecoded.inner]
// key = ""
//
// [[undecoded.array]]
// key = ""
//
// [[undecoded.array]]
// key = ""
//
//`
// var doc struct {
// Decoded struct {
// Key string
// }
// }
//
// expected := `undecoded keys: ["undecoded.array.0.key" "undecoded.array.1.key" "undecoded.inner.key" "undecoded.key"]`
//
// err := NewDecoder(bytes.NewReader([]byte(input))).Strict(true).Decode(&doc)
// if err == nil {
// t.Error("expected error, got none")
// } else if err.Error() != expected {
// t.Errorf("expect err: %s, got: %s", expected, err.Error())
// }
//
// if err := NewDecoder(bytes.NewReader([]byte(input))).Decode(&doc); err != nil {
// t.Errorf("unexpected err: %s", err)
// }
//
// var m map[string]interface{}
// if err := NewDecoder(bytes.NewReader([]byte(input))).Decode(&m); err != nil {
// t.Errorf("unexpected err: %s", err)
// }
input := `
[decoded]
key = ""
[undecoded]
key = ""
[undecoded.inner]
key = ""
[[undecoded.array]]
key = ""
[[undecoded.array]]
key = ""
`
var doc struct {
Decoded struct {
Key string
}
}

err := strictDecoder(input).Decode(&doc)
require.Error(t, err)
require.IsType(t, &toml.StrictMissingError{}, err)
se := err.(*toml.StrictMissingError)

keys := []toml.Key{}

for _, e := range se.Errors {
keys = append(keys, e.Key())
}

expectedKeys := []toml.Key{
{"undecoded"},
{"undecoded", "inner"},
{"undecoded", "array"},
{"undecoded", "array"},
}

require.Equal(t, expectedKeys, keys)

err = decoder(input).Decode(&doc)
require.NoError(t, err)

var m map[string]interface{}
err = decoder(input).Decode(&m)
}

func TestDecoderStrictValid(t *testing.T) {
t.Skip()
// input := `
//[decoded]
// key = ""
//`
// var doc struct {
// Decoded struct {
// Key string
// }
// }
//
// err := NewDecoder(bytes.NewReader([]byte(input))).Strict(true).Decode(&doc)
// if err != nil {
// t.Fatal("unexpected error:", err)
// }
input := `
[decoded]
key = ""
`
var doc struct {
Decoded struct {
Key string
}
}

err := strictDecoder(input).Decode(&doc)
require.NoError(t, err)
}

type docUnmarshalTOML struct {
Expand Down
50 changes: 50 additions & 0 deletions internal/tracker/key.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package tracker

import (
"github.com/pelletier/go-toml/v2/internal/ast"
)

// KeyTracker is a tracker that keeps track of the current Key as the AST is
// walked.
type KeyTracker struct {
k []string
}

// UpdateTable sets the state of the tracker with the AST table node.
func (t *KeyTracker) UpdateTable(node ast.Node) {
t.reset()
t.Push(node)
}

// UpdateArrayTable sets the state of the tracker with the AST array table node.
func (t *KeyTracker) UpdateArrayTable(node ast.Node) {
t.reset()
t.Push(node)
}

// Push the given key on the stack.
func (t *KeyTracker) Push(node ast.Node) {
it := node.Key()
for it.Next() {
t.k = append(t.k, string(it.Node().Data))
}
}

// Pop key from stack.
func (t *KeyTracker) Pop(node ast.Node) {
it := node.Key()
for it.Next() {
t.k = t.k[:len(t.k)-1]
}
}

// Key returns the current key
func (t *KeyTracker) Key() []string {
k := make([]string, len(t.k))
copy(k, t.k)
return k
}

func (t *KeyTracker) reset() {
t.k = t.k[:0]
}

0 comments on commit 9b67e40

Please sign in to comment.