feat: parse into a Document that keeps order and comments
Test / test (push) Successful in 1m34s

Assisted-by: DeepSeek V4.1 Flash
This commit is contained in:
2026-09-20 10:40:57 +02:00
parent a6e3e3fe31
commit 72f8b21ac4
16 changed files with 939 additions and 119 deletions
+314
View File
@@ -0,0 +1,314 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
package interpres
import (
"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 TestMarshalRejectsDocument(t *testing.T) {
// A Document is not a value to marshal: its order and comments would be
// dropped, and a struct walk would silently write nothing at all.
doc, err := Parse([]byte("a = 1\n"))
if err != nil {
t.Fatalf("parse: %v", err)
}
if _, err := Marshal(doc); err == nil {
t.Fatal("expected an error for a Document")
} else if !strings.Contains(err.Error(), "Map()") {
t.Errorf("err = %v, want it to point at Map()", err)
}
if _, err := Marshal(*doc); err == nil {
t.Fatal("expected an error for a Document value")
}
// The tree marshals, which is the way through.
out, err := Marshal(doc.Map())
if err != nil {
t.Fatalf("marshal of the tree: %v", err)
}
if want := "a = 1\n"; string(out) != want {
t.Errorf("output = %q, want %q", out, want)
}
}