feat: decode arrays into [N]T and add MarshalAppend
Test / test (push) Successful in 2m31s

Assisted-by: GLM 5.3 Flash
This commit is contained in:
2026-09-21 23:58:23 +02:00
parent 10d49fbe60
commit 2e5dfc54c9
6 changed files with 140 additions and 18 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 (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.
- A TOML array decodes into a Go fixed-size array, `[N]T`, where only a slice
was accepted before; the encoder could already encode one. A length mismatch
is an error wrapped with the key path.
- `MarshalAppend(buf, v)` appends the TOML encoding of v to buf and returns
the extended buffer, the shape `json.MarshalAppend` has.
- `ParseFile(path)` reads the file and parses it into a `Document`, with the - `ParseFile(path)` reads the file and parses it into a `Document`, with the
file name at the front of every error it returns, read failure and parse file name at the front of every error it returns, read failure and parse
failure alike. `Valid(data)` reports whether a document parses, nil on failure alike. `Valid(data)` reports whether a document parses, nil on
+45 -18
View File
@@ -309,31 +309,58 @@ func (d *decoder) assignMap(tbl map[string]any, dst reflect.Value) error {
} }
func (d *decoder) assignSlice(items []any, dst reflect.Value) error { func (d *decoder) assignSlice(items []any, dst reflect.Value) error {
if dst.Kind() != reflect.Slice { switch dst.Kind() {
case reflect.Slice:
out := reflect.MakeSlice(dst.Type(), len(items), len(items))
for i, item := range items {
if err := d.assign(item, out.Index(i)); err != nil {
return newDecodeError(fmt.Sprintf("[%d]", i), err)
}
}
dst.Set(out)
return nil
case reflect.Array:
// A fixed-size array takes the elements in place; a length mismatch is
// the error, because a TOML array carries no way to name a default for
// the elements it is short of, and the surplus has nowhere to go.
if dst.Len() != len(items) {
return fmt.Errorf("interpres: cannot assign %d elements to %s", len(items), dst.Type())
}
for i, item := range items {
if err := d.assign(item, dst.Index(i)); err != nil {
return newDecodeError(fmt.Sprintf("[%d]", i), err)
}
}
return nil
default:
return fmt.Errorf("interpres: cannot assign array to %s", dst.Type()) return fmt.Errorf("interpres: cannot assign array to %s", dst.Type())
} }
out := reflect.MakeSlice(dst.Type(), len(items), len(items))
for i, item := range items {
if err := d.assign(item, out.Index(i)); err != nil {
return newDecodeError(fmt.Sprintf("[%d]", i), err)
}
}
dst.Set(out)
return nil
} }
func (d *decoder) assignTableSlice(items []map[string]any, dst reflect.Value) error { func (d *decoder) assignTableSlice(items []map[string]any, dst reflect.Value) error {
if dst.Kind() != reflect.Slice { switch dst.Kind() {
case reflect.Slice:
out := reflect.MakeSlice(dst.Type(), len(items), len(items))
for i, item := range items {
if err := d.assign(item, out.Index(i)); err != nil {
return newDecodeError(fmt.Sprintf("[%d]", i), err)
}
}
dst.Set(out)
return nil
case reflect.Array:
if dst.Len() != len(items) {
return fmt.Errorf("interpres: cannot assign %d elements to %s", len(items), dst.Type())
}
for i, item := range items {
if err := d.assign(item, dst.Index(i)); err != nil {
return newDecodeError(fmt.Sprintf("[%d]", i), err)
}
}
return nil
default:
return fmt.Errorf("interpres: cannot assign array of tables to %s", dst.Type()) return fmt.Errorf("interpres: cannot assign array of tables to %s", dst.Type())
} }
out := reflect.MakeSlice(dst.Type(), len(items), len(items))
for i, item := range items {
if err := d.assign(item, out.Index(i)); err != nil {
return newDecodeError(fmt.Sprintf("[%d]", i), err)
}
}
dst.Set(out)
return nil
} }
// --- low-level setters ----------------------------------------------------- // --- low-level setters -----------------------------------------------------
+43
View File
@@ -1273,3 +1273,46 @@ func TestSourceLineEdgePositions(t *testing.T) {
t.Errorf("SourceLine(offset past end) =\n%q\nwant:\n%q", got, want) t.Errorf("SourceLine(offset past end) =\n%q\nwant:\n%q", got, want)
} }
} }
func TestDecodeFixedArray(t *testing.T) {
t.Run("value array", func(t *testing.T) {
var cfg struct {
Ports [2]int `toml:"ports"`
Label [2]string `toml:"label"`
Grid [2][2]int64 `toml:"grid"`
}
in := []byte("ports = [8080, 9090]\nlabel = [\"a\", \"b\"]\ngrid = [[1, 2], [3, 4]]\n")
if err := Unmarshal(in, &cfg); err != nil {
t.Fatal(err)
}
if cfg.Ports != [2]int{8080, 9090} || cfg.Label != [2]string{"a", "b"} || cfg.Grid != [2][2]int64{{1, 2}, {3, 4}} {
t.Errorf("decoded %+v", cfg)
}
})
t.Run("array of tables", func(t *testing.T) {
type Item struct {
Name string `toml:"name"`
Qty int `toml:"qty"`
}
var cfg struct {
Items [2]Item `toml:"items"`
}
in := []byte("[[items]]\nname = \"a\"\nqty = 1\n[[items]]\nname = \"b\"\nqty = 2\n")
if err := Unmarshal(in, &cfg); err != nil {
t.Fatal(err)
}
if cfg.Items != [2]Item{{"a", 1}, {"b", 2}} {
t.Errorf("decoded %+v", cfg)
}
})
t.Run("a length mismatch is an error", func(t *testing.T) {
var cfg struct {
Ports [3]int `toml:"ports"`
}
err := Unmarshal([]byte("ports = [8080, 9090]\n"), &cfg)
want := "ports: interpres: cannot assign 2 elements to [3]int"
if err == nil || err.Error() != want {
t.Errorf("err = %v, want %q", err, want)
}
})
}
+10
View File
@@ -158,6 +158,11 @@ out, err := interpres.Marshal(cfg)
The cancellable variant of `Marshal`. The context is checked before any work The cancellable variant of `Marshal`. The context is checked before any work
and every 64 fields during the reflection walk. and every 64 fields during the reflection walk.
### `func MarshalAppend(buf []byte, v any) ([]byte, error)`
Appends the TOML encoding of `v` to `buf` and returns the extended buffer, the
shape `json.MarshalAppend` has. A failed encoding leaves `buf` untouched.
## Decoding ## Decoding
### Value mapping ### Value mapping
@@ -282,6 +287,11 @@ destination is a slice, each element decodes into the slice's element type
(`[]struct` or `[]map[string]V`); a mismatch on one element surfaces as an (`[]struct` or `[]map[string]V`); a mismatch on one element surfaces as an
error wrapped with `[i]:` and the element index. error wrapped with `[i]:` and the element index.
A value array also decodes into a fixed-size array, `[N]T`, the mirror of the
encoder's ability to encode one. The element count has to match: an array
whose length differs from `N` is an error, `interpres: cannot assign 2
elements to [3]int`, wrapped with the key path.
### Custom decoding: `Unmarshaler` ### Custom decoding: `Unmarshaler`
A type that wants full control of its decode implements: A type that wants full control of its decode implements:
+26
View File
@@ -1998,3 +1998,29 @@ func TestMarshalNumber(t *testing.T) {
} }
}) })
} }
func TestMarshalAppend(t *testing.T) {
buf := []byte("preamble\n")
out, err := MarshalAppend(buf, map[string]any{"a": int64(1)})
if err != nil {
t.Fatal(err)
}
want := "preamble\na = 1\n"
if string(out) != want {
t.Errorf("output %q, want %q", out, want)
}
if &out[0] != &buf[0] {
t.Log("append reallocated; capacity differed")
}
out2, err := MarshalAppend(out, map[string]any{"b": true})
if err != nil {
t.Fatal(err)
}
if string(out2) != want+"b = true\n" {
t.Errorf("second append %q", out2)
}
buf = []byte("keep\n")
if out3, err := MarshalAppend(buf, Document{}); err == nil {
t.Errorf("MarshalAppend with an unencodable value = %q, want an error", out3)
}
}
+11
View File
@@ -399,6 +399,17 @@ func Marshal(v any) ([]byte, error) {
return MarshalContext(context.Background(), v) return MarshalContext(context.Background(), v)
} }
// 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.
func MarshalAppend(buf []byte, v any) ([]byte, error) {
out, err := Marshal(v)
if err != nil {
return nil, err
}
return append(buf, out...), nil
}
// MarshalContext is the cancellable variant of Marshal. // MarshalContext is the cancellable variant of Marshal.
func MarshalContext(ctx context.Context, v any) ([]byte, error) { func MarshalContext(ctx context.Context, v any) ([]byte, error) {
if err := ctx.Err(); err != nil { if err := ctx.Err(); err != nil {