// Copyright (c) 2026 Petr BalvĂ­n (https://petrbalvin.org) // SPDX-License-Identifier: MIT package interpres import ( "reflect" "slices" "strings" "testing" ) // mustEntry returns the entry a table must have, and fails the test when it // does not. func mustEntry(t *testing.T, tbl *Table, key string) *Entry { t.Helper() e, ok := tbl.Get(key) if !ok { t.Fatalf("%q is missing from the table", key) } return e } func TestDocumentKeepsKeyOrder(t *testing.T) { doc, err := Parse([]byte(` b = 1 a = 2 inline = {y = 1, x = 2} [table] z = 3 m = 4 `)) if err != nil { t.Fatalf("parse: %v", err) } // The root's keys come back in written order, not sorted. if got := doc.Root().Keys(); !slices.Equal(got, []string{"b", "a", "inline", "table"}) { t.Errorf("root keys = %v, want [b a inline table]", got) } // So do the keys of an inline table, which the map shape loses. inline, ok := doc.Root().Get("inline") if !ok { t.Fatal("inline is missing from the root") } if !inline.Inline() { t.Error("inline is not marked inline") } if got := inline.Table().Keys(); !slices.Equal(got, []string{"y", "x"}) { t.Errorf("inline keys = %v, want [y x]", got) } // And the keys of a table written under a header, which is not inline. tbl, ok := doc.Root().Get("table") if !ok { t.Fatal("table is missing from the root") } if tbl.Inline() { t.Error("table is marked inline") } if got := tbl.Table().Keys(); !slices.Equal(got, []string{"z", "m"}) { t.Errorf("table keys = %v, want [z m]", got) } } func TestDocumentValuesAreTheTree(t *testing.T) { doc, err := Parse([]byte("n = 7\ns = \"x\"\n\n[t]\nk = true\n")) if err != nil { t.Fatalf("parse: %v", err) } if got := mustEntry(t, doc.Root(), "n").Value(); got != int64(7) { t.Errorf("n = %#v, want int64(7)", got) } tbl := mustEntry(t, doc.Root(), "t").Table() if got := mustEntry(t, tbl, "k").Value(); got != true { t.Errorf("t.k = %#v, want true", got) } // Map gives the tree ParseMap would have returned, the same values. tree := doc.Map() if tree["n"] != int64(7) || tree["s"] != "x" { t.Errorf("Map = %#v", tree) } if tree["t"].(map[string]any)["k"] != true { t.Errorf("Map[t] = %#v", tree["t"]) } if tbl.Values()["k"] != true { t.Errorf("t.Values() = %#v", tbl.Values()) } } func TestDocumentComments(t *testing.T) { doc, err := Parse([]byte(`# above b b = 1 # trailing b # above the table [table] # trailing table # above m m = 2 # footer `)) if err != nil { t.Fatalf("parse: %v", err) } b, ok := doc.Root().Get("b") if !ok { t.Fatal("b is missing") } if got := b.Comments(); !slices.Equal(got, []string{"above b"}) { t.Errorf("b comments = %q, want [above b]", got) } if got := b.Trailing(); got != "trailing b" { t.Errorf("b trailing = %q, want \"trailing b\"", got) } tbl, ok := doc.Root().Get("table") if !ok { t.Fatal("table is missing") } // A [header] line introduces the table, so the comments around it belong // to the table node; the entry that names it stays bare. if got := tbl.Table().Comments(); !slices.Equal(got, []string{"above the table"}) { t.Errorf("table comments = %q, want [above the table]", got) } if got := tbl.Table().Trailing(); got != "trailing table" { t.Errorf("table trailing = %q, want \"trailing table\"", got) } if got := tbl.Comments(); got != nil { t.Errorf("entry comments = %q, want none", got) } m, ok := tbl.Table().Get("m") if !ok { t.Fatal("table.m is missing") } if got := m.Comments(); !slices.Equal(got, []string{"above m"}) { t.Errorf("m comments = %q, want [above m]", got) } if got := doc.Footer(); !slices.Equal(got, []string{"footer"}) { t.Errorf("footer = %q, want [footer]", got) } } func TestDocumentCommentsAreWritable(t *testing.T) { doc, err := Parse([]byte("# above\nk = 1\n")) if err != nil { t.Fatalf("parse: %v", err) } entry, ok := doc.Root().Get("k") if !ok { t.Fatal("k is missing") } entry.SetComments([]string{"first", "second"}) entry.SetTrailing("beside") if got := entry.Comments(); !slices.Equal(got, []string{"first", "second"}) { t.Errorf("comments = %q", got) } if got := entry.Trailing(); got != "beside" { t.Errorf("trailing = %q", got) } tbl := doc.Root() tbl.SetComments([]string{"above the root"}) if got := tbl.Comments(); !slices.Equal(got, []string{"above the root"}) { t.Errorf("root comments = %q", got) } doc.SetFooter([]string{"end"}) if got := doc.Footer(); !slices.Equal(got, []string{"end"}) { t.Errorf("footer = %q", got) } } func TestDocumentArrayOfTables(t *testing.T) { doc, err := Parse([]byte(`# first element [[item]] a = 1 [[item]] b = 2 # beside b `)) if err != nil { t.Fatalf("parse: %v", err) } entry, ok := doc.Root().Get("item") if !ok { t.Fatal("item is missing") } elems := entry.Elements() if len(elems) != 2 { t.Fatalf("elements = %d, want 2", len(elems)) } if got := elems[0].Keys(); !slices.Equal(got, []string{"a"}) { t.Errorf("first element keys = %v, want [a]", got) } if got := elems[0].Comments(); !slices.Equal(got, []string{"first element"}) { t.Errorf("first element comments = %q", got) } if got := elems[1].Keys(); !slices.Equal(got, []string{"b"}) { t.Errorf("second element keys = %v, want [b]", got) } if got := mustEntry(t, elems[1], "b").Trailing(); got != "beside b" { t.Errorf("b trailing = %q, want \"beside b\"", got) } // The value keeps the map shape the decoder reads. if _, ok := entry.Value().([]map[string]any); !ok { t.Errorf("item value = %#v, want []map[string]any", entry.Value()) } } func TestDocumentDottedKeysAndValueArrays(t *testing.T) { doc, err := Parse([]byte("a.b.c = 1\narr = [1, {x = 1}]\n")) if err != nil { t.Fatalf("parse: %v", err) } // A dotted key builds tables, and they are not inline ones. a, ok := doc.Root().Get("a") if !ok { t.Fatal("a is missing") } if a.Inline() { t.Error("a is marked inline") } b, ok := a.Table().Get("b") if !ok { t.Fatal("a.b is missing") } if b.Inline() { t.Error("a.b is marked inline") } if got := b.Table().Keys(); !slices.Equal(got, []string{"c"}) { t.Errorf("a.b keys = %v, want [c]", got) } // An inline table inside a value array keeps its node in the elements // slice; the scalar before it has none. arr, ok := doc.Root().Get("arr") if !ok { t.Fatal("arr is missing") } elems := arr.Elements() if len(elems) != 2 { t.Fatalf("elements = %d, want 2", len(elems)) } if elems[0] != nil { t.Errorf("elements[0] = %#v, want nil for a scalar", elems[0]) } if got := elems[1].Keys(); !slices.Equal(got, []string{"x"}) { t.Errorf("elements[1] keys = %v, want [x]", got) } } func TestDocumentWithoutStatements(t *testing.T) { doc, err := Parse([]byte("# only a comment\n")) if err != nil { t.Fatalf("parse: %v", err) } if got := doc.Root().Keys(); len(got) != 0 { t.Errorf("keys = %v, want none", got) } if got := doc.Footer(); !slices.Equal(got, []string{"only a comment"}) { t.Errorf("footer = %q, want [only a comment]", got) } empty, err := Parse(nil) if err != nil { t.Fatalf("parse of nothing: %v", err) } if len(empty.Root().Keys()) != 0 || len(empty.Footer()) != 0 { t.Errorf("empty document = %v / %q", empty.Root().Keys(), empty.Footer()) } } func TestParseMapIsTheValueTree(t *testing.T) { // ParseMap is the path that does not build a document, and it gives the // tree the decoder reads. tree, err := ParseMap([]byte("a = 1\n\n[t]\nb = \"x\"\n")) if err != nil { t.Fatalf("parse: %v", err) } if tree["a"] != int64(1) { t.Errorf("a = %#v", tree["a"]) } if tree["t"].(map[string]any)["b"] != "x" { t.Errorf("t = %#v", tree["t"]) } } func TestMarshalDocument(t *testing.T) { // A Document writes back: the keys in written order, the comments above // the lines and headers they belonged to, and inline tables inline again. doc, err := Parse([]byte("# leading\na = 1 # trailing\n\n[t]\nb = \"x\"\n\ninline = { n = 1 }\n")) if err != nil { t.Fatalf("parse: %v", err) } out, err := Marshal(doc) if err != nil { t.Fatalf("marshal of a Document: %v", err) } want := "# leading\na = 1 # trailing\n\n[t]\nb = \"x\"\ninline = {n = 1}\n" if string(out) != want { t.Errorf("output:\n%q\nwant:\n%q", out, want) } // The written document parses back to the same values. re, err := Parse(out) if err != nil { t.Fatalf("re-parse: %v", err) } if got := re.Map()["a"]; got != int64(1) { t.Errorf("a = %#v", got) } if _, err := Marshal(*doc); err != nil { t.Errorf("marshal of a Document value: %v", err) } } func TestDocumentEditPipeline(t *testing.T) { doc, err := Parse([]byte("host = \"db\"\nport = 5432\n\n# The cache section\ntimeout = 1.5\n")) if err != nil { t.Fatal(err) } t.Run("typed getters", func(t *testing.T) { if s, ok := doc.GetString("host"); !ok || s != "db" { t.Errorf("host = %q, %v", s, ok) } if i, ok := doc.GetInt("port"); !ok || i != 5432 { t.Errorf("port = %d, %v", i, ok) } if f, ok := doc.GetFloat("timeout"); !ok || f != 1.5 { t.Errorf("timeout = %g, %v", f, ok) } if _, ok := doc.GetBool("host"); ok { t.Error("host claimed as bool") } }) t.Run("set keeps the position and the comments", func(t *testing.T) { doc.Set("port", int64(9090)) if got := doc.Root().Keys(); !slices.Equal(got, []string{"host", "port", "timeout"}) { t.Fatalf("keys = %v", got) } out, err := Marshal(doc) if err != nil { t.Fatal(err) } if !strings.Contains(string(out), "port = 9090") { t.Errorf("output missing the new value:\n%s", out) } }) t.Run("a new key joins the end", func(t *testing.T) { doc.Set("lang", "cs") if got := doc.Root().Keys(); !slices.Equal(got, []string{"host", "port", "timeout", "lang"}) { t.Fatalf("keys = %v", got) } }) t.Run("a set table keeps an order of its own", func(t *testing.T) { sub := map[string]any{"z": int64(1), "a": int64(2)} doc.Set("cache", sub) out, err := Marshal(doc) if err != nil { t.Fatal(err) } if !strings.Contains(string(out), "[cache]\na = 2\nz = 1\n") { t.Errorf("output missing the new table in order:\n%s", out) } }) t.Run("delete removes the key", func(t *testing.T) { doc.Delete("lang") if _, ok := doc.Get("lang"); ok { t.Fatal("lang survived Delete") } out, err := Marshal(doc) if err != nil { t.Fatal(err) } if strings.Contains(string(out), "lang") { t.Errorf("output still names lang:\n%s", out) } }) t.Run("UnmarshalDocument decodes without reparsing", func(t *testing.T) { type Cfg struct { Host string `toml:"host"` Port int `toml:"port"` } var cfg Cfg if err := UnmarshalDocument(doc, &cfg); err != nil { t.Fatal(err) } if cfg.Host != "db" || cfg.Port != 9090 { t.Errorf("decoded %+v", cfg) } }) t.Run("comments survive the round trip", func(t *testing.T) { src := "# header comment\n[a]\n# key comment\nb = 2\n" doc, err := Parse([]byte(src)) if err != nil { t.Fatal(err) } out, err := Marshal(doc) if err != nil { t.Fatal(err) } for _, want := range []string{"# header comment", "[a]", "# key comment", "b = 2"} { if !strings.Contains(string(out), want) { t.Errorf("output missing %q:\n%s", want, out) } } }) t.Run("a nil document refuses to decode", func(t *testing.T) { var cfg struct { A int `toml:"a"` } if err := UnmarshalDocument(nil, &cfg); err == nil { t.Error("UnmarshalDocument(nil) succeeded, want an error") } }) } // TestMarshalDocumentRoundTrips pins that a parsed document written back // re-parses to the same tree: arrays of tables keep exactly one header per // element, dotted keys hold their line position without swallowing the keys // after them, inline tables inside value arrays keep their written order, // and comments travel with their statements. func TestMarshalDocumentRoundTrips(t *testing.T) { tests := []struct { name string src string }{ {"array of tables", "[[items]]\nname = \"a\"\n\n[[items]]\nname = \"b\"\n"}, {"array of tables with comments", "# about items\n[[items]] # first\nname = \"a\"\n"}, {"dotted key before a later key", "a.b = 1\nc = 2\n"}, {"dotted keys grouped", "a.b = 1\na.c = 2\nd = 3\n"}, {"dotted key with a nested leaf", "a.b.c = 1\nz = 2\n"}, {"header section after a dotted key", "a.b = 1\n\n[a.x]\ny = 2\n"}, {"inline tables in a value array keep order", "arr = [{y = 1, x = 2}, {second = true, first = false}]\n"}, {"nested array of tables", "[[items]]\nn = 1\n\n[items.sub]\nk = \"v\"\n"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { doc, err := Parse([]byte(tt.src)) if err != nil { t.Fatalf("Parse: %v", err) } out, err := Marshal(doc) if err != nil { t.Fatalf("Marshal: %v", err) } reparsed, err := Parse(out) if err != nil { t.Fatalf("re-parse of %q: %v", out, err) } if !reflect.DeepEqual(doc.Map(), reparsed.Map()) { t.Errorf("round trip changed the tree:\nin: %#v\nout: %#v", doc.Map(), reparsed.Map()) } if got, want := reparsed.Root().Keys(), doc.Root().Keys(); !slices.Equal(got, want) { t.Errorf("root keys = %v, want %v", got, want) } }) } } // TestMarshalDocumentArrayComments pins where the comments of an array of // tables land: above and beside the [[header]] itself. func TestMarshalDocumentArrayComments(t *testing.T) { doc, err := Parse([]byte("# element one\n[[items]] # trailing\nname = \"a\"\n")) if err != nil { t.Fatalf("Parse: %v", err) } out, err := Marshal(doc) if err != nil { t.Fatalf("Marshal: %v", err) } want := "# element one\n[[items]] # trailing\nname = \"a\"\n" if string(out) != want { t.Errorf("output = %q, want %q", out, want) } } // TestTableSetReplacesTableNode pins that Set over a key holding a table // rebuilds the node, so the new map's keys are the ones written. func TestTableSetReplacesTableNode(t *testing.T) { doc, err := Parse([]byte("[cache]\nz = 1\n")) if err != nil { t.Fatalf("Parse: %v", err) } doc.Set("cache", map[string]any{"a": int64(2)}) out, err := Marshal(doc) if err != nil { t.Fatalf("Marshal: %v", err) } want := "[cache]\na = 2\n" if string(out) != want { t.Errorf("output = %q, want %q", out, want) } } // TestTableSetNestedArraysOfTables pins that a value set through the edit API // carries its arrays of tables into the header form. func TestTableSetNestedArraysOfTables(t *testing.T) { doc, err := Parse([]byte("x = 1\n")) if err != nil { t.Fatalf("Parse: %v", err) } doc.Set("t", map[string]any{"items": []map[string]any{{"n": int64(1)}, {"n": int64(2)}}}) out, err := Marshal(doc) if err != nil { t.Fatalf("Marshal: %v", err) } if !strings.Contains(string(out), "[[t.items]]") { t.Errorf("output = %q, want the array of tables under a header", out) } } // TestTableSetCyclicMapErrors pins that a cyclic map set through the edit API // reaches the depth limit instead of the stack. func TestTableSetCyclicMapErrors(t *testing.T) { doc, err := Parse([]byte("x = 1\n")) if err != nil { t.Fatalf("Parse: %v", err) } m := map[string]any{} m["self"] = m doc.Set("cyclic", m) if _, err := Marshal(doc); err == nil || !strings.Contains(err.Error(), "nests deeper") { t.Errorf("err = %v, want the depth-limit complaint", err) } } // TestDocumentNilSafety pins that the nil document answers its readers // instead of panicking, the contract Root already carries. func TestDocumentNilSafety(t *testing.T) { var doc *Document if doc.Map() != nil { t.Errorf("Map = %v", doc.Map()) } if doc.Footer() != nil { t.Errorf("Footer = %v", doc.Footer()) } doc.SetFooter([]string{"x"}) if e, ok := doc.Get("k"); e != nil || ok { t.Errorf("Get = %v, %v", e, ok) } if _, ok := doc.GetString("k"); ok { t.Error("GetString on a nil document reports a value") } if _, ok := doc.GetTable("k"); ok { t.Error("GetTable on a nil document reports a value") } doc.Set("k", 1) doc.Delete("k") if keys := doc.Root().Keys(); keys != nil { t.Errorf("Keys = %v", keys) } if doc.Root().Entries() != nil { t.Errorf("Entries = %v", doc.Root().Entries()) } }