fix(decode): keep the targeted parse on the tree path's contract
Assisted-by: GLM 5.3
This commit is contained in:
@@ -22,6 +22,14 @@ import (
|
||||
// document through the tree path, so the observable behaviour is the tree
|
||||
// path's, exactly. A targeted parse either completes with the result the
|
||||
// tree path would give, or it erases itself.
|
||||
//
|
||||
// One difference the two paths cannot share: a parse error or a cancellation
|
||||
// deep in the document leaves the statements before it already written into
|
||||
// the destination, where the tree path, which parses the whole document
|
||||
// before it decodes any of it, writes nothing. The value layer shares this
|
||||
// with encoding/json, whose Unmarshal also leaves a partial destination
|
||||
// behind a mid-document failure; a destination that must stay untouched on
|
||||
// error is a destination the caller resets.
|
||||
var errTargetFallback = errors.New("interpres: targeted decode falls back to the tree path")
|
||||
|
||||
// targetCache holds whether a destination type may take the targeted parse.
|
||||
@@ -33,10 +41,12 @@ var targetCache sync.Map // reflect.Type -> bool
|
||||
var mapStringAnyType = reflect.TypeFor[map[string]any]()
|
||||
|
||||
// typeTargetable reports whether decoding into the struct type t can use the
|
||||
// targeted parse. The one structural ban is untagged embedded maps: their
|
||||
// filler-key rule lives in the tree decode, and a targeted document that
|
||||
// meets an unknown table would need a subtree of it. Everything else is safe
|
||||
// to attempt, because the value layer is the ordinary decode and every
|
||||
// targeted parse. The structural bans are the shapes whose tree behaviour
|
||||
// the skeleton cannot model: untagged embedded maps, an OrderedMap anywhere a
|
||||
// table opens, and a custom decode hook on any table the parse would enter
|
||||
// directly (a struct field, a map field, or the element of a table slice),
|
||||
// because the tree hands a hook the whole parsed value. Everything else is
|
||||
// safe to attempt, because the value layer is the ordinary decode and every
|
||||
// mismatch falls back.
|
||||
func typeTargetable(t reflect.Type) bool {
|
||||
if t == nil || t.Kind() != reflect.Struct || t == orderedMapType {
|
||||
@@ -59,22 +69,41 @@ func scanTargetable(t reflect.Type, seen map[reflect.Type]bool) bool {
|
||||
return false
|
||||
}
|
||||
for _, loc := range cachedStructSchema(t).byName {
|
||||
ft := derefType(t.FieldByIndex(loc.index).Type)
|
||||
if !scanTargetableField(derefType(t.FieldByIndex(loc.index).Type), seen) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// scanTargetableField reports whether one field's type is safe for the
|
||||
// targeted skeleton to fill directly.
|
||||
func scanTargetableField(ft reflect.Type, seen map[reflect.Type]bool) bool {
|
||||
switch ft.Kind() {
|
||||
case reflect.Struct:
|
||||
if ft == orderedMapType {
|
||||
return false
|
||||
}
|
||||
if ft.Kind() != reflect.Struct || isScalarStruct(ft) {
|
||||
continue
|
||||
if isScalarStruct(ft) {
|
||||
return true
|
||||
}
|
||||
// A struct field with a custom decode hook receives the whole parsed
|
||||
// value from the tree decode; the targeted skeleton never builds that
|
||||
// value for a table it enters directly, so the hook must win.
|
||||
// A struct the parse enters directly never builds the whole value
|
||||
// the tree hands a hook, so the hook must win.
|
||||
if implementsDecodeHook(ft) || implementsDecodeHook(reflect.PointerTo(ft)) {
|
||||
return false
|
||||
}
|
||||
if !scanTargetable(ft, seen) {
|
||||
return scanTargetable(ft, seen)
|
||||
case reflect.Map:
|
||||
return !implementsDecodeHook(ft) && !implementsDecodeHook(reflect.PointerTo(ft))
|
||||
case reflect.Slice, reflect.Array:
|
||||
et := derefType(ft.Elem())
|
||||
if et == orderedMapType {
|
||||
return false
|
||||
}
|
||||
if et.Kind() == reflect.Struct && !isScalarStruct(et) {
|
||||
return scanTargetableField(et, seen)
|
||||
}
|
||||
return true
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -110,20 +139,45 @@ func canTargetDecode(v any) bool {
|
||||
|
||||
// targetTable is one open table of the targeted parse: the struct (or map)
|
||||
// value its keys fill, the schema that resolves them (nil for a map or sink
|
||||
// destination), the absolute path its errors wrap, and whether it collects
|
||||
// the keys no field claims for the strict check. A sink is the destination
|
||||
// an unknown subtree gets: its statements parse for the syntax and definition
|
||||
// contracts, and its values are discarded.
|
||||
// destination), and the absolute path its errors wrap. A sink is the
|
||||
// destination an unknown subtree gets: its statements parse for the syntax
|
||||
// and definition contracts, and its values are discarded. The strict and
|
||||
// required findings live in the parser's per-address store, not here,
|
||||
// because the table objects a dotted descent builds are transient while the
|
||||
// destination is not.
|
||||
type targetTable struct {
|
||||
rv reflect.Value
|
||||
schema *structSchema
|
||||
path []string
|
||||
sink bool
|
||||
keys []string // the keys defined in the table, interned; struct
|
||||
// tables keep theirs per destination address instead
|
||||
// strict is the strict-decode setting the table was opened with, carried
|
||||
// into the per-address strict state on first sight.
|
||||
strict bool
|
||||
// arrayElem marks a sink created as the element of an unknown array of
|
||||
// tables: a dotted key may not enter it, the tree's own rule for an
|
||||
// array, while a [sub-table] header may, through the last element.
|
||||
arrayElem bool
|
||||
keys []string // the keys defined in a sink, as full path keys
|
||||
}
|
||||
|
||||
// strictState is the strict and required bookkeeping of one struct
|
||||
// destination, keyed by the value's address.
|
||||
type strictState struct {
|
||||
path []string
|
||||
typ reflect.Type
|
||||
schema *structSchema
|
||||
strict bool
|
||||
unknown string // strict: the smallest unclaimed key so far
|
||||
resolvedSeen map[string]bool // the schema keys resolved so far, for required
|
||||
resolved map[string]bool
|
||||
}
|
||||
|
||||
// arrayFill tracks how many elements of one fixed-size array the document
|
||||
// has filled, with what the length mismatch the tree decode reports needs:
|
||||
// the array's type and its field's path.
|
||||
type arrayFill struct {
|
||||
next int
|
||||
typ reflect.Type
|
||||
path []string
|
||||
}
|
||||
|
||||
// targetParser parses a document straight into a struct destination. It
|
||||
@@ -139,61 +193,53 @@ type targetParser struct {
|
||||
rootT *targetTable
|
||||
cur *targetTable
|
||||
|
||||
// arrayNext tracks how many elements of a fixed-size array the document
|
||||
// has filled, per field address.
|
||||
arrayNext map[uintptr]int
|
||||
// arrayFills counts the elements of each fixed-size array the document
|
||||
// has filled, per field address, in the order the arrays were met.
|
||||
arrayFills map[uintptr]*arrayFill
|
||||
fillOrder []*arrayFill
|
||||
|
||||
// appendedHere records the slice fields this document's [[headers]] have
|
||||
// filled: a slice the caller prefilled is replaced by the tree decode,
|
||||
// not appended to, so the first header over one falls back.
|
||||
appendedHere map[uintptr]bool
|
||||
|
||||
// opened registers every opened table by its path key, sinks included: a
|
||||
// later header or dotted key meets the table the tree already built.
|
||||
opened map[string]*targetTable
|
||||
|
||||
// keyAssigned records the fields a key statement assigned directly, per
|
||||
// table address: an array-of-tables header over such a field is the
|
||||
// tree's `not an array of tables` error, where a header over a
|
||||
// header-built array appends.
|
||||
keyAssignedKeys map[uintptr]map[string]bool
|
||||
|
||||
// tableKeysByAddr holds the defined keys of one struct destination, keyed
|
||||
// by the value's address: the table object a dotted descent builds is
|
||||
// transient, the destination is not.
|
||||
tableKeysByAddr map[uintptr][]string
|
||||
}
|
||||
|
||||
// markKeyAssigned records that a key statement assigned the field, and
|
||||
// keyAssigned reports that state. Sinks keep no such bookkeeping.
|
||||
func (tp *targetParser) markKeyAssigned(t *targetTable, key string) {
|
||||
if t.sink || t.schema == nil || !t.rv.CanAddr() || t.rv.Kind() != reflect.Struct {
|
||||
return
|
||||
}
|
||||
addr := t.rv.Addr().Pointer()
|
||||
if tp.keyAssignedKeys == nil {
|
||||
tp.keyAssignedKeys = make(map[uintptr]map[string]bool, 8)
|
||||
}
|
||||
if tp.keyAssignedKeys[addr] == nil {
|
||||
tp.keyAssignedKeys[addr] = make(map[string]bool, 8)
|
||||
}
|
||||
tp.keyAssignedKeys[addr][key] = true
|
||||
}
|
||||
// mapKeysByAddr holds the keys the document has defined in one map
|
||||
// destination, keyed the same way: the destination map the caller
|
||||
// prefilled is not the parser's state, and a key it holds is not the
|
||||
// duplicate a key the document repeats is.
|
||||
mapKeysByAddr map[uintptr]map[string]bool
|
||||
|
||||
func (tp *targetParser) keyAssigned(t *targetTable, key string) bool {
|
||||
if t.sink || t.schema == nil || !t.rv.CanAddr() || t.rv.Kind() != reflect.Struct {
|
||||
return false
|
||||
}
|
||||
addr := t.rv.Addr().Pointer()
|
||||
return tp.keyAssignedKeys[addr][key]
|
||||
// strictByAddr holds each destination's strict and required findings,
|
||||
// with strictOrder keeping the document order they first appeared in.
|
||||
strictByAddr map[uintptr]*strictState
|
||||
strictOrder []uintptr
|
||||
}
|
||||
|
||||
// tableHas reports whether key is already defined in the table. A struct
|
||||
// table's keys are interned parser strings, so the linear scan compares
|
||||
// against a handful of short keys, cheaper than hashing a per-table map.
|
||||
// Struct tables keep their keys by destination address, because the table
|
||||
// object a dotted descent builds is transient while the destination is not.
|
||||
// Struct tables keep their keys by destination address, and map tables keep
|
||||
// theirs there too, because the table object a dotted descent builds is
|
||||
// transient while the destination is not, and the destination map's own
|
||||
// contents are the caller's, not the document's.
|
||||
func (tp *targetParser) tableHas(t *targetTable, key string) bool {
|
||||
switch {
|
||||
case t.sink:
|
||||
return slices.Contains(t.keys, key)
|
||||
case t.schema == nil:
|
||||
return t.rv.Kind() == reflect.Map && t.rv.MapIndex(reflect.ValueOf(key)).IsValid()
|
||||
if t.rv.Kind() != reflect.Map || !t.rv.CanAddr() {
|
||||
return false
|
||||
}
|
||||
return tp.mapKeysByAddr[t.rv.Addr().Pointer()][key]
|
||||
default:
|
||||
return slices.Contains(tp.tableKeys(t), key)
|
||||
}
|
||||
@@ -217,6 +263,17 @@ func (tp *targetParser) tableMark(t *targetTable, key string) {
|
||||
case t.sink:
|
||||
t.keys = append(t.keys, key)
|
||||
case t.schema == nil:
|
||||
if !t.rv.CanAddr() || t.rv.Kind() != reflect.Map {
|
||||
return
|
||||
}
|
||||
addr := t.rv.Addr().Pointer()
|
||||
if tp.mapKeysByAddr == nil {
|
||||
tp.mapKeysByAddr = make(map[uintptr]map[string]bool, 8)
|
||||
}
|
||||
if tp.mapKeysByAddr[addr] == nil {
|
||||
tp.mapKeysByAddr[addr] = make(map[string]bool, 8)
|
||||
}
|
||||
tp.mapKeysByAddr[addr][key] = true
|
||||
default:
|
||||
if !t.rv.CanAddr() || t.rv.Kind() != reflect.Struct {
|
||||
return
|
||||
@@ -229,32 +286,49 @@ func (tp *targetParser) tableMark(t *targetTable, key string) {
|
||||
}
|
||||
}
|
||||
|
||||
// resolvedHas reports whether the resolved schema key has been seen, the
|
||||
// check a required tag runs: the duplicate bookkeeping tracks the key as the
|
||||
// document wrote it, the required bookkeeping the key as the schema
|
||||
// resolved it.
|
||||
func (t *targetTable) resolvedHas(key string) bool {
|
||||
return t.resolvedSeen[key]
|
||||
// strictState returns the strict and required bookkeeping of the struct
|
||||
// destination t fills, registering it on first sight so a finding recorded
|
||||
// on a transient table survives the table.
|
||||
func (tp *targetParser) strictState(t *targetTable) *strictState {
|
||||
addr := t.rv.Addr().Pointer()
|
||||
if st, ok := tp.strictByAddr[addr]; ok {
|
||||
return st
|
||||
}
|
||||
st := &strictState{
|
||||
path: slices.Clone(t.path),
|
||||
typ: t.rv.Type(),
|
||||
schema: t.schema,
|
||||
strict: t.strict,
|
||||
resolved: make(map[string]bool, 8),
|
||||
}
|
||||
tp.strictByAddr[addr] = st
|
||||
tp.strictOrder = append(tp.strictOrder, addr)
|
||||
return st
|
||||
}
|
||||
|
||||
func (t *targetTable) markResolved(key string) {
|
||||
// markResolved records that the document resolved the schema key, the check
|
||||
// a required tag runs: the duplicate bookkeeping tracks the key as the
|
||||
// document wrote it, the required bookkeeping the key as the schema resolved
|
||||
// it.
|
||||
func (tp *targetParser) markResolved(t *targetTable, key string) {
|
||||
if t.schema == nil || len(t.schema.required) == 0 {
|
||||
return
|
||||
}
|
||||
if t.resolvedSeen == nil {
|
||||
t.resolvedSeen = make(map[string]bool, 8)
|
||||
if !t.rv.CanAddr() || t.rv.Kind() != reflect.Struct {
|
||||
return
|
||||
}
|
||||
t.resolvedSeen[key] = true
|
||||
tp.strictState(t).resolved[key] = true
|
||||
}
|
||||
|
||||
// recordStrictUnknown remembers the key no field claims when strict decoding
|
||||
// is on: the smallest one is reported, the tree decode's own choice.
|
||||
func (t *targetTable) recordStrictUnknown(key string) {
|
||||
if !t.strict {
|
||||
func (tp *targetParser) recordStrictUnknown(t *targetTable, key string) {
|
||||
if !t.strict || t.schema == nil || !t.rv.CanAddr() || t.rv.Kind() != reflect.Struct {
|
||||
return
|
||||
}
|
||||
if t.unknown == "" || key < t.unknown {
|
||||
t.unknown = key
|
||||
st := tp.strictState(t)
|
||||
if st.unknown == "" || key < st.unknown {
|
||||
st.unknown = key
|
||||
}
|
||||
}
|
||||
|
||||
@@ -277,9 +351,10 @@ func parseIntoTargeted(ctx context.Context, data []byte, d *decoder, useNumber b
|
||||
parser: &parser{src: data, line: 1, ctx: ctx, maxDepth: maxDepth, useNumber: useNumber},
|
||||
d: d,
|
||||
root: rv.Elem(),
|
||||
arrayNext: make(map[uintptr]int, 4),
|
||||
keyAssignedKeys: make(map[uintptr]map[string]bool, 8),
|
||||
arrayFills: make(map[uintptr]*arrayFill, 4),
|
||||
appendedHere: make(map[uintptr]bool, 4),
|
||||
opened: make(map[string]*targetTable, 8),
|
||||
strictByAddr: make(map[uintptr]*strictState, 8),
|
||||
}
|
||||
tp.rootT = &targetTable{rv: tp.root, schema: schemaRef(tp.root.Type()), strict: d.disallowUnknown}
|
||||
tp.tables = append(tp.tables, tp.rootT)
|
||||
@@ -326,35 +401,57 @@ func (tp *targetParser) run() error {
|
||||
}
|
||||
|
||||
// reportDeferred raises the decode-stage findings in the tree decode's
|
||||
// order: the root table first, then the opened tables in document order. The
|
||||
// tree decode reports them after a full parse, so a later parse error always
|
||||
// won; here the parse has already completed.
|
||||
// order. A fixed-size array the document under-filled is the length mismatch
|
||||
// the tree decode raises, and it comes first. Then the unknown keys, before
|
||||
// the required ones, because the tree decode meets an unknown key while it
|
||||
// assigns and checks a table's required keys only once the whole table has
|
||||
// been; within each class the order is the order the destinations first
|
||||
// appeared in, the document's own order.
|
||||
func (tp *targetParser) reportDeferred() error {
|
||||
for _, t := range append([]*targetTable{tp.rootT}, tp.tables...) {
|
||||
if t.sink {
|
||||
for _, f := range tp.fillOrder {
|
||||
if f.next != f.typ.Len() {
|
||||
return wrapTablePath(f.path, fmt.Errorf("interpres: cannot assign %d elements to %s", f.next, f.typ))
|
||||
}
|
||||
}
|
||||
for _, addr := range tp.strictOrder {
|
||||
if st := tp.strictByAddr[addr]; st.strict && st.unknown != "" {
|
||||
return wrapTablePath(st.path, fmt.Errorf("interpres: unknown field %q for %s", st.unknown, st.typ))
|
||||
}
|
||||
}
|
||||
for _, addr := range tp.strictOrder {
|
||||
st := tp.strictByAddr[addr]
|
||||
if st.schema == nil {
|
||||
continue
|
||||
}
|
||||
if t.strict && t.unknown != "" {
|
||||
return tp.wrapTableErr(t, fmt.Errorf("interpres: unknown field %q for %s", t.unknown, t.rv.Type()))
|
||||
}
|
||||
if t.schema != nil {
|
||||
for _, key := range t.schema.required {
|
||||
if !t.resolvedHas(key) {
|
||||
return tp.wrapTableErr(t, fmt.Errorf("interpres: missing required key %q", key))
|
||||
}
|
||||
for _, key := range st.schema.required {
|
||||
if !st.resolved[key] {
|
||||
return wrapTablePath(st.path, fmt.Errorf("interpres: missing required key %q", key))
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// wrapTableErr wraps a table's finding the way the tree decode wraps it: the
|
||||
// wrapTablePath wraps a table's finding the way the tree decode wraps it: the
|
||||
// root speaks for itself, a nested table gains its path.
|
||||
func (tp *targetParser) wrapTableErr(t *targetTable, err error) error {
|
||||
if len(t.path) == 0 {
|
||||
func wrapTablePath(path []string, err error) error {
|
||||
if len(path) == 0 {
|
||||
return err
|
||||
}
|
||||
return &DecodeError{Path: Path(slices.Clone(t.path)), Err: err}
|
||||
return &DecodeError{Path: Path(slices.Clone(path)), Err: err}
|
||||
}
|
||||
|
||||
// arrayFillFor returns the fill record of the fixed-size array fv, keyed by
|
||||
// its address, created with its field's path on first sight.
|
||||
func (tp *targetParser) arrayFillFor(fv reflect.Value, path []string) *arrayFill {
|
||||
addr := fv.Addr().Pointer()
|
||||
if f, ok := tp.arrayFills[addr]; ok {
|
||||
return f
|
||||
}
|
||||
f := &arrayFill{typ: fv.Type(), path: path}
|
||||
tp.arrayFills[addr] = f
|
||||
tp.fillOrder = append(tp.fillOrder, f)
|
||||
return f
|
||||
}
|
||||
|
||||
// --- headers ---------------------------------------------------------------
|
||||
@@ -474,11 +571,11 @@ func (tp *targetParser) descendOne(parent *targetTable, key string, abs []string
|
||||
return nil, errTargetFallback
|
||||
}
|
||||
if existing := parent.rv.MapIndex(reflect.ValueOf(key)); existing.IsValid() && !existing.IsNil() {
|
||||
return &targetTable{rv: existing.Elem(), path: abs}, nil
|
||||
return &targetTable{rv: existing, path: slices.Clone(abs)}, nil
|
||||
}
|
||||
next := reflect.MakeMap(elemT)
|
||||
parent.rv.SetMapIndex(reflect.ValueOf(key), next)
|
||||
return &targetTable{rv: next, path: abs}, nil
|
||||
return &targetTable{rv: next, path: slices.Clone(abs)}, nil
|
||||
}
|
||||
resolved := key
|
||||
loc, ok := parent.schema.byName[key]
|
||||
@@ -487,18 +584,18 @@ func (tp *targetParser) descendOne(parent *targetTable, key string, abs []string
|
||||
loc, ok = parent.schema.byName[resolved]
|
||||
}
|
||||
if !ok {
|
||||
parent.recordStrictUnknown(key)
|
||||
tp.recordStrictUnknown(parent, key)
|
||||
if opened, ok := tp.opened[pathKey(abs)]; ok {
|
||||
return opened, nil
|
||||
}
|
||||
if tp.tableHas(parent, key) {
|
||||
return nil, tp.errf("key %q is not a table", key)
|
||||
}
|
||||
sink := &targetTable{sink: true, path: abs}
|
||||
sink := &targetTable{sink: true, path: slices.Clone(abs)}
|
||||
tp.opened[pathKey(abs)] = sink
|
||||
return sink, nil
|
||||
}
|
||||
parent.markResolved(resolved)
|
||||
tp.markResolved(parent, resolved)
|
||||
fv, err := fieldByIndex(parent.rv, loc.index)
|
||||
if err != nil {
|
||||
return nil, errTargetFallback
|
||||
@@ -509,10 +606,12 @@ func (tp *targetParser) descendOne(parent *targetTable, key string, abs []string
|
||||
// openValueTable opens a table scope over a placed field value, allocating a
|
||||
// nil pointer on the way. The rules mirror the tree decode's own type
|
||||
// decisions: a struct enters, a map enters (allocated when nil), an array of
|
||||
// tables enters its last element, and anything else is a type mismatch the
|
||||
// tree decode reports, so it falls back. A scalar field the table keys
|
||||
// already define is the tree's `key is not a table` error, checked against
|
||||
// parent, the table the key belongs to.
|
||||
// tables enters its last filled element, a slice enters the last element of
|
||||
// an array this document's [[headers]] built (a prefilled slice is a table
|
||||
// the tree decode rejects, so it falls back), and anything else is a type
|
||||
// mismatch the tree decode reports, so it falls back too. A scalar field the
|
||||
// table keys already define is the tree's `key is not a table` error,
|
||||
// checked against parent, the table the key belongs to.
|
||||
func (tp *targetParser) openValueTable(fv reflect.Value, parent *targetTable, key string, abs []string, strict bool) (*targetTable, error) {
|
||||
if fv.Kind() == reflect.Pointer {
|
||||
if fv.IsNil() {
|
||||
@@ -528,7 +627,7 @@ func (tp *targetParser) openValueTable(fv reflect.Value, parent *targetTable, ke
|
||||
if isScalarStruct(fv.Type()) {
|
||||
return nil, errTargetFallback
|
||||
}
|
||||
return &targetTable{rv: fv, schema: schemaRef(fv.Type()), path: abs, strict: strict}, nil
|
||||
return &targetTable{rv: fv, schema: schemaRef(fv.Type()), path: slices.Clone(abs), strict: strict}, nil
|
||||
case reflect.Map:
|
||||
if fv.Type().Key().Kind() != reflect.String {
|
||||
return nil, errTargetFallback
|
||||
@@ -536,11 +635,12 @@ func (tp *targetParser) openValueTable(fv reflect.Value, parent *targetTable, ke
|
||||
if fv.IsNil() {
|
||||
fv.Set(reflect.MakeMap(fv.Type()))
|
||||
}
|
||||
return &targetTable{rv: fv, path: abs}, nil
|
||||
return &targetTable{rv: fv, path: slices.Clone(abs)}, nil
|
||||
case reflect.Slice:
|
||||
if fv.Len() == 0 {
|
||||
// No [[header]] ever filled it, so the tree holds a map here and
|
||||
// its decode raises the type mismatch.
|
||||
if !tp.appendedHere[fv.Addr().Pointer()] {
|
||||
// No [[header]] of this document filled it, so the tree holds a
|
||||
// map here, whose decode raises the type mismatch; a prefilled
|
||||
// slice is the tree's replacement case, not a table to enter.
|
||||
return nil, errTargetFallback
|
||||
}
|
||||
et := derefType(fv.Type().Elem())
|
||||
@@ -548,6 +648,22 @@ func (tp *targetParser) openValueTable(fv reflect.Value, parent *targetTable, ke
|
||||
return nil, errTargetFallback
|
||||
}
|
||||
return &targetTable{rv: fv.Index(fv.Len() - 1), schema: schemaRef(et), path: elementPath(abs, key, fv.Len()-1), strict: strict}, nil
|
||||
case reflect.Array:
|
||||
fill := tp.arrayFillFor(fv, append(slices.Clone(parent.path), key))
|
||||
if fill.next == 0 {
|
||||
return nil, errTargetFallback
|
||||
}
|
||||
et := derefType(fv.Type().Elem())
|
||||
if et.Kind() == reflect.Struct {
|
||||
if isScalarStruct(et) {
|
||||
return nil, errTargetFallback
|
||||
}
|
||||
return &targetTable{rv: fv.Index(fill.next - 1), schema: schemaRef(et), path: elementPath(abs, key, fill.next-1), strict: strict}, nil
|
||||
}
|
||||
if et.Kind() == reflect.Map && et.Key().Kind() == reflect.String {
|
||||
return &targetTable{rv: fv.Index(fill.next - 1), path: elementPath(abs, key, fill.next-1)}, nil
|
||||
}
|
||||
return nil, errTargetFallback
|
||||
}
|
||||
if tp.tableHas(parent, key) {
|
||||
return nil, tp.errf("key %q is not a table", key)
|
||||
@@ -578,12 +694,30 @@ func (tp *targetParser) appendArrayTable(key []string) error {
|
||||
if tp.parser.dotted[pk] || tp.parser.headers[pk] {
|
||||
return tp.errf("key %q is not an array of tables", leaf)
|
||||
}
|
||||
// A leaf the document already defined as a value or a table is the tree
|
||||
// parser's own parse error, and a leaf an earlier [[header]] defined
|
||||
// opens a new element; the arrays map, read before this header marks it,
|
||||
// is what tells the two apart. A sink parent holds no destination state
|
||||
// worth consulting.
|
||||
if !parent.sink && !tp.parser.arrays[pk] && tp.tableHas(parent, leaf) {
|
||||
return tp.errf("key %q is not an array of tables", leaf)
|
||||
}
|
||||
// A new element starts a fresh scope, exactly as the tree parser's own
|
||||
// header does: sub-headers, inline freezes and nested arrays from the
|
||||
// previous element no longer apply.
|
||||
tp.parser.resetScopeUnder(key)
|
||||
tp.parser.markArray(pk)
|
||||
|
||||
elem, err := tp.appendElement(parent, leaf, key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if elem.sink {
|
||||
// A sink the element scope reuses ([[a.b]] over an unknown a, the
|
||||
// parent sink) starts the new element with no keys, the way a known
|
||||
// array's element does.
|
||||
elem.keys = nil
|
||||
}
|
||||
tp.tableMark(parent, leaf)
|
||||
if !elem.sink {
|
||||
tp.tables = append(tp.tables, elem)
|
||||
@@ -593,9 +727,9 @@ func (tp *targetParser) appendArrayTable(key []string) error {
|
||||
}
|
||||
|
||||
// appendElement appends one element to the array the leaf names in parent
|
||||
// and returns its table. A leaf no field claims sinks; a field whose array
|
||||
// element kind cannot be a table falls back, the tree decode owning the type
|
||||
// error.
|
||||
// and returns its table. A leaf no field claims sinks, a fresh namespace per
|
||||
// element; a field whose array element kind cannot be a table falls back,
|
||||
// the tree decode owning the type error.
|
||||
func (tp *targetParser) appendElement(parent *targetTable, leaf string, key []string) (*targetTable, error) {
|
||||
if parent.sink {
|
||||
return parent, nil
|
||||
@@ -608,10 +742,15 @@ func (tp *targetParser) appendElement(parent *targetTable, leaf string, key []st
|
||||
gk := reflect.ValueOf(leaf)
|
||||
var arr reflect.Value
|
||||
if existing := parent.rv.MapIndex(gk); existing.IsValid() && !existing.IsNil() {
|
||||
if existing.Elem().Kind() != reflect.Slice {
|
||||
// A map[string]any destination boxes its arrays in the
|
||||
// interface; a typed map hands the slice itself.
|
||||
if existing.Kind() == reflect.Interface {
|
||||
existing = existing.Elem()
|
||||
}
|
||||
if existing.Kind() != reflect.Slice {
|
||||
return nil, tp.errf("key %q is not an array of tables", leaf)
|
||||
}
|
||||
arr = existing.Elem()
|
||||
arr = existing
|
||||
}
|
||||
var elem reflect.Value
|
||||
switch {
|
||||
@@ -645,79 +784,103 @@ func (tp *targetParser) appendElement(parent *targetTable, leaf string, key []st
|
||||
loc, ok = parent.schema.byName[resolved]
|
||||
}
|
||||
if !ok {
|
||||
parent.recordStrictUnknown(leaf)
|
||||
if opened, ok := tp.opened[pathKey(parent.path)]; ok {
|
||||
return opened, nil
|
||||
}
|
||||
if tp.tableHas(parent, leaf) {
|
||||
return nil, tp.errf("key %q is not an array of tables", leaf)
|
||||
}
|
||||
sink := &targetTable{sink: true, path: parent.path}
|
||||
tp.opened[pathKey(parent.path)] = sink
|
||||
tp.recordStrictUnknown(parent, leaf)
|
||||
// Every element is a fresh namespace, the way a known array's is,
|
||||
// registered under the header's path so a [sub-table] header reaches
|
||||
// the last element, the tree's rule for a header under an array of
|
||||
// tables; a dotted key skips it, the tree's rule for an array.
|
||||
sink := &targetTable{sink: true, arrayElem: true, path: slices.Clone(key)}
|
||||
tp.opened[pathKey(key)] = sink
|
||||
return sink, nil
|
||||
}
|
||||
parent.markResolved(resolved)
|
||||
if tp.keyAssigned(parent, resolved) {
|
||||
// A key statement already assigned the field its own value; the tree
|
||||
// holds a value array there and its header append is the
|
||||
// `not an array of tables` parse error.
|
||||
return nil, tp.errf("key %q is not an array of tables", leaf)
|
||||
}
|
||||
tp.markResolved(parent, resolved)
|
||||
fv, err := fieldByIndex(parent.rv, loc.index)
|
||||
if err != nil {
|
||||
return nil, errTargetFallback
|
||||
}
|
||||
fieldPath := append(slices.Clone(parent.path), leaf)
|
||||
if fv.Kind() == reflect.Array {
|
||||
// A fixed-size array fills position by position; one element too many
|
||||
// is the length mismatch the tree decode reports.
|
||||
// is the length mismatch the tree decode reports, and one too few is
|
||||
// the same mismatch, checked when the parse completes.
|
||||
fill := tp.arrayFillFor(fv, fieldPath)
|
||||
et := derefType(fv.Type().Elem())
|
||||
switch et.Kind() {
|
||||
case reflect.Struct:
|
||||
if isScalarStruct(et) {
|
||||
switch {
|
||||
case et.Kind() == reflect.Struct && !isScalarStruct(et):
|
||||
if fill.next >= fv.Len() {
|
||||
return nil, errTargetFallback
|
||||
}
|
||||
addr := fv.Addr().Pointer()
|
||||
n := tp.arrayNext[addr]
|
||||
if n >= fv.Len() {
|
||||
n := fill.next
|
||||
fill.next = n + 1
|
||||
return &targetTable{rv: fv.Index(n), schema: schemaRef(et), path: elementPath(parent.path, leaf, n), strict: parent.strict}, nil
|
||||
case et.Kind() == reflect.Map && et.Key().Kind() == reflect.String:
|
||||
if fill.next >= fv.Len() {
|
||||
return nil, errTargetFallback
|
||||
}
|
||||
tp.arrayNext[addr] = n + 1
|
||||
return &targetTable{rv: fv.Index(n), schema: schemaRef(et), path: parent.path, strict: parent.strict}, nil
|
||||
case reflect.Map:
|
||||
if et.Key().Kind() != reflect.String {
|
||||
return nil, errTargetFallback
|
||||
}
|
||||
addr := fv.Addr().Pointer()
|
||||
n := tp.arrayNext[addr]
|
||||
if n >= fv.Len() {
|
||||
return nil, errTargetFallback
|
||||
}
|
||||
tp.arrayNext[addr] = n + 1
|
||||
n := fill.next
|
||||
fill.next = n + 1
|
||||
elem := reflect.MakeMap(et)
|
||||
fv.Index(n).Set(elem)
|
||||
return &targetTable{rv: elem, path: parent.path}, nil
|
||||
return &targetTable{rv: elem, path: elementPath(parent.path, leaf, n)}, nil
|
||||
}
|
||||
return nil, errTargetFallback
|
||||
}
|
||||
if fv.Kind() != reflect.Slice {
|
||||
if tp.tableHas(parent, leaf) {
|
||||
return nil, tp.errf("key %q is not an array of tables", leaf)
|
||||
}
|
||||
// The tree builds an array here without asking the destination, and
|
||||
// its decode answers with the type mismatch; the fallback keeps the
|
||||
// message the tree path gives.
|
||||
return nil, errTargetFallback
|
||||
}
|
||||
addr := fv.Addr().Pointer()
|
||||
if !tp.appendedHere[addr] {
|
||||
if fv.Len() > 0 {
|
||||
// A slice the caller prefilled is replaced by the tree decode,
|
||||
// not appended to; the fallback runs the document the tree's way.
|
||||
return nil, errTargetFallback
|
||||
}
|
||||
tp.appendedHere[addr] = true
|
||||
}
|
||||
et := derefType(fv.Type().Elem())
|
||||
switch et.Kind() {
|
||||
case reflect.Struct:
|
||||
if isScalarStruct(et) {
|
||||
return nil, errTargetFallback
|
||||
}
|
||||
grown := reflect.Append(fv, reflect.New(et).Elem())
|
||||
// A pointer element is appended as the allocated pointer and filled
|
||||
// through its pointee, so []*T takes the same path []T does. The
|
||||
// element the table fills is the slice's own: the value a New built
|
||||
// stands apart from the backing array.
|
||||
var el, appended reflect.Value
|
||||
if fv.Type().Elem().Kind() == reflect.Pointer {
|
||||
p := reflect.New(et)
|
||||
el, appended = p.Elem(), p
|
||||
} else {
|
||||
el = reflect.New(et).Elem()
|
||||
appended = el
|
||||
}
|
||||
grown := reflect.Append(fv, appended)
|
||||
fv.Set(grown)
|
||||
return &targetTable{rv: grown.Index(grown.Len() - 1), schema: schemaRef(et), path: elementPath(parent.path, leaf, grown.Len()-1), strict: parent.strict}, nil
|
||||
if fv.Type().Elem().Kind() != reflect.Pointer {
|
||||
el = grown.Index(grown.Len() - 1)
|
||||
}
|
||||
return &targetTable{rv: el, schema: schemaRef(et), path: elementPath(parent.path, leaf, grown.Len()-1), strict: parent.strict}, nil
|
||||
case reflect.Map:
|
||||
if et.Key().Kind() != reflect.String {
|
||||
return nil, errTargetFallback
|
||||
}
|
||||
grown := reflect.Append(fv, reflect.MakeMap(et))
|
||||
m := reflect.MakeMap(et)
|
||||
var appended reflect.Value = m
|
||||
if fv.Type().Elem().Kind() == reflect.Pointer {
|
||||
p := reflect.New(et)
|
||||
p.Elem().Set(m)
|
||||
appended = p
|
||||
}
|
||||
grown := reflect.Append(fv, appended)
|
||||
fv.Set(grown)
|
||||
return &targetTable{rv: grown.Index(grown.Len() - 1), path: elementPath(parent.path, leaf, grown.Len()-1)}, nil
|
||||
return &targetTable{rv: m, path: elementPath(parent.path, leaf, grown.Len()-1)}, nil
|
||||
}
|
||||
return nil, errTargetFallback
|
||||
}
|
||||
@@ -780,22 +943,18 @@ func (tp *targetParser) parseKeyStatement() error {
|
||||
if verr != nil {
|
||||
return verr
|
||||
}
|
||||
dupKey := first
|
||||
if len(dest.path) > 0 || len(rest) > 0 {
|
||||
full := make([]string, 0, len(dest.path)+len(rest)+1)
|
||||
full = append(full, dest.path...)
|
||||
full = append(full, first)
|
||||
full = append(full, rest...)
|
||||
dupKey = pathKey(full)
|
||||
full := append([]string{first}, rest...)
|
||||
if len(dest.path) > 0 {
|
||||
full = append(slices.Clone(dest.path), full...)
|
||||
}
|
||||
if tp.tableHas(leafTable, dupKey) {
|
||||
if tp.tableHas(leafTable, pathKey(full)) {
|
||||
return p.errf("duplicate key %q", leaf)
|
||||
}
|
||||
tp.tableMark(leafTable, dupKey)
|
||||
tp.tableMark(leafTable, pathKey(full))
|
||||
if m, isMap := val.(map[string]any); isMap {
|
||||
full := make([]string, 0, len(dest.path)+len(leaf)+1)
|
||||
full = append(full, dest.path...)
|
||||
full = append(full, leaf)
|
||||
// An inline table freezes the whole path the statement wrote,
|
||||
// intermediate segments included, so no later header or dotted
|
||||
// key can extend it at any depth.
|
||||
p.freezeInline(full, m)
|
||||
}
|
||||
return nil
|
||||
@@ -804,27 +963,29 @@ func (tp *targetParser) parseKeyStatement() error {
|
||||
return p.errf("duplicate key %q", leaf)
|
||||
}
|
||||
tp.tableMark(leafTable, leaf)
|
||||
if dst.Kind() == reflect.Slice || dst.Kind() == reflect.Array {
|
||||
// Only a field a later [[header]] could append to needs the
|
||||
// key-assigned record; everything else never checks it.
|
||||
et := derefType(dst.Type().Elem())
|
||||
if et.Kind() == reflect.Struct && !isScalarStruct(et) || et.Kind() == reflect.Map {
|
||||
tp.markKeyAssigned(leafTable, leaf)
|
||||
}
|
||||
}
|
||||
val, err := tp.parseValueInto(dst)
|
||||
if err != nil {
|
||||
if errors.Is(err, errTargetFallback) {
|
||||
return err
|
||||
}
|
||||
if _, isSyntax := errors.AsType[*SyntaxError](err); !isSyntax {
|
||||
// A hook's own failure, which the fallback must not rerun: it
|
||||
// returns wrapped the way the tree decode wraps a field's.
|
||||
return newDecodeError(leaf, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
if mapDst.IsValid() {
|
||||
mapDst.SetMapIndex(reflect.ValueOf(leaf), dst)
|
||||
}
|
||||
if m, isMap := val.(map[string]any); isMap {
|
||||
// An inline table freezes its paths; the abs slice is built for it
|
||||
// alone, after the parse proved one is needed.
|
||||
abs := make([]string, 0, len(dest.path)+len(leaf)+1)
|
||||
// An inline table freezes the whole path the statement wrote,
|
||||
// intermediate segments included; the slice is built for it alone,
|
||||
// after the parse proved one is needed.
|
||||
abs := make([]string, 0, len(dest.path)+len(rest)+1)
|
||||
abs = append(abs, dest.path...)
|
||||
abs = append(abs, leaf)
|
||||
abs = append(abs, first)
|
||||
abs = append(abs, rest...)
|
||||
p.freezeInline(abs, m)
|
||||
}
|
||||
return nil
|
||||
@@ -847,7 +1008,10 @@ func (tp *targetParser) parseValueInto(dst reflect.Value) (any, error) {
|
||||
return nil, err
|
||||
}
|
||||
if err := tp.d.assign(v, dst); err != nil {
|
||||
return nil, errTargetFallback
|
||||
// The hook has run; falling back would run it a second time on
|
||||
// tree path, so its error returns as the tree path's own, for
|
||||
// the caller to wrap the way the tree decode wraps a field's.
|
||||
return nil, err
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
@@ -890,7 +1054,16 @@ func (tp *targetParser) parseValueInto(dst reflect.Value) (any, error) {
|
||||
}
|
||||
}
|
||||
tok := tp.scanNumberToken()
|
||||
if dtv, dtok := parseDateTime(tok); dtok {
|
||||
if hasHighByte(tok) && invalidUTF8Offset(tok) >= 0 {
|
||||
// The token route the targeted parse takes validates UTF-8 the
|
||||
// way the tree scanner does, on the byte that does not decode.
|
||||
return nil, p.errf("invalid UTF-8 in value at byte offset %d", start+invalidUTF8Offset(tok))
|
||||
}
|
||||
dtv, isDT, dterr := parseDateTime(tok)
|
||||
if dterr != nil {
|
||||
return nil, p.errf("%s", dterr)
|
||||
}
|
||||
if isDT {
|
||||
if err := tp.d.assign(dtv, dst); err != nil {
|
||||
return nil, errTargetFallback
|
||||
}
|
||||
@@ -1091,10 +1264,10 @@ func (tp *targetParser) descendDotted(dest *targetTable, first string, rest []st
|
||||
loc, found = tbl.schema.byName[resolved]
|
||||
}
|
||||
if !found {
|
||||
tbl.recordStrictUnknown(leaf)
|
||||
tp.recordStrictUnknown(tbl, leaf)
|
||||
return reflect.Value{}, reflect.Value{}, leafTable, false, nil
|
||||
}
|
||||
tbl.markResolved(resolved)
|
||||
tp.markResolved(tbl, resolved)
|
||||
fv, ferr := fieldByIndex(tbl.rv, loc.index)
|
||||
if ferr != nil {
|
||||
return reflect.Value{}, reflect.Value{}, leafTable, false, errTargetFallback
|
||||
@@ -1118,15 +1291,14 @@ func (tp *targetParser) dottedEnter(tbl *targetTable, seg string, segAbs []strin
|
||||
return nil, errTargetFallback
|
||||
}
|
||||
if existing := tbl.rv.MapIndex(reflect.ValueOf(seg)); existing.IsValid() && !existing.IsNil() {
|
||||
ev := existing.Elem()
|
||||
if ev.Kind() != reflect.Map {
|
||||
if existing.Kind() != reflect.Map {
|
||||
return nil, tp.errf("key %q is not a table", seg)
|
||||
}
|
||||
return &targetTable{rv: ev, path: segAbs}, nil
|
||||
return &targetTable{rv: existing, path: slices.Clone(segAbs)}, nil
|
||||
}
|
||||
next := reflect.MakeMap(elemT)
|
||||
tbl.rv.SetMapIndex(reflect.ValueOf(seg), next)
|
||||
return &targetTable{rv: next, path: segAbs}, nil
|
||||
return &targetTable{rv: next, path: slices.Clone(segAbs)}, nil
|
||||
}
|
||||
resolved := seg
|
||||
loc, ok := tbl.schema.byName[seg]
|
||||
@@ -1135,17 +1307,21 @@ func (tp *targetParser) dottedEnter(tbl *targetTable, seg string, segAbs []strin
|
||||
loc, ok = tbl.schema.byName[resolved]
|
||||
}
|
||||
if !ok {
|
||||
tbl.recordStrictUnknown(seg)
|
||||
if opened, ok := tp.opened[pathKey(segAbs)]; ok {
|
||||
tp.recordStrictUnknown(tbl, seg)
|
||||
// A sink an array element created is entered by a [sub-table]
|
||||
// header, through the last element, but never by a dotted key: the
|
||||
// tree's descendKey rejects an array where tableAt follows it.
|
||||
if opened, ok := tp.opened[pathKey(segAbs)]; ok && !opened.arrayElem {
|
||||
return opened, nil
|
||||
}
|
||||
if tp.tableHas(tbl, seg) {
|
||||
return nil, tp.errf("key %q is not a table", seg)
|
||||
}
|
||||
sink := &targetTable{sink: true, path: segAbs}
|
||||
sink := &targetTable{sink: true, path: slices.Clone(segAbs)}
|
||||
tp.opened[pathKey(segAbs)] = sink
|
||||
return sink, nil
|
||||
}
|
||||
tp.markResolved(tbl, resolved)
|
||||
fv, ferr := fieldByIndex(tbl.rv, loc.index)
|
||||
if ferr != nil {
|
||||
return nil, errTargetFallback
|
||||
@@ -1167,7 +1343,7 @@ func (tp *targetParser) dottedEnter(tbl *targetTable, seg string, segAbs []strin
|
||||
}
|
||||
return nil, errTargetFallback
|
||||
}
|
||||
return &targetTable{rv: fv, schema: schemaRef(fv.Type()), path: segAbs, strict: tbl.strict}, nil
|
||||
return &targetTable{rv: fv, schema: schemaRef(fv.Type()), path: slices.Clone(segAbs), strict: tbl.strict}, nil
|
||||
case reflect.Map:
|
||||
if fv.Type().Key().Kind() != reflect.String {
|
||||
return nil, errTargetFallback
|
||||
@@ -1175,7 +1351,7 @@ func (tp *targetParser) dottedEnter(tbl *targetTable, seg string, segAbs []strin
|
||||
if fv.IsNil() {
|
||||
fv.Set(reflect.MakeMap(fv.Type()))
|
||||
}
|
||||
return &targetTable{rv: fv, path: segAbs}, nil
|
||||
return &targetTable{rv: fv, path: slices.Clone(segAbs)}, nil
|
||||
}
|
||||
if tp.tableHas(tbl, seg) {
|
||||
return nil, tp.errf("key %q is not a table", seg)
|
||||
@@ -1193,7 +1369,9 @@ func (tp *targetParser) leafInTable(dest *targetTable, key string) (dst reflect.
|
||||
if dest.rv.Kind() != reflect.Map {
|
||||
return reflect.Value{}, reflect.Value{}, leafTable, false, errTargetFallback
|
||||
}
|
||||
if dest.rv.MapIndex(reflect.ValueOf(key)).IsValid() {
|
||||
if tp.tableHas(dest, key) {
|
||||
// The duplicate check reads the keys the document defined, not
|
||||
// the destination map's own contents, which are the caller's.
|
||||
return reflect.Value{}, reflect.Value{}, leafTable, false, tp.errf("duplicate key %q", key)
|
||||
}
|
||||
elem := reflect.New(dest.rv.Type().Elem()).Elem()
|
||||
@@ -1206,10 +1384,10 @@ func (tp *targetParser) leafInTable(dest *targetTable, key string) (dst reflect.
|
||||
loc, ok = dest.schema.byName[resolved]
|
||||
}
|
||||
if !ok {
|
||||
dest.recordStrictUnknown(key)
|
||||
tp.recordStrictUnknown(dest, key)
|
||||
return reflect.Value{}, reflect.Value{}, leafTable, false, nil
|
||||
}
|
||||
dest.markResolved(resolved)
|
||||
tp.markResolved(dest, resolved)
|
||||
fv, ferr := fieldByIndex(dest.rv, loc.index)
|
||||
if ferr != nil {
|
||||
return reflect.Value{}, reflect.Value{}, leafTable, false, errTargetFallback
|
||||
|
||||
+350
@@ -4,9 +4,12 @@
|
||||
package interpres
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"maps"
|
||||
"net"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
@@ -522,3 +525,350 @@ func TestTargetedMapTableShapes(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestTargetedNestedMapDescents pins the descents into a map of maps that
|
||||
// meet entries the document built earlier: a dotted key twice through the
|
||||
// same sub-table, a header into a dotted-built sub-table, and a typed array
|
||||
// under a map key. Each shape once panicked on a reflect Elem of a map.
|
||||
func TestTargetedNestedMapDescents(t *testing.T) {
|
||||
t.Run("dotted key through one sub-table twice", func(t *testing.T) {
|
||||
var cfg struct {
|
||||
M map[string]map[string]any `toml:"m"`
|
||||
}
|
||||
err := Unmarshal([]byte("m.a.b = 1\nm.a.c = 2\n"), &cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
if cfg.M["a"]["b"] != int64(1) || cfg.M["a"]["c"] != int64(2) {
|
||||
t.Errorf("m = %#v", cfg.M)
|
||||
}
|
||||
})
|
||||
t.Run("header under a dotted-built sub-table", func(t *testing.T) {
|
||||
var cfg struct {
|
||||
M map[string]map[string]any `toml:"m"`
|
||||
}
|
||||
err := Unmarshal([]byte("m.a.b = 1\n[m.a.deep]\nx = 2\n"), &cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
if cfg.M["a"]["b"] != int64(1) || cfg.M["a"]["deep"].(map[string]any)["x"] != int64(2) {
|
||||
t.Errorf("m = %#v", cfg.M)
|
||||
}
|
||||
})
|
||||
t.Run("typed array under a map key", func(t *testing.T) {
|
||||
var cfg struct {
|
||||
M map[string][]map[string]any `toml:"m"`
|
||||
}
|
||||
err := Unmarshal([]byte("[[m.arr]]\nx = 1\n\n[[m.arr]]\ny = 2\n"), &cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
if len(cfg.M["arr"]) != 2 || cfg.M["arr"][1]["y"] != int64(2) {
|
||||
t.Errorf("m = %#v", cfg.M)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestTargetedPointerElementSlice pins that an array of tables over a slice
|
||||
// of pointer elements fills the pointed-to structs.
|
||||
func TestTargetedPointerElementSlice(t *testing.T) {
|
||||
type item struct {
|
||||
N int `toml:"n"`
|
||||
}
|
||||
var cfg struct {
|
||||
Items []*item `toml:"items"`
|
||||
}
|
||||
err := Unmarshal([]byte("[[items]]\nn = 1\n\n[[items]]\nn = 2\n"), &cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
if len(cfg.Items) != 2 || cfg.Items[0] == nil || cfg.Items[1].N != 2 {
|
||||
t.Errorf("items = %#v", cfg.Items)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTargetedArrayScopeResets pins that a new element of an array of tables
|
||||
// starts a fresh definition scope, the contract the changelog documents.
|
||||
func TestTargetedArrayScopeResets(t *testing.T) {
|
||||
doc := "[[a]]\nb.c = 1\n\n[[a]]\n\n[a.b]\nx = 1\n"
|
||||
var ref, tgt targetCfg
|
||||
refErr := treeDecodeInto([]byte(doc), &ref)
|
||||
if refErr != nil {
|
||||
t.Fatalf("tree decode: %v", refErr)
|
||||
}
|
||||
if err := Unmarshal([]byte(doc), &tgt); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(ref, tgt) {
|
||||
t.Errorf("targeted = %#v, tree = %#v", tgt, ref)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTargetedUnknownArrayElements pins that every element of an unknown
|
||||
// array of tables is a fresh namespace, and a sub-table header reaches the
|
||||
// last element the way the tree parser's does.
|
||||
func TestTargetedUnknownArrayElements(t *testing.T) {
|
||||
doc := "[[zz]]\nk = 1\n\n[[zz]]\nk = 2\n\n[zz.sub]\nx = 3\n"
|
||||
var ref, tgt targetCfg
|
||||
refErr := treeDecodeInto([]byte(doc), &ref)
|
||||
tgtErr := Unmarshal([]byte(doc), &tgt)
|
||||
if (refErr == nil) != (tgtErr == nil) {
|
||||
t.Fatalf("error presence disagrees: tree %v, targeted %v", refErr, tgtErr)
|
||||
}
|
||||
if refErr != nil {
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(ref, tgt) {
|
||||
t.Errorf("targeted = %#v, tree = %#v", tgt, ref)
|
||||
}
|
||||
// A dotted key may not enter the array: the tree's own rule.
|
||||
var dotted targetCfg
|
||||
dErr := Unmarshal([]byte("[[zz]]\nk = 1\nzz.x = 2\n"), &dotted)
|
||||
refDotted := treeDecodeInto([]byte("[[zz]]\nk = 1\nzz.x = 2\n"), &dotted)
|
||||
if (dErr == nil) != (refDotted == nil) {
|
||||
t.Errorf("dotted into an array: targeted %v, tree %v", dErr, refDotted)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTargetedFixedArrayUnderFill pins that a fixed-size array the document
|
||||
// under-fills is the length mismatch the tree decode raises, with the
|
||||
// field's path.
|
||||
func TestTargetedFixedArrayUnderFill(t *testing.T) {
|
||||
type item struct {
|
||||
N int `toml:"n"`
|
||||
}
|
||||
var cfg struct {
|
||||
Items [2]item `toml:"items"`
|
||||
}
|
||||
err := Unmarshal([]byte("[[items]]\nn = 1\n"), &cfg)
|
||||
if err == nil {
|
||||
t.Fatal("unmarshal accepted an under-filled array")
|
||||
}
|
||||
want := `interpres: items: cannot assign 1 elements to [2]interpres.item`
|
||||
if err.Error() != want {
|
||||
t.Errorf("err = %v\nwant %q", err, want)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTargetedPrefilledSliceReplaced pins that a prefilled slice is replaced
|
||||
// by the document's elements on both paths, not appended to.
|
||||
func TestTargetedPrefilledSliceReplaced(t *testing.T) {
|
||||
type item struct {
|
||||
N int `toml:"n"`
|
||||
}
|
||||
doc := []byte("[[items]]\nn = 1\n")
|
||||
var ref struct {
|
||||
Items []item `toml:"items"`
|
||||
}
|
||||
ref.Items = []item{{N: 9}}
|
||||
if err := treeDecodeInto(doc, &ref); err != nil {
|
||||
t.Fatalf("tree decode: %v", err)
|
||||
}
|
||||
var tgt struct {
|
||||
Items []item `toml:"items"`
|
||||
}
|
||||
tgt.Items = []item{{N: 9}}
|
||||
if err := Unmarshal(doc, &tgt); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(ref, tgt) {
|
||||
t.Errorf("targeted = %#v, tree = %#v", tgt, ref)
|
||||
}
|
||||
if len(tgt.Items) != 1 || tgt.Items[0].N != 1 {
|
||||
t.Errorf("items = %#v, want the prefilled element replaced", tgt.Items)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTargetedHeaderOverValueArrayKeepsCase pins that a value array assigned
|
||||
// under a differently cased key than the field's name still blocks the
|
||||
// array-of-tables header over it, the tree parse error.
|
||||
func TestTargetedHeaderOverValueArrayKeepsCase(t *testing.T) {
|
||||
var cfg struct {
|
||||
Arr []targetNested `toml:"arr"`
|
||||
}
|
||||
err := Unmarshal([]byte("Arr = [{x = 1}]\n[[Arr]]\nx = 2\n"), &cfg)
|
||||
if err == nil || err.Error() != `interpres: line 2: key "Arr" is not an array of tables` {
|
||||
t.Errorf("err = %v, want the parse error over the assigned field", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTargetedDottedInlineFreezePath pins that an inline table assigned by a
|
||||
// dotted key freezes the whole path the statement wrote: a later header
|
||||
// under that path is the extension error, and a key outside it stays free.
|
||||
func TestTargetedDottedInlineFreezePath(t *testing.T) {
|
||||
var cfg targetCfg
|
||||
err := Unmarshal([]byte("m.a.b = {x = 1}\nb.y = 2\n"), &cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
err = Unmarshal([]byte("m.a.b = {x = 1}\n[m.a.b]\ny = 2\n"), &cfg)
|
||||
want := `interpres: line 2: cannot extend inline table "m.a.b"`
|
||||
if err == nil || err.Error() != want {
|
||||
t.Errorf("err = %v\nwant %q", err, want)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTargetedStrictThroughDottedKeys pins that strict and required findings
|
||||
// survive the transient tables a dotted descent builds.
|
||||
func TestTargetedStrictThroughDottedKeys(t *testing.T) {
|
||||
var cfg targetCfg
|
||||
err := Unmarshal([]byte("tab.zz = 1\n"), &cfg, RejectUnknownFields(true))
|
||||
if err == nil || !strings.Contains(err.Error(), `unknown field "zz"`) {
|
||||
t.Errorf("err = %v, want the strict failure through the dotted key", err)
|
||||
}
|
||||
if err == nil || !strings.HasPrefix(err.Error(), "interpres: tab:") {
|
||||
t.Errorf("err = %v, want the path through the dotted key", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTargetedRequiredThroughDottedKeys pins that a required tag is honoured
|
||||
// when the table is reached only through dotted keys.
|
||||
func TestTargetedRequiredThroughDottedKeys(t *testing.T) {
|
||||
type nested struct {
|
||||
X int `toml:"x,required"`
|
||||
Y int `toml:"y"`
|
||||
}
|
||||
var cfg struct {
|
||||
Tab nested `toml:"tab"`
|
||||
}
|
||||
err := Unmarshal([]byte("tab.y = 1\n"), &cfg)
|
||||
if err == nil || !strings.Contains(err.Error(), `missing required key "x"`) {
|
||||
t.Errorf("err = %v, want the missing required key through the dotted key", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTargetedOrderedMapSliceFallsBack pins that a slice of OrderedMap
|
||||
// elements takes the tree path, whose fill keeps the written order.
|
||||
func TestTargetedOrderedMapSliceFallsBack(t *testing.T) {
|
||||
var cfg struct {
|
||||
Items []OrderedMap `toml:"items"`
|
||||
}
|
||||
err := Unmarshal([]byte("[[items]]\nk = \"v\"\n"), &cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
if len(cfg.Items) != 1 || cfg.Items[0].Keys()[0] != "k" {
|
||||
t.Errorf("items = %#v, want the element filled in written order", cfg.Items)
|
||||
}
|
||||
}
|
||||
|
||||
// hookMap is a named map type whose decode hook counts its calls.
|
||||
type hookMap map[string]any
|
||||
|
||||
var hookMapCalls atomic.Int32
|
||||
|
||||
func (h *hookMap) UnmarshalTOML(data any) error {
|
||||
hookMapCalls.Add(1)
|
||||
m, _ := data.(map[string]any)
|
||||
if *h == nil {
|
||||
*h = hookMap{}
|
||||
}
|
||||
maps.Copy((*h), m)
|
||||
return nil
|
||||
}
|
||||
|
||||
// TestTargetedMapFieldHookGetsWholeTable pins that a named map field with a
|
||||
// decode hook receives the whole parsed table, even in its header form.
|
||||
func TestTargetedMapFieldHookGetsWholeTable(t *testing.T) {
|
||||
type cfg struct {
|
||||
M hookMap `toml:"m"`
|
||||
}
|
||||
var c cfg
|
||||
hookMapCalls.Store(0)
|
||||
err := Unmarshal([]byte("[m]\na = 1\nb = 2\n"), &c)
|
||||
if err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
if hookMapCalls.Load() != 1 {
|
||||
t.Errorf("hook calls = %d, want exactly one with the whole table", hookMapCalls.Load())
|
||||
}
|
||||
if c.M["a"] != int64(1) || c.M["b"] != int64(2) {
|
||||
t.Errorf("m = %#v", c.M)
|
||||
}
|
||||
}
|
||||
|
||||
// errHook fails every decode with a fixed error and counts its calls.
|
||||
type errHook struct{ calls *int }
|
||||
|
||||
func (e *errHook) UnmarshalTOML(any) error {
|
||||
if e.calls != nil {
|
||||
*e.calls++
|
||||
}
|
||||
return errors.New("boom")
|
||||
}
|
||||
|
||||
// TestTargetedHookErrorRunsOnce pins that a failing hook's error is the
|
||||
// tree path's own, wrapped with the key, and that the hook is not run a
|
||||
// second time by a fallback.
|
||||
func TestTargetedHookErrorRunsOnce(t *testing.T) {
|
||||
calls := 0
|
||||
cfg := struct {
|
||||
F errHook `toml:"f"`
|
||||
}{F: errHook{calls: &calls}}
|
||||
err := Unmarshal([]byte("f = 1\n"), &cfg)
|
||||
if err == nil || err.Error() != "interpres: f: unmarshal: boom" {
|
||||
t.Errorf("err = %v, want the wrapped hook failure", err)
|
||||
}
|
||||
if calls != 1 {
|
||||
t.Errorf("hook calls = %d, want one", calls)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTargetedUnknownBeforeRequired pins the report order the tree decode
|
||||
// produces: an unknown key wins over a missing required one.
|
||||
func TestTargetedUnknownBeforeRequired(t *testing.T) {
|
||||
type inner struct {
|
||||
X int `toml:"x,required"`
|
||||
}
|
||||
var cfg struct {
|
||||
Tab inner `toml:"tab"`
|
||||
}
|
||||
err := Unmarshal([]byte("[tab]\nzz = 1\n"), &cfg, RejectUnknownFields(true))
|
||||
if err == nil || !strings.Contains(err.Error(), `unknown field "zz"`) {
|
||||
t.Errorf("err = %v, want the unknown key reported before the required one", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTargetedStrictPathStableAcrossHeaders pins that the path a strict
|
||||
// finding wraps does not alias the parser's key buffer: the table that owns
|
||||
// the unknown key keeps its name after a later header.
|
||||
func TestTargetedStrictPathStableAcrossHeaders(t *testing.T) {
|
||||
var cfg targetCfg
|
||||
err := Unmarshal([]byte("[tab]\nzz = 1\n\n[lims]\nx = 1\n"), &cfg, RejectUnknownFields(true))
|
||||
if err == nil || !strings.HasPrefix(err.Error(), "interpres: tab:") {
|
||||
t.Errorf("err = %v, want the finding on tab, not the later header", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTargetedPrefilledMapFieldMergesUnderHeader pins that a prefilled map
|
||||
// field merges the document's header-form table into it on both paths, the
|
||||
// rule the root map has always followed.
|
||||
func TestTargetedPrefilledMapFieldMergesUnderHeader(t *testing.T) {
|
||||
doc := []byte("[lims]\nnew = 3\n")
|
||||
var ref, tgt targetCfg
|
||||
ref.Lims = map[string]any{"keep": "yes"}
|
||||
if err := treeDecodeInto(doc, &ref); err != nil {
|
||||
t.Fatalf("tree decode: %v", err)
|
||||
}
|
||||
tgt.Lims = map[string]any{"keep": "yes"}
|
||||
if err := Unmarshal(doc, &tgt); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(ref, tgt) {
|
||||
t.Errorf("targeted = %#v, tree = %#v", tgt, ref)
|
||||
}
|
||||
if tgt.Lims["keep"] != "yes" || tgt.Lims["new"] != int64(3) {
|
||||
t.Errorf("lims = %#v, want the merge", tgt.Lims)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTargetedNumberTokenValidatesUTF8 pins that the token route the
|
||||
// targeted parse takes reports invalid UTF-8 with the scanner's own message
|
||||
// and position.
|
||||
func TestTargetedNumberTokenValidatesUTF8(t *testing.T) {
|
||||
var cfg targetCfg
|
||||
err := Unmarshal([]byte("num = 12\xff\n"), &cfg)
|
||||
if err == nil || !strings.Contains(err.Error(), "invalid UTF-8 in value at byte offset 8") {
|
||||
t.Errorf("err = %v, want the UTF-8 complaint on the invalid byte", err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user