perf(decode): parse struct destinations without the value tree
Test / test (push) Successful in 1m50s

Assisted-by: GLM 5.3 Flash
This commit is contained in:
2026-09-22 15:59:03 +02:00
parent ee32490452
commit a7d0041259
7 changed files with 1971 additions and 6 deletions
+8
View File
@@ -188,6 +188,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
**Performance**
- Struct destinations decode directly: for a type the direct skeleton can
model, the parser resolves tables and keys against the struct schema while
the document scans and no intermediate value tree is kept. The strict
decode of the representative document drops from 168 to 160 allocations
per call against the tree path in the same process, and the 2000-element
document reaches allocation parity; every document the skeleton cannot
model falls back to the tree path and its exact error contracts. A
differential fuzz target decodes every generated document both ways.
- Marshal writes plain scalars and typed scalar arrays straight from their
reflect cells instead of boxing them into interface values first, and skips
the per-element resolution for arrays that can never take the `[[header]]`
+37
View File
@@ -4,6 +4,7 @@
package interpres
import (
"context"
"fmt"
"strings"
"testing"
@@ -168,3 +169,39 @@ func BenchmarkMarshalLong(b *testing.B) {
}
}
}
// BenchmarkStrictDecodeTree measures the reference path the targeted decode
// is measured against: the full tree parse followed by the reflection walk.
// The pair runs in one process, so the A/B comparison shares the machine.
func BenchmarkStrictDecodeTree(b *testing.B) {
dec := newDecoder()
dec.disallowUnknown = true
b.ReportAllocs()
for b.Loop() {
tree, _, err := parseWithOptions(context.Background(), benchDoc, parseOptions{}, false)
if err != nil {
b.Fatal(err)
}
var cfg benchConfig
if err := dec.decode(tree, &cfg); err != nil {
b.Fatal(err)
}
}
}
func BenchmarkStrictDecodeTreeLong(b *testing.B) {
dec := newDecoder()
dec.disallowUnknown = true
b.ReportAllocs()
b.SetBytes(int64(len(longDoc)))
for b.Loop() {
tree, _, err := parseWithOptions(context.Background(), longDoc, parseOptions{}, false)
if err != nil {
b.Fatal(err)
}
var doc benchLongDoc
if err := dec.decode(tree, &doc); err != nil {
b.Fatal(err)
}
}
}
+17
View File
@@ -491,6 +491,23 @@ depth, including struct elements inside slices; map destinations accept every
key by nature. When several keys are unknown, the message names the smallest
one, so it does not depend on map iteration order.
### Direct decoding
For a struct destination whose type graph carries no untagged embedded map and
no custom decode hook, `Unmarshal` and `(*Decoder).Decode` parse straight into
the destination: the table skeleton is resolved against the struct schema while
the document scans, and no intermediate value tree is kept. Values still flow
through the ordinary assignment rules, so every conversion, hook and error the
[Decoding](#decoding) section states holds verbatim; the parity with the tree
path is pinned by a differential fuzz target that decodes every generated
document both ways and compares the results.
A document or destination the direct skeleton cannot model — an unknown table
under strictness it must sink, a hook that needs the whole parsed value, an
embedded map filler — falls back to the tree path and reruns, so the
observable behaviour is always the tree path's, exactly. Nothing changes for
`Parse`, `ParseMap` or the document API: the tree remains theirs.
### Cancellation
`ParseContext`, `UnmarshalContext` and `(*Decoder).DecodeContext` accept a
+140
View File
@@ -0,0 +1,140 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
package interpres
import (
"context"
"errors"
"reflect"
"strings"
"testing"
"time"
)
type fuzzNested struct {
X int `toml:"x"`
Y string `toml:"y"`
}
type fuzzDoc struct {
Num int `toml:"num"`
Flt float64 `toml:"flt"`
Str string `toml:"str"`
Flag bool `toml:"flag"`
Small uint8 `toml:"small"`
When time.Time `toml:"when"`
Tags []string `toml:"tags"`
Lims map[string]any `toml:"lims"`
Tab fuzzNested `toml:"tab"`
Arr []fuzzNested `toml:"arr"`
Other string `toml:"other"`
}
// fuzzStmts is the statement pool the generated documents draw from: every
// destination kind the targeted parse handles, beside the shapes that make
// it fall back (overflow, unknown tables, duplicate keys).
var fuzzStmts = []string{
`num = 1`, `num = 300`, `small = 300`, `small = 7`,
`flt = 2.5`, `str = "x"`, `flag = true`,
`when = 1979-05-27T07:32:00Z`,
`tags = ["a", "b"]`, `tags = []`, `lims = { k = 1 }`,
`[tab]`, `tab.x = 1`, `tab.y = "s"`, `x = 2`, `y = "t"`,
`[[arr]]`, `x = 3`, `y = "u"`,
`[tab.nested]`, `x = 4`,
`other = "o"`, `zz = 1`, `[zz]`, `k = 1`,
`num = 2`,
}
func fuzzDocument(data []byte) []byte {
var b strings.Builder
for i, by := range data {
if i > 0 {
b.WriteByte('\n')
}
b.WriteString(fuzzStmts[int(by)%len(fuzzStmts)])
}
return []byte(b.String())
}
// treeDecodeInto is the reference decode: the ordinary tree path, non-strict
// like the fuzz decode; the strict contracts have their own deterministic
// tests.
func treeDecodeInto(data []byte, v any) error {
dec := newDecoder()
tree, _, err := parseWithOptions(context.Background(), data, parseOptions{}, false)
if err != nil {
return err
}
return dec.decode(tree, v)
}
// decodeFinding normalises an error for the comparison. Decode-stage
// findings several tables may produce (an unknown field, a missing required
// key) compare as their class alone: the tree decode picks the reporting
// table by map order and so does not promise one. Everything else compares
// as its exact text.
func decodeFinding(err error) string {
if err == nil {
return ""
}
if de, ok := errors.AsType[*DecodeError](err); ok {
if strings.Contains(de.Err.Error(), "unknown field") {
return "unknown"
}
if strings.Contains(de.Err.Error(), "missing required key") {
return "required"
}
return de.Path.String() + ": " + de.Err.Error()
}
return err.Error()
}
// FuzzTargetedDecode holds the targeted parse to the tree decode as its
// reference: for every generated document the two paths must agree on the
// error class and on the decoded value.
func FuzzTargetedDecode(f *testing.F) {
seeds := []string{
"num = 1\nstr = \"x\"\n[tab]\nx = 2\n[[arr]]\nx = 3\n",
"small = 300\n",
"[tab]\ntab.x = 1\n",
"lims = { k = 1 }\ntags = [\"a\"]\n",
"[[arr]]\ny = \"u\"\n[zz]\nk = 1\n",
}
for _, s := range seeds {
f.Add([]byte(s))
}
f.Fuzz(func(t *testing.T, data []byte) {
doc := fuzzDocument(data)
var tgt fuzzDoc
tgtErr := NewDecoder().Decode(doc, &tgt)
if tgtErr != nil {
// A document with several decode-stage findings reports a different
// one per run (the tree decode walks its maps in random order), so the
// reference gets a few chances to produce the finding the targeted
// side carries. The targeted error is either the tree's own or the
// fallback already reran the tree.
for i := range 8 {
var ref fuzzDoc
refErr := treeDecodeInto(doc, &ref)
if refErr == nil {
t.Fatalf("reference succeeded on retry %d, targeted failed: %v\ndoc:\n%s", i, tgtErr, doc)
}
if decodeFinding(refErr) == decodeFinding(tgtErr) {
return
}
if i == 7 {
t.Fatalf("errors disagree after retries:\ntargeted: %v\nlast tree: %v\ndoc:\n%s", tgtErr, refErr, doc)
}
}
}
var ref fuzzDoc
refErr := treeDecodeInto(doc, &ref)
if refErr != nil {
t.Fatalf("reference failed, targeted succeeded: %v\ndoc:\n%s", refErr, doc)
}
if !reflect.DeepEqual(ref, tgt) {
t.Fatalf("values disagree:\ntree: %#v\ntargeted: %#v\ndoc:\n%s", ref, tgt, doc)
}
})
}
+26 -6
View File
@@ -305,14 +305,22 @@ func NewSchema[T any]() {
// UnmarshalContext is the cancellable variant of Unmarshal.
func UnmarshalContext(ctx context.Context, data []byte, v any) error {
dec := newDecoder()
dec.ctx = ctx
if canTargetDecode(v) {
// The targeted parse fills struct destinations without the
// intermediate tree; a document or destination it cannot model falls
// back to the tree path, whose contracts it keeps.
if err := parseIntoTargeted(ctx, data, dec, false, 0, v); err != errTargetFallback {
return err
}
}
// Only a destination that can reach an OrderedMap needs the node tree the
// written key order is read from; every other decode skips building it.
tree, doc, err := parseWithOptions(ctx, data, parseOptions{}, typeWantsOrder(reflect.TypeOf(v)))
if err != nil {
return err
}
dec := newDecoder()
dec.ctx = ctx
dec.nodes = indexNodes(doc.Root())
return dec.decode(tree, v)
}
@@ -390,6 +398,22 @@ func (d *Decoder) Decode(data []byte, v any) error {
// DecodeContext is the cancellable variant of Decode.
func (d *Decoder) DecodeContext(ctx context.Context, data []byte, v any) error {
dec := newDecoder()
dec.disallowUnknown = d.disallowUnknown
dec.ctx = ctx
dec.loc = d.localLoc
if canTargetDecode(v) {
// The targeted parse fills struct destinations without the
// intermediate tree; a document or destination it cannot model falls
// back to the tree path, whose contracts it keeps. The size limit is
// checked here, the targeted parse being the parse itself.
if d.maxInputSize > 0 && len(data) > d.maxInputSize {
return fmt.Errorf("interpres: input is %d bytes, over the limit of %d", len(data), d.maxInputSize)
}
if err := parseIntoTargeted(ctx, data, dec, d.useNumber, d.maxDepth, v); err != errTargetFallback {
return err
}
}
opts := parseOptions{
maxDepth: d.maxDepth,
maxInputSize: d.maxInputSize,
@@ -399,11 +423,7 @@ func (d *Decoder) DecodeContext(ctx context.Context, data []byte, v any) error {
if err != nil {
return err
}
dec := newDecoder()
dec.disallowUnknown = d.disallowUnknown
dec.ctx = ctx
dec.nodes = indexNodes(doc.Root())
dec.loc = d.localLoc
return dec.decode(tree, v)
}
+1218
View File
File diff suppressed because it is too large Load Diff
+525
View File
@@ -0,0 +1,525 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
package interpres
import (
"net"
"reflect"
"strings"
"testing"
"time"
)
type targetNested struct {
X int `toml:"x"`
Y string `toml:"y"`
}
type targetCfg struct {
Num int `toml:"num"`
Small uint8 `toml:"small"`
Tags []string `toml:"tags"`
Lims map[string]any `toml:"lims"`
Tab targetNested `toml:"tab"`
Arr []targetNested `toml:"arr"`
Other string `toml:"other"`
}
// TestTargetedStrictFindings pins the strict findings of the targeted parse
// to the tree decode's own texts, paths included. Every case here was first
// surfaced by FuzzTargetedDecode.
func TestTargetedStrictFindings(t *testing.T) {
tests := []struct {
name string
doc string
want string
}{
{
name: "unknown key in a header table",
doc: "[tab]\nother = \"o\"\n",
want: `interpres: tab: unknown field "other" for interpres.targetNested`,
},
{
name: "unknown nested header without the parent header",
doc: "[tab.nested]\nx = 1\n",
want: `interpres: tab: unknown field "nested" for interpres.targetNested`,
},
{
name: "unknown key in an array-of-tables element",
doc: "[[arr]]\nother = \"o\"\n",
want: `interpres: arr[0]: unknown field "other" for interpres.targetNested`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var cfg targetCfg
err := NewDecoder().DisallowUnknownFields().Decode([]byte(tt.doc), &cfg)
if err == nil {
t.Fatalf("no error, want %q", tt.want)
}
if err.Error() != tt.want {
t.Errorf("message = %q, want %q", err.Error(), tt.want)
}
})
}
}
// TestTargetedParseErrors pins the parse-stage errors the targeted skeleton
// raises, whose texts and lines are the tree parser's own.
func TestTargetedParseErrors(t *testing.T) {
tests := []struct {
name string
doc string
want string
}{
{
name: "header on an assigned scalar",
doc: "zz = 1\n[zz]\nx = 4\n",
want: "interpres: line 2: key \"zz\" is not a table",
},
{
name: "dotted key on an assigned scalar",
doc: "zz = 1\nzz.x = 2\n",
want: "interpres: line 2: key \"zz\" is not a table",
},
{
name: "duplicate unknown keys",
doc: "zz = 1\nzz = 2\n",
want: "interpres: line 2: duplicate key \"zz\"",
},
{
name: "duplicate inside an unknown table",
doc: "[zz]\nk = 1\nk = 2\n",
want: "interpres: line 3: duplicate key \"k\"",
},
{
name: "duplicate across a sink's dotted keys",
doc: "[zz]\na.b = 1\na.b = 2\n",
want: "interpres: line 3: duplicate key \"b\"",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var cfg targetCfg
err := NewDecoder().Decode([]byte(tt.doc), &cfg)
if err == nil {
t.Fatalf("no error, want %q", tt.want)
}
if err.Error() != tt.want {
t.Errorf("message = %q, want %q", err.Error(), tt.want)
}
})
}
}
// TestTargetedSilentShapes covers the documents the targeted parse accepts
// with the values the tree decode gives.
func TestTargetedSilentShapes(t *testing.T) {
t.Run("dotted key after an unknown nested header", func(t *testing.T) {
// [tab.nested] is unknown and sinks; tab.x then lands in tab, and the
// sink's own x is a different key, the tree's shape exactly.
var cfg, ref targetCfg
in := []byte("[tab]\nx = 1\n[tab.nested]\n")
if err := NewDecoder().Decode(in, &cfg); err != nil {
t.Fatalf("decode: %v", err)
}
if err := treeDecodeInto(in, &ref); err != nil {
t.Fatalf("reference: %v", err)
}
if !reflect.DeepEqual(cfg, ref) {
t.Errorf("values disagree: targeted %+v, tree %+v", cfg, ref)
}
if cfg.Tab.X != 1 {
t.Errorf("tab.x = %d, want 1", cfg.Tab.X)
}
})
t.Run("unknown keys are ignored without strict", func(t *testing.T) {
var cfg, ref targetCfg
in := []byte("num = 5\nz1 = 1\n[zz]\nk = 1\n")
if err := NewDecoder().Decode(in, &cfg); err != nil {
t.Fatalf("decode: %v", err)
}
if err := treeDecodeInto(in, &ref); err != nil {
t.Fatalf("reference: %v", err)
}
if !reflect.DeepEqual(cfg, ref) {
t.Errorf("values disagree: targeted %+v, tree %+v", cfg, ref)
}
if cfg.Num != 5 {
t.Errorf("num = %d, want 5", cfg.Num)
}
})
t.Run("an inline table into a map field", func(t *testing.T) {
var cfg targetCfg
in := []byte("lims = { cpu = 4, deep = { a = true } }\n")
if err := NewDecoder().Decode(in, &cfg); err != nil {
t.Fatalf("decode: %v", err)
}
if cfg.Lims["cpu"] != int64(4) {
t.Errorf("lims = %v", cfg.Lims)
}
})
t.Run("an overflow falls back to the decode error", func(t *testing.T) {
var cfg targetCfg
err := NewDecoder().Decode([]byte("small = 300\n"), &cfg)
want := "interpres: small: integer 300 overflows uint8"
if err == nil || err.Error() != want {
t.Errorf("err = %v, want %q", err, want)
}
})
t.Run("too many array-of-tables elements falls back", func(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[[items]]\nn = 2\n[[items]]\nn = 3\n"), &cfg)
want := "interpres: items: cannot assign 3 elements to [2]interpres.Item"
if err == nil || err.Error() != want {
t.Errorf("err = %v, want %q", err, want)
}
})
t.Run("a UseNumber tree keeps literals in the targeted path", func(t *testing.T) {
var cfg struct {
Rate Number `toml:"rate"`
}
if err := NewDecoder().UseNumber().Decode([]byte("rate = 1_000\n"), &cfg); err != nil {
t.Fatal(err)
}
if cfg.Rate != "1_000" {
t.Errorf("rate = %q, want 1_000", cfg.Rate)
}
})
t.Run("dotted keys fill a map field", func(t *testing.T) {
var cfg targetCfg
in := []byte("lims.a.b = true\nlims.c = 3\n")
if err := NewDecoder().Decode(in, &cfg); err != nil {
t.Fatalf("decode: %v", err)
}
if cfg.Lims["c"] != int64(3) {
t.Errorf("lims = %v", cfg.Lims)
}
})
t.Run("an inline table cannot be extended", func(t *testing.T) {
var cfg targetCfg
in := []byte("lims = { a = 1 }\n[lims.deep]\nb = 2\n")
err := NewDecoder().Decode(in, &cfg)
if err == nil || !strings.Contains(err.Error(), "cannot extend inline table") {
t.Errorf("err = %v, want the inline-table extension error", err)
}
})
}
// TestTargetedShapesMatrix walks a document per destination shape, both
// through the targeted path and the tree reference, so the two agree on
// every branch the skeleton carries.
func TestTargetedShapesMatrix(t *testing.T) {
docs := []string{
// Scalars of every kind, arrays, maps, tables, arrays of tables.
"num = 7\nflt = 1.25\nstr = \"s\"\nflag = false\nsmall = 9\ntags = [\"a\"]\nlims = { a = 1 }\n\n[tab]\nx = 1\ny = \"t\"\n\n[[arr]]\nx = 2\ny = \"u\"\n\n[[arr]]\nx = 3\ny = \"v\"\n",
// Dotted keys through nested tables and maps.
"tab.x = 1\ntab.y = \"s\"\nlims.a.b = true\nlims.c = 3\nnum = 2\n",
// Inline tables nested in arrays, mixed value arrays.
"lims = { a = { b = 1 } }\ntags = []\nother = \"o\"\n",
// A sub-table of an array of tables, then a second element.
"[[arr]]\nx = 1\n[arr.nested]\ny = \"n\"\n[[arr]]\ny = \"m\"\n",
// Negative and signed numbers, exponents, radix forms into floats.
"flt = -3.5e2\nnum = -42\nflt = +1.0\n",
// A quoted key and a defined-string-shaped value.
"\"quoted key\" = 1\nstr = \"multi\"\n",
}
for i, doc := range docs {
var ref, tgt targetCfg
refErr := treeDecodeInto([]byte(doc), &ref)
dec := NewDecoder()
tgtErr := dec.Decode([]byte(doc), &tgt)
if (refErr == nil) != (tgtErr == nil) {
t.Errorf("doc %d: error presence disagrees: tree %v, targeted %v", i, refErr, tgtErr)
continue
}
if refErr != nil {
continue
}
if !reflect.DeepEqual(ref, tgt) {
t.Errorf("doc %d: values disagree:\ntree: %#v\ntargeted: %#v", i, ref, tgt)
}
}
}
// TestTargetedFallbackContracts pins the documents that must fall back and
// produce the tree decode's exact error.
func TestTargetedFallbackContracts(t *testing.T) {
type Item struct {
N int `toml:"n"`
}
tests := []struct {
name string
doc string
want string
}{
{
name: "uint8 overflow",
doc: "small = 300\n",
want: "interpres: small: integer 300 overflows uint8",
},
{
name: "negative into uint",
doc: "small = -1\n",
want: "interpres: small: cannot assign negative -1 to uint8",
},
{
name: "a table into a scalar",
doc: "num = { a = 1 }\n",
want: "interpres: num: cannot assign table to int",
},
{
name: "an integer into a string field",
doc: "other = 5\n",
want: "interpres: other: cannot assign integer to string",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var cfg targetCfg
err := NewDecoder().Decode([]byte(tt.doc), &cfg)
if err == nil || err.Error() != tt.want {
t.Errorf("err = %v, want %q", err, tt.want)
}
})
}
_ = Item{}
}
// TestTargetedHeaderOnAssignedScalar pins the parse error a header raises
// when the key already holds a scalar, before any fallback can happen.
func TestTargetedHeaderOnAssignedScalarArray(t *testing.T) {
var cfg targetCfg
in := []byte("arr = []\n[[arr]]\nx = 1\n")
err := NewDecoder().Decode(in, &cfg)
want := "interpres: line 2: key \"arr\" is not an array of tables"
if err == nil || err.Error() != want {
t.Errorf("err = %v, want %q", err, want)
}
}
// TestTargetedBranchParity walks the fallback branches of the targeted
// skeleton: every document here takes the tree path on a rerun, and must
// carry the tree decode's exact error text.
func TestTargetedBranchParity(t *testing.T) {
tests := []struct {
name string
doc string
want string
}{
{
name: "a header over a value array",
doc: "tags = [\"x\"]\n[tags]\na = 1\n",
want: "interpres: line 2: key \"tags\" is not a table",
},
{
name: "an array header over a value array",
doc: "tags = [\"x\"]\n[[tags]]\na = 1\n",
want: "interpres: line 2: key \"tags\" is not an array of tables",
},
{
name: "an array header over a datetime field",
doc: "when = 1979-05-27T07:32:00Z\n[[when]]\nx = 1\n",
want: "interpres: line 2: key \"when\" is not an array of tables",
},
{
name: "a boolean into a string field",
doc: "other = true\n",
want: "interpres: other: cannot assign bool to string",
},
{
name: "a leading-zero integer",
doc: "num = 01\n",
want: "interpres: line 1: leading zeros are not allowed in numbers",
},
{
name: "an int64-overflowing integer",
doc: "num = 99999999999999999999\n",
want: "interpres: line 1: integer \"99999999999999999999\" out of range",
},
{
name: "a malformed boolean",
doc: "flag = tru\n",
want: "interpres: line 1: invalid value",
},
{
name: "a negative number into an unsigned field",
doc: "small = -5\n",
want: "interpres: small: cannot assign negative -5 to uint8",
},
{
name: "an integer into a string field via the generic path",
doc: "other = 5\n",
want: "interpres: other: cannot assign integer to string",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var cfg targetCfg
err := NewDecoder().DisallowUnknownFields().Decode([]byte(tt.doc), &cfg)
if tt.want == "" {
if err != nil {
t.Fatalf("err = %v, want nil", err)
}
return
}
if err == nil || err.Error() != tt.want {
t.Errorf("err = %v, want %q", err, tt.want)
}
})
}
}
// TestTargetedDecodeHookFields keeps the custom decode hooks of scalar-typed
// fields working in the targeted path.
func TestTargetedDecodeHookFields(t *testing.T) {
type Cfg struct {
IP net.IP `toml:"ip"`
Dur time.Duration `toml:"dur"`
Unm *scalarUnmarshaler `toml:"unm"`
}
var cfg Cfg
in := []byte("ip = \"192.0.2.1\"\ndur = \"1h30m\"\nunm = \"hello\"\n")
if err := NewDecoder().Decode(in, &cfg); err != nil {
t.Fatal(err)
}
if cfg.IP.String() != "192.0.2.1" {
t.Errorf("ip = %v", cfg.IP)
}
if cfg.Dur != 90*time.Minute {
t.Errorf("dur = %v", cfg.Dur)
}
if cfg.Unm == nil || cfg.Unm.val != "hello" {
t.Errorf("unm = %+v", cfg.Unm)
}
}
// TestTargetedOddShapes pins the fallback and value shapes the matrix does
// not reach: space-separated date-times, non-string map keys and repeated
// dotted map keys.
func TestTargetedOddShapes(t *testing.T) {
t.Run("a space-separated date-time", func(t *testing.T) {
type Cfg struct {
When time.Time `toml:"when"`
}
var cfg, ref Cfg
doc := []byte("when = 1979-05-27 07:32:00Z\n")
if err := NewDecoder().Decode(doc, &cfg); err != nil {
t.Fatal(err)
}
if err := treeDecodeInto(doc, &ref); err != nil {
t.Fatal(err)
}
if !cfg.When.Equal(ref.When) {
t.Errorf("when = %v, want %v", cfg.When, ref.When)
}
})
t.Run("a map with a non-string key falls back", func(t *testing.T) {
type Cfg struct {
M map[int]string `toml:"m"`
}
var cfg, ref Cfg
doc := []byte("m = { a = 1 }\n")
err := NewDecoder().Decode(doc, &cfg)
refErr := treeDecodeInto(doc, &ref)
if err == nil || refErr == nil {
t.Fatalf("err = %v, refErr = %v, want both to fail", err, refErr)
}
if err.Error() != refErr.Error() {
t.Errorf("errors disagree: targeted %q, tree %q", err, refErr)
}
})
t.Run("a repeated dotted map key is a duplicate", func(t *testing.T) {
var cfg targetCfg
doc := []byte("lims.a.b = 1\nlims.a.b = 2\n")
err := NewDecoder().Decode(doc, &cfg)
want := "interpres: line 2: duplicate key \"b\""
if err == nil || err.Error() != want {
t.Errorf("err = %v, want %q", err, want)
}
})
t.Run("an underscored integer takes the token path", func(t *testing.T) {
var cfg targetCfg
doc := []byte("num = 1_000\n")
if err := NewDecoder().Decode(doc, &cfg); err != nil {
t.Fatal(err)
}
if cfg.Num != 1000 {
t.Errorf("num = %d, want 1000", cfg.Num)
}
})
t.Run("an 18-digit integer takes the fast path", func(t *testing.T) {
var cfg struct {
Big int64 `toml:"big"`
}
doc := []byte("big = 999999999999999999\n")
if err := NewDecoder().Decode(doc, &cfg); err != nil {
t.Fatal(err)
}
if cfg.Big != 999999999999999999 {
t.Errorf("big = %d", cfg.Big)
}
})
}
// TestTargetedMapTableShapes covers the map-entry branches of the targeted
// skeleton: entries that become tables, entries that refuse them, and the
// duplicate checks across them.
func TestTargetedMapTableShapes(t *testing.T) {
tests := []struct {
name string
doc string
want string
}{
{
name: "a header opens a map entry table",
doc: "lims.c = 1\n[lims.d]\nk = 1\n",
want: "",
},
{
name: "a header over an assigned map entry",
doc: "lims.a = 1\n[lims.a]\nk = 1\n",
want: "interpres: line 2: key \"a\" is not a table",
},
{
name: "a dotted key over an assigned map entry",
doc: "lims.a = 1\nlims.a.b = 2\n",
want: "interpres: line 2: key \"a\" is not a table",
},
{
name: "a duplicate plain map entry",
doc: "lims.a = 1\nlims.a = 2\n",
want: "interpres: line 2: duplicate key \"a\"",
},
{
name: "an array of tables inside a map entry",
doc: "lims.c = 1\n[[lims.items]]\nk = 1\n",
want: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var cfg, ref targetCfg
err := NewDecoder().Decode([]byte(tt.doc), &cfg)
refErr := treeDecodeInto([]byte(tt.doc), &ref)
if (err == nil) != (refErr == nil) {
t.Fatalf("error presence disagrees: tree %v, targeted %v", refErr, err)
}
if err != nil {
if err.Error() != refErr.Error() {
t.Fatalf("errors disagree:\ntree: %v\ntargeted: %v", refErr, err)
}
return
}
if !reflect.DeepEqual(cfg, ref) {
t.Errorf("values disagree: targeted %+v, tree %+v", cfg, ref)
}
})
}
}