fix(decode): report the smallest unknown field deterministically
Assisted-by: GLM 5.3
This commit is contained in:
@@ -45,6 +45,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
- Strict decoding reports the smallest unknown key. Several unknown keys in
|
||||||
|
one table made the message depend on Go's random map iteration order, so
|
||||||
|
the same document reported different keys across runs.
|
||||||
- A `MarshalTOML` result of `nil` with a nil error fails the marshal with
|
- A `MarshalTOML` result of `nil` with a nil error fails the marshal with
|
||||||
`MarshalTOML returned a nil value`. The field silently vanished before, and
|
`MarshalTOML returned a nil value`. The field silently vanished before, and
|
||||||
inside a value array the nil result reached reflection as a zero value and
|
inside a value array the nil result reached reflection as a zero value and
|
||||||
|
|||||||
@@ -107,12 +107,25 @@ func (d *decoder) assignTable(tbl map[string]any, dst reflect.Value) error {
|
|||||||
|
|
||||||
func (d *decoder) assignStruct(tbl map[string]any, dst reflect.Value) error {
|
func (d *decoder) assignStruct(tbl map[string]any, dst reflect.Value) error {
|
||||||
schema := cachedStructSchema(dst.Type())
|
schema := cachedStructSchema(dst.Type())
|
||||||
|
if d.disallowUnknown {
|
||||||
|
// Map iteration order is random, so pick the unknown key to report
|
||||||
|
// deterministically: the smallest one.
|
||||||
|
unknown := ""
|
||||||
|
for key := range tbl {
|
||||||
|
if _, ok := schema.byName[strings.ToLower(key)]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if unknown == "" || key < unknown {
|
||||||
|
unknown = key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if unknown != "" {
|
||||||
|
return fmt.Errorf("interpres: unknown field %q for %s", unknown, dst.Type())
|
||||||
|
}
|
||||||
|
}
|
||||||
for key, val := range tbl {
|
for key, val := range tbl {
|
||||||
field, ok := schema.byName[strings.ToLower(key)]
|
field, ok := schema.byName[strings.ToLower(key)]
|
||||||
if !ok {
|
if !ok {
|
||||||
if d.disallowUnknown {
|
|
||||||
return fmt.Errorf("interpres: unknown field %q for %s", key, dst.Type())
|
|
||||||
}
|
|
||||||
if schema.embedMaps != nil {
|
if schema.embedMaps != nil {
|
||||||
// Leftover keys land in an untagged embedded map, the inverse
|
// Leftover keys land in an untagged embedded map, the inverse
|
||||||
// of the encoder inlining that map's entries.
|
// of the encoder inlining that map's entries.
|
||||||
|
|||||||
+2
-1
@@ -186,7 +186,8 @@ A typo such as `database_urls` then fails with
|
|||||||
`interpres: unknown field "database_urls" for main.Config` instead of a silent
|
`interpres: unknown field "database_urls" for main.Config` instead of a silent
|
||||||
default-zero run. Strictness applies to every struct the decode reaches, at any
|
default-zero run. Strictness applies to every struct the decode reaches, at any
|
||||||
depth, including struct elements inside slices; map destinations accept every
|
depth, including struct elements inside slices; map destinations accept every
|
||||||
key by nature.
|
key by nature. When several keys are unknown, the message names the smallest
|
||||||
|
one, so it does not depend on map iteration order.
|
||||||
|
|
||||||
### Cancellation
|
### Cancellation
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ package interpres
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"math"
|
"math"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -316,6 +317,26 @@ func TestDisallowUnknownFields(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDisallowUnknownFieldsReportsSmallestKey(t *testing.T) {
|
||||||
|
// Map iteration order is random, so the reported key must be chosen
|
||||||
|
// deterministically: the smallest unknown key, whichever order the map
|
||||||
|
// iterates in.
|
||||||
|
type C struct {
|
||||||
|
Known string `toml:"known"`
|
||||||
|
}
|
||||||
|
data := []byte("known = \"x\"\nzeta = 1\nalpha = 2\nmu = 3\n")
|
||||||
|
for range 20 {
|
||||||
|
var c C
|
||||||
|
err := NewDecoder().DisallowUnknownFields().Decode(data, &c)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for unknown fields")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), `unknown field "alpha"`) {
|
||||||
|
t.Fatalf("err = %v, want the smallest unknown key alpha", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSkippedFieldTag(t *testing.T) {
|
func TestSkippedFieldTag(t *testing.T) {
|
||||||
type C struct {
|
type C struct {
|
||||||
Keep string `toml:"keep"`
|
Keep string `toml:"keep"`
|
||||||
|
|||||||
Reference in New Issue
Block a user