From 71bd82a7a514dd1dbd324c326bc4bf8e3235e8a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Balv=C3=ADn?= Date: Tue, 22 Sep 2026 00:36:10 +0200 Subject: [PATCH] feat: add ParseAs and NewSchema generics Assisted-by: GLM 5.3 Flash --- CHANGELOG.md | 3 +++ docs/API.md | 17 +++++++++++++++++ interpres.go | 30 ++++++++++++++++++++++++++++++ interpres_test.go | 23 +++++++++++++++++++++++ 4 files changed, 73 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 30676d6..00c6627 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/docs/API.md b/docs/API.md index caeebfe..a43b57b 100644 --- a/docs/API.md +++ b/docs/API.md @@ -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 diff --git a/interpres.go b/interpres.go index f9d21bd..edaf8be 100644 --- a/interpres.go +++ b/interpres.go @@ -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 diff --git a/interpres_test.go b/interpres_test.go index 3aed974..1af0f98 100644 --- a/interpres_test.go +++ b/interpres_test.go @@ -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 +}