feat: add OffsetDateTime, nesting limits and uniform Marshaler dispatch
Test / test (push) Successful in 2m18s
Test / test (push) Successful in 2m18s
Assisted-by: DeepSeek V4.1 Flash
This commit is contained in:
+146
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user