// Copyright (c) 2026 Petr BalvĂ­n (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) { _, err := ParseMap([]byte("v = \"\xff\"\n")) if err == nil { t.Fatal("expected a UTF-8 validation error") } se, ok := err.(*SyntaxError) if !ok { t.Fatalf("err is %T, want *SyntaxError", err) } if !strings.Contains(se.Msg, "UTF-8") { t.Errorf("Msg = %q, want it to mention UTF-8", se.Msg) } if se.Line != 1 { t.Errorf("Line = %d, want 1", se.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) } }