// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) // SPDX-License-Identifier: MIT package interpres import ( "bytes" "context" "errors" "math" "net" "reflect" "strings" "testing" "time" ) func TestMarshalScalars(t *testing.T) { type Cfg struct { Title string `toml:"title"` Count int `toml:"count"` Unsigned uint64 `toml:"unsigned"` Ratio float64 `toml:"ratio"` Enabled bool `toml:"enabled"` Disabled bool `toml:"disabled"` } out, err := Marshal(Cfg{ Title: "demo", Count: 42, Unsigned: 99, Ratio: 3.14, Enabled: true, Disabled: false, }) if err != nil { t.Fatalf("marshal: %v", err) } want := "title = \"demo\"\ncount = 42\nunsigned = 99\nratio = 3.14\nenabled = true\ndisabled = false\n" if string(out) != want { t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want) } } func TestMarshalFloatSpecials(t *testing.T) { type Cfg struct { PosInf float64 `toml:"pos_inf"` NegInf float64 `toml:"neg_inf"` NaN float64 `toml:"nan"` Zero float64 `toml:"zero"` IntVal float64 `toml:"int_val"` NegZ float64 `toml:"neg_zero"` } out, err := Marshal(Cfg{ PosInf: math.Inf(1), NegInf: math.Inf(-1), NaN: math.NaN(), Zero: 0, IntVal: 7, NegZ: math.Copysign(0, -1), }) if err != nil { t.Fatalf("marshal: %v", err) } want := "pos_inf = inf\nneg_inf = -inf\nnan = nan\nzero = 0.0\nint_val = 7.0\nneg_zero = 0.0\n" if string(out) != want { t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want) } } func TestMarshalFloatNormalizesNegativeZero(t *testing.T) { // The output contract normalises negative zero to "0.0". type Cfg struct { Z float64 `toml:"z"` } out, err := Marshal(Cfg{Z: math.Copysign(0, -1)}) if err != nil { t.Fatalf("marshal: %v", err) } want := "z = 0.0\n" if string(out) != want { t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want) } } func TestMarshalContextHonoursCancellation(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) cancel() type C struct { A int `toml:"a"` } if _, err := MarshalContext(ctx, C{A: 1}); !errors.Is(err, context.Canceled) { t.Fatalf("MarshalContext returned %v, want context.Canceled", err) } } func TestEncoderLayoutGroupedDefault(t *testing.T) { // NewEncoder must default to LayoutKindGrouped so legacy callers keep the // scalars-first ordering. type Cfg struct { Name string `toml:"name"` S struct { Host string `toml:"host"` } `toml:"s"` } out, err := Marshal(Cfg{Name: "x", S: struct { Host string `toml:"host"` }{Host: "h"}}) if err != nil { t.Fatalf("marshal: %v", err) } want := "name = \"x\"\n\n[s]\nhost = \"h\"\n" if string(out) != want { t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want) } } func TestEncoderLayoutDeclarationPreservesOrder(t *testing.T) { type Inner struct { Host string `toml:"host"` } type Cfg struct { Name string `toml:"name"` Server Inner `toml:"server"` Debug bool `toml:"debug"` } in := Cfg{ Name: "x", Server: Inner{Host: "h"}, Debug: true, } out, err := Marshal(in, Layout(LayoutKindDeclaration)) if err != nil { t.Fatalf("marshal: %v", err) } // With Layout(LayoutKindDeclaration) the encoder walks entries in declaration order. // The output is still parseable, but a scalar that follows a header is // parsed as a sub-table key. That is the user's trade-off; see // docs/API.md. want := "name = \"x\"\n\n[server]\nhost = \"h\"\ndebug = true\n" if string(out) != want { t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want) } } func TestEncoderLayoutGroupedDefaultOrder(t *testing.T) { // The default (LayoutKindGrouped) must lift the trailing scalar ahead of // the [server] block so the document round-trips losslessly. type Inner struct { Host string `toml:"host"` } type Cfg struct { Name string `toml:"name"` Server Inner `toml:"server"` Debug bool `toml:"debug"` } in := Cfg{ Name: "x", Server: Inner{Host: "h"}, Debug: true, } out, err := Marshal(in) if err != nil { t.Fatalf("marshal: %v", err) } want := "name = \"x\"\ndebug = true\n\n[server]\nhost = \"h\"\n" if string(out) != want { t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want) } } func TestEncoderOmitEmptyArrays(t *testing.T) { type Cfg struct { Tags []string `toml:"tags"` Secrets []string `toml:"secrets"` } out, err := Marshal(Cfg{ Tags: []string{"a", "b"}, Secrets: []string{}, }, OmitEmptyArrays(true)) if err != nil { t.Fatalf("marshal: %v", err) } want := "tags = [\"a\", \"b\"]\n" if string(out) != want { t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want) } } func TestEncoderDefaultEmitsEmptyArray(t *testing.T) { type Cfg struct { Tags []string `toml:"tags"` } out, err := Marshal(Cfg{Tags: []string{}}) if err != nil { t.Fatalf("marshal: %v", err) } want := "tags = []\n" if string(out) != want { t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want) } } func TestEncoderOmitEmptyArrayOfTablesStillSkipped(t *testing.T) { type Item struct { Name string `toml:"name"` } type Cfg struct { Title string `toml:"title"` Items []Item `toml:"items"` } out, err := Marshal(Cfg{ Title: "demo", Items: nil, }, OmitEmptyArrays(true)) if err != nil { t.Fatalf("marshal: %v", err) } want := "title = \"demo\"\n" if string(out) != want { t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want) } } func TestEncoderLiteralMultiline(t *testing.T) { type Cfg struct { Long string `toml:"long"` } long := strings.Repeat("a", 50) + "\nline two\nline three" out, err := Marshal(Cfg{Long: long}, LiteralMultiline(20)) if err != nil { t.Fatalf("marshal: %v", err) } want := "long = '''\n" + long + "\n'''\n" if string(out) != want { t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want) } } func TestEncoderLiteralMultilineBelowThreshold(t *testing.T) { // A multi-line value shorter than the threshold must remain escaped. type Cfg struct { Short string `toml:"short"` } out, err := Marshal(Cfg{Short: "one\ntwo"}, LiteralMultiline(1000)) if err != nil { t.Fatalf("marshal: %v", err) } want := "short = \"one\\ntwo\"\n" if string(out) != want { t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want) } } func TestEncoderLiteralMultilineThresholdZero(t *testing.T) { // LiteralMultiline(0) disables the literal form entirely. type Cfg struct { S string `toml:"s"` } out, err := Marshal(Cfg{S: "a\nb\nc\nd"}, LiteralMultiline(0)) if err != nil { t.Fatalf("marshal: %v", err) } if !bytes.HasPrefix(out, []byte("s = \"")) { t.Errorf("output mismatch, expected basic quoted form:\ngot: %q", out) } } func TestEncoderLiteralMultilineFallsBackWhenUnsafe(t *testing.T) { // The literal form carries the value verbatim, so content it cannot // represent must fall back to the escaped basic string instead of // producing output that does not re-parse. cases := []struct { name string in string }{ {"embedded delimiter", "before ''' after\nsecond line"}, {"control character", "a\x01b\nsecond"}, {"delete character", "a\x7fb\nsecond"}, {"lone carriage return", "first\rsecond\nthird"}, } for _, c := range cases { out, err := Marshal(map[string]any{"s": c.in}, LiteralMultiline(5)) if err != nil { t.Fatalf("%s: marshal: %v", c.name, err) } if !bytes.HasPrefix(out, []byte("s = \"")) { t.Errorf("%s: expected the basic quoted form, got:\n%s", c.name, out) } re, err := ParseMap(out) if err != nil { t.Errorf("%s: re-parse: %v\ndoc:\n%s", c.name, err, out) continue } if re["s"] != c.in { t.Errorf("%s: round-trip changed the value: %q", c.name, re["s"]) } } } // marshalerFunc adapts a plain function value to the Marshaler interface. // Tests use it to express "this field produces this TOML value" without a // dedicated struct definition. type marshalerFunc func() (any, error) func (f marshalerFunc) MarshalTOML() (any, error) { return f() } // failingMarshalerFunc invokes MarshalTOML to a fixed error; it lets us check // that a MarshalTOML failure propagates back to Marshal. type failingMarshalerFunc struct{} func (failingMarshalerFunc) MarshalTOML() (any, error) { return nil, errors.New("oops") } func TestMarshalerReturningTime(t *testing.T) { // A Marshaler may return a date-time scalar; the encoder must emit it // using its canonical form. when := time.Date(2026, 6, 26, 10, 0, 0, 0, time.UTC) type Cfg struct { M marshalerFunc `toml:"m"` } out, err := Marshal(Cfg{M: marshalerFunc(func() (any, error) { return when, nil })}) if err != nil { t.Fatalf("marshal: %v", err) } want := "m = 2026-06-26T10:00Z\n" if string(out) != want { t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want) } } func TestMarshalerReturningDifferentStruct(t *testing.T) { // A Marshaler returning a struct (not a scalar) is treated as a sub-table // by the encoder. type Inner struct { V string `toml:"v"` } type Cfg struct { P marshalerFunc `toml:"p"` } out, err := Marshal(Cfg{P: marshalerFunc(func() (any, error) { return Inner{V: "x"}, nil })}) if err != nil { t.Fatalf("marshal: %v", err) } want := "[p]\nv = \"x\"\n" if string(out) != want { t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want) } } func TestMarshalerReturningSliceOfMaps(t *testing.T) { // A Marshaler returning []map[string]any becomes an array of tables. type Cfg struct { Items marshalerFunc `toml:"items"` } out, err := Marshal(Cfg{Items: marshalerFunc(func() (any, error) { return []map[string]any{ {"k": "a"}, {"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 TestMarshalerErrorPropagates(t *testing.T) { type Cfg struct { F failingMarshalerFunc `toml:"f"` } if _, err := Marshal(Cfg{F: failingMarshalerFunc{}}); err == nil { t.Fatal("expected an error from MarshalTOML") } else if !strings.Contains(err.Error(), "oops") { t.Errorf("err = %v, want substring \"oops\"", err) } } // nilMarshalerFunc is a Marshaler whose method returns nil with no error. type nilMarshalerFunc struct{} func (nilMarshalerFunc) MarshalTOML() (any, error) { return nil, nil } func TestMarshalRejectsNilMarshalerResult(t *testing.T) { // nil has no TOML representation, so a MarshalTOML result of nil is an // error, not a silently dropped field. _, err := Marshal(struct { F nilMarshalerFunc `toml:"f"` }{}) if err == nil { t.Fatal("expected an error for a nil MarshalTOML result") } ee, ok := errors.AsType[*EncodeError](err) if !ok { t.Fatalf("expected an *EncodeError, got %T: %v", err, err) } if ee.Path.String() != "f" { t.Fatalf("Path = %v, want f", ee.Path) } // Inside a value array the nil result used to reach reflection as a zero // Value and panic. _, err = Marshal(map[string]any{"arr": []any{1, nilMarshalerFunc{}}}) if err == nil { t.Fatal("expected an error for a nil MarshalTOML result in an array") } if !strings.Contains(err.Error(), "MarshalTOML returned a nil value") { t.Errorf("err = %v, want the nil-result message", err) } } // Two fields that resolve to one TOML key must marshal as one key, resolved // the way the decoder resolves it, or the output would carry a duplicate key // and never re-parse. func TestMarshalDuplicateKeyResolvesToOneField(t *testing.T) { type SameLevel struct { First int `toml:"v"` Second string `toml:"v"` } out, err := Marshal(SameLevel{First: 1, Second: "s"}) if err != nil { t.Fatalf("marshal: %v", err) } if want := "v = \"s\"\n"; string(out) != want { t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want) } type Base struct { Name string `toml:"name"` } type Embedded struct { Base Name string `toml:"name"` } out, err = Marshal(Embedded{Base: Base{Name: "inner"}, Name: "outer"}) if err != nil { t.Fatalf("marshal: %v", err) } // The shallower field wins, matching the decoder. if want := "name = \"outer\"\n"; string(out) != want { t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want) } if _, err := ParseMap(out); err != nil { t.Errorf("re-parse: %v\ndoc:\n%s", err, out) } } func TestMarshalEmbeddedScalarStruct(t *testing.T) { // A field declared directly as a scalar-struct type (here LocalDateTime) // must be encoded as a TOML scalar at the parent level, not rendered as // a sub-table. ldt := LocalDateTime{Time: time.Date(2026, 6, 26, 0, 0, 0, 0, time.UTC)} type Cfg struct { Name string `toml:"name"` S LocalDateTime `toml:"s"` } out, err := Marshal(Cfg{Name: "x", S: ldt}) if err != nil { t.Fatalf("marshal: %v", err) } want := "name = \"x\"\ns = 2026-06-26T00:00\n" if string(out) != want { t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want) } } func TestEncoderChainedOptions(t *testing.T) { // All chainable options combined; verify they compose without errors. type Inner struct { V string `toml:"v"` } type Cfg struct { S string `toml:"s"` I Inner `toml:"i"` } long := strings.Repeat("x", 200) out, err := Marshal(Cfg{S: "short", I: Inner{V: long}}, Layout(LayoutKindDeclaration), OmitEmptyArrays(true), LiteralMultiline(50)) if err != nil { t.Fatalf("marshal: %v", err) } _ = out // success path is enough; per-option correctness is exercised above. } func TestMarshalStringEscapes(t *testing.T) { cases := []struct { name string in string want string // the TOML scalar value (without "s = " prefix) }{ {"plain", "hello", `"hello"`}, {"quote", `say "hi"`, `"say \"hi\""`}, {"backslash", `a\b`, `"a\\b"`}, {"newline", "line1\nline2", `"line1\nline2"`}, {"tab", "col1\tcol2", `"col1\tcol2"`}, {"cr", "line\rmore", `"line\rmore"`}, {"control", "a\x01b", `"a\u0001b"`}, {"unicode", "\u201csmart\u201d", `"“smart”"`}, // printable unicode; not escaped {"empty", "", `""`}, {"slash_only", "a/b", `"a/b"`}, } for _, c := range cases { out, err := Marshal(struct { S string `toml:"s"` }{S: c.in}) if err != nil { t.Fatalf("%s: marshal: %v", c.name, err) } got := strings.TrimSuffix(string(out), "\n") want := "s = " + c.want if got != want { t.Errorf("%s:\ngot: %s\nwant: %s", c.name, got, want) } } } func TestMarshalDateTime(t *testing.T) { type Cfg struct { Offset time.Time `toml:"offset"` Local LocalDateTime `toml:"local"` Day LocalDate `toml:"day"` Clock LocalTime `toml:"clock"` } out, err := Marshal(Cfg{ Offset: time.Date(2026, 6, 26, 10, 0, 0, 0, time.UTC), Local: LocalDateTime{Time: time.Date(2026, 6, 26, 7, 32, 0, 0, time.UTC)}, Day: LocalDate{Time: time.Date(2026, 6, 26, 0, 0, 0, 0, time.UTC)}, Clock: LocalTime{Time: time.Date(0, 1, 1, 7, 32, 0, 0, time.UTC)}, }) if err != nil { t.Fatalf("marshal: %v", err) } want := "offset = 2026-06-26T10:00Z\nlocal = 2026-06-26T07:32\nday = 2026-06-26\nclock = 07:32\n" if string(out) != want { t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want) } } func TestMarshalDateTimeFractional(t *testing.T) { out, err := Marshal(struct { LDT LocalDateTime `toml:"ldt"` LT LocalTime `toml:"lt"` }{ LDT: LocalDateTime{Time: time.Date(2026, 6, 26, 7, 32, 0, 123456789, time.UTC)}, LT: LocalTime{Time: time.Date(0, 1, 1, 7, 32, 0, 123, time.UTC)}, }) if err != nil { t.Fatalf("marshal: %v", err) } want := "ldt = 2026-06-26T07:32:00.123456789\nlt = 07:32:00.000000123\n" if string(out) != want { t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want) } } func TestMarshalArraysOfScalars(t *testing.T) { type Cfg struct { Tags []string `toml:"tags"` Ports []int `toml:"ports"` Mixed []any `toml:"mixed"` Empty []int `toml:"empty"` EmptyS []string `toml:"empty_s"` } out, err := Marshal(Cfg{ Tags: []string{"a", "b"}, Ports: []int{80, 443}, Mixed: []any{int64(1), "x", true}, Empty: nil, EmptyS: []string{}, }) if err != nil { t.Fatalf("marshal: %v", err) } want := "tags = [\"a\", \"b\"]\nports = [80, 443]\nmixed = [1, \"x\", true]\nempty_s = []\n" if string(out) != want { t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want) } } func TestMarshalNestedArrays(t *testing.T) { type Cfg struct { Matrix [][]int `toml:"matrix"` Words [][]string `toml:"words"` } out, err := Marshal(Cfg{ Matrix: [][]int{{1, 2}, {3, 4}}, Words: [][]string{{"a", "b"}, {"c"}}, }) if err != nil { t.Fatalf("marshal: %v", err) } want := "matrix = [[1, 2], [3, 4]]\nwords = [[\"a\", \"b\"], [\"c\"]]\n" if string(out) != want { t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want) } } func TestMarshalMixedArrayWithInlineTable(t *testing.T) { // Parse accepts a mixed array (TOML allows any value kinds in one array), // so Marshal of the parsed tree must re-emit it. The table element has no // header form inside a value array and renders inline. tree, err := ParseMap([]byte("arr = [1, {a = 2}, \"x\"]\n")) if err != nil { t.Fatalf("parse: %v", err) } out, err := Marshal(tree) if err != nil { t.Fatalf("marshal: %v", err) } want := "arr = [1, {a = 2}, \"x\"]\n" if string(out) != want { t.Fatalf("output mismatch:\ngot: %q\nwant: %q", out, want) } re, err := ParseMap(out) if err != nil { t.Fatalf("re-parse: %v", err) } if !reflect.DeepEqual(tree, re) { t.Fatalf("round-trip changed the tree:\nwas: %#v\nnow: %#v", tree, re) } } // A []any of tables is what Parse produces for a value array of inline // tables; it must stay in the value-array form, or the output would re-parse // as []map[string]any and the round-trip would change the value's type. func TestMarshalValueArrayOfTablesStaysInline(t *testing.T) { for _, doc := range []string{ "0=[{}]", "a = [{x = 1}, {x = 2}]\n", "b = [{x = 1}, 2, \"three\"]\n", } { tree, err := ParseMap([]byte(doc)) if err != nil { t.Fatalf("%s: parse: %v", doc, err) } out, err := Marshal(tree) if err != nil { t.Fatalf("%s: marshal: %v", doc, err) } if bytes.HasPrefix(out, []byte("[[")) { t.Errorf("%s: emitted the [[header]] form for a value array:\n%s", doc, out) } re, err := ParseMap(out) if err != nil { t.Fatalf("%s: re-parse: %v\ndoc:\n%s", doc, err, out) } if !tomlEqual(tree, re) { t.Errorf("%s: round-trip changed the tree:\nwas: %#v\nnow: %#v\ndoc:\n%s", doc, tree, re, out) } } } func TestMarshalNestedInlineTables(t *testing.T) { tree := map[string]any{ "mix": []any{ int64(1), map[string]any{"deep": map[string]any{"n": int64(0)}, "list": []any{"a", true}}, map[string]any{}, }, } out, err := Marshal(tree) if err != nil { t.Fatalf("marshal: %v", err) } want := "mix = [1, {deep = {n = 0}, list = [\"a\", true]}, {}]\n" if string(out) != want { t.Fatalf("output mismatch:\ngot: %q\nwant: %q", out, want) } } func TestMarshalInlineTableWithDatetime(t *testing.T) { when := time.Date(1979, 5, 27, 7, 32, 0, 0, time.UTC) tree := map[string]any{ "mix": []any{when, map[string]any{"t": LocalDateTime{when}}}, } out, err := Marshal(tree) if err != nil { t.Fatalf("marshal: %v", err) } want := "mix = [1979-05-27T07:32Z, {t = 1979-05-27T07:32}]\n" if string(out) != want { t.Fatalf("output mismatch:\ngot: %q\nwant: %q", out, want) } } func TestMarshalArrayOfTablesStaysHeaderForm(t *testing.T) { tree, err := ParseMap([]byte("[[items]]\nname = \"a\"\n\n[[items]]\nname = \"b\"\n")) if err != nil { t.Fatalf("parse: %v", err) } out, err := Marshal(tree) if err != nil { t.Fatalf("marshal: %v", err) } want := "[[items]]\nname = \"a\"\n\n[[items]]\nname = \"b\"\n" if string(out) != want { t.Fatalf("output mismatch:\ngot: %q\nwant: %q", out, want) } } func TestMarshalFloatExponentNoLeadingZero(t *testing.T) { // strconv.FormatFloat with 'g' would produce "1e+06" (leading zero in // exponent). The encoder must strip it so the output is "1e+6". type Cfg struct { Large float64 `toml:"large"` Small float64 `toml:"small"` } out, err := Marshal(Cfg{Large: 1e6, Small: 1e-5}) if err != nil { t.Fatalf("marshal: %v", err) } // Parse to check the output is valid TOML (for a strict parser that // rejects leading zeros in exponents). if _, err := ParseMap(out); err != nil { t.Fatalf("marshalled output is not valid TOML:\n%s\nerror: %v", out, err) } if string(out) != "large = 1e+6\nsmall = 1e-5\n" { t.Errorf("output mismatch:\ngot: %q", out) } } func TestMarshalStructAsTable(t *testing.T) { type Server struct { Host string `toml:"host"` Port int `toml:"port"` } type Cfg struct { Title string `toml:"title"` Server Server `toml:"server"` } out, err := Marshal(Cfg{ Title: "demo", Server: Server{Host: "127.0.0.1", Port: 9090}, }) if err != nil { t.Fatalf("marshal: %v", err) } want := "title = \"demo\"\n\n[server]\nhost = \"127.0.0.1\"\nport = 9090\n" if string(out) != want { t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want) } } func TestMarshalArrayOfTables(t *testing.T) { type Item struct { Name string `toml:"name"` Qty int `toml:"qty"` } type Cfg struct { Items []Item `toml:"items"` } out, err := Marshal(Cfg{ Items: []Item{ {Name: "a", Qty: 1}, {Name: "b", Qty: 2}, }, }) if err != nil { t.Fatalf("marshal: %v", err) } want := "[[items]]\nname = \"a\"\nqty = 1\n\n[[items]]\nname = \"b\"\nqty = 2\n" if string(out) != want { t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want) } } func TestMarshalEmptyArrayOfTablesIsSkipped(t *testing.T) { type Item struct { Name string `toml:"name"` } type Cfg struct { Title string `toml:"title"` Items []Item `toml:"items"` } out, err := Marshal(Cfg{ Title: "demo", Items: nil, }) if err != nil { t.Fatalf("marshal: %v", err) } want := "title = \"demo\"\n" if string(out) != want { t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want) } } func TestMarshalNestedTablesAndArrays(t *testing.T) { type SMTP struct { Host string `toml:"host"` Port int `toml:"port"` } type Form struct { Name string `toml:"name"` SMTP SMTP `toml:"smtp"` } type Cfg struct { Port int `toml:"port"` Forms []Form `toml:"forms"` } out, err := Marshal(Cfg{ Port: 8080, Forms: []Form{ {Name: "contact", SMTP: SMTP{Host: "h1", Port: 587}}, {Name: "feedback", SMTP: SMTP{Host: "h2", Port: 25}}, }, }) if err != nil { t.Fatalf("marshal: %v", err) } want := "port = 8080\n\n[[forms]]\nname = \"contact\"\n\n[forms.smtp]\nhost = \"h1\"\nport = 587\n\n[[forms]]\nname = \"feedback\"\n\n[forms.smtp]\nhost = \"h2\"\nport = 25\n" if string(out) != want { t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want) } } func TestMarshalStructTags(t *testing.T) { type Cfg struct { Keep string `toml:"keep"` Rename string `toml:"renamed"` Skip string `toml:"-"` Untagged string } out, err := Marshal(Cfg{ Keep: "k", Rename: "r", Skip: "s", Untagged: "u", }) if err != nil { t.Fatalf("marshal: %v", err) } want := "keep = \"k\"\nrenamed = \"r\"\nuntagged = \"u\"\n" if string(out) != want { t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want) } } func TestMarshalEmbeddedStructPromoted(t *testing.T) { type Base struct { ID int `toml:"id"` } type Derived struct { Base Name string `toml:"name"` } out, err := Marshal(Derived{ID: 1, Name: "x"}) if err != nil { t.Fatalf("marshal: %v", err) } want := "id = 1\nname = \"x\"\n" if string(out) != want { t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want) } } func TestMarshalEmbeddedStructAsTable(t *testing.T) { type Inner struct { Host string `toml:"host"` } type Cfg struct { Inner Inner `toml:"inner"` Name string `toml:"name"` } out, err := Marshal(Cfg{Inner: Inner{Host: "h"}, Name: "n"}) if err != nil { t.Fatalf("marshal: %v", err) } want := "name = \"n\"\n\n[inner]\nhost = \"h\"\n" if string(out) != want { t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want) } } func TestMarshalTagOptionOmitZero(t *testing.T) { type Server struct { Host string `toml:"host"` } type Cfg struct { Name string `toml:"name,omitzero"` Count int `toml:"count,omitzero"` Ratio float64 `toml:"ratio,omitzero"` When time.Time `toml:"when,omitzero"` Server Server `toml:"server,omitzero"` Always string `toml:"always"` } out, err := Marshal(Cfg{Always: "kept"}) if err != nil { t.Fatalf("marshal: %v", err) } // Every omitzero field sits at its zero value, so only always is emitted. want := "always = \"kept\"\n" if string(out) != want { t.Fatalf("output mismatch:\ngot: %q\nwant: %q", out, want) } when := time.Date(2026, 9, 17, 12, 0, 0, 0, time.UTC) out, err = Marshal(Cfg{Name: "x", Count: 1, Ratio: 0.5, When: when, Server: Server{Host: "h"}, Always: "kept"}) if err != nil { t.Fatalf("marshal: %v", err) } want = "name = \"x\"\ncount = 1\nratio = 0.5\nwhen = 2026-09-17T12:00Z\nalways = \"kept\"\n\n[server]\nhost = \"h\"\n" if string(out) != want { t.Fatalf("output mismatch:\ngot: %q\nwant: %q", out, want) } } func TestMarshalTagOptionOmitEmpty(t *testing.T) { type Cfg struct { Tags []string `toml:"tags,omitempty"` Ports []int `toml:"ports,omitempty"` Matrix [][]int `toml:"matrix,omitempty"` Extra map[string]any `toml:"extra,omitempty"` Name string `toml:"name,omitempty"` Keep []string `toml:"keep"` } out, err := Marshal(Cfg{ Ports: []int{}, Matrix: [][]int{{1}}, Extra: map[string]any{}, Name: "set", Keep: []string{}, }) if err != nil { t.Fatalf("marshal: %v", err) } // tags is nil (omitted anyway), ports and extra are empty collections // dropped by omitempty, matrix is populated, name is a string the option // does not cover, keep is empty but carries no option so it emits []. want := "matrix = [[1]]\nname = \"set\"\nkeep = []\n" if string(out) != want { t.Fatalf("output mismatch:\ngot: %q\nwant: %q", out, want) } } func TestMarshalTagOptionOnTaggedEmbeddedStruct(t *testing.T) { type Inner struct { N int `toml:"n"` } type Cfg struct { Inner Inner `toml:"inner,omitzero"` Name string `toml:"name"` } out, err := Marshal(Cfg{Name: "x"}) if err != nil { t.Fatalf("marshal: %v", err) } want := "name = \"x\"\n" if string(out) != want { t.Fatalf("output mismatch:\ngot: %q\nwant: %q", out, want) } } func TestMarshalMapKeysSorted(t *testing.T) { m := map[string]any{ "zeta": 1, "alpha": 2, "mu": 3, } out, err := Marshal(m) if err != nil { t.Fatalf("marshal: %v", err) } want := "alpha = 2\nmu = 3\nzeta = 1\n" if string(out) != want { t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want) } } func TestMarshalMapWithSubMap(t *testing.T) { m := map[string]any{ "meta": map[string]any{"x": 1, "y": 2}, "a": "z", } out, err := Marshal(m) if err != nil { t.Fatalf("marshal: %v", err) } want := "a = \"z\"\n\n[meta]\nx = 1\ny = 2\n" if string(out) != want { t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want) } } func TestMarshalMarshaler(t *testing.T) { type Port int type Cfg struct { P Port `toml:"p"` } out, err := Marshal(Cfg{P: 8080}) if err != nil { t.Fatalf("marshal: %v", err) } want := "p = 8080\n" if string(out) != want { t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want) } } func TestMarshalMarshalerReturningScalar(t *testing.T) { type Wrapped struct { Value string `toml:"value"` } type Alias struct{} out, err := Marshal(struct { W Wrapped `toml:"w"` }{W: Wrapped{Value: "hello"}}) if err != nil { t.Fatalf("marshal: %v", err) } want := "[w]\nvalue = \"hello\"\n" if string(out) != want { t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want) } _ = Alias{} } func TestMarshalMarshalerReturningDifferentShape(t *testing.T) { out, err := Marshal(struct { C Custom `toml:"c"` }{C: Custom{tag: "x"}}) if err != nil { t.Fatalf("marshal: %v", err) } // Custom returns a string from MarshalTOML. want := "c = \"x\"\n" if string(out) != want { t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want) } } func TestMarshalNilPointerFieldSkipped(t *testing.T) { type Cfg struct { Name string `toml:"name"` Hidden *string `toml:"hidden"` } out, err := Marshal(Cfg{Name: "x"}) if err != nil { t.Fatalf("marshal: %v", err) } want := "name = \"x\"\n" if string(out) != want { t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want) } } func TestMarshalNonNilPointerFollowed(t *testing.T) { v := "v" type Cfg struct { Name string `toml:"name"` Hidden *string `toml:"hidden"` } out, err := Marshal(Cfg{Name: "n", Hidden: &v}) if err != nil { t.Fatalf("marshal: %v", err) } want := "name = \"n\"\nhidden = \"v\"\n" if string(out) != want { t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want) } } func TestMarshalTopLevelMustBeStructOrMap(t *testing.T) { if _, err := Marshal(42); err == nil { t.Errorf("expected error marshalling int at top level") } if _, err := Marshal("hello"); err == nil { t.Errorf("expected error marshalling string at top level") } if _, err := Marshal(nil); err == nil { t.Errorf("expected error marshalling nil") } } func TestMarshalMapKeyMustBeString(t *testing.T) { m := map[int]any{1: "x"} if _, err := Marshal(m); err == nil { t.Errorf("expected error for non-string map key") } } func TestMarshalUnexportedFieldSkipped(t *testing.T) { type Cfg struct { Pub string `toml:"pub"` priv string } out, err := Marshal(Cfg{Pub: "p", priv: "s"}) if err != nil { t.Fatalf("marshal: %v", err) } want := "pub = \"p\"\n" if string(out) != want { t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want) } } func TestMarshalBareAndQuotedKeys(t *testing.T) { type Cfg struct { Bare string `toml:"bare_key"` Dash string `toml:"with-dash"` Num string `toml:"num123"` Q string `toml:"needs space"` Dot string `toml:"needs.dot"` } out, err := Marshal(Cfg{ Bare: "a", Dash: "b", Num: "c", Q: "d", Dot: "e", }) if err != nil { t.Fatalf("marshal: %v", err) } want := "bare_key = \"a\"\nwith-dash = \"b\"\nnum123 = \"c\"\n\"needs space\" = \"d\"\n\"needs.dot\" = \"e\"\n" if string(out) != want { t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want) } } func TestMarshalThenParseRoundTrip(t *testing.T) { type Server struct { Host string `toml:"host"` Port int `toml:"port"` Enabled bool `toml:"enabled"` Tags []string `toml:"tags"` } type Form struct { Name string `toml:"name"` Allowed []string `toml:"allowed"` } type Cfg struct { Title string `toml:"title"` Count int `toml:"count"` Ratio float64 `toml:"ratio"` Server Server `toml:"server"` Forms []Form `toml:"forms"` Due time.Time `toml:"due"` Day LocalDate `toml:"day"` } in := Cfg{ Title: "demo", Count: 42, Ratio: 3.14, Server: Server{ Host: "127.0.0.1", Port: 9090, Enabled: true, Tags: []string{"a", "b"}, }, Forms: []Form{ {Name: "contact", Allowed: []string{"x"}}, {Name: "feedback", Allowed: nil}, }, Due: time.Date(2026, 6, 26, 10, 0, 0, 0, time.UTC), Day: LocalDate{Time: time.Date(2026, 6, 26, 0, 0, 0, 0, time.UTC)}, } out, err := Marshal(in) if err != nil { t.Fatalf("marshal: %v", err) } tree1, err := ParseMap(out) if err != nil { t.Fatalf("parse of marshalled: %v\noutput:\n%s", err, out) } // Decode back into the struct. var out2 Cfg if err := Unmarshal(out, &out2); err != nil { t.Fatalf("unmarshal of marshalled: %v", err) } if !reflect.DeepEqual(in, out2) { t.Errorf("round-trip mismatch:\nin: %#v\nout: %#v", in, out2) } _ = tree1 } func TestMarshalRoundTripFromUntypedTree(t *testing.T) { src := []byte(`title = "demo" count = 42 ratio = 3.14 enabled = true [server] host = "127.0.0.1" port = 9090 [[items]] name = "a" qty = 1 [[items]] name = "b" qty = 2 [meta] created = 2026-06-26T10:00:00Z mixed = [1, {n = 1, name = "a value long enough to push this line well past the one hundred column limit"}] `) tree1, err := ParseMap(src) if err != nil { t.Fatalf("parse src: %v", err) } out, err := Marshal(tree1) if err != nil { t.Fatalf("marshal: %v", err) } tree2, err := ParseMap(out) if err != nil { t.Fatalf("re-parse marshalled: %v\noutput:\n%s", err, out) } if !reflect.DeepEqual(tree1, tree2) { t.Errorf("round-trip mismatch:\nbefore: %#v\nafter: %#v", tree1, tree2) } } func TestMarshalUintOverflow(t *testing.T) { type Cfg struct { Big uint64 `toml:"big"` } if _, err := Marshal(Cfg{Big: 1<<63 + 1}); err == nil { t.Errorf("expected overflow error") } } func TestMarshalKeyRequiresUTF8(t *testing.T) { m := map[string]any{"\xff": "x"} if _, err := Marshal(m); err == nil { t.Errorf("expected error for invalid UTF-8 key") } // The check must reach the keys of table headers and of inline tables // nested inside value arrays, not only scalar keys: both write keys // through the same path. nested := map[string]any{"\xff": map[string]any{"k": "v"}} if _, err := Marshal(nested); err == nil { t.Errorf("expected error for invalid UTF-8 table header key") } inline := map[string]any{"mix": []any{1, map[string]any{"\xff": 1}}} if _, err := Marshal(inline); err == nil { t.Errorf("expected error for invalid UTF-8 inline table key") } } func TestMarshalStringRequiresUTF8(t *testing.T) { type Cfg struct { S string `toml:"s"` } if _, err := Marshal(Cfg{S: "abc\xff"}); err == nil { t.Errorf("expected error for invalid UTF-8 string") } } func TestLocalDateString(t *testing.T) { ld := LocalDate{Time: time.Date(1979, 5, 27, 0, 0, 0, 0, time.UTC)} if got := ld.String(); got != "1979-05-27" { t.Errorf("LocalDate.String() = %q, want 1979-05-27", got) } } func TestLocalDateTimeString(t *testing.T) { // The rendering drops zero seconds and the trailing zeros of a fraction, // which TOML 1.1 allows and which keeps a value written without seconds // written without them. ldt := LocalDateTime{Time: time.Date(1979, 5, 27, 7, 32, 0, 0, time.UTC)} if got := ldt.String(); got != "1979-05-27T07:32" { t.Errorf("LocalDateTime.String() = %q, want 1979-05-27T07:32", got) } ldt2 := LocalDateTime{Time: time.Date(1979, 5, 27, 7, 32, 0, 5, time.UTC)} if got := ldt2.String(); got != "1979-05-27T07:32:00.000000005" { t.Errorf("LocalDateTime.String() = %q, want 1979-05-27T07:32:00.000000005", got) } ldt3 := LocalDateTime{Time: time.Date(1979, 5, 27, 7, 32, 0, 500, time.UTC)} if got := ldt3.String(); got != "1979-05-27T07:32:00.0000005" { t.Errorf("LocalDateTime.String() = %q, want 1979-05-27T07:32:00.0000005", got) } ldt4 := LocalDateTime{Time: time.Date(1979, 5, 27, 7, 32, 30, 500000000, time.UTC)} if got := ldt4.String(); got != "1979-05-27T07:32:30.5" { t.Errorf("LocalDateTime.String() = %q, want 1979-05-27T07:32:30.5", got) } } func TestLocalTimeString(t *testing.T) { lt := LocalTime{Time: time.Date(0, 1, 1, 7, 32, 0, 0, time.UTC)} if got := lt.String(); got != "07:32" { t.Errorf("LocalTime.String() = %q, want 07:32", got) } lt2 := LocalTime{Time: time.Date(0, 1, 1, 7, 32, 15, 250000000, time.UTC)} if got := lt2.String(); got != "07:32:15.25" { t.Errorf("LocalTime.String() = %q, want 07:32:15.25", got) } } func TestEncoderEquivalenceToMarshal(t *testing.T) { type Cfg struct { Title string `toml:"title"` Count int `toml:"count"` } in := Cfg{Title: "x", Count: 7} a, err := Marshal(in) if err != nil { t.Fatalf("marshal: %v", err) } b, err := Marshal(in) if err != nil { t.Fatalf("encoder marshal: %v", err) } if !reflect.DeepEqual(a, b) { t.Errorf("Marshal and Encoder disagree:\n%s\n%s", a, b) } } type Custom struct { tag string } func (c Custom) MarshalTOML() (any, error) { return c.tag, nil } // encodeErrBad is a Marshaler whose MarshalTOML always fails. type encodeErrBad struct { msg string } func (encodeErrBad) MarshalTOML() (any, error) { return nil, errors.New("bad timestamp") } func TestEncodeErrorCarriesPath(t *testing.T) { type Inner struct { Port encodeErrBad `toml:"port"` } type Cfg struct { Server Inner `toml:"server"` } _, err := Marshal(Cfg{Server: Inner{Port: encodeErrBad{}}}) if err == nil { t.Fatal("expected a marshal error") } ee, ok := errors.AsType[*EncodeError](err) if !ok { t.Fatalf("expected an *EncodeError, got %T: %v", err, err) } if ee.Path.String() != "server.port" { t.Fatalf("Path = %v, want server.port", ee.Path) } if ee.Err == nil || ee.Err.Error() != "bad timestamp" { t.Fatalf("Err = %v", ee.Err) } if err.Error() != "interpres: server.port: bad timestamp" { t.Fatalf("message = %q", err.Error()) } } func TestEncodeErrorTopLevelPathHasNoLeadingDot(t *testing.T) { type Cfg struct { Port encodeErrBad `toml:"port"` } _, err := Marshal(Cfg{}) ee, ok := errors.AsType[*EncodeError](err) if !ok { t.Fatalf("expected an *EncodeError, got %T: %v", err, err) } if ee.Path.String() != "port" { t.Fatalf("Path = %v, want port", ee.Path) } if err.Error() != "interpres: port: bad timestamp" { t.Fatalf("message = %q", err.Error()) } } func TestEncodeErrorHeterogeneousArrayPath(t *testing.T) { type Item struct { N int `toml:"n"` } cfg := map[string]any{ "items": []any{Item{}, 3}, } _, err := Marshal(cfg) if err == nil { t.Fatal("expected a heterogeneous array error") } ee, ok := errors.AsType[*EncodeError](err) if !ok { t.Fatalf("expected an *EncodeError, got %T: %v", err, err) } if ee.Path.String() != "items[0]" { t.Fatalf("Path = %v, want items[0]", ee.Path) } } // --- encoding.TextMarshaler and time.Duration ------------------------------ // textTag is a value-receiver encoding.TextMarshaler, so the encoder finds the // method on the value itself. type textTag string func (t textTag) MarshalText() ([]byte, error) { return []byte("tag:" + string(t)), nil } // textPointer carries MarshalText on the pointer receiver only, so the encoder // has to look at the address of an addressable field. type textPointer struct{ V string } func (t *textPointer) MarshalText() ([]byte, error) { return []byte(strings.ToUpper(t.V)), nil } // textAndTOML implements both encoding interfaces; the TOML method wins. type textAndTOML struct{} func (textAndTOML) MarshalTOML() (any, error) { return "toml", nil } func (textAndTOML) MarshalText() ([]byte, error) { return []byte("text"), nil } // brokenText fails the marshal from MarshalText. type brokenText struct{} func (brokenText) MarshalText() ([]byte, error) { return nil, errors.New("text boom") } // notUTF8 renders bytes that no TOML string can carry. type notUTF8 struct{} func (notUTF8) MarshalText() ([]byte, error) { return []byte{0xff, 0xfe}, nil } // textTagBoth renders itself with a prefix and strips it again on decode, so // the round trip through a TOML string is lossless. type textTagBoth string func (t textTagBoth) MarshalText() ([]byte, error) { return []byte("tag:" + string(t)), nil } func (t *textTagBoth) UnmarshalText(text []byte) error { trimmed, ok := strings.CutPrefix(string(text), "tag:") if !ok { return errors.New("textTagBoth: missing the tag prefix") } *t = textTagBoth(trimmed) return nil } func TestMarshalTextValues(t *testing.T) { // The pointer receiver is reachable only through an addressable field, so // the whole value is marshalled through a pointer here. type Cfg struct { IP net.IP `toml:"ip"` Duration time.Duration `toml:"duration"` Tag textTag `toml:"tag"` Pointer textPointer `toml:"pointer"` Both textAndTOML `toml:"both"` } out, err := Marshal(&Cfg{ IP: net.IPv4(192, 0, 2, 1), Duration: 90 * time.Minute, Tag: "x", Pointer: textPointer{V: "abc"}, }) if err != nil { t.Fatalf("marshal: %v", err) } want := "ip = \"192.0.2.1\"\nduration = \"1h30m0s\"\ntag = \"tag:x\"\npointer = \"ABC\"\nboth = \"toml\"\n" if string(out) != want { t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want) } } func TestMarshalTextValuesInContainers(t *testing.T) { // Slice elements are addressable, so a pointer-receiver MarshalText is used // there too, and an array of such values stays a value array: each element's // TOML form is a string, so the [[header]] form cannot carry it. type Cfg struct { Map map[string]net.IP `toml:"map"` Durs []time.Duration `toml:"durs"` Ptrs []textPointer `toml:"ptrs"` Empty []textPointer `toml:"empty"` } out, err := Marshal(Cfg{ Map: map[string]net.IP{"a": net.IPv4(10, 0, 0, 1)}, Durs: []time.Duration{0, 250 * time.Millisecond}, Ptrs: []textPointer{{V: "a"}, {V: "b"}}, Empty: []textPointer{}, }) if err != nil { t.Fatalf("marshal: %v", err) } want := "durs = [\"0s\", \"250ms\"]\nptrs = [\"A\", \"B\"]\nempty = []\n\n[map]\na = \"10.0.0.1\"\n" if string(out) != want { t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want) } } func TestMarshalTextLeavesDateTimesAlone(t *testing.T) { // The four date-time types carry time.Time's text methods through an // embedded field; their TOML form is a bare atom, never a quoted string. stamp := time.Date(2026, 6, 26, 10, 0, 0, 0, time.UTC) type Cfg struct { Stamp time.Time `toml:"stamp"` Ptr *time.Time `toml:"ptr"` Day LocalDate `toml:"day"` At LocalDateTime `toml:"at"` Clock LocalTime `toml:"clock"` } out, err := Marshal(&Cfg{ Stamp: stamp, Ptr: &stamp, Day: LocalDate{time.Date(1979, 5, 27, 0, 0, 0, 0, time.UTC)}, At: LocalDateTime{time.Date(1979, 5, 27, 7, 32, 0, 0, time.UTC)}, Clock: LocalTime{time.Date(0, 1, 1, 7, 32, 0, 0, time.UTC)}, }) if err != nil { t.Fatalf("marshal: %v", err) } want := "stamp = 2026-06-26T10:00Z\nptr = 2026-06-26T10:00Z\nday = 1979-05-27\nat = 1979-05-27T07:32\nclock = 07:32\n" if string(out) != want { t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want) } } func TestMarshalTextNilPointerOmitted(t *testing.T) { type Cfg struct { P *textPointer `toml:"p"` K string `toml:"k"` } out, err := Marshal(&Cfg{K: "x"}) if err != nil { t.Fatalf("marshal: %v", err) } if want := "k = \"x\"\n"; string(out) != want { t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want) } } func TestMarshalTextErrorCarriesPath(t *testing.T) { type Inner struct { F brokenText `toml:"f"` } type Cfg struct { Inner Inner `toml:"inner"` } _, err := Marshal(Cfg{}) if err == nil { t.Fatal("expected an error from MarshalText") } if !strings.Contains(err.Error(), "text boom") { t.Errorf("err = %v, want substring \"text boom\"", err) } ee, ok := errors.AsType[*EncodeError](err) if !ok { t.Fatalf("expected an *EncodeError, got %T: %v", err, err) } if ee.Path.String() != "inner.f" { t.Fatalf("Path = %v, want inner.f", ee.Path) } } func TestMarshalTextRejectsInvalidUTF8(t *testing.T) { // A TOML string holds UTF-8 only, so text that is not gets an error rather // than replacement characters. _, err := Marshal(struct { V notUTF8 `toml:"v"` }{}) if err == nil { t.Fatal("expected an error for text that is not valid UTF-8") } if !strings.Contains(err.Error(), "UTF-8") { t.Errorf("err = %v, want a UTF-8 message", err) } } func TestMarshalTextValuesRoundTrip(t *testing.T) { type Cfg struct { Duration time.Duration `toml:"duration"` IP net.IP `toml:"ip"` Tag textTagBoth `toml:"tag"` } in := Cfg{Duration: 90 * time.Minute, IP: net.IPv4(198, 51, 100, 7), Tag: "y"} out, err := Marshal(&in) if err != nil { t.Fatalf("marshal: %v", err) } var back Cfg if err := Unmarshal(out, &back); err != nil { t.Fatalf("unmarshal: %v", err) } if back.Duration != in.Duration { t.Errorf("Duration = %v, want %v", back.Duration, in.Duration) } if !back.IP.Equal(in.IP) { t.Errorf("IP = %v, want %v", back.IP, in.IP) } if back.Tag != in.Tag { t.Errorf("Tag = %q, want %q", back.Tag, in.Tag) } } // --- TOML 1.1 output forms ------------------------------------------------- func TestMarshalDateTimeRendering(t *testing.T) { // The seconds are written only when the value carries them, and a fraction // drops its trailing zeros. Both are the same value either way; the shorter // form is the one TOML 1.1 allows. base := time.Date(2026, 6, 26, 10, 0, 0, 0, time.UTC) cases := []struct { name string val any 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"}, {"local-zero-seconds", LocalDateTime{Time: base}, "v = 2026-06-26T10:00\n"}, {"local-fraction", LocalDateTime{Time: base.Add(2500 * time.Millisecond)}, "v = 2026-06-26T10:00:02.5\n"}, {"date", LocalDate{Time: base}, "v = 2026-06-26\n"}, {"time-zero-seconds", LocalTime{Time: base}, "v = 10:00\n"}, {"time-seconds", LocalTime{Time: base.Add(15 * time.Second)}, "v = 10:00:15\n"}, {"time-nanoseconds", LocalTime{Time: base.Add(123456789 * time.Nanosecond)}, "v = 10:00:00.123456789\n"}, } for _, c := range cases { out, err := Marshal(map[string]any{"v": c.val}) if err != nil { t.Errorf("%s: marshal: %v", c.name, err) continue } if string(out) != c.want { t.Errorf("%s: output mismatch:\ngot: %q\nwant: %q", c.name, out, c.want) } } } func TestMarshalInlineTableBreaksWhenLong(t *testing.T) { // A table element of a value array is written inline; a long one carries // newlines and a trailing comma instead of running past the line limit, // which TOML 1.1 allows an inline table to do. const long = "a-very-long-value-that-pushes-the-line-well-past-the-one-hundred-column-limit" type Cfg struct { Arr []any `toml:"arr"` } out, err := Marshal(Cfg{Arr: []any{int64(1), map[string]any{"n": int64(1), "name": long}}}) if err != nil { t.Fatalf("marshal: %v", err) } want := "arr = [1, {\n\tn = 1,\n\tname = \"" + long + "\",\n}]\n" if string(out) != want { t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want) } // The same values without the long string stay on one line. out, err = Marshal(Cfg{Arr: []any{int64(1), map[string]any{"n": int64(1), "name": "short"}}}) if err != nil { t.Fatalf("marshal: %v", err) } if want := "arr = [1, {n = 1, name = \"short\"}]\n"; string(out) != want { t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want) } // The broken form parses back to the same tree. tree, err := ParseMap(out) if err != nil { t.Fatalf("parse of the encoder output: %v", err) } if got := len(tree["arr"].([]any)); got != 2 { t.Fatalf("arr has %d elements, want 2", got) } } func TestMarshalNestedInlineTableBreaksIndependently(t *testing.T) { // A nested table breaks on its own measure, so a table whose entries stay // short keeps the one-line form inside a parent that broke. const long = "a-very-long-value-that-pushes-the-line-well-past-the-one-hundred-column-limit" type Cfg struct { Arr []any `toml:"arr"` } out, err := Marshal(Cfg{Arr: []any{int64(1), map[string]any{"n": int64(1), "sub": map[string]any{"name": long}}}}) if err != nil { t.Fatalf("marshal: %v", err) } want := "arr = [1, {\n\tn = 1,\n\tsub = {name = \"" + long + "\"},\n}]\n" if string(out) != want { t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want) } } type inlineTLS struct { On bool `toml:"on"` } type inlineServer struct { Host string `toml:"host"` Port int `toml:"port"` TLS inlineTLS `toml:"tls"` } type inlineBig struct { A int `toml:"a"` B int `toml:"b"` C int `toml:"c"` } func TestEncoderInlineTables(t *testing.T) { type Cfg struct { Server inlineServer `toml:"server"` Big inlineBig `toml:"big"` } cfg := Cfg{Server: inlineServer{Host: "127.0.0.1", Port: 9090}, Big: inlineBig{A: 1, B: 2, C: 3}} // The default keeps every sub-table a header section. headerForm, err := Marshal(cfg) if err != nil { t.Fatalf("marshal: %v", err) } want := "[server]\nhost = \"127.0.0.1\"\nport = 9090\n\n[server.tls]\non = false\n\n[big]\na = 1\nb = 2\nc = 3\n" if string(headerForm) != want { t.Errorf("default output mismatch:\ngot: %q\nwant: %q", headerForm, want) } // With the option both fit the threshold and become inline tables, nested // ones included. out, err := Marshal(cfg, InlineTables(60)) if err != nil { t.Fatalf("marshal: %v", err) } want = "server = {host = \"127.0.0.1\", port = 9090, tls = {on = false}}\nbig = {a = 1, b = 2, c = 3}\n" if string(out) != want { t.Errorf("compact output mismatch:\ngot: %q\nwant: %q", out, want) } // A threshold below the rendering keeps the header form. out, err = Marshal(cfg, InlineTables(10)) if err != nil { t.Fatalf("marshal: %v", err) } if string(out) != string(headerForm) { t.Errorf("small threshold output mismatch:\ngot: %q\nwant: %q", out, headerForm) } } func TestEncoderInlineTablesOrderAndRoundTrip(t *testing.T) { // An inlined sub-table is a value line, so it precedes every header of the // document; written after a header it would be read back as part of that // table. The compact form and the header form parse to the same tree. type Four struct { A int `toml:"a"` B int `toml:"b"` C int `toml:"c"` D int `toml:"d"` } type Cfg struct { Small inlineTLS `toml:"small"` Big Four `toml:"big"` } cfg := Cfg{Small: inlineTLS{On: true}, Big: Four{A: 1, B: 2, C: 3, D: 4}} headerForm, err := Marshal(cfg) if err != nil { t.Fatalf("marshal: %v", err) } compact, err := Marshal(cfg, InlineTables(20)) if err != nil { t.Fatalf("marshal: %v", err) } want := "small = {on = true}\n\n[big]\na = 1\nb = 2\nc = 3\nd = 4\n" if string(compact) != want { t.Errorf("output mismatch:\ngot: %q\nwant: %q", compact, want) } got, err := ParseMap(compact) if err != nil { t.Fatalf("parse of the compact output: %v", err) } ref, err := ParseMap(headerForm) if err != nil { t.Fatalf("parse of the header output: %v", err) } if !reflect.DeepEqual(got, ref) { t.Errorf("the compact form changed the tree:\ncompact: %#v\nheaders: %#v", got, ref) } if _, ok := got["big"].(map[string]any); !ok { t.Errorf("big = %#v, want a table", got["big"]) } } func TestEncoderInlineTablesKeepsArraysOfTables(t *testing.T) { // An array of tables has no inline form that keeps the value's type, so the // option leaves it alone and the tree keeps its []map[string]any shape. type Item struct { N int `toml:"n"` } type Cfg struct { Items []Item `toml:"items"` Small inlineTLS `toml:"small"` } cfg := Cfg{Items: []Item{{N: 1}}, Small: inlineTLS{On: true}} out, err := Marshal(cfg, InlineTables(60)) if err != nil { t.Fatalf("marshal: %v", err) } want := "small = {on = true}\n\n[[items]]\nn = 1\n" if string(out) != want { t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want) } tree, err := ParseMap(out) if err != nil { t.Fatalf("parse: %v", err) } if _, ok := tree["items"].([]map[string]any); !ok { 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.String() != "items[1]" { t.Fatalf("Path = %v, want items[1]", ee.Path) } } 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) } } func TestMarshalNumber(t *testing.T) { t.Run("the literal is written as it is", func(t *testing.T) { out, err := Marshal(map[string]any{ "hex": Number("0x1f"), "sep": Number("1_000"), "signed": Number("+1.0"), "inf": Number("inf"), }) if err != nil { t.Fatal(err) } want := "hex = 0x1f\ninf = inf\nsep = 1_000\nsigned = +1.0\n" if string(out) != want { t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want) } }) t.Run("a Number field round-trips", func(t *testing.T) { type Cfg struct { Rate Number `toml:"rate"` } out, err := Marshal(Cfg{Rate: "1_000"}) if err != nil { t.Fatal(err) } if string(out) != "rate = 1_000\n" { t.Fatalf("output %q", out) } var back map[string]any if err := Unmarshal(out, &back, NumbersAsLiterals(true)); err != nil { t.Fatal(err) } if got, ok := back["rate"].(Number); !ok || got != "1_000" { t.Errorf("round trip = %#v, want Number(\"1_000\")", back["rate"]) } }) t.Run("a Number inside a value array", func(t *testing.T) { out, err := Marshal(map[string]any{"vals": []any{Number("0x1f"), "s", int64(2)}}) if err != nil { t.Fatal(err) } want := "vals = [0x1f, \"s\", 2]\n" if string(out) != want { t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want) } }) t.Run("an invalid literal is an error", func(t *testing.T) { for _, lit := range []Number{"01", "1__0", "abc", "1.2.3"} { if _, err := Marshal(map[string]any{"n": lit}); err == nil { t.Errorf("Number(%q) encoded without an error", lit) } } }) } func TestMarshalAppend(t *testing.T) { buf := []byte("preamble\n") out, err := MarshalAppend(buf, map[string]any{"a": int64(1)}) if err != nil { t.Fatal(err) } want := "preamble\na = 1\n" if string(out) != want { t.Errorf("output %q, want %q", out, want) } if &out[0] != &buf[0] { t.Log("append reallocated; capacity differed") } out2, err := MarshalAppend(out, map[string]any{"b": true}) if err != nil { t.Fatal(err) } if string(out2) != want+"b = true\n" { t.Errorf("second append %q", out2) } buf = []byte("keep\n") if out3, err := MarshalAppend(buf, Document{}); err == nil { t.Errorf("MarshalAppend with an unencodable value = %q, want an error", out3) } } func TestMarshalCyclicData(t *testing.T) { t.Run("a cyclic struct is an error, not a crash", func(t *testing.T) { type Node struct { Name string `toml:"name"` Next *Node `toml:"next"` } a := &Node{Name: "a"} b := &Node{Name: "b"} a.Next = b b.Next = a _, err := Marshal(a) if err == nil { t.Fatal("Marshal(cyclic) succeeded, want an error") } if !strings.Contains(err.Error(), "may be cyclic") { t.Errorf("err = %v, want it to name the cycle", err) } }) t.Run("a cyclic map is an error", func(t *testing.T) { m := map[string]any{} m["self"] = m if _, err := Marshal(m); err == nil { t.Fatal("Marshal(cyclic map) succeeded, want an error") } }) t.Run("a cyclic value array is an error", func(t *testing.T) { m := map[string]any{} m["items"] = []any{int64(1), m} if _, err := Marshal(map[string]any{"outer": m}); err == nil { t.Fatal("Marshal(cyclic array) succeeded, want an error") } }) t.Run("a deeply nested but finite value encodes", func(t *testing.T) { type Node struct { Next *Node `toml:"next"` } root := &Node{} cur := root for range 5000 { cur.Next = &Node{} cur = cur.Next } if _, err := Marshal(root); err != nil { t.Errorf("Marshal(deep) = %v, want nil", err) } }) } func TestUnmarshalOptionsShape(t *testing.T) { data := []byte("host = \"db\"\nextra = 1\n") type Config struct { Host string `toml:"host,required"` } t.Run("the zero value takes the defaults", func(t *testing.T) { var cfg struct { Host string `toml:"host"` Extra int `toml:"extra"` } if err := Unmarshal(data, &cfg); err != nil { t.Fatal(err) } if cfg.Host != "db" || cfg.Extra != 1 { t.Errorf("decoded %+v", cfg) } }) t.Run("strict and required work in one call", func(t *testing.T) { err := Unmarshal(data, &Config{}, RejectUnknownFields(true)) want := `interpres: unknown field "extra" for interpres.Config` if err == nil || err.Error() != want { t.Errorf("err = %v, want %q", err, want) } }) t.Run("UseNumber keeps the literal", func(t *testing.T) { var tree map[string]any in := []byte("n = 1_000\n") if err := Unmarshal(in, &tree, NumbersAsLiterals(true)); err != nil { t.Fatal(err) } if got, ok := tree["n"].(Number); !ok || got != "1_000" { t.Errorf("n = %#v, want Number(\"1_000\")", tree["n"]) } }) t.Run("the limits apply", func(t *testing.T) { var nested strings.Builder nested.WriteString("x = ") for range 20 { nested.WriteString("[") } nested.WriteString("1") for range 20 { nested.WriteString("]") } var tree map[string]any if err := Unmarshal([]byte(nested.String()), &tree, MaxNestingDepth(10)); err == nil { t.Error("a document over MaxDepth decoded, want an error") } if err := Unmarshal([]byte("a = 1\n"), &tree, MaxInputSize(2)); err == nil { t.Error("a document over MaxInputSize decoded, want an error") } }) } func TestInlineTag(t *testing.T) { type Inner struct { A int `toml:"a"` B int `toml:"b"` } t.Run("a struct field writes inline", func(t *testing.T) { type Cfg struct { Inner Inner `toml:"inner,inline"` } out, err := Marshal(Cfg{Inner: Inner{1, 2}}) if err != nil { t.Fatal(err) } if string(out) != "inner = {a = 1, b = 2}\n" { t.Errorf("output %q", out) } }) t.Run("a map field writes inline", func(t *testing.T) { type Cfg struct { Opts map[string]int `toml:"opts,inline"` } out, err := Marshal(Cfg{Opts: map[string]int{"x": 1}}) if err != nil { t.Fatal(err) } if string(out) != "opts = {x = 1}\n" { t.Errorf("output %q", out) } }) t.Run("a named embedded struct writes inline", func(t *testing.T) { type Cfg struct { Inner `toml:"inner,inline"` } out, err := Marshal(Cfg{Inner: Inner{1, 2}}) if err != nil { t.Fatal(err) } if string(out) != "inner = {a = 1, b = 2}\n" { t.Errorf("output %q", out) } }) t.Run("an inline field decodes back", func(t *testing.T) { type Cfg struct { Inner Inner `toml:"inner,inline"` } var cfg Cfg if err := Unmarshal([]byte("inner = {a = 3, b = 4}\n"), &cfg); err != nil { t.Fatal(err) } if cfg.Inner != (Inner{3, 4}) { t.Errorf("decoded %+v", cfg.Inner) } }) t.Run("a forced inline of an array of tables is an error", func(t *testing.T) { type Item struct { N int `toml:"n"` } type Cfg struct { Items []Item `toml:"items,inline"` } if _, err := Marshal(Cfg{Items: []Item{{1}}}); err == nil { t.Error("forced inline of an array of tables succeeded, want an error") } }) t.Run("without the tag the header form stands", func(t *testing.T) { type Cfg struct { Inner Inner `toml:"inner"` } out, err := Marshal(Cfg{Inner: Inner{1, 2}}) if err != nil { t.Fatal(err) } if string(out) != "[inner]\na = 1\nb = 2\n" { t.Errorf("output %q", out) } }) } func TestOmitEmptyJSONSemantics(t *testing.T) { type Cfg struct { Empty string `toml:"empty,omitempty"` Full string `toml:"full,omitempty"` Zero int `toml:"zero,omitempty"` One int `toml:"one,omitempty"` Off bool `toml:"off,omitempty"` On bool `toml:"on,omitempty"` Nil *string `toml:"nil,omitempty"` Set *string `toml:"set,omitempty"` Nothing map[string]string `toml:"nothing,omitempty"` Somethg map[string]string `toml:"somethg,omitempty"` } s := "x" out, err := Marshal(Cfg{ Full: "y", One: 1, On: true, Set: &s, Somethg: map[string]string{"k": "v"}, }) if err != nil { t.Fatal(err) } want := "full = \"y\"\none = 1\non = true\nset = \"x\"\n\n[somethg]\nk = \"v\"\n" if string(out) != want { t.Errorf("output:\n%q\nwant:\n%q", out, want) } } func TestEmitFieldComments(t *testing.T) { type Cfg struct { Host string `toml:"host,comment=The host to dial"` Port int `toml:"port,comment=The port to listen on.\nThe default is 8080."` User string `toml:"user"` } cfg := Cfg{Host: "db", Port: 5432, User: "admin"} t.Run("off by default", func(t *testing.T) { out, err := Marshal(cfg) if err != nil { t.Fatal(err) } want := "host = \"db\"\nport = 5432\nuser = \"admin\"\n" if string(out) != want { t.Errorf("output:\n%q", out) } }) t.Run("on, the comments print above their lines", func(t *testing.T) { out, err := Marshal(cfg, EmitFieldComments(true)) if err != nil { t.Fatal(err) } want := "# The host to dial\nhost = \"db\"\n" + "# The port to listen on.\n# The default is 8080.\nport = 5432\n" + "user = \"admin\"\n" if string(out) != want { t.Errorf("output:\n%q\nwant:\n%q", out, want) } var back Cfg if err := Unmarshal(out, &back); err != nil { t.Fatalf("the output does not re-parse: %v", err) } if back != cfg { t.Errorf("round trip = %+v", back) } }) t.Run("a table header carries its comment", func(t *testing.T) { type Inner struct { A int `toml:"a,comment=The a"` } type Nested struct { Inner Inner `toml:"inner,comment=The inner table"` } out, err := Marshal(Nested{Inner: Inner{1}}, EmitFieldComments(true)) if err != nil { t.Fatal(err) } want := "# The inner table\n[inner]\n# The a\na = 1\n" if string(out) != want { t.Errorf("output:\n%q\nwant:\n%q", out, want) } }) }