Files
interpres/decode_test.go
T
petrbalvin ce0c1ebd9d
Test / test (push) Successful in 1m52s
feat: add Decoder.UseNumber and the Number type
Assisted-by: GLM 5.3 Flash
2026-09-21 23:49:39 +02:00

1223 lines
33 KiB
Go

// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
package interpres
import (
"context"
"errors"
"fmt"
"math"
"net"
"slices"
"strings"
"testing"
"time"
)
func TestSyntaxErrorMessage(t *testing.T) {
err := &SyntaxError{Line: 7, Msg: "expected '=' after key"}
want := "interpres: line 7: expected '=' after key"
if got := err.Error(); got != want {
t.Errorf("Error() = %q, want %q", got, want)
}
}
func TestParseRejectsInvalidUTF8(t *testing.T) {
// The scan validates UTF-8 where it meets the byte, so the reported line
// is the invalid byte's own, wherever in the document it sits.
cases := []struct {
name string
doc string
line int
}{
{"in a basic string", "v = \"\xff\"\n", 1},
{"in a literal string", "v = '\xff'\n", 1},
{"in a multiline string", "v = \"\"\"\n\xff\"\"\"\n", 2},
{"in a comment", "v = 1\n# caf\xe9\xff\n", 2},
{"in a bare key", "va\xfflue = 1\n", 1},
{"as a statement", "\xff = 1\n", 1},
{"in a bare value", "v = \xff1\n", 1},
{"after a value", "v = 1 \xff\n", 1},
{"after the first line", "a = 1\nb = \"\xff\"\n", 2},
}
for _, c := range cases {
_, err := ParseMap([]byte(c.doc))
if err == nil {
t.Fatalf("%s: expected a UTF-8 validation error", c.name)
}
se, ok := err.(*SyntaxError)
if !ok {
t.Fatalf("%s: err is %T, want *SyntaxError", c.name, err)
}
if !strings.Contains(se.Msg, "UTF-8") {
t.Errorf("%s: Msg = %q, want it to mention UTF-8", c.name, se.Msg)
}
if se.Line != c.line {
t.Errorf("%s: Line = %d, want %d", c.name, se.Line, c.line)
}
}
}
func TestUnmarshalIntoMap(t *testing.T) {
var m map[string]any
if err := Unmarshal([]byte(`name = "x"
count = 3
`), &m); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if m["name"] != "x" {
t.Errorf("name = %#v", m["name"])
}
if m["count"] != int64(3) {
t.Errorf("count = %#v (%T)", m["count"], m["count"])
}
}
func TestUnmarshalIntoMapNested(t *testing.T) {
var m map[string]any
if err := Unmarshal([]byte("[a]\nb = 2\n"), &m); err != nil {
t.Fatalf("unmarshal: %v", err)
}
a, ok := m["a"].(map[string]any)
if !ok {
t.Fatalf("a = %T, want map[string]any", m["a"])
}
if a["b"] != int64(2) {
t.Errorf("a.b = %#v", a["b"])
}
}
func TestUnmarshalPrefilledMap(t *testing.T) {
m := map[string]any{"keep": "yes"}
if err := Unmarshal([]byte(`name = "x"`), &m); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if m["keep"] != "yes" {
t.Errorf("keep = %#v", m["keep"])
}
if m["name"] != "x" {
t.Errorf("name = %#v", m["name"])
}
}
func TestUnmarshalTableToNonStructOrMap(t *testing.T) {
var s string
if err := Unmarshal([]byte("v = 1\n"), &s); err == nil {
t.Fatal("expected an error for table-to-scalar")
}
}
func TestUnmarshalIntoAny(t *testing.T) {
// A non-nil any destination must accept the parsed tree.
var x any
if err := Unmarshal([]byte("[a]\nb = 2\n"), &x); err != nil {
t.Fatalf("unmarshal: %v", err)
}
tree, ok := x.(map[string]any)
if !ok {
t.Fatalf("x is %T, want map[string]any", x)
}
a, ok := tree["a"].(map[string]any)
if !ok {
t.Fatalf("a is %T, want map[string]any", tree["a"])
}
if a["b"] != int64(2) {
t.Errorf("a.b = %#v", a["b"])
}
}
func TestUnmarshalIntoNilAny(t *testing.T) {
// A nil any target must still receive the parsed tree without
// panicking.
var x any
if err := Unmarshal([]byte("v = 1\n"), &x); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if x == nil {
t.Fatal("x is still nil after Unmarshal")
}
tree, ok := x.(map[string]any)
if !ok {
t.Fatalf("x is %T, want map[string]any", x)
}
if tree["v"] != int64(1) {
t.Errorf("v = %#v", tree["v"])
}
}
func TestParseContextHonoursCancellation(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel()
if _, err := ParseMapContext(ctx, []byte("a = 1\n")); !errors.Is(err, context.Canceled) {
t.Fatalf("ParseContext returned %v, want context.Canceled", err)
}
}
func TestUnmarshalContextHonoursCancellation(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel()
var cfg map[string]any
if err := UnmarshalContext(ctx, []byte("a = 1\n"), &cfg); !errors.Is(err, context.Canceled) {
t.Fatalf("UnmarshalContext returned %v, want context.Canceled", err)
}
}
func TestDecoderDecodeContextHonoursCancellation(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel()
var cfg map[string]any
err := NewDecoder().DecodeContext(ctx, []byte("a = 1\n"), &cfg)
if !errors.Is(err, context.Canceled) {
t.Fatalf("DecodeContext returned %v, want context.Canceled", err)
}
}
func TestContextRoundTrip(t *testing.T) {
// The *Context variants with a Background context must produce the same
// result as the non-context variants for ordinary inputs.
in := []byte(`title = "x"
count = 3
`)
if _, err := ParseMapContext(context.Background(), in); err != nil {
t.Fatalf("ParseContext: %v", err)
}
var out struct {
Title string `toml:"title"`
Count int `toml:"count"`
}
if err := UnmarshalContext(context.Background(), in, &out); err != nil {
t.Fatalf("UnmarshalContext: %v", err)
}
if out.Title != "x" || out.Count != 3 {
t.Errorf("out = %#v", out)
}
if err := NewDecoder().DecodeContext(context.Background(), in, &map[string]any{}); err != nil {
t.Fatalf("Decoder.DecodeContext: %v", err)
}
}
func TestUnmarshalIntToUintOverflow(t *testing.T) {
type C struct {
X uint8 `toml:"x"`
}
var c C
err := Unmarshal([]byte("x = 300\n"), &c)
if err == nil {
t.Fatal("expected overflow error")
}
if !strings.Contains(err.Error(), "overflow") {
t.Errorf("err = %v, want substring 'overflow'", err)
}
}
func TestUnmarshalIntToUint16Boundary(t *testing.T) {
type C struct {
X uint16 `toml:"x"`
}
// Exactly 65535 fits, 65536 does not.
var ok C
if err := Unmarshal([]byte("x = 65535\n"), &ok); err != nil {
t.Fatalf("65535 should fit uint16, got %v", err)
}
if ok.X != 65535 {
t.Errorf("X = %d, want 65535", ok.X)
}
var bad C
if err := Unmarshal([]byte("x = 65536\n"), &bad); err == nil {
t.Fatal("65536 must not fit uint16")
}
}
func TestUnmarshalIntToUint64FitsMaxInt64(t *testing.T) {
type C struct {
X uint64 `toml:"x"`
}
var c C
tok := "x = 9223372036854775807\n" // math.MaxInt64
if err := Unmarshal([]byte(tok), &c); err != nil {
t.Fatalf("MaxInt64 should fit uint64, got %v", err)
}
if c.X != math.MaxInt64 {
t.Errorf("X = %d, want %d", uint64(c.X), uint64(math.MaxInt64))
}
}
func TestUnmarshalFloat32Overflow(t *testing.T) {
// A finite float64 beyond the float32 range must not decode silently as
// an infinity.
type C struct {
X float32 `toml:"x"`
}
var c C
err := Unmarshal([]byte("x = 1e300\n"), &c)
if err == nil {
t.Fatal("expected overflow error for float32")
}
if !strings.Contains(err.Error(), "overflow") {
t.Errorf("err = %v, want substring 'overflow'", err.Error())
}
// Infinities themselves pass through, and in-range values are untouched.
var ok C
if err := Unmarshal([]byte("x = inf\n"), &ok); err != nil {
t.Fatalf("inf should decode into float32, got %v", err)
}
if !math.IsInf(float64(ok.X), 1) {
t.Errorf("X = %v, want +Inf", ok.X)
}
if err := Unmarshal([]byte("x = 1.5\n"), &ok); err != nil || ok.X != 1.5 {
t.Fatalf("1.5 should decode into float32, got %v (X=%v)", err, ok.X)
}
}
func TestUnmarshalNegativeIntToUint(t *testing.T) {
type C struct {
X uint8 `toml:"x"`
}
var c C
err := Unmarshal([]byte("x = -1\n"), &c)
if err == nil {
t.Fatal("expected negative-to-uint error")
}
if !strings.Contains(err.Error(), "negative") {
t.Errorf("err = %v, want substring 'negative'", err)
}
}
func TestUnmarshalIntToFloat(t *testing.T) {
type C struct {
X float64 `toml:"x"`
}
var c C
if err := Unmarshal([]byte("x = 5\n"), &c); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if c.X != 5.0 {
t.Errorf("X = %v, want 5.0", c.X)
}
}
func TestUnmarshalIntToStringFails(t *testing.T) {
type C struct {
X string `toml:"x"`
}
var c C
if err := Unmarshal([]byte(`x = 5`), &c); err == nil {
t.Fatal("expected type-mismatch error")
}
}
func TestUnmarshalStringToIntFails(t *testing.T) {
type C struct {
X int `toml:"x"`
}
var c C
if err := Unmarshal([]byte(`x = "hello"`), &c); err == nil {
t.Fatal("expected type-mismatch error")
}
}
func TestUnmarshalArrayToScalarFails(t *testing.T) {
type C struct {
X int `toml:"x"`
}
var c C
if err := Unmarshal([]byte("x = [1, 2]\n"), &c); err == nil {
t.Fatal("expected type-mismatch error")
}
}
func TestUnmarshalArrayOfTablesToScalarFails(t *testing.T) {
type Item struct {
Name string `toml:"name"`
}
type C struct {
X Item `toml:"x"`
}
var c C
if err := Unmarshal([]byte("[[x]]\nname = \"a\"\n"), &c); err == nil {
t.Fatal("expected type-mismatch error")
}
}
func TestUnmarshalFloatToIntFails(t *testing.T) {
type C struct {
X int `toml:"x"`
}
var c C
if err := Unmarshal([]byte("x = 1.5\n"), &c); err == nil {
t.Fatal("expected type-mismatch error")
}
}
func TestUnmarshalBoolToIntFails(t *testing.T) {
type C struct {
X int `toml:"x"`
}
var c C
if err := Unmarshal([]byte("x = true\n"), &c); err == nil {
t.Fatal("expected type-mismatch error")
}
}
func TestUnmarshalNonPointerRejected(t *testing.T) {
var v int
if err := Unmarshal([]byte("x = 1\n"), v); err == nil {
t.Fatal("expected error for non-pointer target")
}
}
func TestUnmarshalAssignErrorWrapped(t *testing.T) {
// assignStruct wraps inner assignment errors with the key name.
type C struct {
Inner struct {
X int `toml:"x"`
} `toml:"inner"`
}
var c C
err := Unmarshal([]byte("[inner]\nx = \"oops\"\n"), &c)
if err == nil {
t.Fatal("expected an assignment error")
}
if !strings.Contains(err.Error(), "x") {
t.Errorf("err = %v, want it to mention key x", err)
}
}
func TestUnmarshalAssignMapErrorWrapped(t *testing.T) {
// assignMap wraps inner errors with the key of the bad element.
m := map[string]int{}
err := Unmarshal([]byte("[s]\nx = 1\n"), &m)
if err == nil {
t.Fatal("expected an assignment error")
}
if !strings.Contains(err.Error(), "s") {
t.Errorf("err = %v, want it to mention key 's'", err)
}
}
func TestUnmarshalAssignSliceErrorWrapped(t *testing.T) {
// assignSlice wraps inner errors with the bad element's index.
type C struct {
Items []int `toml:"items"`
}
var c C
err := Unmarshal([]byte(`items = [1, "oops"]`), &c)
if err == nil {
t.Fatal("expected an assignment error")
}
if !strings.Contains(err.Error(), "[1]") {
t.Errorf("err = %v, want it to mention index [1]", err)
}
}
func TestUnmarshalAssignTimeToWrongTypeFails(t *testing.T) {
// assign maps time.Time to time.Time only.
type C struct {
T string `toml:"t"`
}
var c C
err := Unmarshal([]byte("t = 2026-01-01T00:00:00Z\n"), &c)
if err == nil {
t.Fatal("expected time-to-string assignment to fail")
}
}
func TestMarshalerUsesCustomEncoding(t *testing.T) {
// A type that implements Marshaler can be encoded through a wrapping struct.
val := inlineMarshaler(func() (any, error) {
return map[string]any{"k": "v"}, nil
})
type wrap struct {
Inner inlineMarshaler `toml:"inner"`
}
out, err := Marshal(wrap{Inner: val})
if err != nil {
t.Fatalf("marshal: %v", err)
}
got := string(out)
if !strings.Contains(got, "[inner]") || !strings.Contains(got, "k = \"v\"") {
t.Errorf("out = %q, want a [inner] table with k = \"v\"", got)
}
}
type inlineMarshaler func() (any, error)
func (i inlineMarshaler) MarshalTOML() (any, error) { return i() }
// --- Unmarshaler -----------------------------------------------------------
// receiverSetter implements *Unmarshaler by reshaping a parsed table.
type receiverSetter struct {
Field string
Received any
}
func (r *receiverSetter) UnmarshalTOML(data any) error {
m, ok := data.(map[string]any)
if !ok {
return fmt.Errorf("interpres: receiverSetter expects table, got %T", data)
}
if v, ok := m["field"].(string); ok {
r.Field = v
}
r.Received = data
return nil
}
func TestUnmarshalerByPointer(t *testing.T) {
type Cfg struct {
R receiverSetter `toml:"r"`
}
var cfg Cfg
if err := Unmarshal([]byte(`[r]
field = "x"
`), &cfg); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if cfg.R.Field != "x" {
t.Errorf("Field = %q, want \"x\"", cfg.R.Field)
}
if cfg.R.Received == nil {
t.Error("Receiver did not see parsed data")
}
}
type scalarUnmarshaler struct{ val string }
func (s *scalarUnmarshaler) UnmarshalTOML(data any) error {
str, ok := data.(string)
if !ok {
return fmt.Errorf("interpres: scalarUnmarshaler expects string, got %T", data)
}
s.val = str
return nil
}
func TestUnmarshalerReceivesRawScalar(t *testing.T) {
// Place the Unmarshaler-implementing field inside a wrapper struct so
// the decoder dispatches the scalar value to its UnmarshalTOML.
type Cfg struct {
S scalarUnmarshaler `toml:"s"`
}
var cfg Cfg
if err := Unmarshal([]byte(`s = "hello"`), &cfg); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if cfg.S.val != "hello" {
t.Errorf("cfg.S.val = %q, want \"hello\"", cfg.S.val)
}
}
type failingUnmarshaler struct{}
func (f *failingUnmarshaler) UnmarshalTOML(_ any) error { return errors.New("boom") }
func TestUnmarshalerErrorPropagates(t *testing.T) {
type Cfg struct {
F failingUnmarshaler `toml:"f"`
}
var cfg Cfg
if err := Unmarshal([]byte("f = 1"), &cfg); err == nil {
t.Fatal("expected error from UnmarshalTOML")
} else if !strings.Contains(err.Error(), "boom") {
t.Errorf("err = %v, want substring \"boom\"", err)
}
}
func TestUnmarshalerTakesPrecedenceOverDefault(t *testing.T) {
// Even when the field is a scalar type and the value is a table,
// UnmarshalTOML wins, because the receiver decides.
type Cfg struct {
R receiverSetter `toml:"r"`
}
var cfg Cfg
in := []byte(`[r]
field = "y"
`)
if err := Unmarshal(in, &cfg); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if cfg.R.Field != "y" {
t.Errorf("Field = %q, want \"y\"", cfg.R.Field)
}
}
// --- embedded field symmetry -----------------------------------------------
type RoundTripBase struct {
ID int `toml:"id"`
Name string `toml:"name"`
}
type RoundTripDerived struct {
RoundTripBase
X string `toml:"x"`
}
func TestUnmarshalEmbeddedStructRoundTrip(t *testing.T) {
orig := RoundTripDerived{ID: 1, Name: "b", X: "x"}
out, err := Marshal(orig)
if err != nil {
t.Fatalf("marshal: %v", err)
}
var back RoundTripDerived
if err := Unmarshal(out, &back); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if back != orig {
t.Fatalf("round-trip mismatch:\nwas: %+v\nnow: %+v", orig, back)
}
}
type RoundTripPtrCfg struct {
*RoundTripBase
X string `toml:"x"`
}
func TestUnmarshalEmbeddedPointerStruct(t *testing.T) {
var cfg RoundTripPtrCfg
if err := Unmarshal([]byte("id = 7\nname = \"n\"\nx = \"x\"\n"), &cfg); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if cfg.RoundTripBase == nil || cfg.ID != 7 || cfg.Name != "n" || cfg.X != "x" {
t.Fatalf("decoded: %+v", cfg)
}
}
// A struct embedding a pointer to itself is legal Go; decoding into it must
// terminate. The schema walk used to recurse through the embedded type
// forever.
func TestUnmarshalSelfEmbeddedPointerStructTerminates(t *testing.T) {
type SelfLink struct {
*SelfLink
X int `toml:"x"`
Y string `toml:"y"`
}
var n SelfLink
if err := Unmarshal([]byte("x = 1\ny = \"s\"\n"), &n); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if n.X != 1 || n.Y != "s" {
t.Fatalf("decoded: %+v", n)
}
// A nil self pointer on the encode side stays skippable, as any nil
// embedded pointer is.
out, err := Marshal(SelfLink{X: 2})
if err != nil {
t.Fatalf("marshal: %v", err)
}
if want := "x = 2\ny = \"\"\n"; string(out) != want {
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
}
}
type RoundTripExtra map[string]int
type RoundTripMapCfg struct {
RoundTripExtra
X string `toml:"x"`
}
func TestUnmarshalEmbeddedMap(t *testing.T) {
var cfg RoundTripMapCfg
if err := Unmarshal([]byte("alpha = 1\nx = \"x\"\n"), &cfg); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if cfg.RoundTripExtra["alpha"] != 1 || cfg.X != "x" {
t.Fatalf("decoded: %+v", cfg)
}
orig := RoundTripMapCfg{RoundTripExtra: RoundTripExtra{"a": 1}, X: "x"}
out, err := Marshal(orig)
if err != nil {
t.Fatalf("marshal: %v", err)
}
var back RoundTripMapCfg
if err := Unmarshal(out, &back); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if back.X != "x" || back.RoundTripExtra["a"] != 1 {
t.Fatalf("round-trip mismatch: %+v", back)
}
}
func TestUnmarshalEmbeddedNameClashShallowerWins(t *testing.T) {
type Inner struct {
Name string `toml:"name"`
Deep string `toml:"deep"`
}
type Outer struct {
Inner
Name string `toml:"name"`
}
var v Outer
if err := Unmarshal([]byte("name = \"outer\"\ndeep = \"d\"\n"), &v); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if v.Name != "outer" || v.Deep != "d" {
t.Fatalf("decoded: %+v", v)
}
}
func TestUnmarshalNameClashEqualDepthLaterWins(t *testing.T) {
// At equal depth the field declared later resolves the name, matching the
// documented rule.
type C struct {
First string `toml:"v"`
Second int `toml:"v"`
}
var c C
if err := Unmarshal([]byte("v = 1\n"), &c); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if c.Second != 1 {
t.Fatalf("decoded: %+v, want the later field to take the value", c)
}
}
func TestUnmarshalUnknownKeyWithoutEmbeddedMap(t *testing.T) {
var cfg RoundTripDerived
if err := Unmarshal([]byte("rogue = 1\n"), &cfg); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if cfg.ID != 0 || cfg.X != "" {
t.Fatalf("decoded: %+v", cfg)
}
}
func TestUnmarshalStrictEmbeddedMapStaysStrict(t *testing.T) {
type Cfg struct {
RoundTripExtra
Name string `toml:"name"`
}
dec := NewDecoder().DisallowUnknownFields()
err := dec.Decode([]byte("name = \"n\"\nrogue = 1\n"), &Cfg{})
if err == nil || !strings.Contains(err.Error(), "unknown field") {
t.Fatalf("expected unknown field error, got: %v", err)
}
}
func TestDecodeErrorCarriesPath(t *testing.T) {
type Item struct {
Name string `toml:"name"`
Weight uint8 `toml:"weight"`
}
type Cfg struct {
Tags []string `toml:"tags"`
Items []Item `toml:"items"`
}
var cfg Cfg
err := Unmarshal([]byte("[[items]]\nname = \"a\"\nweight = 300\n"), &cfg)
if err == nil {
t.Fatal("expected an overflow error")
}
de, ok := errors.AsType[*DecodeError](err)
if !ok {
t.Fatalf("expected a *DecodeError, got %T: %v", err, err)
}
want := []string{"items", "[0]", "weight"}
if !slices.Equal(de.Path, want) {
t.Fatalf("Path = %v, want %v", de.Path, want)
}
if de.Err == nil || !strings.Contains(de.Err.Error(), "overflows uint8") {
t.Fatalf("Err = %v", de.Err)
}
// The rendered message keeps its shape: segments joined with ": ".
wantMsg := "items: [0]: weight: interpres: integer 300 overflows uint8"
if err.Error() != wantMsg {
t.Fatalf("message = %q, want %q", err.Error(), wantMsg)
}
}
func TestDecodeErrorOnMapDestination(t *testing.T) {
var m map[string]uint8
err := Unmarshal([]byte("count = -1\n"), &m)
if err == nil {
t.Fatal("expected an error")
}
de, ok := errors.AsType[*DecodeError](err)
if !ok {
t.Fatalf("expected a *DecodeError, got %T: %v", err, err)
}
if !slices.Equal(de.Path, []string{"count"}) {
t.Fatalf("Path = %v", de.Path)
}
}
func TestUnmarshalIntoDefinedScalarTypes(t *testing.T) {
// A defined type whose underlying kind is string or bool takes the value.
// A bare reflect Set panics on such a type, because a string is not
// assignable to a defined string type without a conversion.
type Name string
type Flag bool
type Cfg struct {
N Name `toml:"n"`
F Flag `toml:"f"`
}
var cfg Cfg
if err := Unmarshal([]byte("n = \"x\"\nf = true\n"), &cfg); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if cfg.N != "x" {
t.Errorf("N = %q, want \"x\"", cfg.N)
}
if !cfg.F {
t.Error("F = false, want true")
}
}
// --- encoding.TextUnmarshaler and time.Duration ----------------------------
// textReceiver implements encoding.TextUnmarshaler on the pointer receiver.
type textReceiver struct{ Text string }
func (t *textReceiver) UnmarshalText(text []byte) error {
t.Text = "got:" + string(text)
return nil
}
// upperText is a defined string type whose UnmarshalText transforms the
// content, so a plain string assignment would leave the wrong value behind.
type upperText string
func (u *upperText) UnmarshalText(text []byte) error {
*u = upperText(strings.ToUpper(string(text)))
return nil
}
// failingTextUnmarshaler fails the decode from UnmarshalText.
type failingTextUnmarshaler struct{}
func (f *failingTextUnmarshaler) UnmarshalText(_ []byte) error { return errors.New("text boom") }
// textAndTOMLReceiver implements both decode interfaces; the TOML method wins.
type textAndTOMLReceiver struct{ From string }
func (t *textAndTOMLReceiver) UnmarshalTOML(any) error { t.From = "toml"; return nil }
func (t *textAndTOMLReceiver) UnmarshalText([]byte) error { t.From = "text"; return nil }
func TestTextUnmarshalerByPointer(t *testing.T) {
type Cfg struct {
R textReceiver `toml:"r"`
}
var cfg Cfg
if err := Unmarshal([]byte(`r = "hello"`), &cfg); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if cfg.R.Text != "got:hello" {
t.Errorf("Text = %q, want \"got:hello\"", cfg.R.Text)
}
}
func TestTextUnmarshalerWinsOverKindAssignment(t *testing.T) {
type Cfg struct {
U upperText `toml:"u"`
}
var cfg Cfg
if err := Unmarshal([]byte(`u = "abc"`), &cfg); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if cfg.U != "ABC" {
t.Errorf("U = %q, want \"ABC\"", cfg.U)
}
}
func TestTextUnmarshalerForNetIP(t *testing.T) {
type Cfg struct {
V4 net.IP `toml:"v4"`
V6 net.IP `toml:"v6"`
IPs []net.IP `toml:"ips"`
}
in := "v4 = \"192.0.2.1\"\nv6 = \"2001:db8::68\"\nips = [\"198.51.100.7\", \"203.0.113.9\"]\n"
var cfg Cfg
if err := Unmarshal([]byte(in), &cfg); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if got := cfg.V4.String(); got != "192.0.2.1" {
t.Errorf("V4 = %q, want \"192.0.2.1\"", got)
}
if got := cfg.V6.String(); got != "2001:db8::68" {
t.Errorf("V6 = %q, want \"2001:db8::68\"", got)
}
if len(cfg.IPs) != 2 || cfg.IPs[0].String() != "198.51.100.7" || cfg.IPs[1].String() != "203.0.113.9" {
t.Errorf("IPs = %v, want two addresses", cfg.IPs)
}
}
func TestTextUnmarshalerSeesStringsOnly(t *testing.T) {
// An integer keeps its own rule: the text method is not consulted, and the
// value does not reach the receiver.
type Cfg struct {
R textReceiver `toml:"r"`
}
var cfg Cfg
err := Unmarshal([]byte("r = 1\n"), &cfg)
if err == nil {
t.Fatal("expected an integer to be rejected for a text receiver")
}
if cfg.R.Text != "" {
t.Errorf("Text = %q, want it untouched", cfg.R.Text)
}
}
func TestUnmarshalTOMLWinsOverTextUnmarshaler(t *testing.T) {
type Cfg struct {
B textAndTOMLReceiver `toml:"b"`
}
var cfg Cfg
if err := Unmarshal([]byte(`b = "x"`), &cfg); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if cfg.B.From != "toml" {
t.Errorf("From = %q, want \"toml\"", cfg.B.From)
}
}
func TestTextUnmarshalerErrorCarriesPath(t *testing.T) {
type Inner struct {
F failingTextUnmarshaler `toml:"f"`
}
type Cfg struct {
Inner Inner `toml:"inner"`
}
var cfg Cfg
err := Unmarshal([]byte("[inner]\nf = \"x\"\n"), &cfg)
if err == nil {
t.Fatal("expected an error from UnmarshalText")
}
if !strings.Contains(err.Error(), "unmarshal text: text boom") {
t.Errorf("err = %v, want the text error wrapped", err)
}
de, ok := errors.AsType[*DecodeError](err)
if !ok {
t.Fatalf("expected a *DecodeError, got %T: %v", err, err)
}
if !slices.Equal(de.Path, []string{"inner", "f"}) {
t.Fatalf("Path = %v, want [inner f]", de.Path)
}
}
func TestTextUnmarshalerReportsBadText(t *testing.T) {
var cfg struct {
IP net.IP `toml:"ip"`
}
err := Unmarshal([]byte(`ip = "not-an-ip"`), &cfg)
if err == nil {
t.Fatal("expected an error for a malformed address")
}
if !strings.Contains(err.Error(), "unmarshal text:") {
t.Errorf("err = %v, want it wrapped as a text error", err)
}
}
func TestUnmarshalDurations(t *testing.T) {
type Cfg struct {
FromText time.Duration `toml:"from_text"`
FromInt time.Duration `toml:"from_int"`
Fraction time.Duration `toml:"fraction"`
}
in := "from_text = \"1h30m\"\nfrom_int = 5400000000000\nfraction = \"1.5s\"\n"
var cfg Cfg
if err := Unmarshal([]byte(in), &cfg); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if cfg.FromText != 90*time.Minute {
t.Errorf("FromText = %v, want %v", cfg.FromText, 90*time.Minute)
}
if cfg.FromInt != 90*time.Minute {
t.Errorf("FromInt = %v, want %v", cfg.FromInt, 90*time.Minute)
}
if cfg.Fraction != 1500*time.Millisecond {
t.Errorf("Fraction = %v, want %v", cfg.Fraction, 1500*time.Millisecond)
}
}
func TestUnmarshalDurationRejectsMalformedText(t *testing.T) {
var cfg struct {
D time.Duration `toml:"d"`
}
err := Unmarshal([]byte("d = \"90\"\n"), &cfg)
if err == nil {
t.Fatal("expected an error for a duration without a unit")
}
if !strings.Contains(err.Error(), "invalid duration") {
t.Errorf("err = %v, want an invalid-duration message", err)
}
}
func TestQuotedStringNeverBecomesDateTime(t *testing.T) {
// The date-time types take a bare timestamp only, so the text path is
// excluded for them and a quoted string stays a string.
var stamp struct {
S time.Time `toml:"s"`
}
err := Unmarshal([]byte("s = \"2026-06-26T10:00:00Z\"\n"), &stamp)
if err == nil {
t.Fatal("expected a quoted string to be rejected for time.Time")
}
if !strings.Contains(err.Error(), "cannot assign string") {
t.Errorf("err = %v, want a cannot-assign message", err)
}
var day struct {
D LocalDate `toml:"d"`
}
if err := Unmarshal([]byte("d = \"1979-05-27\"\n"), &day); err == nil {
t.Fatal("expected a quoted string to be rejected for LocalDate")
}
}
func TestDecoderMaxDepth(t *testing.T) {
deep := func(n int) []byte {
return []byte("v = " + strings.Repeat("[", n) + strings.Repeat("]", n) + "\n")
}
var cfg struct {
V any `toml:"v"`
}
if err := NewDecoder().MaxDepth(4).Decode(deep(4), &cfg); err != nil {
t.Fatalf("at the limit: %v", err)
}
err := NewDecoder().MaxDepth(4).Decode(deep(5), &cfg)
if err == nil {
t.Fatal("expected a nesting error")
}
if !strings.Contains(err.Error(), "limit of 4") {
t.Errorf("err = %v, want it to name the limit", err)
}
}
func TestDecoderMaxInputSize(t *testing.T) {
doc := []byte("v = \"ab\"\n")
var cfg struct {
V string `toml:"v"`
}
if err := NewDecoder().MaxInputSize(len(doc)).Decode(doc, &cfg); err != nil {
t.Fatalf("at the limit: %v", err)
}
err := NewDecoder().MaxInputSize(len(doc)-1).Decode(doc, &cfg)
if err == nil {
t.Fatal("expected a size error")
}
if !strings.Contains(err.Error(), "over the limit of 8") {
t.Errorf("err = %v, want it to name the limit", err)
}
// Parse carries the nesting default and no size limit.
if _, err := ParseMap(doc); err != nil {
t.Fatalf("parse: %v", err)
}
}
// --- OffsetDateTime --------------------------------------------------------
func TestOffsetDateTimeIsTheParsedType(t *testing.T) {
// A document's offset date-time arrives as the wrapper, and a plain
// time.Time destination still takes it, so a timestamp field needs no
// change to keep working.
in := []byte("stamp = 2026-06-26T10:00:00-07:00\n")
want := time.Date(2026, 6, 26, 10, 0, 0, 0, time.FixedZone("", -7*3600))
var plain struct {
Stamp time.Time `toml:"stamp"`
}
if err := Unmarshal(in, &plain); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if !plain.Stamp.Equal(want) {
t.Errorf("time.Time destination = %v, want %v", plain.Stamp, want)
}
var wrapped struct {
Stamp OffsetDateTime `toml:"stamp"`
}
if err := Unmarshal(in, &wrapped); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if !wrapped.Stamp.Time.Equal(want) {
t.Errorf("OffsetDateTime destination = %v, want %v", wrapped.Stamp.Time, want)
}
if got := wrapped.Stamp.String(); got != "2026-06-26T10:00-07:00" {
t.Errorf("String() = %q, want 2026-06-26T10:00-07:00", got)
}
}
func TestOffsetDateTimeFromHandBuiltTree(t *testing.T) {
// A tree built by hand may carry a plain time.Time, which is the other
// source of the offset kind; both date-time destinations take it.
tree := map[string]any{"stamp": time.Date(2026, 6, 26, 10, 0, 0, 0, time.UTC)}
want := time.Date(2026, 6, 26, 10, 0, 0, 0, time.UTC)
var plain struct {
Stamp time.Time `toml:"stamp"`
}
if err := newDecoder().decode(tree, &plain); err != nil {
t.Fatalf("decode: %v", err)
}
if !plain.Stamp.Equal(want) {
t.Errorf("time.Time destination = %v, want %v", plain.Stamp, want)
}
var wrapped struct {
Stamp OffsetDateTime `toml:"stamp"`
}
if err := newDecoder().decode(tree, &wrapped); err != nil {
t.Fatalf("decode: %v", err)
}
if !wrapped.Stamp.Time.Equal(want) {
t.Errorf("OffsetDateTime destination = %v, want %v", wrapped.Stamp.Time, want)
}
}
// dateKindReceiver records the Go type UnmarshalTOML was handed.
type dateKindReceiver struct{ Kind string }
func (r *dateKindReceiver) UnmarshalTOML(data any) error {
r.Kind = fmt.Sprintf("%T", data)
return nil
}
func TestUnmarshalerReceivesOffsetDateTime(t *testing.T) {
// The interface sees the wrapper, which names the date-time kind on its
// own; the local kinds keep their own wrappers.
var cfg struct {
O dateKindReceiver `toml:"o"`
L dateKindReceiver `toml:"l"`
}
in := []byte("o = 2026-06-26T10:00:00Z\nl = 2026-06-26T10:00:00\n")
if err := Unmarshal(in, &cfg); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if cfg.O.Kind != "interpres.OffsetDateTime" {
t.Errorf("offset kind = %q, want interpres.OffsetDateTime", cfg.O.Kind)
}
if cfg.L.Kind != "interpres.LocalDateTime" {
t.Errorf("local kind = %q, want interpres.LocalDateTime", cfg.L.Kind)
}
}
func TestDecoderUseNumber(t *testing.T) {
data := []byte(`hex = 0x1f
sep = 1_000
signed = +1.0
exp = 1e6
posinf = inf
negzero = -0.0
plain = 42
frac = 2.5
`)
t.Run("the tree keeps the literal", func(t *testing.T) {
var tree map[string]any
if err := NewDecoder().UseNumber().Decode(data, &tree); err != nil {
t.Fatal(err)
}
for lit, key := range map[string]string{
"0x1f": "hex", "1_000": "sep", "+1.0": "signed", "1e6": "exp",
"inf": "posinf", "-0.0": "negzero", "42": "plain", "2.5": "frac",
} {
got, ok := tree[key].(Number)
if !ok {
t.Errorf("%s = %T, want Number", key, tree[key])
continue
}
if string(got) != lit {
t.Errorf("%s = %q, want %q", key, got, lit)
}
}
})
t.Run("typed fields take the evaluated value", func(t *testing.T) {
var cfg struct {
Hex Number `toml:"hex"`
Plain int64 `toml:"plain"`
Frac float64 `toml:"frac"`
Rate time.Duration
}
dec := NewDecoder().UseNumber()
if err := dec.Decode([]byte("hex = 0x1f\nplain = 42\nfrac = 2.5\nRate = 1_000\n"), &cfg); err != nil {
t.Fatal(err)
}
if cfg.Hex != "0x1f" {
t.Errorf("hex = %q, want 0x1f", cfg.Hex)
}
if cfg.Plain != 42 {
t.Errorf("plain = %d, want 42", cfg.Plain)
}
if cfg.Frac != 2.5 {
t.Errorf("frac = %g, want 2.5", cfg.Frac)
}
if cfg.Rate != 1000 {
t.Errorf("rate = %s, want 1µs", cfg.Rate)
}
})
t.Run("invalid numbers are still parse errors", func(t *testing.T) {
for _, in := range []string{"a = 01\n", "a = 1__0\n", "a = 1x\n"} {
var tree map[string]any
if err := NewDecoder().UseNumber().Decode([]byte(in), &tree); err == nil {
t.Errorf("%q decoded without an error", in)
}
}
})
t.Run("without UseNumber the tree holds the evaluated kinds", func(t *testing.T) {
var tree map[string]any
if err := NewDecoder().Decode([]byte("hex = 0x1f\nfrac = 2.5\n"), &tree); err != nil {
t.Fatal(err)
}
if v, ok := tree["hex"].(int64); !ok || v != 31 {
t.Errorf("hex = %#v, want int64 31", tree["hex"])
}
if v, ok := tree["frac"].(float64); !ok || v != 2.5 {
t.Errorf("frac = %#v, want float64 2.5", tree["frac"])
}
})
}
func TestNumberMethods(t *testing.T) {
tests := []struct {
lit Number
wantI int64
wantF float64
intErr bool
}{
{lit: "42", wantI: 42, wantF: 42},
{lit: "0x1f", wantI: 31, wantF: 31},
{lit: "1_000", wantI: 1000, wantF: 1000},
{lit: "+1.0", wantF: 1, intErr: true},
{lit: "1e6", wantF: 1e6, intErr: true},
{lit: "inf", wantF: math.Inf(1), intErr: true},
{lit: "-2.5", wantF: -2.5, intErr: true},
}
for _, tt := range tests {
i, err := tt.lit.Int64()
if tt.intErr && err == nil {
t.Errorf("%q.Int64() succeeded with %d, want an error", tt.lit, i)
}
if !tt.intErr {
if err != nil {
t.Errorf("%q.Int64() = %v", tt.lit, err)
continue
}
if i != tt.wantI {
t.Errorf("%q.Int64() = %d, want %d", tt.lit, i, tt.wantI)
}
}
f, err := tt.lit.Float64()
if err != nil {
t.Errorf("%q.Float64() = %v", tt.lit, err)
continue
}
if f != tt.wantF {
t.Errorf("%q.Float64() = %g, want %g", tt.lit, f, tt.wantF)
}
}
for _, lit := range []Number{"01", "1__0", "abc", ""} {
if _, err := lit.Float64(); err == nil {
t.Errorf("%q.Float64() succeeded, want an error", lit)
}
if _, err := lit.Int64(); err == nil {
t.Errorf("%q.Int64() succeeded, want an error", lit)
}
}
}