v0.5.0 Unmapped reads any parsed value, UnknownKeys refuses what it names, decode errors name the key path
Test / test (push) Canceled after 0s

* Unmapped reads any parsed value; decode errors name the key path

Unmapped(data, v) names the keys of a value Parse produced that no field
of v's type takes, a table inside a document included, so a caller holding
a tree (after macro expansion, say) can refuse them as a Document's caller
does; Document.Unmapped calls it.

A decode error names where it failed by the key path the document spells,
rooms[1].width or by_key."a b".width, rather than by Go type and field
names and "index n" / "map key" segments; a key that cannot be bare, or
runs past 32 bytes, is quoted and cut as parse errors quote one.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rsnp29663hDvPHPua37MhN

* UnknownKeys, the error naming the keys a caller refuses

A caller that refuses what Unmapped names returns UnknownKeys(keys),
"unknown key \"a\"" or "unknown keys \"a\", \"b.c\"", so every refusal
words them alike. vif's settings already format their own; its FSM
dialect would be the second copy.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rsnp29663hDvPHPua37MhN

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
LixenandClaude Opus 5.5 authored and GitHub committed 2026-10-26 03:30:55 -04:00
1 parent 170e71f10a
commit 5c0a1c757a
5 files changed
+80 -16

No files matched your search

+20 -9
View File
@@ -42,19 +42,23 @@ func decodeValue(data any, val reflect.Value) error {
return decodeAt(data, val, 0)
}
// pathError names where decoding failed. Each level adds its segment instead
// of wrapping the message, which would copy the message once per level.
// pathError names where decoding failed as the key path a document spells,
// 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 {
path []string // innermost first
path []string // innermost first: a key as keySegment spells it, an index as [n]
err error
}
func (e *pathError) Error() string {
var b strings.Builder
for i := len(e.path) - 1; i >= 0; i-- {
if i < len(e.path)-1 && e.path[i][0] != '[' {
b.WriteByte('.')
}
b.WriteString(e.path[i])
b.WriteString(": ")
}
b.WriteString(": ")
b.WriteString(e.err.Error())
return b.String()
}
@@ -69,6 +73,14 @@ func atPath(segment string, err error) error {
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 {
if depth > 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++ {
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)
@@ -140,7 +152,7 @@ func decodeAt(data any, val reflect.Value, depth int) error {
for k, vData := range dataMap {
elem.SetZero()
if err := decodeAt(vData, elem, depth+1); err != nil {
return atPath("map key "+brief(k), err)
return atPath(keySegment(k), err)
}
key.SetString(k)
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)
if vData, ok := data[f.key]; ok {
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 {
index int
key string // the tag's name, else the Go name
name string // the Go name
omitempty bool
}
@@ -246,7 +257,7 @@ func fieldsOf(t reflect.Type) []field {
if !f.IsExported() || name == "-" {
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, ",") {
x.omitempty = x.omitempty || o == "omitempty"
}