2026-09-22 15:59:03 +02:00
|
|
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
|
|
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
|
|
|
|
|
|
package interpres
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
|
|
|
|
"reflect"
|
|
|
|
|
"slices"
|
|
|
|
|
"strconv"
|
|
|
|
|
"strings"
|
|
|
|
|
"sync"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// errTargetFallback aborts a targeted parse and hands the document back to
|
|
|
|
|
// the ordinary tree path. It is the contract-keeping device of this file:
|
|
|
|
|
// every condition the tree path answers with a decode-stage error, an
|
|
|
|
|
// Unmarshaler hook, an embedded map filler or any other machinery the
|
|
|
|
|
// targeted skeleton does not model, ends here, and the caller reruns the
|
|
|
|
|
// 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.
|
2026-09-22 21:15:00 +02:00
|
|
|
//
|
|
|
|
|
// 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.
|
2026-09-22 15:59:03 +02:00
|
|
|
var errTargetFallback = errors.New("interpres: targeted decode falls back to the tree path")
|
|
|
|
|
|
|
|
|
|
// targetCache holds whether a destination type may take the targeted parse.
|
|
|
|
|
// One computed answer per type, the same trade-off structSchemaCache makes.
|
|
|
|
|
var targetCache sync.Map // reflect.Type -> bool
|
|
|
|
|
|
|
|
|
|
// mapStringAnyType is the map shape the tree builds for an any destination's
|
|
|
|
|
// tables, reused by the any-map element branch.
|
|
|
|
|
var mapStringAnyType = reflect.TypeFor[map[string]any]()
|
|
|
|
|
|
|
|
|
|
// typeTargetable reports whether decoding into the struct type t can use the
|
2026-09-22 21:15:00 +02:00
|
|
|
// 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
|
2026-09-22 15:59:03 +02:00
|
|
|
// mismatch falls back.
|
|
|
|
|
func typeTargetable(t reflect.Type) bool {
|
|
|
|
|
if t == nil || t.Kind() != reflect.Struct || t == orderedMapType {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
if v, ok := targetCache.Load(t); ok {
|
|
|
|
|
return v.(bool)
|
|
|
|
|
}
|
|
|
|
|
r := scanTargetable(t, make(map[reflect.Type]bool))
|
|
|
|
|
v, _ := targetCache.LoadOrStore(t, r)
|
|
|
|
|
return v.(bool)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func scanTargetable(t reflect.Type, seen map[reflect.Type]bool) bool {
|
|
|
|
|
if seen[t] {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
seen[t] = true
|
|
|
|
|
if len(cachedStructSchema(t).embedMaps) != 0 {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
for _, loc := range cachedStructSchema(t).byName {
|
2026-09-22 21:15:00 +02:00
|
|
|
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:
|
2026-09-22 15:59:03 +02:00
|
|
|
if ft == orderedMapType {
|
|
|
|
|
return false
|
|
|
|
|
}
|
2026-09-22 21:15:00 +02:00
|
|
|
if isScalarStruct(ft) {
|
|
|
|
|
return true
|
2026-09-22 15:59:03 +02:00
|
|
|
}
|
2026-09-22 21:15:00 +02:00
|
|
|
// A struct the parse enters directly never builds the whole value
|
|
|
|
|
// the tree hands a hook, so the hook must win.
|
2026-09-22 15:59:03 +02:00
|
|
|
if implementsDecodeHook(ft) || implementsDecodeHook(reflect.PointerTo(ft)) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
2026-09-22 21:15:00 +02:00
|
|
|
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 {
|
2026-09-22 15:59:03 +02:00
|
|
|
return false
|
|
|
|
|
}
|
2026-09-22 21:15:00 +02:00
|
|
|
if et.Kind() == reflect.Struct && !isScalarStruct(et) {
|
|
|
|
|
return scanTargetableField(et, seen)
|
|
|
|
|
}
|
|
|
|
|
return true
|
2026-09-22 15:59:03 +02:00
|
|
|
}
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func derefType(t reflect.Type) reflect.Type {
|
|
|
|
|
for t.Kind() == reflect.Pointer {
|
|
|
|
|
t = t.Elem()
|
|
|
|
|
}
|
|
|
|
|
return t
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// implementsDecodeHook reports whether t carries one of the custom decode
|
|
|
|
|
// interfaces the tree decode honours.
|
|
|
|
|
func implementsDecodeHook(t reflect.Type) bool {
|
|
|
|
|
return t.Implements(unmarshalerType) || t.Implements(ctxUnmarshalerType) || t.Implements(textUnmarshalerType)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// canTargetDecode reports whether the decoder can take the targeted path for
|
|
|
|
|
// the destination v: a non-nil pointer to a struct whose graph carries no
|
|
|
|
|
// embedded map filler, and that is not itself a custom decode hook (the tree
|
|
|
|
|
// decode hands a hook the whole parsed tree).
|
|
|
|
|
func canTargetDecode(v any) bool {
|
|
|
|
|
rv := reflect.ValueOf(v)
|
|
|
|
|
if rv.Kind() != reflect.Pointer || rv.IsNil() {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
et := rv.Type().Elem()
|
|
|
|
|
if et.Kind() != reflect.Struct || !typeTargetable(et) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
return !implementsDecodeHook(et) && !implementsDecodeHook(reflect.PointerTo(et))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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
|
2026-09-22 21:15:00 +02:00
|
|
|
// 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.
|
2026-09-22 15:59:03 +02:00
|
|
|
type targetTable struct {
|
|
|
|
|
rv reflect.Value
|
|
|
|
|
schema *structSchema
|
|
|
|
|
path []string
|
|
|
|
|
sink bool
|
2026-09-22 21:15:00 +02:00
|
|
|
// 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
|
|
|
|
|
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
|
2026-09-22 15:59:03 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// targetParser parses a document straight into a struct destination. It
|
|
|
|
|
// reuses the parser's scanner, grammar errors and definition maps, and the
|
|
|
|
|
// decoder's value assignment; its own work is the table skeleton a struct
|
|
|
|
|
// destination needs: which field does this header or key land in.
|
|
|
|
|
type targetParser struct {
|
|
|
|
|
*parser
|
|
|
|
|
d *decoder
|
|
|
|
|
root reflect.Value
|
|
|
|
|
|
|
|
|
|
tables []*targetTable // every opened table, in document order
|
|
|
|
|
rootT *targetTable
|
|
|
|
|
cur *targetTable
|
|
|
|
|
|
2026-09-22 21:15:00 +02:00
|
|
|
// 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
|
2026-09-22 15:59:03 +02:00
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
|
2026-09-22 21:15:00 +02:00
|
|
|
// 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
|
2026-09-22 15:59:03 +02:00
|
|
|
|
2026-09-22 21:15:00 +02:00
|
|
|
// 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
|
2026-09-22 15:59:03 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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.
|
2026-09-22 21:15:00 +02:00
|
|
|
// 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.
|
2026-09-22 15:59:03 +02:00
|
|
|
func (tp *targetParser) tableHas(t *targetTable, key string) bool {
|
|
|
|
|
switch {
|
|
|
|
|
case t.sink:
|
|
|
|
|
return slices.Contains(t.keys, key)
|
|
|
|
|
case t.schema == nil:
|
2026-09-22 21:15:00 +02:00
|
|
|
if t.rv.Kind() != reflect.Map || !t.rv.CanAddr() {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
return tp.mapKeysByAddr[t.rv.Addr().Pointer()][key]
|
2026-09-22 15:59:03 +02:00
|
|
|
default:
|
|
|
|
|
return slices.Contains(tp.tableKeys(t), key)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// tableKeys returns the persistent keys slice of a struct table.
|
|
|
|
|
func (tp *targetParser) tableKeys(t *targetTable) []string {
|
|
|
|
|
if !t.rv.CanAddr() || t.rv.Kind() != reflect.Struct {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
addr := t.rv.Addr().Pointer()
|
|
|
|
|
if tp.tableKeysByAddr == nil {
|
|
|
|
|
tp.tableKeysByAddr = make(map[uintptr][]string, 8)
|
|
|
|
|
}
|
|
|
|
|
return tp.tableKeysByAddr[addr]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// tableMark records the key as defined.
|
|
|
|
|
func (tp *targetParser) tableMark(t *targetTable, key string) {
|
|
|
|
|
switch {
|
|
|
|
|
case t.sink:
|
|
|
|
|
t.keys = append(t.keys, key)
|
|
|
|
|
case t.schema == nil:
|
2026-09-22 21:15:00 +02:00
|
|
|
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
|
2026-09-22 15:59:03 +02:00
|
|
|
default:
|
|
|
|
|
if !t.rv.CanAddr() || t.rv.Kind() != reflect.Struct {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
addr := t.rv.Addr().Pointer()
|
|
|
|
|
if tp.tableKeysByAddr == nil {
|
|
|
|
|
tp.tableKeysByAddr = make(map[uintptr][]string, 8)
|
|
|
|
|
}
|
|
|
|
|
tp.tableKeysByAddr[addr] = append(tp.tableKeysByAddr[addr], key)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-22 21:15:00 +02:00
|
|
|
// 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
|
2026-09-22 15:59:03 +02:00
|
|
|
}
|
|
|
|
|
|
2026-09-22 21:15:00 +02:00
|
|
|
// 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) {
|
2026-09-22 15:59:03 +02:00
|
|
|
if t.schema == nil || len(t.schema.required) == 0 {
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-09-22 21:15:00 +02:00
|
|
|
if !t.rv.CanAddr() || t.rv.Kind() != reflect.Struct {
|
|
|
|
|
return
|
2026-09-22 15:59:03 +02:00
|
|
|
}
|
2026-09-22 21:15:00 +02:00
|
|
|
tp.strictState(t).resolved[key] = true
|
2026-09-22 15:59:03 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// recordStrictUnknown remembers the key no field claims when strict decoding
|
|
|
|
|
// is on: the smallest one is reported, the tree decode's own choice.
|
2026-09-22 21:15:00 +02:00
|
|
|
func (tp *targetParser) recordStrictUnknown(t *targetTable, key string) {
|
|
|
|
|
if !t.strict || t.schema == nil || !t.rv.CanAddr() || t.rv.Kind() != reflect.Struct {
|
2026-09-22 15:59:03 +02:00
|
|
|
return
|
|
|
|
|
}
|
2026-09-22 21:15:00 +02:00
|
|
|
st := tp.strictState(t)
|
|
|
|
|
if st.unknown == "" || key < st.unknown {
|
|
|
|
|
st.unknown = key
|
2026-09-22 15:59:03 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// schemaRef hands out the pointer form the target tables hold. The schema
|
|
|
|
|
// is immutable once published, so sharing one copy is safe.
|
|
|
|
|
func schemaRef(t reflect.Type) *structSchema {
|
|
|
|
|
s := cachedStructSchema(t)
|
|
|
|
|
return &s
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// parseIntoTargeted runs the targeted parse of data into v. It returns
|
|
|
|
|
// errTargetFallback when the document or the destination needs the tree
|
|
|
|
|
// path, and any parse error the tree path would return.
|
|
|
|
|
func parseIntoTargeted(ctx context.Context, data []byte, d *decoder, useNumber bool, maxDepth int, v any) error {
|
|
|
|
|
if maxDepth <= 0 {
|
|
|
|
|
maxDepth = maxNestingDepth
|
|
|
|
|
}
|
|
|
|
|
rv := reflect.ValueOf(v)
|
|
|
|
|
tp := &targetParser{
|
2026-09-22 21:15:00 +02:00
|
|
|
parser: &parser{src: data, line: 1, ctx: ctx, maxDepth: maxDepth, useNumber: useNumber},
|
|
|
|
|
d: d,
|
|
|
|
|
root: rv.Elem(),
|
|
|
|
|
arrayFills: make(map[uintptr]*arrayFill, 4),
|
|
|
|
|
appendedHere: make(map[uintptr]bool, 4),
|
|
|
|
|
opened: make(map[string]*targetTable, 8),
|
|
|
|
|
strictByAddr: make(map[uintptr]*strictState, 8),
|
2026-09-22 15:59:03 +02:00
|
|
|
}
|
|
|
|
|
tp.rootT = &targetTable{rv: tp.root, schema: schemaRef(tp.root.Type()), strict: d.disallowUnknown}
|
|
|
|
|
tp.tables = append(tp.tables, tp.rootT)
|
|
|
|
|
tp.cur = tp.rootT
|
|
|
|
|
if err := tp.run(); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
return tp.reportDeferred()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// run walks the statements; the cadence and the end conditions mirror the
|
|
|
|
|
// tree parser's loop.
|
|
|
|
|
func (tp *targetParser) run() error {
|
|
|
|
|
p := tp.parser
|
|
|
|
|
for i := 0; ; i++ {
|
|
|
|
|
if i%ctxCheckInterval == 0 {
|
|
|
|
|
if err := p.checkCtx(); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if err := p.skipBlank(); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
p.pending = nil
|
|
|
|
|
if p.eof() {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
c := p.peek()
|
|
|
|
|
switch {
|
|
|
|
|
case c == '[':
|
|
|
|
|
if err := tp.parseHeader(); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
default:
|
|
|
|
|
if err := tp.parseKeyStatement(); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if err := p.expectLineEnd(); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// reportDeferred raises the decode-stage findings in the tree decode's
|
2026-09-22 21:15:00 +02:00
|
|
|
// 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.
|
2026-09-22 15:59:03 +02:00
|
|
|
func (tp *targetParser) reportDeferred() error {
|
2026-09-22 21:15:00 +02:00
|
|
|
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 {
|
2026-09-22 15:59:03 +02:00
|
|
|
continue
|
|
|
|
|
}
|
2026-09-22 21:15:00 +02:00
|
|
|
for _, key := range st.schema.required {
|
|
|
|
|
if !st.resolved[key] {
|
|
|
|
|
return wrapTablePath(st.path, fmt.Errorf("interpres: missing required key %q", key))
|
2026-09-22 15:59:03 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-22 21:15:00 +02:00
|
|
|
// wrapTablePath wraps a table's finding the way the tree decode wraps it: the
|
2026-09-22 15:59:03 +02:00
|
|
|
// root speaks for itself, a nested table gains its path.
|
2026-09-22 21:15:00 +02:00
|
|
|
func wrapTablePath(path []string, err error) error {
|
|
|
|
|
if len(path) == 0 {
|
2026-09-22 15:59:03 +02:00
|
|
|
return err
|
|
|
|
|
}
|
2026-09-22 21:15:00 +02:00
|
|
|
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
|
2026-09-22 15:59:03 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- headers ---------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
// parseHeader parses a [table] or [[array of tables]] header and makes it the
|
|
|
|
|
// current table. The definition checks and their messages are the tree
|
|
|
|
|
// parser's.
|
|
|
|
|
func (tp *targetParser) parseHeader() error {
|
|
|
|
|
p := tp.parser
|
|
|
|
|
array := false
|
|
|
|
|
p.pos++ // consume '['
|
|
|
|
|
if !p.eof() && p.peek() == '[' {
|
|
|
|
|
array = true
|
|
|
|
|
p.pos++
|
|
|
|
|
}
|
|
|
|
|
first, rest, err := p.parseKeyPath()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
p.skipInline()
|
|
|
|
|
if p.eof() || p.peek() != ']' {
|
|
|
|
|
return p.errf("expected ']' to close table header")
|
|
|
|
|
}
|
|
|
|
|
p.pos++
|
|
|
|
|
if array {
|
|
|
|
|
if p.eof() || p.peek() != ']' {
|
|
|
|
|
return p.errf("expected ']]' to close array-of-tables header")
|
|
|
|
|
}
|
|
|
|
|
p.pos++
|
|
|
|
|
}
|
|
|
|
|
key := p.keyBuf[:1]
|
|
|
|
|
key[0] = first
|
|
|
|
|
if len(rest) > 0 {
|
|
|
|
|
key = append([]string{first}, rest...)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if array {
|
|
|
|
|
return tp.appendArrayTable(key)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pk := pathKey(key)
|
|
|
|
|
if p.headers[pk] || p.dotted[pk] || p.arrays[pk] {
|
|
|
|
|
return p.errf("table %q is defined more than once", strings.Join(key, "."))
|
|
|
|
|
}
|
|
|
|
|
p.markHeader(pk)
|
|
|
|
|
|
|
|
|
|
tbl, err := tp.openTablePath(key)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
if !tbl.sink {
|
|
|
|
|
tp.tables = append(tp.tables, tbl)
|
|
|
|
|
}
|
|
|
|
|
tp.cur = tbl
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// openTablePath walks the header path from the root and returns the table it
|
|
|
|
|
// names. The frozen checks are the tree parser's; a segment no field claims
|
|
|
|
|
// opens a sink, and a segment whose destination cannot be a table falls
|
|
|
|
|
// back, because the tree decode answers with its own type error.
|
|
|
|
|
func (tp *targetParser) openTablePath(key []string) (*targetTable, error) {
|
|
|
|
|
parent := tp.rootT
|
|
|
|
|
for i, k := range key[:len(key)-1] {
|
|
|
|
|
if tp.frozenAt(key[:i+1]) {
|
|
|
|
|
return nil, tp.errf("cannot extend inline table %q", strings.Join(key[:i+1], "."))
|
|
|
|
|
}
|
|
|
|
|
child, err := tp.descendOne(parent, k, key[:i+1])
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
tp.tableMark(parent, k)
|
|
|
|
|
if !child.sink {
|
|
|
|
|
tp.tables = append(tp.tables, child)
|
|
|
|
|
}
|
|
|
|
|
parent = child
|
|
|
|
|
}
|
|
|
|
|
leaf := key[len(key)-1]
|
|
|
|
|
if tp.frozenAt(key) {
|
|
|
|
|
return nil, tp.errf("cannot extend inline table %q", strings.Join(key, "."))
|
|
|
|
|
}
|
|
|
|
|
tbl, err := tp.descendOne(parent, leaf, key)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
tp.tableMark(parent, leaf)
|
|
|
|
|
return tbl, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// frozenAt reports whether the path was frozen as an inline table.
|
|
|
|
|
func (tp *targetParser) frozenAt(path []string) bool {
|
|
|
|
|
return tp.parser.frozen[pathKey(path)]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// elementPath extends a table's path with an array-of-tables element's
|
|
|
|
|
// key and bracketed index, the path the tree decode wraps an element's
|
|
|
|
|
// errors with.
|
|
|
|
|
func elementPath(base []string, key string, index int) []string {
|
|
|
|
|
out := make([]string, 0, len(base)+2)
|
|
|
|
|
out = append(out, base...)
|
|
|
|
|
out = append(out, key, "["+strconv.Itoa(index)+"]")
|
|
|
|
|
return out
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// descendOne enters the table one header segment names inside parent.
|
|
|
|
|
func (tp *targetParser) descendOne(parent *targetTable, key string, abs []string) (*targetTable, error) {
|
|
|
|
|
if parent.sink {
|
|
|
|
|
return parent, nil
|
|
|
|
|
}
|
|
|
|
|
if parent.schema == nil {
|
|
|
|
|
// A map destination: the entry must be (or become) a table.
|
|
|
|
|
if parent.rv.Kind() != reflect.Map {
|
|
|
|
|
return nil, errTargetFallback
|
|
|
|
|
}
|
|
|
|
|
elemT := parent.rv.Type().Elem()
|
|
|
|
|
if elemT.Kind() != reflect.Map || elemT.Key().Kind() != reflect.String {
|
|
|
|
|
return nil, errTargetFallback
|
|
|
|
|
}
|
|
|
|
|
if existing := parent.rv.MapIndex(reflect.ValueOf(key)); existing.IsValid() && !existing.IsNil() {
|
2026-09-22 21:15:00 +02:00
|
|
|
return &targetTable{rv: existing, path: slices.Clone(abs)}, nil
|
2026-09-22 15:59:03 +02:00
|
|
|
}
|
|
|
|
|
next := reflect.MakeMap(elemT)
|
|
|
|
|
parent.rv.SetMapIndex(reflect.ValueOf(key), next)
|
2026-09-22 21:15:00 +02:00
|
|
|
return &targetTable{rv: next, path: slices.Clone(abs)}, nil
|
2026-09-22 15:59:03 +02:00
|
|
|
}
|
|
|
|
|
resolved := key
|
|
|
|
|
loc, ok := parent.schema.byName[key]
|
|
|
|
|
if !ok {
|
|
|
|
|
resolved = strings.ToLower(key)
|
|
|
|
|
loc, ok = parent.schema.byName[resolved]
|
|
|
|
|
}
|
|
|
|
|
if !ok {
|
2026-09-22 21:15:00 +02:00
|
|
|
tp.recordStrictUnknown(parent, key)
|
2026-09-22 15:59:03 +02:00
|
|
|
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)
|
|
|
|
|
}
|
2026-09-22 21:15:00 +02:00
|
|
|
sink := &targetTable{sink: true, path: slices.Clone(abs)}
|
2026-09-22 15:59:03 +02:00
|
|
|
tp.opened[pathKey(abs)] = sink
|
|
|
|
|
return sink, nil
|
|
|
|
|
}
|
2026-09-22 21:15:00 +02:00
|
|
|
tp.markResolved(parent, resolved)
|
2026-09-22 15:59:03 +02:00
|
|
|
fv, err := fieldByIndex(parent.rv, loc.index)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, errTargetFallback
|
|
|
|
|
}
|
|
|
|
|
return tp.openValueTable(fv, parent, key, abs, parent.strict)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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
|
2026-09-22 21:15:00 +02:00
|
|
|
// 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.
|
2026-09-22 15:59:03 +02:00
|
|
|
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() {
|
|
|
|
|
if !fv.CanSet() {
|
|
|
|
|
return nil, errTargetFallback
|
|
|
|
|
}
|
|
|
|
|
fv.Set(reflect.New(fv.Type().Elem()))
|
|
|
|
|
}
|
|
|
|
|
fv = fv.Elem()
|
|
|
|
|
}
|
|
|
|
|
switch fv.Kind() {
|
|
|
|
|
case reflect.Struct:
|
|
|
|
|
if isScalarStruct(fv.Type()) {
|
|
|
|
|
return nil, errTargetFallback
|
|
|
|
|
}
|
2026-09-22 21:15:00 +02:00
|
|
|
return &targetTable{rv: fv, schema: schemaRef(fv.Type()), path: slices.Clone(abs), strict: strict}, nil
|
2026-09-22 15:59:03 +02:00
|
|
|
case reflect.Map:
|
|
|
|
|
if fv.Type().Key().Kind() != reflect.String {
|
|
|
|
|
return nil, errTargetFallback
|
|
|
|
|
}
|
|
|
|
|
if fv.IsNil() {
|
|
|
|
|
fv.Set(reflect.MakeMap(fv.Type()))
|
|
|
|
|
}
|
2026-09-22 21:15:00 +02:00
|
|
|
return &targetTable{rv: fv, path: slices.Clone(abs)}, nil
|
2026-09-22 15:59:03 +02:00
|
|
|
case reflect.Slice:
|
2026-09-22 21:15:00 +02:00
|
|
|
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.
|
2026-09-22 15:59:03 +02:00
|
|
|
return nil, errTargetFallback
|
|
|
|
|
}
|
|
|
|
|
et := derefType(fv.Type().Elem())
|
|
|
|
|
if et.Kind() != reflect.Struct || isScalarStruct(et) {
|
|
|
|
|
return nil, errTargetFallback
|
|
|
|
|
}
|
|
|
|
|
return &targetTable{rv: fv.Index(fv.Len() - 1), schema: schemaRef(et), path: elementPath(abs, key, fv.Len()-1), strict: strict}, nil
|
2026-09-22 21:15:00 +02:00
|
|
|
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
|
2026-09-22 15:59:03 +02:00
|
|
|
}
|
|
|
|
|
if tp.tableHas(parent, key) {
|
|
|
|
|
return nil, tp.errf("key %q is not a table", key)
|
|
|
|
|
}
|
|
|
|
|
return nil, errTargetFallback
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// appendArrayTable appends a new element to the array of tables the leaf
|
|
|
|
|
// names and makes it the current table.
|
|
|
|
|
func (tp *targetParser) appendArrayTable(key []string) error {
|
|
|
|
|
parent := tp.rootT
|
|
|
|
|
for i, k := range key[:len(key)-1] {
|
|
|
|
|
if tp.frozenAt(key[:i+1]) {
|
|
|
|
|
return tp.errf("cannot extend inline table %q", strings.Join(key[:i+1], "."))
|
|
|
|
|
}
|
|
|
|
|
child, err := tp.descendOne(parent, k, key[:i+1])
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
tp.tableMark(parent, k)
|
|
|
|
|
parent = child
|
|
|
|
|
}
|
|
|
|
|
leaf := key[len(key)-1]
|
|
|
|
|
if tp.frozenAt(key) {
|
|
|
|
|
return tp.errf("cannot extend inline table %q", strings.Join(key, "."))
|
|
|
|
|
}
|
|
|
|
|
pk := pathKey(key)
|
|
|
|
|
if tp.parser.dotted[pk] || tp.parser.headers[pk] {
|
|
|
|
|
return tp.errf("key %q is not an array of tables", leaf)
|
|
|
|
|
}
|
2026-09-22 21:15:00 +02:00
|
|
|
// 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)
|
2026-09-22 15:59:03 +02:00
|
|
|
tp.parser.markArray(pk)
|
|
|
|
|
|
|
|
|
|
elem, err := tp.appendElement(parent, leaf, key)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
2026-09-22 21:15:00 +02:00
|
|
|
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
|
|
|
|
|
}
|
2026-09-22 15:59:03 +02:00
|
|
|
tp.tableMark(parent, leaf)
|
|
|
|
|
if !elem.sink {
|
|
|
|
|
tp.tables = append(tp.tables, elem)
|
|
|
|
|
}
|
|
|
|
|
tp.cur = elem
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// appendElement appends one element to the array the leaf names in parent
|
2026-09-22 21:15:00 +02:00
|
|
|
// 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.
|
2026-09-22 15:59:03 +02:00
|
|
|
func (tp *targetParser) appendElement(parent *targetTable, leaf string, key []string) (*targetTable, error) {
|
|
|
|
|
if parent.sink {
|
|
|
|
|
return parent, nil
|
|
|
|
|
}
|
|
|
|
|
if parent.schema == nil {
|
|
|
|
|
if parent.rv.Kind() != reflect.Map {
|
|
|
|
|
return nil, errTargetFallback
|
|
|
|
|
}
|
|
|
|
|
elemT := parent.rv.Type().Elem()
|
|
|
|
|
gk := reflect.ValueOf(leaf)
|
|
|
|
|
var arr reflect.Value
|
|
|
|
|
if existing := parent.rv.MapIndex(gk); existing.IsValid() && !existing.IsNil() {
|
2026-09-22 21:15:00 +02:00
|
|
|
// 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 {
|
2026-09-22 15:59:03 +02:00
|
|
|
return nil, tp.errf("key %q is not an array of tables", leaf)
|
|
|
|
|
}
|
2026-09-22 21:15:00 +02:00
|
|
|
arr = existing
|
2026-09-22 15:59:03 +02:00
|
|
|
}
|
|
|
|
|
var elem reflect.Value
|
|
|
|
|
switch {
|
|
|
|
|
case elemT.Kind() == reflect.Slice:
|
|
|
|
|
et := elemT.Elem()
|
|
|
|
|
if et.Kind() != reflect.Map || et.Key().Kind() != reflect.String {
|
|
|
|
|
return nil, errTargetFallback
|
|
|
|
|
}
|
|
|
|
|
if !arr.IsValid() {
|
|
|
|
|
arr = reflect.MakeSlice(elemT, 0, 4)
|
|
|
|
|
}
|
|
|
|
|
elem = reflect.MakeMap(et)
|
|
|
|
|
case elemT.Kind() == reflect.Interface:
|
|
|
|
|
// An any-map entry is built exactly as the tree builds it: a
|
|
|
|
|
// []map[string]any slice of entry maps.
|
|
|
|
|
if !arr.IsValid() {
|
|
|
|
|
arr = reflect.ValueOf([]map[string]any{})
|
|
|
|
|
}
|
|
|
|
|
elem = reflect.MakeMap(mapStringAnyType)
|
|
|
|
|
default:
|
|
|
|
|
return nil, errTargetFallback
|
|
|
|
|
}
|
|
|
|
|
grown := reflect.Append(arr, elem)
|
|
|
|
|
parent.rv.SetMapIndex(gk, grown)
|
|
|
|
|
return &targetTable{rv: grown.Index(grown.Len() - 1), path: elementPath(parent.path, leaf, grown.Len()-1)}, nil
|
|
|
|
|
}
|
|
|
|
|
resolved := leaf
|
|
|
|
|
loc, ok := parent.schema.byName[leaf]
|
|
|
|
|
if !ok {
|
|
|
|
|
resolved = strings.ToLower(leaf)
|
|
|
|
|
loc, ok = parent.schema.byName[resolved]
|
|
|
|
|
}
|
|
|
|
|
if !ok {
|
2026-09-22 21:15:00 +02:00
|
|
|
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
|
2026-09-22 15:59:03 +02:00
|
|
|
return sink, nil
|
|
|
|
|
}
|
2026-09-22 21:15:00 +02:00
|
|
|
tp.markResolved(parent, resolved)
|
2026-09-22 15:59:03 +02:00
|
|
|
fv, err := fieldByIndex(parent.rv, loc.index)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, errTargetFallback
|
|
|
|
|
}
|
2026-09-22 21:15:00 +02:00
|
|
|
fieldPath := append(slices.Clone(parent.path), leaf)
|
2026-09-22 15:59:03 +02:00
|
|
|
if fv.Kind() == reflect.Array {
|
|
|
|
|
// A fixed-size array fills position by position; one element too many
|
2026-09-22 21:15:00 +02:00
|
|
|
// 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)
|
2026-09-22 15:59:03 +02:00
|
|
|
et := derefType(fv.Type().Elem())
|
2026-09-22 21:15:00 +02:00
|
|
|
switch {
|
|
|
|
|
case et.Kind() == reflect.Struct && !isScalarStruct(et):
|
|
|
|
|
if fill.next >= fv.Len() {
|
2026-09-22 15:59:03 +02:00
|
|
|
return nil, errTargetFallback
|
|
|
|
|
}
|
2026-09-22 21:15:00 +02:00
|
|
|
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() {
|
2026-09-22 15:59:03 +02:00
|
|
|
return nil, errTargetFallback
|
|
|
|
|
}
|
2026-09-22 21:15:00 +02:00
|
|
|
n := fill.next
|
|
|
|
|
fill.next = n + 1
|
2026-09-22 15:59:03 +02:00
|
|
|
elem := reflect.MakeMap(et)
|
|
|
|
|
fv.Index(n).Set(elem)
|
2026-09-22 21:15:00 +02:00
|
|
|
return &targetTable{rv: elem, path: elementPath(parent.path, leaf, n)}, nil
|
2026-09-22 15:59:03 +02:00
|
|
|
}
|
|
|
|
|
return nil, errTargetFallback
|
|
|
|
|
}
|
|
|
|
|
if fv.Kind() != reflect.Slice {
|
2026-09-22 21:15:00 +02:00
|
|
|
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
|
2026-09-22 15:59:03 +02:00
|
|
|
}
|
|
|
|
|
et := derefType(fv.Type().Elem())
|
|
|
|
|
switch et.Kind() {
|
|
|
|
|
case reflect.Struct:
|
|
|
|
|
if isScalarStruct(et) {
|
|
|
|
|
return nil, errTargetFallback
|
|
|
|
|
}
|
2026-09-22 21:15:00 +02:00
|
|
|
// 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)
|
2026-09-22 15:59:03 +02:00
|
|
|
fv.Set(grown)
|
2026-09-22 21:15:00 +02:00
|
|
|
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
|
2026-09-22 15:59:03 +02:00
|
|
|
case reflect.Map:
|
|
|
|
|
if et.Key().Kind() != reflect.String {
|
|
|
|
|
return nil, errTargetFallback
|
|
|
|
|
}
|
2026-09-22 21:15:00 +02:00
|
|
|
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)
|
2026-09-22 15:59:03 +02:00
|
|
|
fv.Set(grown)
|
2026-09-22 21:15:00 +02:00
|
|
|
return &targetTable{rv: m, path: elementPath(parent.path, leaf, grown.Len()-1)}, nil
|
2026-09-22 15:59:03 +02:00
|
|
|
}
|
|
|
|
|
return nil, errTargetFallback
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- keys ------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
// parseKeyStatement parses one `key = value` statement into the current
|
|
|
|
|
// table, mirroring the tree parser's dotted descent and definition checks.
|
|
|
|
|
// The value parses after the descent here, straight into the destination
|
|
|
|
|
// where the destination is a plain scalar: the descent and the value scan
|
|
|
|
|
// are independent, so the only observable difference is which error a line
|
|
|
|
|
// with two faults reports.
|
|
|
|
|
func (tp *targetParser) parseKeyStatement() error {
|
|
|
|
|
p := tp.parser
|
|
|
|
|
first, rest, err := p.parseKeyPath()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
p.skipInline()
|
|
|
|
|
if p.eof() || p.peek() != '=' {
|
|
|
|
|
return p.errf("expected '=' after key")
|
|
|
|
|
}
|
|
|
|
|
p.pos++
|
|
|
|
|
p.skipInline()
|
|
|
|
|
|
|
|
|
|
dest := tp.cur
|
|
|
|
|
leaf := first
|
|
|
|
|
var dst reflect.Value
|
|
|
|
|
var mapDst reflect.Value
|
|
|
|
|
var leafTable *targetTable
|
|
|
|
|
placed := false
|
|
|
|
|
if len(rest) == 0 {
|
|
|
|
|
dst, mapDst, leafTable, placed, err = tp.leafInTable(dest, first)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// A dotted key is the one shape whose bookkeeping needs the statement
|
|
|
|
|
// path (the segment freeze and definition checks), so only here does
|
|
|
|
|
// the path slice get built. A plain key at the root has no path, and
|
|
|
|
|
// a plain key inside a table needs none either.
|
|
|
|
|
abs := make([]string, 0, len(dest.path)+len(rest)+1)
|
|
|
|
|
abs = append(abs, dest.path...)
|
|
|
|
|
abs = append(abs, first)
|
|
|
|
|
abs = append(abs, rest...)
|
|
|
|
|
dst, mapDst, leafTable, placed, err = tp.descendDotted(dest, first, rest, abs)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
leaf = rest[len(rest)-1]
|
|
|
|
|
}
|
|
|
|
|
if !placed {
|
|
|
|
|
// A sink or an unknown key: the value parses for the syntax contract
|
|
|
|
|
// and is dropped, but the key still takes the duplicate check, the way
|
|
|
|
|
// the tree's maps record every key they receive. The sink's flat key
|
|
|
|
|
// set tracks the full statement path, because a dotted key inside a
|
|
|
|
|
// sink lands in a sub-table of its own in the tree, not beside the
|
|
|
|
|
// leaf name.
|
|
|
|
|
val, verr := p.parseValue()
|
|
|
|
|
if verr != nil {
|
|
|
|
|
return verr
|
|
|
|
|
}
|
2026-09-22 21:15:00 +02:00
|
|
|
full := append([]string{first}, rest...)
|
|
|
|
|
if len(dest.path) > 0 {
|
|
|
|
|
full = append(slices.Clone(dest.path), full...)
|
2026-09-22 15:59:03 +02:00
|
|
|
}
|
2026-09-22 21:15:00 +02:00
|
|
|
if tp.tableHas(leafTable, pathKey(full)) {
|
2026-09-22 15:59:03 +02:00
|
|
|
return p.errf("duplicate key %q", leaf)
|
|
|
|
|
}
|
2026-09-22 21:15:00 +02:00
|
|
|
tp.tableMark(leafTable, pathKey(full))
|
2026-09-22 15:59:03 +02:00
|
|
|
if m, isMap := val.(map[string]any); isMap {
|
2026-09-22 21:15:00 +02:00
|
|
|
// 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.
|
2026-09-22 15:59:03 +02:00
|
|
|
p.freezeInline(full, m)
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
if tp.tableHas(leafTable, leaf) {
|
|
|
|
|
return p.errf("duplicate key %q", leaf)
|
|
|
|
|
}
|
|
|
|
|
tp.tableMark(leafTable, leaf)
|
|
|
|
|
val, err := tp.parseValueInto(dst)
|
|
|
|
|
if err != nil {
|
2026-09-22 21:15:00 +02:00
|
|
|
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)
|
|
|
|
|
}
|
2026-09-22 15:59:03 +02:00
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
if mapDst.IsValid() {
|
|
|
|
|
mapDst.SetMapIndex(reflect.ValueOf(leaf), dst)
|
|
|
|
|
}
|
|
|
|
|
if m, isMap := val.(map[string]any); isMap {
|
2026-09-22 21:15:00 +02:00
|
|
|
// 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)
|
2026-09-22 15:59:03 +02:00
|
|
|
abs = append(abs, dest.path...)
|
2026-09-22 21:15:00 +02:00
|
|
|
abs = append(abs, first)
|
|
|
|
|
abs = append(abs, rest...)
|
2026-09-22 15:59:03 +02:00
|
|
|
p.freezeInline(abs, m)
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// parseValueInto parses the value at the cursor straight into the
|
|
|
|
|
// destination and returns the boxed value the freeze bookkeeping may need
|
|
|
|
|
// (non-nil only for inline tables and other composites). Scalars are written
|
|
|
|
|
// into the destination without the boxing the tree layer requires.
|
|
|
|
|
func (tp *targetParser) parseValueInto(dst reflect.Value) (any, error) {
|
|
|
|
|
p := tp.parser
|
|
|
|
|
if p.eof() {
|
|
|
|
|
return nil, p.errf("expected a value")
|
|
|
|
|
}
|
|
|
|
|
start := p.pos
|
|
|
|
|
if dstHasDecodeHook(dst) {
|
|
|
|
|
// The custom hooks take the boxed value the tree layer produces.
|
|
|
|
|
v, err := p.parseValue()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
if err := tp.d.assign(v, dst); err != nil {
|
2026-09-22 21:15:00 +02:00
|
|
|
// 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
|
2026-09-22 15:59:03 +02:00
|
|
|
}
|
|
|
|
|
return v, nil
|
|
|
|
|
}
|
|
|
|
|
switch c := p.peek(); {
|
|
|
|
|
case c == '"' || c == '\'':
|
|
|
|
|
if dst.Kind() == reflect.String {
|
|
|
|
|
var s string
|
|
|
|
|
var err error
|
|
|
|
|
if c == '"' {
|
|
|
|
|
s, err = p.parseBasicString()
|
|
|
|
|
} else {
|
|
|
|
|
s, err = p.parseLiteralString()
|
|
|
|
|
}
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
dst.SetString(s)
|
|
|
|
|
return nil, nil
|
|
|
|
|
}
|
|
|
|
|
case c == 't' || c == 'f':
|
|
|
|
|
b, ok := tp.scanBool()
|
|
|
|
|
if !ok {
|
|
|
|
|
return nil, p.errf("invalid value")
|
|
|
|
|
}
|
|
|
|
|
if dst.Kind() == reflect.Bool {
|
|
|
|
|
dst.SetBool(b)
|
|
|
|
|
return nil, nil
|
|
|
|
|
}
|
|
|
|
|
p.pos = start
|
|
|
|
|
case (c >= '0' && c <= '9') || c == '+' || c == '-':
|
|
|
|
|
// The plain-digit fast path parses the common integer without a
|
|
|
|
|
// token copy; anything else takes the token route, where the strict
|
|
|
|
|
// number rules live.
|
|
|
|
|
if dstNumericKind(dst) && dst.Kind() != reflect.Float32 && dst.Kind() != reflect.Float64 {
|
|
|
|
|
if n, ok := tp.tryFastInt(); ok {
|
|
|
|
|
if err := setInt(dst, n); err != nil {
|
|
|
|
|
return nil, errTargetFallback
|
|
|
|
|
}
|
|
|
|
|
return nil, nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
tok := tp.scanNumberToken()
|
2026-09-22 21:15:00 +02:00
|
|
|
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 {
|
2026-09-22 15:59:03 +02:00
|
|
|
if err := tp.d.assign(dtv, dst); err != nil {
|
|
|
|
|
return nil, errTargetFallback
|
|
|
|
|
}
|
|
|
|
|
return dtv, nil
|
|
|
|
|
}
|
|
|
|
|
if dstNumericKind(dst) {
|
|
|
|
|
fallback, syntaxErr := numberIntoReflect(dst, tok)
|
|
|
|
|
if syntaxErr != nil {
|
|
|
|
|
return nil, p.errf("%s", syntaxErr)
|
|
|
|
|
}
|
|
|
|
|
if fallback {
|
|
|
|
|
return nil, errTargetFallback
|
|
|
|
|
}
|
|
|
|
|
return nil, nil
|
|
|
|
|
}
|
|
|
|
|
p.pos = start
|
|
|
|
|
}
|
|
|
|
|
v, err := p.parseValue()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
if err := tp.d.assign(v, dst); err != nil {
|
|
|
|
|
return nil, errTargetFallback
|
|
|
|
|
}
|
|
|
|
|
return v, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// dstHasDecodeHook reports whether the destination carries one of the custom
|
|
|
|
|
// decode interfaces, whose hooks need the boxed value the tree layer makes.
|
|
|
|
|
func dstHasDecodeHook(dst reflect.Value) bool {
|
|
|
|
|
if _, ok := unmarshalerOf(dst); ok {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
if _, ok := ctxUnmarshalerOf(dst); ok {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
if _, ok := textUnmarshalerOf(dst); ok {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// scanBool consumes true or false and returns the value, reporting whether
|
|
|
|
|
// the token was a boolean at all.
|
|
|
|
|
func (tp *targetParser) scanBool() (bool, bool) {
|
|
|
|
|
if tp.parser.match("true") {
|
|
|
|
|
return true, true
|
|
|
|
|
}
|
|
|
|
|
if tp.parser.match("false") {
|
|
|
|
|
return false, true
|
|
|
|
|
}
|
|
|
|
|
return false, false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// tryFastInt parses a run of plain decimal digits at the cursor into an
|
|
|
|
|
// int64 without materialising the token, reporting whether the token was
|
|
|
|
|
// one. A leading zero, an underscore, or any trailing character that is not
|
|
|
|
|
// a token terminator hands the token back to the strict number rules.
|
|
|
|
|
func (tp *targetParser) tryFastInt() (int64, bool) {
|
|
|
|
|
p := tp.parser
|
|
|
|
|
i := p.pos
|
|
|
|
|
if i >= len(p.src) {
|
|
|
|
|
return 0, false
|
|
|
|
|
}
|
|
|
|
|
if p.src[i] == '+' || p.src[i] == '-' {
|
|
|
|
|
return 0, false
|
|
|
|
|
}
|
|
|
|
|
if p.src[i] == '0' && i+1 < len(p.src) && p.src[i+1] >= '0' && p.src[i+1] <= '9' {
|
|
|
|
|
return 0, false
|
|
|
|
|
}
|
|
|
|
|
var n int64
|
|
|
|
|
digits := 0
|
|
|
|
|
for i < len(p.src) && p.src[i] >= '0' && p.src[i] <= '9' {
|
|
|
|
|
if digits >= 18 {
|
|
|
|
|
return 0, false
|
|
|
|
|
}
|
|
|
|
|
n = n*10 + int64(p.src[i]-'0')
|
|
|
|
|
i++
|
|
|
|
|
digits++
|
|
|
|
|
}
|
|
|
|
|
if digits == 0 {
|
|
|
|
|
return 0, false
|
|
|
|
|
}
|
|
|
|
|
if i < len(p.src) {
|
|
|
|
|
switch p.src[i] {
|
|
|
|
|
case ' ', '\t', '\n', '\r', ',', ']', '}', '#':
|
|
|
|
|
default:
|
|
|
|
|
return 0, false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
p.pos = i
|
|
|
|
|
return n, true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// scanNumberToken scans a bare number (or date-time) token, including the
|
|
|
|
|
// space-separated date and time forms, and returns it as text.
|
|
|
|
|
func (tp *targetParser) scanNumberToken() string {
|
|
|
|
|
p := tp.parser
|
|
|
|
|
start := p.pos
|
|
|
|
|
p.scanBareToken()
|
|
|
|
|
tok := string(p.src[start:p.pos])
|
|
|
|
|
if isDateToken(tok) && !p.eof() && p.peek() == ' ' {
|
|
|
|
|
if next, ok := p.peekAt(1); ok && next >= '0' && next <= '9' {
|
|
|
|
|
p.pos++ // consume the separating space
|
|
|
|
|
timeStart := p.pos
|
|
|
|
|
p.scanBareToken()
|
|
|
|
|
tok = tok + " " + string(p.src[timeStart:p.pos])
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return tok
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// dstNumericKind reports whether the destination takes a parsed number.
|
|
|
|
|
func dstNumericKind(dst reflect.Value) bool {
|
|
|
|
|
switch dst.Kind() {
|
|
|
|
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
|
|
|
|
|
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
|
|
|
|
|
reflect.Float32, reflect.Float64:
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// numberIntoReflect parses a number token straight into a numeric
|
|
|
|
|
// destination, with the strict validation the tree parser applies. The
|
|
|
|
|
// fallback flag reports a destination-level overflow, the tree decode's
|
|
|
|
|
// own error; syntaxErr is the token-level error the parser reports.
|
|
|
|
|
func numberIntoReflect(dst reflect.Value, tok string) (fallback bool, syntaxErr error) {
|
|
|
|
|
v, err := decodeNumber(tok)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return false, err
|
|
|
|
|
}
|
|
|
|
|
switch n := v.(type) {
|
|
|
|
|
case int64:
|
|
|
|
|
if err := setInt(dst, n); err != nil {
|
|
|
|
|
return true, nil
|
|
|
|
|
}
|
|
|
|
|
return false, nil
|
|
|
|
|
case float64:
|
|
|
|
|
if err := setFloat(dst, n); err != nil {
|
|
|
|
|
return true, nil
|
|
|
|
|
}
|
|
|
|
|
return false, nil
|
|
|
|
|
}
|
|
|
|
|
return true, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// descendDotted walks the dotted segments first..rest[:len(rest)-1] and
|
|
|
|
|
// returns the leaf destination: a struct field, or a map entry to set after
|
|
|
|
|
// the value assigns. A segment no field claims sinks the rest of the dotted
|
|
|
|
|
// key. The per-segment checks and their messages are the tree parser's
|
|
|
|
|
// descendKey: an existing value that is not a table is the `not a table`
|
|
|
|
|
// error, a missing one is created where the tree creates it and falls back
|
|
|
|
|
// where the tree's creation meets a decode-stage type error.
|
|
|
|
|
func (tp *targetParser) descendDotted(dest *targetTable, first string, rest []string, abs []string) (dst reflect.Value, mapDst reflect.Value, leafTable *targetTable, placed bool, err error) {
|
|
|
|
|
leafTable = dest
|
|
|
|
|
if dest.sink {
|
|
|
|
|
return reflect.Value{}, reflect.Value{}, leafTable, false, nil
|
|
|
|
|
}
|
|
|
|
|
tbl := dest
|
|
|
|
|
segments := append([]string{first}, rest[:len(rest)-1]...)
|
|
|
|
|
for i, seg := range segments {
|
|
|
|
|
segAbs := abs[:len(dest.path)+i+1]
|
|
|
|
|
if tp.frozenAt(segAbs) {
|
|
|
|
|
return reflect.Value{}, reflect.Value{}, leafTable, false, tp.errf("cannot extend inline table %q", strings.Join(segAbs, "."))
|
|
|
|
|
}
|
|
|
|
|
if tp.parser.headers[pathKey(segAbs)] {
|
|
|
|
|
return reflect.Value{}, reflect.Value{}, leafTable, false, tp.errf("cannot extend table %q with a dotted key", strings.Join(segAbs, "."))
|
|
|
|
|
}
|
|
|
|
|
tp.parser.markDotted(pathKey(segAbs))
|
|
|
|
|
child, err := tp.dottedEnter(tbl, seg, segAbs)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return reflect.Value{}, reflect.Value{}, leafTable, false, err
|
|
|
|
|
}
|
|
|
|
|
tp.tableMark(tbl, seg)
|
|
|
|
|
tbl = child
|
|
|
|
|
if tbl.sink {
|
|
|
|
|
leafTable = tbl
|
|
|
|
|
return reflect.Value{}, reflect.Value{}, leafTable, false, nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
leaf := rest[len(rest)-1]
|
|
|
|
|
leafTable = tbl
|
|
|
|
|
if tp.tableHas(tbl, leaf) {
|
|
|
|
|
return reflect.Value{}, reflect.Value{}, leafTable, false, tp.errf("duplicate key %q", leaf)
|
|
|
|
|
}
|
|
|
|
|
if tbl.schema == nil {
|
|
|
|
|
if tbl.rv.Kind() != reflect.Map {
|
|
|
|
|
return reflect.Value{}, reflect.Value{}, leafTable, false, errTargetFallback
|
|
|
|
|
}
|
|
|
|
|
elem := reflect.New(tbl.rv.Type().Elem()).Elem()
|
|
|
|
|
return elem, tbl.rv, leafTable, true, nil
|
|
|
|
|
}
|
|
|
|
|
resolved := leaf
|
|
|
|
|
loc, found := tbl.schema.byName[leaf]
|
|
|
|
|
if !found {
|
|
|
|
|
resolved = strings.ToLower(leaf)
|
|
|
|
|
loc, found = tbl.schema.byName[resolved]
|
|
|
|
|
}
|
|
|
|
|
if !found {
|
2026-09-22 21:15:00 +02:00
|
|
|
tp.recordStrictUnknown(tbl, leaf)
|
2026-09-22 15:59:03 +02:00
|
|
|
return reflect.Value{}, reflect.Value{}, leafTable, false, nil
|
|
|
|
|
}
|
2026-09-22 21:15:00 +02:00
|
|
|
tp.markResolved(tbl, resolved)
|
2026-09-22 15:59:03 +02:00
|
|
|
fv, ferr := fieldByIndex(tbl.rv, loc.index)
|
|
|
|
|
if ferr != nil {
|
|
|
|
|
return reflect.Value{}, reflect.Value{}, leafTable, false, errTargetFallback
|
|
|
|
|
}
|
|
|
|
|
return fv, reflect.Value{}, leafTable, true, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// dottedEnter enters one dotted segment inside tbl. It differs from the
|
|
|
|
|
// header descent in the slice and assigned-scalar cases, which the tree's
|
|
|
|
|
// descendKey answers with `key is not a table`.
|
|
|
|
|
func (tp *targetParser) dottedEnter(tbl *targetTable, seg string, segAbs []string) (*targetTable, error) {
|
|
|
|
|
if tbl.sink {
|
|
|
|
|
return tbl, nil
|
|
|
|
|
}
|
|
|
|
|
if tbl.schema == nil {
|
|
|
|
|
if tbl.rv.Kind() != reflect.Map {
|
|
|
|
|
return nil, errTargetFallback
|
|
|
|
|
}
|
|
|
|
|
elemT := tbl.rv.Type().Elem()
|
|
|
|
|
if elemT.Kind() != reflect.Map || elemT.Key().Kind() != reflect.String {
|
|
|
|
|
return nil, errTargetFallback
|
|
|
|
|
}
|
|
|
|
|
if existing := tbl.rv.MapIndex(reflect.ValueOf(seg)); existing.IsValid() && !existing.IsNil() {
|
2026-09-22 21:15:00 +02:00
|
|
|
if existing.Kind() != reflect.Map {
|
2026-09-22 15:59:03 +02:00
|
|
|
return nil, tp.errf("key %q is not a table", seg)
|
|
|
|
|
}
|
2026-09-22 21:15:00 +02:00
|
|
|
return &targetTable{rv: existing, path: slices.Clone(segAbs)}, nil
|
2026-09-22 15:59:03 +02:00
|
|
|
}
|
|
|
|
|
next := reflect.MakeMap(elemT)
|
|
|
|
|
tbl.rv.SetMapIndex(reflect.ValueOf(seg), next)
|
2026-09-22 21:15:00 +02:00
|
|
|
return &targetTable{rv: next, path: slices.Clone(segAbs)}, nil
|
2026-09-22 15:59:03 +02:00
|
|
|
}
|
|
|
|
|
resolved := seg
|
|
|
|
|
loc, ok := tbl.schema.byName[seg]
|
|
|
|
|
if !ok {
|
|
|
|
|
resolved = strings.ToLower(seg)
|
|
|
|
|
loc, ok = tbl.schema.byName[resolved]
|
|
|
|
|
}
|
|
|
|
|
if !ok {
|
2026-09-22 21:15:00 +02:00
|
|
|
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 {
|
2026-09-22 15:59:03 +02:00
|
|
|
return opened, nil
|
|
|
|
|
}
|
|
|
|
|
if tp.tableHas(tbl, seg) {
|
|
|
|
|
return nil, tp.errf("key %q is not a table", seg)
|
|
|
|
|
}
|
2026-09-22 21:15:00 +02:00
|
|
|
sink := &targetTable{sink: true, path: slices.Clone(segAbs)}
|
2026-09-22 15:59:03 +02:00
|
|
|
tp.opened[pathKey(segAbs)] = sink
|
|
|
|
|
return sink, nil
|
|
|
|
|
}
|
2026-09-22 21:15:00 +02:00
|
|
|
tp.markResolved(tbl, resolved)
|
2026-09-22 15:59:03 +02:00
|
|
|
fv, ferr := fieldByIndex(tbl.rv, loc.index)
|
|
|
|
|
if ferr != nil {
|
|
|
|
|
return nil, errTargetFallback
|
|
|
|
|
}
|
|
|
|
|
if fv.Kind() == reflect.Pointer {
|
|
|
|
|
if fv.IsNil() {
|
|
|
|
|
if !fv.CanSet() {
|
|
|
|
|
return nil, errTargetFallback
|
|
|
|
|
}
|
|
|
|
|
fv.Set(reflect.New(fv.Type().Elem()))
|
|
|
|
|
}
|
|
|
|
|
fv = fv.Elem()
|
|
|
|
|
}
|
|
|
|
|
switch fv.Kind() {
|
|
|
|
|
case reflect.Struct:
|
|
|
|
|
if isScalarStruct(fv.Type()) {
|
|
|
|
|
if tp.tableHas(tbl, seg) {
|
|
|
|
|
return nil, tp.errf("key %q is not a table", seg)
|
|
|
|
|
}
|
|
|
|
|
return nil, errTargetFallback
|
|
|
|
|
}
|
2026-09-22 21:15:00 +02:00
|
|
|
return &targetTable{rv: fv, schema: schemaRef(fv.Type()), path: slices.Clone(segAbs), strict: tbl.strict}, nil
|
2026-09-22 15:59:03 +02:00
|
|
|
case reflect.Map:
|
|
|
|
|
if fv.Type().Key().Kind() != reflect.String {
|
|
|
|
|
return nil, errTargetFallback
|
|
|
|
|
}
|
|
|
|
|
if fv.IsNil() {
|
|
|
|
|
fv.Set(reflect.MakeMap(fv.Type()))
|
|
|
|
|
}
|
2026-09-22 21:15:00 +02:00
|
|
|
return &targetTable{rv: fv, path: slices.Clone(segAbs)}, nil
|
2026-09-22 15:59:03 +02:00
|
|
|
}
|
|
|
|
|
if tp.tableHas(tbl, seg) {
|
|
|
|
|
return nil, tp.errf("key %q is not a table", seg)
|
|
|
|
|
}
|
|
|
|
|
return nil, errTargetFallback
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// leafInTable resolves a plain key in the table.
|
|
|
|
|
func (tp *targetParser) leafInTable(dest *targetTable, key string) (dst reflect.Value, mapDst reflect.Value, leafTable *targetTable, placed bool, err error) {
|
|
|
|
|
leafTable = dest
|
|
|
|
|
if dest.sink {
|
|
|
|
|
return reflect.Value{}, reflect.Value{}, leafTable, false, nil
|
|
|
|
|
}
|
|
|
|
|
if dest.schema == nil {
|
|
|
|
|
if dest.rv.Kind() != reflect.Map {
|
|
|
|
|
return reflect.Value{}, reflect.Value{}, leafTable, false, errTargetFallback
|
|
|
|
|
}
|
2026-09-22 21:15:00 +02:00
|
|
|
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.
|
2026-09-22 15:59:03 +02:00
|
|
|
return reflect.Value{}, reflect.Value{}, leafTable, false, tp.errf("duplicate key %q", key)
|
|
|
|
|
}
|
|
|
|
|
elem := reflect.New(dest.rv.Type().Elem()).Elem()
|
|
|
|
|
return elem, dest.rv, leafTable, true, nil
|
|
|
|
|
}
|
|
|
|
|
resolved := key
|
|
|
|
|
loc, ok := dest.schema.byName[key]
|
|
|
|
|
if !ok {
|
|
|
|
|
resolved = strings.ToLower(key)
|
|
|
|
|
loc, ok = dest.schema.byName[resolved]
|
|
|
|
|
}
|
|
|
|
|
if !ok {
|
2026-09-22 21:15:00 +02:00
|
|
|
tp.recordStrictUnknown(dest, key)
|
2026-09-22 15:59:03 +02:00
|
|
|
return reflect.Value{}, reflect.Value{}, leafTable, false, nil
|
|
|
|
|
}
|
2026-09-22 21:15:00 +02:00
|
|
|
tp.markResolved(dest, resolved)
|
2026-09-22 15:59:03 +02:00
|
|
|
fv, ferr := fieldByIndex(dest.rv, loc.index)
|
|
|
|
|
if ferr != nil {
|
|
|
|
|
return reflect.Value{}, reflect.Value{}, leafTable, false, errTargetFallback
|
|
|
|
|
}
|
|
|
|
|
return fv, reflect.Value{}, leafTable, true, nil
|
|
|
|
|
}
|