v0.4.0 Unmapped reads any parsed value, UnknownKeys refuses what it names, decode errors name the key path
Test / test (push) Canceled after 0s
Test / test (push) Canceled after 0s
This commit is contained in:
5 files changed
+81
-17
No files matched your search
@@ -20,7 +20,10 @@ replace old containers. Existing pointer defaults are retained without writing
|
|||||||
through the original pointer. Fixed arrays require an exact input length.
|
through the original pointer. Fixed arrays require an exact input length.
|
||||||
Tags use `toml:"name,omitempty"`; an empty tag name
|
Tags use `toml:"name,omitempty"`; an empty tag name
|
||||||
falls back to the Go field name. Matching is case sensitive. Unknown input keys
|
falls back to the Go field name. Matching is case sensitive. Unknown input keys
|
||||||
and unexported fields are ignored.
|
and unexported fields are ignored; `Unmapped(data, &target)` names the keys a
|
||||||
|
parsed value holds that no field takes, and a caller refusing them returns
|
||||||
|
`UnknownKeys(keys)`, which quotes each. A decode error names where it failed by
|
||||||
|
the key path the document spells: `rooms[1].width: cannot convert string to int`.
|
||||||
|
|
||||||
## Values and conversions
|
## Values and conversions
|
||||||
|
|
||||||
@@ -121,7 +124,8 @@ split at the widest gap.
|
|||||||
- `Unmapped` names the keys no field of the destination takes, matched as `Decode`
|
- `Unmapped` names the keys no field of the destination takes, matched as `Decode`
|
||||||
matches them (tags, then field names; unexported and `-` fields take nothing),
|
matches them (tags, then field names; unexported and `-` fields take nothing),
|
||||||
each once and sorted. A map or interface destination takes every key under it;
|
each once and sorted. A map or interface destination takes every key under it;
|
||||||
a key under an array of tables is named without its index.
|
a key under an array of tables is named without its index. The function of the
|
||||||
|
same name reads any value `Parse` produced, a table inside one included.
|
||||||
- `Key` is a dotted path, one segment per part; `Key.String` spells it as a
|
- `Key` is a dotted path, one segment per part; `Key.String` spells it as a
|
||||||
document would, quoting segments that need it.
|
document would, quoting segments that need it.
|
||||||
|
|
||||||
|
|||||||
@@ -303,6 +303,29 @@ func TestDecodeErrorPathIsLinear(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A decode error names where it failed by the key path the document spells
|
||||||
|
func TestDecodeErrorNamesItsKeyPath(t *testing.T) {
|
||||||
|
type room struct {
|
||||||
|
Width int `toml:"width"`
|
||||||
|
}
|
||||||
|
var v struct {
|
||||||
|
Play struct{ Sound bool } `toml:"play"`
|
||||||
|
Rooms []room `toml:"rooms"`
|
||||||
|
ByKey map[string]room `toml:"by_key"`
|
||||||
|
}
|
||||||
|
for _, tt := range []struct {
|
||||||
|
doc, want string
|
||||||
|
}{
|
||||||
|
{"play.Sound = 1", "play.Sound: cannot convert int64 to bool"},
|
||||||
|
{"rooms = [{ width = 1 }, { width = \"x\" }]", "rooms[1].width: cannot convert string to int"},
|
||||||
|
{"by_key.\"a b\".width = true", `by_key."a b".width: cannot convert bool to int`},
|
||||||
|
} {
|
||||||
|
if err := Unmarshal([]byte(tt.doc+"\n"), &v); err == nil || err.Error() != tt.want {
|
||||||
|
t.Errorf("%s: error %v, want %s", tt.doc, err, tt.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Header paths are bounded: deep nesting is written inline past the bound,
|
// Header paths are bounded: deep nesting is written inline past the bound,
|
||||||
// so output stays linear and still round-trips
|
// so output stays linear and still round-trips
|
||||||
func TestMarshal_HeaderLengthBounded(t *testing.T) {
|
func TestMarshal_HeaderLengthBounded(t *testing.T) {
|
||||||
|
|||||||
@@ -42,19 +42,23 @@ func decodeValue(data any, val reflect.Value) error {
|
|||||||
return decodeAt(data, val, 0)
|
return decodeAt(data, val, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// pathError names where decoding failed. Each level adds its segment instead
|
// pathError names where decoding failed as the key path a document spells,
|
||||||
// of wrapping the message, which would copy the message once per level.
|
// play.sound or rooms[0].width. Each level adds its segment instead of wrapping
|
||||||
|
// the message, which would copy the message once per level.
|
||||||
type pathError struct {
|
type pathError struct {
|
||||||
path []string // innermost first
|
path []string // innermost first: a key as keySegment spells it, an index as [n]
|
||||||
err error
|
err error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *pathError) Error() string {
|
func (e *pathError) Error() string {
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
for i := len(e.path) - 1; i >= 0; i-- {
|
for i := len(e.path) - 1; i >= 0; i-- {
|
||||||
b.WriteString(e.path[i])
|
if i < len(e.path)-1 && e.path[i][0] != '[' {
|
||||||
b.WriteString(": ")
|
b.WriteByte('.')
|
||||||
}
|
}
|
||||||
|
b.WriteString(e.path[i])
|
||||||
|
}
|
||||||
|
b.WriteString(": ")
|
||||||
b.WriteString(e.err.Error())
|
b.WriteString(e.err.Error())
|
||||||
return b.String()
|
return b.String()
|
||||||
}
|
}
|
||||||
@@ -69,6 +73,14 @@ func atPath(segment string, err error) error {
|
|||||||
return &pathError{path: []string{segment}, err: err}
|
return &pathError{path: []string{segment}, err: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// keySegment spells a key in an error path: bare when it can be, else quoted and cut
|
||||||
|
func keySegment(k string) string {
|
||||||
|
if len(k) <= 32 && isBareKey(k) {
|
||||||
|
return k
|
||||||
|
}
|
||||||
|
return brief(k)
|
||||||
|
}
|
||||||
|
|
||||||
func decodeAt(data any, val reflect.Value, depth int) error {
|
func decodeAt(data any, val reflect.Value, depth int) error {
|
||||||
if depth > maxValueDepth {
|
if depth > maxValueDepth {
|
||||||
return fmt.Errorf("decode nesting exceeds %d", maxValueDepth)
|
return fmt.Errorf("decode nesting exceeds %d", maxValueDepth)
|
||||||
@@ -120,7 +132,7 @@ func decodeAt(data any, val reflect.Value, depth int) error {
|
|||||||
}
|
}
|
||||||
for i := 0; i < len(dataSlice); i++ {
|
for i := 0; i < len(dataSlice); i++ {
|
||||||
if err := decodeAt(dataSlice[i], newSlice.Index(i), depth+1); err != nil {
|
if err := decodeAt(dataSlice[i], newSlice.Index(i), depth+1); err != nil {
|
||||||
return atPath(fmt.Sprintf("index %d", i), err)
|
return atPath(fmt.Sprintf("[%d]", i), err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val.Set(newSlice)
|
val.Set(newSlice)
|
||||||
@@ -140,7 +152,7 @@ func decodeAt(data any, val reflect.Value, depth int) error {
|
|||||||
for k, vData := range dataMap {
|
for k, vData := range dataMap {
|
||||||
elem.SetZero()
|
elem.SetZero()
|
||||||
if err := decodeAt(vData, elem, depth+1); err != nil {
|
if err := decodeAt(vData, elem, depth+1); err != nil {
|
||||||
return atPath("map key "+brief(k), err)
|
return atPath(keySegment(k), err)
|
||||||
}
|
}
|
||||||
key.SetString(k)
|
key.SetString(k)
|
||||||
newMap.SetMapIndex(key, elem)
|
newMap.SetMapIndex(key, elem)
|
||||||
@@ -216,7 +228,7 @@ func decodeStruct(data map[string]any, val reflect.Value, depth int) error {
|
|||||||
// Look up in data map (case sensitive)
|
// Look up in data map (case sensitive)
|
||||||
if vData, ok := data[f.key]; ok {
|
if vData, ok := data[f.key]; ok {
|
||||||
if err := decodeAt(vData, val.Field(f.index), depth+1); err != nil {
|
if err := decodeAt(vData, val.Field(f.index), depth+1); err != nil {
|
||||||
return atPath(val.Type().Name()+"."+f.name, err)
|
return atPath(keySegment(f.key), err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -227,7 +239,6 @@ func decodeStruct(data map[string]any, val reflect.Value, depth int) error {
|
|||||||
type field struct {
|
type field struct {
|
||||||
index int
|
index int
|
||||||
key string // the tag's name, else the Go name
|
key string // the tag's name, else the Go name
|
||||||
name string // the Go name
|
|
||||||
omitempty bool
|
omitempty bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,7 +257,7 @@ func fieldsOf(t reflect.Type) []field {
|
|||||||
if !f.IsExported() || name == "-" {
|
if !f.IsExported() || name == "-" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
x := field{index: i, key: cmp.Or(name, f.Name), name: f.Name}
|
x := field{index: i, key: cmp.Or(name, f.Name)}
|
||||||
for o := range strings.SplitSeq(opts, ",") {
|
for o := range strings.SplitSeq(opts, ",") {
|
||||||
x.omitempty = x.omitempty || o == "omitempty"
|
x.omitempty = x.omitempty || o == "omitempty"
|
||||||
}
|
}
|
||||||
|
|||||||
+24
-5
@@ -5,6 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
"slices"
|
"slices"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -253,12 +254,30 @@ func (d *Document) Tree() map[string]any { return d.tree }
|
|||||||
func (d *Document) Decode(v any) error { return Decode(d.tree, v) }
|
func (d *Document) Decode(v any) error { return Decode(d.tree, v) }
|
||||||
|
|
||||||
// Unmapped names the keys of the document that no field of v's type takes, as
|
// Unmapped names the keys of the document that no field of v's type takes, as
|
||||||
// Decode, which ignores them, would match them; each once and sorted. A key under
|
// the function Unmapped does
|
||||||
// a map or interface destination is taken, and one under an array of tables is
|
func (d *Document) Unmapped(v any) []Key { return Unmapped(d.tree, v) }
|
||||||
// named without its index.
|
|
||||||
func (d *Document) Unmapped(v any) []Key {
|
// UnknownKeys is the error of a caller that refuses the keys Unmapped names
|
||||||
|
type UnknownKeys []Key
|
||||||
|
|
||||||
|
func (u UnknownKeys) Error() string {
|
||||||
|
q := make([]string, len(u))
|
||||||
|
for i, k := range u {
|
||||||
|
q[i] = strconv.Quote(k.String())
|
||||||
|
}
|
||||||
|
if len(q) == 1 {
|
||||||
|
return "unknown key " + q[0]
|
||||||
|
}
|
||||||
|
return "unknown keys " + strings.Join(q, ", ")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unmapped names the keys of data, a value Parse produced, that no field of v's
|
||||||
|
// type takes, as Decode, which ignores them, would match them; each once and
|
||||||
|
// sorted. A key under a map or interface destination is taken, and one under an
|
||||||
|
// array of tables is named without its index.
|
||||||
|
func Unmapped(data any, v any) []Key {
|
||||||
seen := make(map[string]Key)
|
seen := make(map[string]Key)
|
||||||
unmapped(d.tree, reflect.TypeOf(v), nil, seen, 0)
|
unmapped(data, reflect.TypeOf(v), nil, seen, 0)
|
||||||
out := make([]Key, 0, len(seen))
|
out := make([]Key, 0, len(seen))
|
||||||
for _, k := range seen {
|
for _, k := range seen {
|
||||||
out = append(out, k)
|
out = append(out, k)
|
||||||
|
|||||||
@@ -195,6 +195,7 @@ func TestDocumentRefusesAnEditItCannotPlace(t *testing.T) {
|
|||||||
// TestUnmappedNamesWhatDecodeIgnores: keys no field takes, by tag or field name,
|
// TestUnmappedNamesWhatDecodeIgnores: keys no field takes, by tag or field name,
|
||||||
// unexported and "-" fields included, are named once each, under arrays of tables
|
// unexported and "-" fields included, are named once each, under arrays of tables
|
||||||
// without their index; a map or interface destination takes every key under it.
|
// without their index; a map or interface destination takes every key under it.
|
||||||
|
// A parsed tree is read as its document is, and UnknownKeys quotes each.
|
||||||
func TestUnmappedNamesWhatDecodeIgnores(t *testing.T) {
|
func TestUnmappedNamesWhatDecodeIgnores(t *testing.T) {
|
||||||
doc := `top = 1
|
doc := `top = 1
|
||||||
skip = 2
|
skip = 2
|
||||||
@@ -248,6 +249,12 @@ nope = 1
|
|||||||
if !slices.Equal(got, want) {
|
if !slices.Equal(got, want) {
|
||||||
t.Fatalf("unmapped %q, want %q", got, want)
|
t.Fatalf("unmapped %q, want %q", got, want)
|
||||||
}
|
}
|
||||||
|
if tree := Unmapped(d.Tree(), &v); !slices.EqualFunc(tree, d.Unmapped(&v), slices.Equal) {
|
||||||
|
t.Fatalf("the tree's unmapped %q differ from the document's", tree)
|
||||||
|
}
|
||||||
|
if err := UnknownKeys(d.Unmapped(&v)[:2]).Error(); err != `unknown keys "byname.one.nope", "hidden"` {
|
||||||
|
t.Fatalf("error %s", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestKeyStringQuotesWhatABareKeyCannotHold
|
// TestKeyStringQuotesWhatABareKeyCannotHold
|
||||||
|
|||||||
Reference in new issue
Block a user