feat: add Statements, the top-level statement iterator
Test / test (push) Successful in 1m36s

Assisted-by: GLM 5.3 Flash
This commit is contained in:
2026-09-22 01:12:04 +02:00
parent d92bb56853
commit a7a942a8e1
6 changed files with 233 additions and 0 deletions
+5
View File
@@ -46,6 +46,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
(10000 levels, which no hand-written document approaches): a document that
nests arrays or inline tables deeper used to run the stack out and is now
rejected with a `SyntaxError` naming the limit.
- `Statements(r)`, an iterator over the top-level statements of the document
the reader carries, in written order: key/value statements, a `[table]`
header as one statement with its node, an `[[array of tables]]` as one
statement per element. A caller that breaks after the statement it wanted
reads no further ones. `examples/statements` shows the walk.
- `ParseAs[T](data)`, the generic one-line decode, and `NewSchema[T]()`,
which precompiles the struct schema and the interface flags for a hot path
before the first document arrives.
+21
View File
@@ -97,6 +97,27 @@ interface flags the decoder and encoder resolve through are built once and
cached, so the first document pays the cost instead of the hot path. A `T`
that is not a struct warms nothing.
### `func Statements(r io.Reader) iter.Seq2[Statement, error]`
Iterates the top-level statements of the document r carries, in written
order: key/value statements, a `[table]` header as one statement carrying
its `Table` node, and an `[[array of tables]]` as one statement per element
with the element's node and its `Index`. Iteration stops at the first error
and at a false yield, so a caller looking for one section reads no further.
The reader is consumed in full before the first yield, because the parser
scans the source in place.
```go
for stmt, err := range interpres.Statements(file) {
if err != nil {
return err
}
if stmt.Table != nil {
fmt.Println(stmt.Key, stmt.Table.Keys())
}
}
```
## Documents
`Parse` returns a `Document`: the value tree together with what a map cannot
+39
View File
@@ -0,0 +1,39 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
// Command statements walks the top-level statements of a TOML document with
// interpres.Statements, the shape a configuration tool uses to read the
// sections it cares about and skip the rest.
package main
import (
"fmt"
"io"
"os"
"sourcedock.dev/petrbalvin/interpres/v2"
)
func main() {
if err := run(os.Stdin, os.Stdout); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func run(stdin io.Reader, stdout io.Writer) error {
for stmt, err := range interpres.Statements(stdin) {
if err != nil {
return err
}
switch {
case stmt.Index >= 0:
fmt.Fprintf(stdout, "[[%s]] #%d\n", stmt.Key, stmt.Index)
case stmt.Table != nil:
fmt.Fprintf(stdout, "[%s] keys: %v\n", stmt.Key, stmt.Table.Keys())
default:
fmt.Fprintf(stdout, "%s = %v\n", stmt.Key, stmt.Value)
}
}
return nil
}
+39
View File
@@ -0,0 +1,39 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
package main
import (
"strings"
"testing"
)
func TestStatementsExample(t *testing.T) {
in := strings.NewReader(`title = "demo"
port = 8080
[server]
host = "127.0.0.1"
[[items]]
name = "a"
[[items]]
name = "b"
`)
var out strings.Builder
if err := run(in, &out); err != nil {
t.Fatal(err)
}
for _, want := range []string{
"title = demo",
"port = 8080",
"[server] keys: [host]",
"[[items]] #0",
"[[items]] #1",
} {
if !strings.Contains(out.String(), want) {
t.Errorf("output missing %q:\n%s", want, out.String())
}
}
}
+65
View File
@@ -25,6 +25,8 @@ import (
"context"
"errors"
"fmt"
"io"
"iter"
"os"
"reflect"
"slices"
@@ -526,6 +528,69 @@ func Marshal(v any) ([]byte, error) {
return MarshalContext(context.Background(), v)
}
// A Statement is one top-level statement of a document, what Statements
// yields: a key with its value, a table with its node, or one element of an
// array of tables with its node.
type Statement struct {
// Key is the key as the document wrote it.
Key string
// Value is the value of a key/value statement, and the value map of a
// table statement.
Value any
// Table is the node of a table or array-of-tables statement, carrying the
// written key order and the comments; nil for a plain key/value.
Table *Table
// Index is the element's position when the statement is one element of an
// array of tables, and -1 otherwise.
Index int
}
// Statements reads a TOML document from r and returns an iterator over its
// top-level statements in written order: key/value statements, a [table]
// header as one statement carrying its Table node, and an [[array of
// tables]] as one statement per element, each with the element's node and
// its Index. Iteration stops at the first error, which arrives as the second
// value, and at a false yield: a caller that breaks after the statement it
// wanted reads no further ones.
//
// The reader is consumed in full before the first statement is yielded,
// because the parser scans the source in place; processing the yielded
// statements one at a time is what bounds what the caller holds, and a
// later direct-to-target parse removes the whole-source hold.
func Statements(r io.Reader) iter.Seq2[Statement, error] {
return func(yield func(Statement, error) bool) {
data, err := io.ReadAll(r)
if err != nil {
yield(Statement{Index: -1}, err)
return
}
doc, err := Parse(data)
if err != nil {
yield(Statement{Index: -1}, err)
return
}
for _, e := range doc.Root().Entries() {
if els := e.Elements(); len(els) > 0 {
for i, el := range els {
if !yield(Statement{Key: e.Key(), Value: e.Value(), Table: el, Index: i}, nil) {
return
}
}
continue
}
if child := e.Table(); child != nil {
if !yield(Statement{Key: e.Key(), Value: e.Value(), Table: child, Index: -1}, nil) {
return
}
continue
}
if !yield(Statement{Key: e.Key(), Value: e.Value(), Index: -1}, nil) {
return
}
}
}
}
// MarshalAppend appends the TOML encoding of v to buf and returns the extended
// buffer, the shape json.MarshalAppend has. A failed encoding leaves buf
// untouched and comes back with a nil slice.
+64
View File
@@ -5,10 +5,12 @@ package interpres
import (
"errors"
"fmt"
"math"
"os"
"path/filepath"
"reflect"
"slices"
"strings"
"testing"
"time"
@@ -821,3 +823,65 @@ func TestZeroOffsetRoundTrip(t *testing.T) {
t.Errorf("a = %q, want 1979-05-27T07:32Z", got)
}
}
func TestStatements(t *testing.T) {
src := strings.NewReader(`title = "demo"
port = 8080
[server]
host = "127.0.0.1"
[[items]]
name = "a"
[[items]]
name = "b"
`)
var lines []string
for stmt, err := range Statements(src) {
if err != nil {
t.Fatal(err)
}
switch {
case stmt.Index >= 0:
lines = append(lines, fmt.Sprintf("%s #%d", stmt.Key, stmt.Index))
case stmt.Table != nil:
lines = append(lines, fmt.Sprintf("[%s] %v", stmt.Key, stmt.Table.Keys()))
default:
lines = append(lines, fmt.Sprintf("%s = %v", stmt.Key, stmt.Value))
}
}
want := []string{
`title = demo`,
`port = 8080`,
`[server] [host]`,
`items #0`,
`items #1`,
}
if !slices.Equal(lines, want) {
t.Errorf("statements =\n%v\nwant:\n%v", lines, want)
}
t.Run("breaking stops the iteration", func(t *testing.T) {
src := strings.NewReader("a = 1\nb = 2\nc = 3\n")
count := 0
for range Statements(src) {
count++
break
}
if count != 1 {
t.Errorf("iterated %d statements after break, want 1", count)
}
})
t.Run("a parse error arrives as the second value", func(t *testing.T) {
for stmt, err := range Statements(strings.NewReader("broken =\n")) {
if err == nil {
t.Fatalf("statement %+v without an error", stmt)
}
if _, ok := errors.AsType[*SyntaxError](err); !ok {
t.Errorf("err = %v, want a SyntaxError", err)
}
break
}
})
}