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
|
// document through the tree path, so the observable behaviour is the tree
|
||||||
// path's, exactly. A targeted parse either completes with the result the
|
// path's, exactly. A targeted parse either completes with the result the
|
||||||
// tree path would give, or it erases itself.
|
// 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")
|
var errTargetFallback = errors.New("interpres: targeted decode falls back to the tree path")
|
||||||
|
|
||||||
// targetCache holds whether a destination type may take the targeted parse.
|
// 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]()
|
var mapStringAnyType = reflect.TypeFor[map[string]any]()
|
||||||
|
|
||||||
// typeTargetable reports whether decoding into the struct type t can use the
|
// typeTargetable reports whether decoding into the struct type t can use the
|
||||||
// targeted parse. The one structural ban is untagged embedded maps: their
|
// targeted parse. The structural bans are the shapes whose tree behaviour
|
||||||
// filler-key rule lives in the tree decode, and a targeted document that
|
// the skeleton cannot model: untagged embedded maps, an OrderedMap anywhere a
|
||||||
// meets an unknown table would need a subtree of it. Everything else is safe
|
// table opens, and a custom decode hook on any table the parse would enter
|
||||||
// to attempt, because the value layer is the ordinary decode and every
|
// 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.
|
// mismatch falls back.
|
||||||
func typeTargetable(t reflect.Type) bool {
|
func typeTargetable(t reflect.Type) bool {
|
||||||
if t == nil || t.Kind() != reflect.Struct || t == orderedMapType {
|
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
|
return false
|
||||||
}
|
}
|
||||||
for _, loc := range cachedStructSchema(t).byName {
|
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 {
|
if ft == orderedMapType {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if ft.Kind() != reflect.Struct || isScalarStruct(ft) {
|
if isScalarStruct(ft) {
|
||||||
continue
|
return true
|
||||||
}
|
}
|
||||||
// A struct field with a custom decode hook receives the whole parsed
|
// A struct the parse enters directly never builds the whole value
|
||||||
// value from the tree decode; the targeted skeleton never builds that
|
// the tree hands a hook, so the hook must win.
|
||||||
// value for a table it enters directly, so the hook must win.
|
|
||||||
if implementsDecodeHook(ft) || implementsDecodeHook(reflect.PointerTo(ft)) {
|
if implementsDecodeHook(ft) || implementsDecodeHook(reflect.PointerTo(ft)) {
|
||||||
return false
|
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
|
return false
|
||||||
}
|
}
|
||||||
|
if et.Kind() == reflect.Struct && !isScalarStruct(et) {
|
||||||
|
return scanTargetableField(et, seen)
|
||||||
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
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)
|
// 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
|
// 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
|
// destination), and the absolute path its errors wrap. A sink is the
|
||||||
// the keys no field claims for the strict check. A sink is the destination
|
// destination an unknown subtree gets: its statements parse for the syntax
|
||||||
// an unknown subtree gets: its statements parse for the syntax and definition
|
// and definition contracts, and its values are discarded. The strict and
|
||||||
// contracts, and its values are discarded.
|
// 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 {
|
type targetTable struct {
|
||||||
rv reflect.Value
|
rv reflect.Value
|
||||||
schema *structSchema
|
schema *structSchema
|
||||||
path []string
|
path []string
|
||||||
sink bool
|
sink bool
|
||||||
keys []string // the keys defined in the table, interned; struct
|
// strict is the strict-decode setting the table was opened with, carried
|
||||||
// tables keep theirs per destination address instead
|
// into the per-address strict state on first sight.
|
||||||
strict bool
|
strict bool
|
||||||
unknown string // strict: the smallest unclaimed key so far
|
// arrayElem marks a sink created as the element of an unknown array of
|
||||||
resolvedSeen map[string]bool // the schema keys resolved so far, for required
|
// 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
|
||||||
|
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
|
// targetParser parses a document straight into a struct destination. It
|
||||||
@@ -139,61 +193,53 @@ type targetParser struct {
|
|||||||
rootT *targetTable
|
rootT *targetTable
|
||||||
cur *targetTable
|
cur *targetTable
|
||||||
|
|
||||||
// arrayNext tracks how many elements of a fixed-size array the document
|
// arrayFills counts the elements of each fixed-size array the document
|
||||||
// has filled, per field address.
|
// has filled, per field address, in the order the arrays were met.
|
||||||
arrayNext map[uintptr]int
|
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
|
// opened registers every opened table by its path key, sinks included: a
|
||||||
// later header or dotted key meets the table the tree already built.
|
// later header or dotted key meets the table the tree already built.
|
||||||
opened map[string]*targetTable
|
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
|
// tableKeysByAddr holds the defined keys of one struct destination, keyed
|
||||||
// by the value's address: the table object a dotted descent builds is
|
// by the value's address: the table object a dotted descent builds is
|
||||||
// transient, the destination is not.
|
// transient, the destination is not.
|
||||||
tableKeysByAddr map[uintptr][]string
|
tableKeysByAddr map[uintptr][]string
|
||||||
}
|
|
||||||
|
|
||||||
// markKeyAssigned records that a key statement assigned the field, and
|
// mapKeysByAddr holds the keys the document has defined in one map
|
||||||
// keyAssigned reports that state. Sinks keep no such bookkeeping.
|
// destination, keyed the same way: the destination map the caller
|
||||||
func (tp *targetParser) markKeyAssigned(t *targetTable, key string) {
|
// prefilled is not the parser's state, and a key it holds is not the
|
||||||
if t.sink || t.schema == nil || !t.rv.CanAddr() || t.rv.Kind() != reflect.Struct {
|
// duplicate a key the document repeats is.
|
||||||
return
|
mapKeysByAddr map[uintptr]map[string]bool
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tp *targetParser) keyAssigned(t *targetTable, key string) bool {
|
// strictByAddr holds each destination's strict and required findings,
|
||||||
if t.sink || t.schema == nil || !t.rv.CanAddr() || t.rv.Kind() != reflect.Struct {
|
// with strictOrder keeping the document order they first appeared in.
|
||||||
return false
|
strictByAddr map[uintptr]*strictState
|
||||||
}
|
strictOrder []uintptr
|
||||||
addr := t.rv.Addr().Pointer()
|
|
||||||
return tp.keyAssignedKeys[addr][key]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// tableHas reports whether key is already defined in the table. A struct
|
// tableHas reports whether key is already defined in the table. A struct
|
||||||
// table's keys are interned parser strings, so the linear scan compares
|
// 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.
|
// against a handful of short keys, cheaper than hashing a per-table map.
|
||||||
// Struct tables keep their keys by destination address, because the table
|
// Struct tables keep their keys by destination address, and map tables keep
|
||||||
// object a dotted descent builds is transient while the destination is not.
|
// 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 {
|
func (tp *targetParser) tableHas(t *targetTable, key string) bool {
|
||||||
switch {
|
switch {
|
||||||
case t.sink:
|
case t.sink:
|
||||||
return slices.Contains(t.keys, key)
|
return slices.Contains(t.keys, key)
|
||||||
case t.schema == nil:
|
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:
|
default:
|
||||||
return slices.Contains(tp.tableKeys(t), key)
|
return slices.Contains(tp.tableKeys(t), key)
|
||||||
}
|
}
|
||||||
@@ -217,6 +263,17 @@ func (tp *targetParser) tableMark(t *targetTable, key string) {
|
|||||||
case t.sink:
|
case t.sink:
|
||||||
t.keys = append(t.keys, key)
|
t.keys = append(t.keys, key)
|
||||||
case t.schema == nil:
|
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:
|
default:
|
||||||
if !t.rv.CanAddr() || t.rv.Kind() != reflect.Struct {
|
if !t.rv.CanAddr() || t.rv.Kind() != reflect.Struct {
|
||||||
return
|
return
|
||||||
@@ -229,32 +286,49 @@ func (tp *targetParser) tableMark(t *targetTable, key string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// resolvedHas reports whether the resolved schema key has been seen, the
|
// strictState returns the strict and required bookkeeping of the struct
|
||||||
// check a required tag runs: the duplicate bookkeeping tracks the key as the
|
// destination t fills, registering it on first sight so a finding recorded
|
||||||
// document wrote it, the required bookkeeping the key as the schema
|
// on a transient table survives the table.
|
||||||
// resolved it.
|
func (tp *targetParser) strictState(t *targetTable) *strictState {
|
||||||
func (t *targetTable) resolvedHas(key string) bool {
|
addr := t.rv.Addr().Pointer()
|
||||||
return t.resolvedSeen[key]
|
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 {
|
if t.schema == nil || len(t.schema.required) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if t.resolvedSeen == nil {
|
if !t.rv.CanAddr() || t.rv.Kind() != reflect.Struct {
|
||||||
t.resolvedSeen = make(map[string]bool, 8)
|
return
|
||||||
}
|
}
|
||||||
t.resolvedSeen[key] = true
|
tp.strictState(t).resolved[key] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// recordStrictUnknown remembers the key no field claims when strict decoding
|
// recordStrictUnknown remembers the key no field claims when strict decoding
|
||||||
// is on: the smallest one is reported, the tree decode's own choice.
|
// is on: the smallest one is reported, the tree decode's own choice.
|
||||||
func (t *targetTable) recordStrictUnknown(key string) {
|
func (tp *targetParser) recordStrictUnknown(t *targetTable, key string) {
|
||||||
if !t.strict {
|
if !t.strict || t.schema == nil || !t.rv.CanAddr() || t.rv.Kind() != reflect.Struct {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if t.unknown == "" || key < t.unknown {
|
st := tp.strictState(t)
|
||||||
t.unknown = key
|
if st.unknown == "" || key < st.unknown {
|
||||||
|
st.unknown = key
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,12 +348,13 @@ func parseIntoTargeted(ctx context.Context, data []byte, d *decoder, useNumber b
|
|||||||
}
|
}
|
||||||
rv := reflect.ValueOf(v)
|
rv := reflect.ValueOf(v)
|
||||||
tp := &targetParser{
|
tp := &targetParser{
|
||||||
parser: &parser{src: data, line: 1, ctx: ctx, maxDepth: maxDepth, useNumber: useNumber},
|
parser: &parser{src: data, line: 1, ctx: ctx, maxDepth: maxDepth, useNumber: useNumber},
|
||||||
d: d,
|
d: d,
|
||||||
root: rv.Elem(),
|
root: rv.Elem(),
|
||||||
arrayNext: make(map[uintptr]int, 4),
|
arrayFills: make(map[uintptr]*arrayFill, 4),
|
||||||
keyAssignedKeys: make(map[uintptr]map[string]bool, 8),
|
appendedHere: make(map[uintptr]bool, 4),
|
||||||
opened: make(map[string]*targetTable, 8),
|
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.rootT = &targetTable{rv: tp.root, schema: schemaRef(tp.root.Type()), strict: d.disallowUnknown}
|
||||||
tp.tables = append(tp.tables, tp.rootT)
|
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
|
// reportDeferred raises the decode-stage findings in the tree decode's
|
||||||
// order: the root table first, then the opened tables in document order. The
|
// order. A fixed-size array the document under-filled is the length mismatch
|
||||||
// tree decode reports them after a full parse, so a later parse error always
|
// the tree decode raises, and it comes first. Then the unknown keys, before
|
||||||
// won; here the parse has already completed.
|
// 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 {
|
func (tp *targetParser) reportDeferred() error {
|
||||||
for _, t := range append([]*targetTable{tp.rootT}, tp.tables...) {
|
for _, f := range tp.fillOrder {
|
||||||
if t.sink {
|
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
|
continue
|
||||||
}
|
}
|
||||||
if t.strict && t.unknown != "" {
|
for _, key := range st.schema.required {
|
||||||
return tp.wrapTableErr(t, fmt.Errorf("interpres: unknown field %q for %s", t.unknown, t.rv.Type()))
|
if !st.resolved[key] {
|
||||||
}
|
return wrapTablePath(st.path, fmt.Errorf("interpres: missing required key %q", key))
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
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.
|
// root speaks for itself, a nested table gains its path.
|
||||||
func (tp *targetParser) wrapTableErr(t *targetTable, err error) error {
|
func wrapTablePath(path []string, err error) error {
|
||||||
if len(t.path) == 0 {
|
if len(path) == 0 {
|
||||||
return err
|
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 ---------------------------------------------------------------
|
// --- headers ---------------------------------------------------------------
|
||||||
@@ -474,11 +571,11 @@ func (tp *targetParser) descendOne(parent *targetTable, key string, abs []string
|
|||||||
return nil, errTargetFallback
|
return nil, errTargetFallback
|
||||||
}
|
}
|
||||||
if existing := parent.rv.MapIndex(reflect.ValueOf(key)); existing.IsValid() && !existing.IsNil() {
|
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)
|
next := reflect.MakeMap(elemT)
|
||||||
parent.rv.SetMapIndex(reflect.ValueOf(key), next)
|
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
|
resolved := key
|
||||||
loc, ok := parent.schema.byName[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]
|
loc, ok = parent.schema.byName[resolved]
|
||||||
}
|
}
|
||||||
if !ok {
|
if !ok {
|
||||||
parent.recordStrictUnknown(key)
|
tp.recordStrictUnknown(parent, key)
|
||||||
if opened, ok := tp.opened[pathKey(abs)]; ok {
|
if opened, ok := tp.opened[pathKey(abs)]; ok {
|
||||||
return opened, nil
|
return opened, nil
|
||||||
}
|
}
|
||||||
if tp.tableHas(parent, key) {
|
if tp.tableHas(parent, key) {
|
||||||
return nil, tp.errf("key %q is not a table", 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
|
tp.opened[pathKey(abs)] = sink
|
||||||
return sink, nil
|
return sink, nil
|
||||||
}
|
}
|
||||||
parent.markResolved(resolved)
|
tp.markResolved(parent, resolved)
|
||||||
fv, err := fieldByIndex(parent.rv, loc.index)
|
fv, err := fieldByIndex(parent.rv, loc.index)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errTargetFallback
|
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
|
// 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
|
// 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
|
// 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
|
// tables enters its last filled element, a slice enters the last element of
|
||||||
// tree decode reports, so it falls back. A scalar field the table keys
|
// an array this document's [[headers]] built (a prefilled slice is a table
|
||||||
// already define is the tree's `key is not a table` error, checked against
|
// the tree decode rejects, so it falls back), and anything else is a type
|
||||||
// parent, the table the key belongs to.
|
// 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) {
|
func (tp *targetParser) openValueTable(fv reflect.Value, parent *targetTable, key string, abs []string, strict bool) (*targetTable, error) {
|
||||||
if fv.Kind() == reflect.Pointer {
|
if fv.Kind() == reflect.Pointer {
|
||||||
if fv.IsNil() {
|
if fv.IsNil() {
|
||||||
@@ -528,7 +627,7 @@ func (tp *targetParser) openValueTable(fv reflect.Value, parent *targetTable, ke
|
|||||||
if isScalarStruct(fv.Type()) {
|
if isScalarStruct(fv.Type()) {
|
||||||
return nil, errTargetFallback
|
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:
|
case reflect.Map:
|
||||||
if fv.Type().Key().Kind() != reflect.String {
|
if fv.Type().Key().Kind() != reflect.String {
|
||||||
return nil, errTargetFallback
|
return nil, errTargetFallback
|
||||||
@@ -536,11 +635,12 @@ func (tp *targetParser) openValueTable(fv reflect.Value, parent *targetTable, ke
|
|||||||
if fv.IsNil() {
|
if fv.IsNil() {
|
||||||
fv.Set(reflect.MakeMap(fv.Type()))
|
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:
|
case reflect.Slice:
|
||||||
if fv.Len() == 0 {
|
if !tp.appendedHere[fv.Addr().Pointer()] {
|
||||||
// No [[header]] ever filled it, so the tree holds a map here and
|
// No [[header]] of this document filled it, so the tree holds a
|
||||||
// its decode raises the type mismatch.
|
// 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
|
return nil, errTargetFallback
|
||||||
}
|
}
|
||||||
et := derefType(fv.Type().Elem())
|
et := derefType(fv.Type().Elem())
|
||||||
@@ -548,6 +648,22 @@ func (tp *targetParser) openValueTable(fv reflect.Value, parent *targetTable, ke
|
|||||||
return nil, errTargetFallback
|
return nil, errTargetFallback
|
||||||
}
|
}
|
||||||
return &targetTable{rv: fv.Index(fv.Len() - 1), schema: schemaRef(et), path: elementPath(abs, key, fv.Len()-1), strict: strict}, nil
|
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) {
|
if tp.tableHas(parent, key) {
|
||||||
return nil, tp.errf("key %q is not a table", 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] {
|
if tp.parser.dotted[pk] || tp.parser.headers[pk] {
|
||||||
return tp.errf("key %q is not an array of tables", leaf)
|
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)
|
tp.parser.markArray(pk)
|
||||||
|
|
||||||
elem, err := tp.appendElement(parent, leaf, key)
|
elem, err := tp.appendElement(parent, leaf, key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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)
|
tp.tableMark(parent, leaf)
|
||||||
if !elem.sink {
|
if !elem.sink {
|
||||||
tp.tables = append(tp.tables, elem)
|
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
|
// 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
|
// and returns its table. A leaf no field claims sinks, a fresh namespace per
|
||||||
// element kind cannot be a table falls back, the tree decode owning the type
|
// element; a field whose array element kind cannot be a table falls back,
|
||||||
// error.
|
// the tree decode owning the type error.
|
||||||
func (tp *targetParser) appendElement(parent *targetTable, leaf string, key []string) (*targetTable, error) {
|
func (tp *targetParser) appendElement(parent *targetTable, leaf string, key []string) (*targetTable, error) {
|
||||||
if parent.sink {
|
if parent.sink {
|
||||||
return parent, nil
|
return parent, nil
|
||||||
@@ -608,10 +742,15 @@ func (tp *targetParser) appendElement(parent *targetTable, leaf string, key []st
|
|||||||
gk := reflect.ValueOf(leaf)
|
gk := reflect.ValueOf(leaf)
|
||||||
var arr reflect.Value
|
var arr reflect.Value
|
||||||
if existing := parent.rv.MapIndex(gk); existing.IsValid() && !existing.IsNil() {
|
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)
|
return nil, tp.errf("key %q is not an array of tables", leaf)
|
||||||
}
|
}
|
||||||
arr = existing.Elem()
|
arr = existing
|
||||||
}
|
}
|
||||||
var elem reflect.Value
|
var elem reflect.Value
|
||||||
switch {
|
switch {
|
||||||
@@ -645,62 +784,64 @@ func (tp *targetParser) appendElement(parent *targetTable, leaf string, key []st
|
|||||||
loc, ok = parent.schema.byName[resolved]
|
loc, ok = parent.schema.byName[resolved]
|
||||||
}
|
}
|
||||||
if !ok {
|
if !ok {
|
||||||
parent.recordStrictUnknown(leaf)
|
tp.recordStrictUnknown(parent, leaf)
|
||||||
if opened, ok := tp.opened[pathKey(parent.path)]; ok {
|
// Every element is a fresh namespace, the way a known array's is,
|
||||||
return opened, nil
|
// 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
|
||||||
if tp.tableHas(parent, leaf) {
|
// tables; a dotted key skips it, the tree's rule for an array.
|
||||||
return nil, tp.errf("key %q is not an array of tables", leaf)
|
sink := &targetTable{sink: true, arrayElem: true, path: slices.Clone(key)}
|
||||||
}
|
tp.opened[pathKey(key)] = sink
|
||||||
sink := &targetTable{sink: true, path: parent.path}
|
|
||||||
tp.opened[pathKey(parent.path)] = sink
|
|
||||||
return sink, nil
|
return sink, nil
|
||||||
}
|
}
|
||||||
parent.markResolved(resolved)
|
tp.markResolved(parent, 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)
|
|
||||||
}
|
|
||||||
fv, err := fieldByIndex(parent.rv, loc.index)
|
fv, err := fieldByIndex(parent.rv, loc.index)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errTargetFallback
|
return nil, errTargetFallback
|
||||||
}
|
}
|
||||||
|
fieldPath := append(slices.Clone(parent.path), leaf)
|
||||||
if fv.Kind() == reflect.Array {
|
if fv.Kind() == reflect.Array {
|
||||||
// A fixed-size array fills position by position; one element too many
|
// 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())
|
et := derefType(fv.Type().Elem())
|
||||||
switch et.Kind() {
|
switch {
|
||||||
case reflect.Struct:
|
case et.Kind() == reflect.Struct && !isScalarStruct(et):
|
||||||
if isScalarStruct(et) {
|
if fill.next >= fv.Len() {
|
||||||
return nil, errTargetFallback
|
return nil, errTargetFallback
|
||||||
}
|
}
|
||||||
addr := fv.Addr().Pointer()
|
n := fill.next
|
||||||
n := tp.arrayNext[addr]
|
fill.next = n + 1
|
||||||
if n >= fv.Len() {
|
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
|
return nil, errTargetFallback
|
||||||
}
|
}
|
||||||
tp.arrayNext[addr] = n + 1
|
n := fill.next
|
||||||
return &targetTable{rv: fv.Index(n), schema: schemaRef(et), path: parent.path, strict: parent.strict}, nil
|
fill.next = n + 1
|
||||||
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
|
|
||||||
elem := reflect.MakeMap(et)
|
elem := reflect.MakeMap(et)
|
||||||
fv.Index(n).Set(elem)
|
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
|
return nil, errTargetFallback
|
||||||
}
|
}
|
||||||
if fv.Kind() != reflect.Slice {
|
if fv.Kind() != reflect.Slice {
|
||||||
return nil, tp.errf("key %q is not an array of tables", leaf)
|
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())
|
et := derefType(fv.Type().Elem())
|
||||||
switch et.Kind() {
|
switch et.Kind() {
|
||||||
@@ -708,16 +849,38 @@ func (tp *targetParser) appendElement(parent *targetTable, leaf string, key []st
|
|||||||
if isScalarStruct(et) {
|
if isScalarStruct(et) {
|
||||||
return nil, errTargetFallback
|
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)
|
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:
|
case reflect.Map:
|
||||||
if et.Key().Kind() != reflect.String {
|
if et.Key().Kind() != reflect.String {
|
||||||
return nil, errTargetFallback
|
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)
|
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
|
return nil, errTargetFallback
|
||||||
}
|
}
|
||||||
@@ -780,22 +943,18 @@ func (tp *targetParser) parseKeyStatement() error {
|
|||||||
if verr != nil {
|
if verr != nil {
|
||||||
return verr
|
return verr
|
||||||
}
|
}
|
||||||
dupKey := first
|
full := append([]string{first}, rest...)
|
||||||
if len(dest.path) > 0 || len(rest) > 0 {
|
if len(dest.path) > 0 {
|
||||||
full := make([]string, 0, len(dest.path)+len(rest)+1)
|
full = append(slices.Clone(dest.path), full...)
|
||||||
full = append(full, dest.path...)
|
|
||||||
full = append(full, first)
|
|
||||||
full = append(full, rest...)
|
|
||||||
dupKey = pathKey(full)
|
|
||||||
}
|
}
|
||||||
if tp.tableHas(leafTable, dupKey) {
|
if tp.tableHas(leafTable, pathKey(full)) {
|
||||||
return p.errf("duplicate key %q", leaf)
|
return p.errf("duplicate key %q", leaf)
|
||||||
}
|
}
|
||||||
tp.tableMark(leafTable, dupKey)
|
tp.tableMark(leafTable, pathKey(full))
|
||||||
if m, isMap := val.(map[string]any); isMap {
|
if m, isMap := val.(map[string]any); isMap {
|
||||||
full := make([]string, 0, len(dest.path)+len(leaf)+1)
|
// An inline table freezes the whole path the statement wrote,
|
||||||
full = append(full, dest.path...)
|
// intermediate segments included, so no later header or dotted
|
||||||
full = append(full, leaf)
|
// key can extend it at any depth.
|
||||||
p.freezeInline(full, m)
|
p.freezeInline(full, m)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -804,27 +963,29 @@ func (tp *targetParser) parseKeyStatement() error {
|
|||||||
return p.errf("duplicate key %q", leaf)
|
return p.errf("duplicate key %q", leaf)
|
||||||
}
|
}
|
||||||
tp.tableMark(leafTable, 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)
|
val, err := tp.parseValueInto(dst)
|
||||||
if err != nil {
|
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
|
return err
|
||||||
}
|
}
|
||||||
if mapDst.IsValid() {
|
if mapDst.IsValid() {
|
||||||
mapDst.SetMapIndex(reflect.ValueOf(leaf), dst)
|
mapDst.SetMapIndex(reflect.ValueOf(leaf), dst)
|
||||||
}
|
}
|
||||||
if m, isMap := val.(map[string]any); isMap {
|
if m, isMap := val.(map[string]any); isMap {
|
||||||
// An inline table freezes its paths; the abs slice is built for it
|
// An inline table freezes the whole path the statement wrote,
|
||||||
// alone, after the parse proved one is needed.
|
// intermediate segments included; the slice is built for it alone,
|
||||||
abs := make([]string, 0, len(dest.path)+len(leaf)+1)
|
// 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, dest.path...)
|
||||||
abs = append(abs, leaf)
|
abs = append(abs, first)
|
||||||
|
abs = append(abs, rest...)
|
||||||
p.freezeInline(abs, m)
|
p.freezeInline(abs, m)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -847,7 +1008,10 @@ func (tp *targetParser) parseValueInto(dst reflect.Value) (any, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if err := tp.d.assign(v, dst); err != nil {
|
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
|
return v, nil
|
||||||
}
|
}
|
||||||
@@ -890,7 +1054,16 @@ func (tp *targetParser) parseValueInto(dst reflect.Value) (any, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
tok := tp.scanNumberToken()
|
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 {
|
if err := tp.d.assign(dtv, dst); err != nil {
|
||||||
return nil, errTargetFallback
|
return nil, errTargetFallback
|
||||||
}
|
}
|
||||||
@@ -1091,10 +1264,10 @@ func (tp *targetParser) descendDotted(dest *targetTable, first string, rest []st
|
|||||||
loc, found = tbl.schema.byName[resolved]
|
loc, found = tbl.schema.byName[resolved]
|
||||||
}
|
}
|
||||||
if !found {
|
if !found {
|
||||||
tbl.recordStrictUnknown(leaf)
|
tp.recordStrictUnknown(tbl, leaf)
|
||||||
return reflect.Value{}, reflect.Value{}, leafTable, false, nil
|
return reflect.Value{}, reflect.Value{}, leafTable, false, nil
|
||||||
}
|
}
|
||||||
tbl.markResolved(resolved)
|
tp.markResolved(tbl, resolved)
|
||||||
fv, ferr := fieldByIndex(tbl.rv, loc.index)
|
fv, ferr := fieldByIndex(tbl.rv, loc.index)
|
||||||
if ferr != nil {
|
if ferr != nil {
|
||||||
return reflect.Value{}, reflect.Value{}, leafTable, false, errTargetFallback
|
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
|
return nil, errTargetFallback
|
||||||
}
|
}
|
||||||
if existing := tbl.rv.MapIndex(reflect.ValueOf(seg)); existing.IsValid() && !existing.IsNil() {
|
if existing := tbl.rv.MapIndex(reflect.ValueOf(seg)); existing.IsValid() && !existing.IsNil() {
|
||||||
ev := existing.Elem()
|
if existing.Kind() != reflect.Map {
|
||||||
if ev.Kind() != reflect.Map {
|
|
||||||
return nil, tp.errf("key %q is not a table", seg)
|
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)
|
next := reflect.MakeMap(elemT)
|
||||||
tbl.rv.SetMapIndex(reflect.ValueOf(seg), next)
|
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
|
resolved := seg
|
||||||
loc, ok := tbl.schema.byName[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]
|
loc, ok = tbl.schema.byName[resolved]
|
||||||
}
|
}
|
||||||
if !ok {
|
if !ok {
|
||||||
tbl.recordStrictUnknown(seg)
|
tp.recordStrictUnknown(tbl, seg)
|
||||||
if opened, ok := tp.opened[pathKey(segAbs)]; ok {
|
// 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
|
return opened, nil
|
||||||
}
|
}
|
||||||
if tp.tableHas(tbl, seg) {
|
if tp.tableHas(tbl, seg) {
|
||||||
return nil, tp.errf("key %q is not a table", 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
|
tp.opened[pathKey(segAbs)] = sink
|
||||||
return sink, nil
|
return sink, nil
|
||||||
}
|
}
|
||||||
|
tp.markResolved(tbl, resolved)
|
||||||
fv, ferr := fieldByIndex(tbl.rv, loc.index)
|
fv, ferr := fieldByIndex(tbl.rv, loc.index)
|
||||||
if ferr != nil {
|
if ferr != nil {
|
||||||
return nil, errTargetFallback
|
return nil, errTargetFallback
|
||||||
@@ -1167,7 +1343,7 @@ func (tp *targetParser) dottedEnter(tbl *targetTable, seg string, segAbs []strin
|
|||||||
}
|
}
|
||||||
return nil, errTargetFallback
|
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:
|
case reflect.Map:
|
||||||
if fv.Type().Key().Kind() != reflect.String {
|
if fv.Type().Key().Kind() != reflect.String {
|
||||||
return nil, errTargetFallback
|
return nil, errTargetFallback
|
||||||
@@ -1175,7 +1351,7 @@ func (tp *targetParser) dottedEnter(tbl *targetTable, seg string, segAbs []strin
|
|||||||
if fv.IsNil() {
|
if fv.IsNil() {
|
||||||
fv.Set(reflect.MakeMap(fv.Type()))
|
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) {
|
if tp.tableHas(tbl, seg) {
|
||||||
return nil, tp.errf("key %q is not a table", 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 {
|
if dest.rv.Kind() != reflect.Map {
|
||||||
return reflect.Value{}, reflect.Value{}, leafTable, false, errTargetFallback
|
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)
|
return reflect.Value{}, reflect.Value{}, leafTable, false, tp.errf("duplicate key %q", key)
|
||||||
}
|
}
|
||||||
elem := reflect.New(dest.rv.Type().Elem()).Elem()
|
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]
|
loc, ok = dest.schema.byName[resolved]
|
||||||
}
|
}
|
||||||
if !ok {
|
if !ok {
|
||||||
dest.recordStrictUnknown(key)
|
tp.recordStrictUnknown(dest, key)
|
||||||
return reflect.Value{}, reflect.Value{}, leafTable, false, nil
|
return reflect.Value{}, reflect.Value{}, leafTable, false, nil
|
||||||
}
|
}
|
||||||
dest.markResolved(resolved)
|
tp.markResolved(dest, resolved)
|
||||||
fv, ferr := fieldByIndex(dest.rv, loc.index)
|
fv, ferr := fieldByIndex(dest.rv, loc.index)
|
||||||
if ferr != nil {
|
if ferr != nil {
|
||||||
return reflect.Value{}, reflect.Value{}, leafTable, false, errTargetFallback
|
return reflect.Value{}, reflect.Value{}, leafTable, false, errTargetFallback
|
||||||
|
|||||||
+350
@@ -4,9 +4,12 @@
|
|||||||
package interpres
|
package interpres
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
"maps"
|
||||||
"net"
|
"net"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"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