diff --git a/CHANGELOG.md b/CHANGELOG.md index 465b809..f3c2018 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. +- 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 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 diff --git a/decode.go b/decode.go index 492b6dc..e7d39e1 100644 --- a/decode.go +++ b/decode.go @@ -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 { - 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()) } - 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 { - 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()) } - 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 ----------------------------------------------------- diff --git a/decode_test.go b/decode_test.go index 4271748..b06dc73 100644 --- a/decode_test.go +++ b/decode_test.go @@ -1273,3 +1273,46 @@ func TestSourceLineEdgePositions(t *testing.T) { 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) + } + }) +} diff --git a/docs/API.md b/docs/API.md index a6ed4c7..6ef5154 100644 --- a/docs/API.md +++ b/docs/API.md @@ -158,6 +158,11 @@ out, err := interpres.Marshal(cfg) The cancellable variant of `Marshal`. The context is checked before any work 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 ### 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 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` A type that wants full control of its decode implements: diff --git a/encode_test.go b/encode_test.go index 754c7e4..c29a424 100644 --- a/encode_test.go +++ b/encode_test.go @@ -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) + } +} diff --git a/interpres.go b/interpres.go index 2307123..98962d1 100644 --- a/interpres.go +++ b/interpres.go @@ -399,6 +399,17 @@ func Marshal(v any) ([]byte, error) { 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. func MarshalContext(ctx context.Context, v any) ([]byte, error) { if err := ctx.Err(); err != nil {