Assisted-by: GLM 5.3 Flash
This commit is contained in:
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user