feat: add OffsetDateTime, nesting limits and uniform Marshaler dispatch
Test / test (push) Successful in 2m18s

Assisted-by: DeepSeek V4.1 Flash
This commit is contained in:
2026-09-19 19:36:24 +02:00
parent b695b69768
commit a6e3e3fe31
12 changed files with 631 additions and 117 deletions
+146
View File
@@ -1599,6 +1599,7 @@ func TestMarshalDateTimeRendering(t *testing.T) {
want string
}{
{"offset-zero-seconds", base, "v = 2026-06-26T10:00Z\n"},
{"offset-wrapper", OffsetDateTime{Time: base}, "v = 2026-06-26T10:00Z\n"},
{"offset-seconds", base.Add(30 * time.Second), "v = 2026-06-26T10:00:30Z\n"},
{"offset-fraction", base.Add(500 * time.Millisecond), "v = 2026-06-26T10:00:00.5Z\n"},
{"offset-zone", time.Date(2026, 6, 26, 10, 0, 0, 0, time.FixedZone("", -7*3600)), "v = 2026-06-26T10:00-07:00\n"},
@@ -1800,3 +1801,148 @@ func TestEncoderInlineTablesKeepsArraysOfTables(t *testing.T) {
t.Errorf("items = %#v, want []map[string]any", tree["items"])
}
}
// --- Marshaler on array elements -------------------------------------------
// countingMarshaler reports how often its method ran, so a test can check that
// the encoder resolves an element once.
type countingMarshaler struct{ calls *int }
func (c countingMarshaler) MarshalTOML() (any, error) {
*c.calls++
return map[string]any{"n": int64(*c.calls)}, nil
}
// ptrMarshaler carries MarshalTOML on the pointer receiver only.
type ptrMarshaler struct{ V string }
func (p *ptrMarshaler) MarshalTOML() (any, error) { return map[string]any{"v": p.V}, nil }
func TestMarshalerElementOfArrayOfTables(t *testing.T) {
// An element is classified by what MarshalTOML returns, so methods that
// render tables keep the [[header]] form the Go kind would have given them.
type Cfg struct {
Items []marshalerFunc `toml:"items"`
}
out, err := Marshal(Cfg{Items: []marshalerFunc{
func() (any, error) { return map[string]any{"k": "a"}, nil },
func() (any, error) { return map[string]any{"k": "b"}, nil },
}})
if err != nil {
t.Fatalf("marshal: %v", err)
}
want := "[[items]]\nk = \"a\"\n\n[[items]]\nk = \"b\"\n"
if string(out) != want {
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
}
}
func TestMarshalerElementScalarResultMakesValueArray(t *testing.T) {
// One element rendering itself as a scalar turns the whole array into a
// value array, with the table elements written inline.
type Cfg struct {
Items []marshalerFunc `toml:"items"`
}
out, err := Marshal(Cfg{Items: []marshalerFunc{
func() (any, error) { return map[string]any{"k": "a"}, nil },
func() (any, error) { return "x", nil },
}})
if err != nil {
t.Fatalf("marshal: %v", err)
}
want := "items = [{k = \"a\"}, \"x\"]\n"
if string(out) != want {
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
}
}
func TestMarshalerElementRunsOnce(t *testing.T) {
// Classification and emission share one result, so the method runs exactly
// once per element even when it decides the array's form.
calls := 0
type Cfg struct {
Items []countingMarshaler `toml:"items"`
}
out, err := Marshal(Cfg{Items: []countingMarshaler{{calls: &calls}, {calls: &calls}, {calls: &calls}}})
if err != nil {
t.Fatalf("marshal: %v", err)
}
if calls != 3 {
t.Errorf("MarshalTOML ran %d times, want 3", calls)
}
want := "[[items]]\nn = 1\n\n[[items]]\nn = 2\n\n[[items]]\nn = 3\n"
if string(out) != want {
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
}
}
func TestMarshalerPointerReceiverOnElement(t *testing.T) {
// A slice element is addressable, so a pointer-receiver MarshalTOML is
// found there, and the method's table keeps the [[header]] form.
type Cfg struct {
Items []ptrMarshaler `toml:"items"`
}
out, err := Marshal(&Cfg{Items: []ptrMarshaler{{V: "a"}, {V: "b"}}})
if err != nil {
t.Fatalf("marshal: %v", err)
}
want := "[[items]]\nv = \"a\"\n\n[[items]]\nv = \"b\"\n"
if string(out) != want {
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
}
// The same method is found on an addressable struct field, whose result is
// a table and so keeps a header section.
type Field struct {
F ptrMarshaler `toml:"f"`
}
out, err = Marshal(&Field{F: ptrMarshaler{V: "c"}})
if err != nil {
t.Fatalf("marshal: %v", err)
}
if want := "[f]\nv = \"c\"\n"; string(out) != want {
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
}
}
func TestMarshalerElementErrorCarriesPath(t *testing.T) {
type Cfg struct {
Items []any `toml:"items"`
}
_, err := Marshal(Cfg{Items: []any{map[string]any{"k": "a"}, failingMarshalerFunc{}}})
if err == nil {
t.Fatal("expected an error from MarshalTOML")
}
if !strings.Contains(err.Error(), "oops") {
t.Errorf("err = %v, want substring \"oops\"", err)
}
ee, ok := errors.AsType[*EncodeError](err)
if !ok {
t.Fatalf("expected an *EncodeError, got %T: %v", err, err)
}
if ee.Path != "items[1]" {
t.Fatalf("Path = %q, want %q", ee.Path, "items[1]")
}
}
func TestMarshalerResultIsNormalised(t *testing.T) {
// A result is normalised like any other value, so a method may return a
// plain int or a duration where the Go kind alone would not encode.
type Cfg struct {
Plain marshalerFunc `toml:"plain"`
Duration marshalerFunc `toml:"duration"`
Elements []any `toml:"elements"`
}
out, err := Marshal(Cfg{
Plain: func() (any, error) { return 7, nil },
Duration: func() (any, error) { return 90 * time.Minute, nil },
Elements: []any{marshalerFunc(func() (any, error) { return 8, nil })},
})
if err != nil {
t.Fatalf("marshal: %v", err)
}
want := "plain = 7\nduration = \"1h30m0s\"\nelements = [8]\n"
if string(out) != want {
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
}
}