feat: honour TextMarshaler and TextUnmarshaler by default
Test / test (push) Successful in 1m35s

Assisted-by: DeepSeek V4.1 Flash
This commit is contained in:
2026-09-19 02:41:09 +02:00
parent 9023784da3
commit 815141440e
8 changed files with 632 additions and 3 deletions
+204
View File
@@ -8,9 +8,11 @@ import (
"errors"
"fmt"
"math"
"net"
"slices"
"strings"
"testing"
"time"
)
func TestSyntaxErrorMessage(t *testing.T) {
@@ -747,3 +749,205 @@ func TestUnmarshalIntoDefinedScalarTypes(t *testing.T) {
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")
}
}