feat: add ParseAs and NewSchema generics
Test / test (push) Successful in 1m38s

Assisted-by: GLM 5.3 Flash
This commit is contained in:
2026-09-22 00:36:10 +02:00
parent b02471c09a
commit 71bd82a7a5
4 changed files with 73 additions and 0 deletions
+3
View File
@@ -46,6 +46,9 @@ 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.
- `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.
- `OrderedMap`, the string-keyed table that remembers the order its keys were
set in: decoding into one fills it in the order the document wrote the
keys, and `Marshal` writes one back in that order, where a map carries no
+17
View File
@@ -80,6 +80,23 @@ The one-shot form of a configured `Decoder`: the same options as
The zero value takes the defaults: unknown keys ignored, numbers evaluated
as `int64` and `float64`, no size limit and the 10000-level nesting default.
### `func ParseAs[T any](data []byte) (T, error)`
The generic shorthand for `Unmarshal` with a destination variable:
```go
cfg, err := interpres.ParseAs[Config](data)
```
The zero `T` comes back with the error.
### `func NewSchema[T any]()`
Precompiles the codec for `T`: the struct schema both directions walk and the
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.
## Documents
`Parse` returns a `Document`: the value tree together with what a map cannot
+30
View File
@@ -270,6 +270,36 @@ func Unmarshal(data []byte, v any) error {
return UnmarshalContext(context.Background(), data, v)
}
// ParseAs decodes a TOML document into T in one call, the generic shorthand
// for Unmarshal with a destination variable:
//
// cfg, err := interpres.ParseAs[Config](data)
//
// The zero T comes back with the error.
func ParseAs[T any](data []byte) (T, error) {
var v T
err := Unmarshal(data, &v)
return v, err
}
// NewSchema precompiles the codec for T: the struct schema both directions
// walk and the interface flags the decoder and the 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; there is nothing to
// precompute for a map or a slice.
func NewSchema[T any]() {
t := reflect.TypeFor[T]()
if t.Kind() != reflect.Struct {
return
}
cachedStructSchema(t)
_ = typeFlags(t)
_ = encTypeFlags(t)
pt := reflect.PointerTo(t)
_ = typeFlags(pt)
_ = encTypeFlags(pt)
}
// UnmarshalContext is the cancellable variant of Unmarshal.
func UnmarshalContext(ctx context.Context, data []byte, v any) error {
// Only a destination that can reach an OrderedMap needs the node tree the
+23
View File
@@ -8,6 +8,7 @@ import (
"math"
"os"
"path/filepath"
"reflect"
"strings"
"testing"
"time"
@@ -773,3 +774,25 @@ func TestValid(t *testing.T) {
t.Errorf("Valid(invalid) = %v, want a SyntaxError", err)
}
}
func TestParseAsAndNewSchema(t *testing.T) {
type Config struct {
Host string `toml:"host"`
Port int `toml:"port"`
}
cfg, err := ParseAs[Config]([]byte("host = \"db\"\nport = 5432\n"))
if err != nil {
t.Fatal(err)
}
if cfg.Host != "db" || cfg.Port != 5432 {
t.Errorf("decoded %+v", cfg)
}
if _, err := ParseAs[Config]([]byte("port =\n")); err == nil {
t.Error("ParseAs(invalid) succeeded, want an error and the zero value")
}
NewSchema[Config]()
if _, ok := structSchemaCache.Load(reflect.TypeFor[Config]()); !ok {
t.Error("NewSchema left no schema in the cache")
}
NewSchema[map[string]any]() // must not panic
}