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
|
(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
|
nests arrays or inline tables deeper used to run the stack out and is now
|
||||||
rejected with a `SyntaxError` naming the limit.
|
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]()`,
|
- `ParseAs[T](data)`, the generic one-line decode, and `NewSchema[T]()`,
|
||||||
which precompiles the struct schema and the interface flags for a hot path
|
which precompiles the struct schema and the interface flags for a hot path
|
||||||
before the first document arrives.
|
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`
|
cached, so the first document pays the cost instead of the hot path. A `T`
|
||||||
that is not a struct warms nothing.
|
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
|
## Documents
|
||||||
|
|
||||||
`Parse` returns a `Document`: the value tree together with what a map cannot
|
`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"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"iter"
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
"slices"
|
"slices"
|
||||||
@@ -526,6 +528,69 @@ func Marshal(v any) ([]byte, error) {
|
|||||||
return MarshalContext(context.Background(), v)
|
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
|
// MarshalAppend appends the TOML encoding of v to buf and returns the extended
|
||||||
// buffer, the shape json.MarshalAppend has. A failed encoding leaves buf
|
// buffer, the shape json.MarshalAppend has. A failed encoding leaves buf
|
||||||
// untouched and comes back with a nil slice.
|
// untouched and comes back with a nil slice.
|
||||||
|
|||||||
@@ -5,10 +5,12 @@ package interpres
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -821,3 +823,65 @@ func TestZeroOffsetRoundTrip(t *testing.T) {
|
|||||||
t.Errorf("a = %q, want 1979-05-27T07:32Z", got)
|
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