feat!: rework the public API to json/v2-style variadic options
Test / test (push) Successful in 1m49s

Assisted-by: GLM 5.3 Flash
This commit is contained in:
2026-09-22 18:42:18 +02:00
parent 7ee155d1e9
commit 2e61ad0ba9
18 changed files with 398 additions and 393 deletions
+24 -18
View File
@@ -24,7 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
toml-test tagged JSON from stdin and writes the TOML document it describes. toml-test tagged JSON from stdin and writes the TOML document it describes.
The compliance suite now runs the encoder as well as the decoder, 214 The compliance suite now runs the encoder as well as the decoder, 214
encoder cases against the tagged JSON of the valid corpus. encoder cases against the tagged JSON of the valid corpus.
- `Encoder.InlineTables(threshold)`: a sub-table whose single-line rendering is - `InlineTables(threshold)`: a sub-table whose single-line rendering is
at most `threshold` bytes is written as an inline table instead of a header at most `threshold` bytes is written as an inline table instead of a header
section, which shortens a document of small tables. An array of tables keeps section, which shortens a document of small tables. An array of tables keeps
its header form, because its inline form would re-parse as a value array. its header form, because its inline form would re-parse as a value array.
@@ -47,7 +47,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
the plain type takes an offset date-time as it always did; code that asserts the plain type takes an offset date-time as it always did; code that asserts
the tree's type, and `UnmarshalTOML` implementations that expect a the tree's type, and `UnmarshalTOML` implementations that expect a
`time.Time`, need the new type. `time.Time`, need the new type.
- `Decoder.MaxDepth(depth)` and `Decoder.MaxInputSize(size)` bound the parse a - `MaxNestingDepth(depth)` and `MaxInputSize(size)` options bound the parse a
`Decode` performs, and every parse carries a nesting limit in any case `Decode` performs, and every parse carries a nesting limit in any case
(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
@@ -60,12 +60,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `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.
- `Encoder.EmitFieldComments()` prints the comment a field's `toml` tag - `EmitFieldComments(true)` prints the comment a field's `toml` tag
carries in a `comment=` option above the field's line or header, the carries in a `comment=` option above the field's line or header, the
comments a round trip through the Go type would otherwise drop. Go doc comments a round trip through the Go type would otherwise drop. Go doc
comments are not visible to reflection, so the tag is the channel that comments are not visible to reflection, so the tag is the channel that
carries the text. carries the text.
- `Decoder.LocalTimeLocation(loc)` lets a local date-time fill a plain - `LocalTimeLocation(loc)` lets a local date-time fill a plain
`time.Time` destination in the location given, relabelled rather than `time.Time` destination in the location given, relabelled rather than
shifted: `07:32` in the document is `07:32` in the zone. Without the shifted: `07:32` in the document is `07:32` in the zone. Without the
option the wrapper types remain the only destinations a local kind fills. option the wrapper types remain the only destinations a local kind fills.
@@ -78,9 +78,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
order on decode and sorts on encode. It works as a decode target on its order on decode and sorts on encode. It works as a decode target on its
own, in a struct field, and as the element of an array of tables; its own, in a struct field, and as the element of an array of tables; its
values are untyped, so a nested table stays a `map[string]any`. values are untyped, so a nested table stays a `map[string]any`.
- `UnmarshalWithOptions(data, v, opts)` decodes with a `DecodeOptions` struct - `Unmarshal(data, v, opts...)` and the other entries take variadic options,
in one call, the options a `Decoder` sets without building one: unknown the shape encoding/json/v2 uses: `RejectUnknownFields`,
keys, `Number` literals, and the parse limits. `NumbersAsLiterals`, `MaxNestingDepth`, `MaxInputSize`,
`LocalTimeLocation`. `MarshalWrite(w, v, opts...)` and
`UnmarshalRead(r, v, opts...)` are the streaming forms.
- `Marshal` carries a nesting limit of 10000 levels, the parser's own figure: - `Marshal` carries a nesting limit of 10000 levels, the parser's own figure:
cyclic data, which used to run the stack out, is now rejected with an error cyclic data, which used to run the stack out, is now rejected with an error
that names the limit and the path it was met at. that names the limit and the path it was met at.
@@ -126,7 +128,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
the input. An input that is not valid UTF-8 names the offset of the first the input. An input that is not valid UTF-8 names the offset of the first
invalid byte in its message. The new fields are additive: a `SyntaxError` invalid byte in its message. The new fields are additive: a `SyntaxError`
built from a line and a message alone is unchanged. built from a line and a message alone is unchanged.
- `Decoder.UseNumber()` decodes the integers and floats of the document into - `NumbersAsLiterals(true)` decodes the integers and floats of the document into
`Number`, which carries the literal the document wrote, so `0x1f`, `1_000`, `Number`, which carries the literal the document wrote, so `0x1f`, `1_000`,
`+1.0` and `inf` survive a round trip with their spelling instead of the `+1.0` and `inf` survive a round trip with their spelling instead of the
normalised `31`, `1000` and `1.0`. Typed destinations take the evaluated normalised `31`, `1000` and `1.0`. Typed destinations take the evaluated
@@ -136,11 +138,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- `Encoder.GroupByKind(bool)` is renamed to `Encoder.Layout(kind)` and takes - The stateful `Decoder` and `Encoder` of 1.x are replaced by variadic
a `LayoutKind`: `LayoutKindGrouped`, the default, or options on the entries, the shape encoding/json/v2 uses: `Layout(kind)`
`LayoutKindDeclaration` for the declaration order. `UseLiteralMultiline` with `LayoutKindGrouped` or `LayoutKindDeclaration`, `OmitEmptyArrays`,
is renamed to `LiteralMultiline`. The behaviour is unchanged; 2.0 is the `LiteralMultiline(threshold)`, `InlineTables(threshold)`,
only chance a rename has, and the migrator updates the calls mechanically. `EmitFieldComments`, `RejectUnknownFields`, `NumbersAsLiterals`,
`MaxNestingDepth`, `MaxInputSize`, `LocalTimeLocation`.
- `DecodeError` and `EncodeError` carry one `Path` type, a list of segments - `DecodeError` and `EncodeError` carry one `Path` type, a list of segments
(`"items"`, `"[0]"`, `"weight"`) with a `String()` rendering the TOML (`"items"`, `"[0]"`, `"weight"`) with a `String()` rendering the TOML
notation, `items[0].weight`. The decode error used to hold a bare notation, `items[0].weight`. The decode error used to hold a bare
@@ -275,9 +278,12 @@ destination field of type `time.Time` keeps working.
`ParseMap` gives the plain `map[string]any` tree the old `Parse` returned. `ParseMap` gives the plain `map[string]any` tree the old `Parse` returned.
The document is writable, and `Marshal` writes it back with its comments. The document is writable, and `Marshal` writes it back with its comments.
**Renamed API.** `Encoder.GroupByKind(bool)` is `Encoder.Layout(kind)` with **Options instead of Decoder and Encoder.** The stateful types of 1.x are
`LayoutKindGrouped` (the old default) and `LayoutKindDeclaration` (the old gone; the entries take variadic options, the shape encoding/json/v2 uses.
`false`); `UseLiteralMultiline` is `LiteralMultiline`. `NewDecoder().DisallowUnknownFields().Decode(data, &cfg)` becomes
`Unmarshal(data, &cfg, RejectUnknownFields(true))`, and the encoder
methods become options: `Layout(LayoutKindDeclaration)` replaces
`GroupByKind(false)`, `LiteralMultiline` replaces `UseLiteralMultiline`.
**Tag options.** `omitempty` follows encoding/json: it now also drops empty **Tag options.** `omitempty` follows encoding/json: it now also drops empty
strings, zero numbers, `false`, nil pointers and nil interfaces. `required` strings, zero numbers, `false`, nil pointers and nil interfaces. `required`
@@ -293,7 +299,7 @@ match on the old messages would not see.
**Decoding shapes.** `map[string]any` values merge into a non-empty map **Decoding shapes.** `map[string]any` values merge into a non-empty map
destination; untagged embedded maps beyond the first stay empty; numbers can destination; untagged embedded maps beyond the first stay empty; numbers can
stay literals under `UseNumber`; local date-times can decode into stay literals under `NumbersAsLiterals`; local date-times can decode into
`time.Time` under `LocalTimeLocation`. All three are opt-in or additive `time.Time` under `LocalTimeLocation`. All three are opt-in or additive
except where noted above. except where noted above.
@@ -442,7 +448,7 @@ uses only the standard library and passes the entire
nested structs, slices, and `map[string]T`. nested structs, slices, and `map[string]T`.
- `toml:"name"` field tags, case-insensitive name fallback, and `toml:"-"` to - `toml:"name"` field tags, case-insensitive name fallback, and `toml:"-"` to
skip a field. skip a field.
- `Decoder` with `DisallowUnknownFields` for strict decoding that rejects keys - `RejectUnknownFields(true)` option for strict decoding that rejects keys
without a destination field, at every struct depth. without a destination field, at every struct depth.
- `Unmarshaler` interface (`UnmarshalTOML(data any) error`) for types that take - `Unmarshaler` interface (`UnmarshalTOML(data any) error`) for types that take
full control of their decode. full control of their decode.
+13 -12
View File
@@ -15,7 +15,7 @@ the entire official [toml-test](https://github.com/toml-lang/toml-test) suite:
optional as of 1.1; arrays and inline tables, multi-line as of 1.1. optional as of 1.1; arrays and inline tables, multi-line as of 1.1.
- **Decoding and encoding**: `Parse` for an untyped tree, `Unmarshal` and - **Decoding and encoding**: `Parse` for an untyped tree, `Unmarshal` and
`Marshal` for structs and maps, mirroring `encoding/json`. `Marshal` for structs and maps, mirroring `encoding/json`.
- **Strict decoding**: `NewDecoder().DisallowUnknownFields()` rejects keys that - **Strict decoding**: `RejectUnknownFields(true)` rejects keys that
match no destination field, at every struct depth. match no destination field, at every struct depth.
- **Custom types**: `Marshaler` and `Unmarshaler` let a type control its own - **Custom types**: `Marshaler` and `Unmarshaler` let a type control its own
TOML representation in both directions, and `encoding.TextMarshaler` and TOML representation in both directions, and `encoding.TextMarshaler` and
@@ -101,9 +101,7 @@ which is the layout that re-parses to the same tree.
### Strict decoding ### Strict decoding
```go ```go
err := interpres.NewDecoder(). err := interpres.Unmarshal(data, &cfg, interpres.RejectUnknownFields(true))
DisallowUnknownFields().
Decode(data, &cfg)
``` ```
A key with no matching field becomes an error instead of a silent drop. A key with no matching field becomes an error instead of a silent drop.
@@ -132,16 +130,20 @@ func (ip *IP) UnmarshalTOML(data any) error {
The value `MarshalTOML` returns is encoded in place of the receiver; The value `MarshalTOML` returns is encoded in place of the receiver;
`UnmarshalTOML` receives the parsed value verbatim. `UnmarshalTOML` receives the parsed value verbatim.
### Encoder options ### Options
```go ```go
out, err := interpres.NewEncoder(). out, err := interpres.Marshal(cfg,
Layout(interpres.LayoutKindDeclaration), // preserve declaration order interpres.Layout(interpres.LayoutKindDeclaration), // preserve declaration order
OmitEmptyArrays(). // skip empty scalar arrays interpres.OmitEmptyArrays(true), // skip empty scalar arrays
LiteralMultiline(80), // long multi-line strings as literal blocks interpres.LiteralMultiline(80), // long multi-line strings as literal blocks
Marshal(cfg) )
``` ```
The decode and encode calls take variadic options, the shape
encoding/json/v2 uses for its own. `UnmarshalRead(r, v, opts...)` and
`MarshalWrite(w, v, opts...)` are the streaming forms.
### Cancellation ### Cancellation
```go ```go
@@ -151,8 +153,7 @@ defer cancel()
out, err := interpres.MarshalContext(ctx, cfg) out, err := interpres.MarshalContext(ctx, cfg)
``` ```
`ParseContext`, `UnmarshalContext`, `(*Decoder).DecodeContext` and `ParseContext`, `UnmarshalContext` and `MarshalContext` follow the same pattern.
`(*Encoder).MarshalContext` follow the same pattern.
The full rules for field matching, numeric conversion and emission live in The full rules for field matching, numeric conversion and emission live in
[docs/API.md](docs/API.md). [docs/API.md](docs/API.md).
+2 -4
View File
@@ -109,11 +109,10 @@ func BenchmarkMarshal(b *testing.B) {
} }
func BenchmarkStrictDecode(b *testing.B) { func BenchmarkStrictDecode(b *testing.B) {
dec := NewDecoder().DisallowUnknownFields()
b.ReportAllocs() b.ReportAllocs()
for b.Loop() { for b.Loop() {
var cfg benchConfig var cfg benchConfig
if err := dec.Decode(benchDoc, &cfg); err != nil { if err := Unmarshal(benchDoc, &cfg, RejectUnknownFields(true)); err != nil {
b.Fatal(err) b.Fatal(err)
} }
} }
@@ -145,12 +144,11 @@ type benchLongDoc struct {
} }
func BenchmarkStrictDecodeLong(b *testing.B) { func BenchmarkStrictDecodeLong(b *testing.B) {
dec := NewDecoder().DisallowUnknownFields()
b.ReportAllocs() b.ReportAllocs()
b.SetBytes(int64(len(longDoc))) b.SetBytes(int64(len(longDoc)))
for b.Loop() { for b.Loop() {
var doc benchLongDoc var doc benchLongDoc
if err := dec.Decode(longDoc, &doc); err != nil { if err := Unmarshal(longDoc, &doc, RejectUnknownFields(true)); err != nil {
b.Fatal(err) b.Fatal(err)
} }
} }
+1 -1
View File
@@ -528,7 +528,7 @@ func setDuration(dst reflect.Value, s string) error {
return nil return nil
} }
// setNumber stores a Number, the literal UseNumber keeps. A Number destination // setNumber stores a Number, the literal NumbersAsLiterals keeps. A Number destination
// takes the literal as it is; every other destination takes the evaluated // takes the literal as it is; every other destination takes the evaluated
// value through the ordinary rules, so an integer field, a float field and a // value through the ordinary rules, so an integer field, a float field and a
// duration field all read a Number the way they read the evaluated kind. // duration field all read a Number the way they read the evaluated kind.
+17 -21
View File
@@ -169,7 +169,7 @@ func TestDecoderDecodeContextHonoursCancellation(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
cancel() cancel()
var cfg map[string]any var cfg map[string]any
err := NewDecoder().DecodeContext(ctx, []byte("a = 1\n"), &cfg) err := UnmarshalContext(ctx, []byte("a = 1\n"), &cfg)
if !errors.Is(err, context.Canceled) { if !errors.Is(err, context.Canceled) {
t.Fatalf("DecodeContext returned %v, want context.Canceled", err) t.Fatalf("DecodeContext returned %v, want context.Canceled", err)
} }
@@ -194,7 +194,7 @@ count = 3
if out.Title != "x" || out.Count != 3 { if out.Title != "x" || out.Count != 3 {
t.Errorf("out = %#v", out) t.Errorf("out = %#v", out)
} }
if err := NewDecoder().DecodeContext(context.Background(), in, &map[string]any{}); err != nil { if err := Unmarshal(in, &map[string]any{}); err != nil {
t.Fatalf("Decoder.DecodeContext: %v", err) t.Fatalf("Decoder.DecodeContext: %v", err)
} }
} }
@@ -695,8 +695,7 @@ func TestUnmarshalStrictEmbeddedMapStaysStrict(t *testing.T) {
RoundTripExtra RoundTripExtra
Name string `toml:"name"` Name string `toml:"name"`
} }
dec := NewDecoder().DisallowUnknownFields() err := Unmarshal([]byte("name = \"n\"\nrogue = 1\n"), &Cfg{}, RejectUnknownFields(true))
err := dec.Decode([]byte("name = \"n\"\nrogue = 1\n"), &Cfg{})
if err == nil || !strings.Contains(err.Error(), "unknown field") { if err == nil || !strings.Contains(err.Error(), "unknown field") {
t.Fatalf("expected unknown field error, got: %v", err) t.Fatalf("expected unknown field error, got: %v", err)
} }
@@ -980,10 +979,10 @@ func TestDecoderMaxDepth(t *testing.T) {
var cfg struct { var cfg struct {
V any `toml:"v"` V any `toml:"v"`
} }
if err := NewDecoder().MaxDepth(4).Decode(deep(4), &cfg); err != nil { if err := Unmarshal(deep(4), &cfg, MaxNestingDepth(4)); err != nil {
t.Fatalf("at the limit: %v", err) t.Fatalf("at the limit: %v", err)
} }
err := NewDecoder().MaxDepth(4).Decode(deep(5), &cfg) err := Unmarshal(deep(5), &cfg, MaxNestingDepth(4))
if err == nil { if err == nil {
t.Fatal("expected a nesting error") t.Fatal("expected a nesting error")
} }
@@ -997,10 +996,10 @@ func TestDecoderMaxInputSize(t *testing.T) {
var cfg struct { var cfg struct {
V string `toml:"v"` V string `toml:"v"`
} }
if err := NewDecoder().MaxInputSize(len(doc)).Decode(doc, &cfg); err != nil { if err := Unmarshal(doc, &cfg, MaxInputSize(len(doc))); err != nil {
t.Fatalf("at the limit: %v", err) t.Fatalf("at the limit: %v", err)
} }
err := NewDecoder().MaxInputSize(len(doc)-1).Decode(doc, &cfg) err := Unmarshal(doc, &cfg, MaxInputSize(len(doc)-1))
if err == nil { if err == nil {
t.Fatal("expected a size error") t.Fatal("expected a size error")
} }
@@ -1112,7 +1111,7 @@ frac = 2.5
`) `)
t.Run("the tree keeps the literal", func(t *testing.T) { t.Run("the tree keeps the literal", func(t *testing.T) {
var tree map[string]any var tree map[string]any
if err := NewDecoder().UseNumber().Decode(data, &tree); err != nil { if err := Unmarshal(data, &tree, NumbersAsLiterals(true)); err != nil {
t.Fatal(err) t.Fatal(err)
} }
for lit, key := range map[string]string{ for lit, key := range map[string]string{
@@ -1136,8 +1135,7 @@ frac = 2.5
Frac float64 `toml:"frac"` Frac float64 `toml:"frac"`
Rate time.Duration Rate time.Duration
} }
dec := NewDecoder().UseNumber() if err := Unmarshal([]byte("hex = 0x1f\nplain = 42\nfrac = 2.5\nRate = 1_000\n"), &cfg, NumbersAsLiterals(true)); err != nil {
if err := dec.Decode([]byte("hex = 0x1f\nplain = 42\nfrac = 2.5\nRate = 1_000\n"), &cfg); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if cfg.Hex != "0x1f" { if cfg.Hex != "0x1f" {
@@ -1156,14 +1154,14 @@ frac = 2.5
t.Run("invalid numbers are still parse errors", func(t *testing.T) { t.Run("invalid numbers are still parse errors", func(t *testing.T) {
for _, in := range []string{"a = 01\n", "a = 1__0\n", "a = 1x\n"} { for _, in := range []string{"a = 01\n", "a = 1__0\n", "a = 1x\n"} {
var tree map[string]any var tree map[string]any
if err := NewDecoder().UseNumber().Decode([]byte(in), &tree); err == nil { if err := Unmarshal([]byte(in), &tree, NumbersAsLiterals(true)); err == nil {
t.Errorf("%q decoded without an error", in) t.Errorf("%q decoded without an error", in)
} }
} }
}) })
t.Run("without UseNumber the tree holds the evaluated kinds", func(t *testing.T) { t.Run("without UseNumber the tree holds the evaluated kinds", func(t *testing.T) {
var tree map[string]any var tree map[string]any
if err := NewDecoder().Decode([]byte("hex = 0x1f\nfrac = 2.5\n"), &tree); err != nil { if err := Unmarshal([]byte("hex = 0x1f\nfrac = 2.5\n"), &tree); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if v, ok := tree["hex"].(int64); !ok || v != 31 { if v, ok := tree["hex"].(int64); !ok || v != 31 {
@@ -1507,9 +1505,8 @@ func TestLocalTimeLocation(t *testing.T) {
Date LocalDate `toml:"date"` Date LocalDate `toml:"date"`
Wall LocalDateTime `toml:"wall"` Wall LocalDateTime `toml:"wall"`
} }
dec := NewDecoder().LocalTimeLocation(zone)
in := []byte("when = 1979-05-27T07:32:00\ndate = 1979-05-27\nwall = 1979-05-27T07:32:00\n") in := []byte("when = 1979-05-27T07:32:00\ndate = 1979-05-27\nwall = 1979-05-27T07:32:00\n")
if err := dec.Decode(in, &cfg); err != nil { if err := Unmarshal(in, &cfg, LocalTimeLocation(zone)); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if got := cfg.When.Format("15:04:05 MST"); got != "07:32:00 CET" { if got := cfg.When.Format("15:04:05 MST"); got != "07:32:00 CET" {
@@ -1523,8 +1520,7 @@ func TestLocalTimeLocation(t *testing.T) {
var cfg struct { var cfg struct {
Wall LocalDateTime `toml:"wall"` Wall LocalDateTime `toml:"wall"`
} }
dec := NewDecoder().LocalTimeLocation(zone) if err := Unmarshal([]byte("wall = 1979-05-27T07:32:00\n"), &cfg, LocalTimeLocation(zone)); err != nil {
if err := dec.Decode([]byte("wall = 1979-05-27T07:32:00\n"), &cfg); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if cfg.Wall.Hour() != 7 { if cfg.Wall.Hour() != 7 {
@@ -1567,7 +1563,7 @@ func TestCancelInsideValue(t *testing.T) {
b.WriteString("]\n") b.WriteString("]\n")
ctx := &errAfterN{Context: context.Background(), n: 2} ctx := &errAfterN{Context: context.Background(), n: 2}
var tree map[string]any var tree map[string]any
err := NewDecoder().DecodeContext(ctx, []byte(b.String()), &tree) err := UnmarshalContext(ctx, []byte(b.String()), &tree)
if err == nil { if err == nil {
t.Fatal("a cancelled context did not stop the parse inside the value") t.Fatal("a cancelled context did not stop the parse inside the value")
} }
@@ -1585,7 +1581,7 @@ func TestCancellationSynctest(t *testing.T) {
cancel() cancel()
start := time.Now() start := time.Now()
var tree map[string]any var tree map[string]any
err := NewDecoder().DecodeContext(ctx, []byte("a = 1\n"), &tree) err := UnmarshalContext(ctx, []byte("a = 1\n"), &tree)
if !errors.Is(err, context.Canceled) { if !errors.Is(err, context.Canceled) {
t.Errorf("err = %v, want context.Canceled", err) t.Errorf("err = %v, want context.Canceled", err)
} }
@@ -1644,7 +1640,7 @@ func TestErrorMessagesGolden(t *testing.T) {
} }
b.WriteString("\n") b.WriteString("\n")
var tree map[string]any var tree map[string]any
err := NewDecoder().MaxDepth(10).Decode([]byte(b.String()), &tree) err := Unmarshal([]byte(b.String()), &tree, MaxNestingDepth(10))
return err return err
}, },
want: "interpres: line 1: nesting exceeds the limit of 10", want: "interpres: line 1: nesting exceeds the limit of 10",
@@ -1663,7 +1659,7 @@ func TestErrorMessagesGolden(t *testing.T) {
var cfg struct { var cfg struct {
Known int `toml:"known"` Known int `toml:"known"`
} }
return NewDecoder().DisallowUnknownFields().Decode([]byte("mystery = 1\n"), &cfg) return Unmarshal([]byte("mystery = 1\n"), &cfg, RejectUnknownFields(true))
}, },
want: `interpres: unknown field "mystery" for struct { Known int "toml:\"known\"" }`, want: `interpres: unknown field "mystery" for struct { Known int "toml:\"known\"" }`,
}, },
+91 -71
View File
@@ -73,12 +73,37 @@ if err := interpres.Valid(data); err != nil {
} }
``` ```
### `func UnmarshalWithOptions(data []byte, v any, opts DecodeOptions) error` ### Options
The one-shot form of a configured `Decoder`: the same options as The decode and encode calls take variadic options, the shape
`NewDecoder` sets, in a `DecodeOptions` struct, applied to a single call. encoding/json/v2 uses for its own. Each is a function value over the private
The zero value takes the defaults: unknown keys ignored, numbers evaluated settings of one call, and they compose by listing:
as `int64` and `float64`, no size limit and the 10000-level nesting default.
```go
cfg, err := interpres.Unmarshal(data, &cfg2,
interpres.RejectUnknownFields(true),
interpres.NumbersAsLiterals(true))
```
Decode options:
| Option | Default | Effect |
|---|---|---|
| `RejectUnknownFields(v bool)` | off | a key with no matching struct field is an error |
| `NumbersAsLiterals(v bool)` | off | integers and floats decode into `Number`, which carries the literal; see [Numbers as literals](#numbers-as-literals) |
| `MaxNestingDepth(depth int)` | `10000` | bound how deeply arrays and inline tables may nest |
| `MaxInputSize(size int)` | no limit | bound the size of the document, in bytes |
| `LocalTimeLocation(loc)` | nil | the zone a local date-time is carried in when it decodes into a `time.Time` |
Encode options:
| Option | Default | Effect |
|---|---|---|
| `Layout(kind LayoutKind)` | `LayoutKindGrouped` | group entries as scalars, then sub-tables, then arrays of tables; `LayoutKindDeclaration` preserves declaration order |
| `OmitEmptyArrays(v bool)` | off | skip `key = []` for empty scalar arrays |
| `LiteralMultiline(threshold int)` | `0` | emit multi-line strings of at least `threshold` bytes as literal `'''...'''` |
| `InlineTables(threshold int)` | `0` | write a sub-table inline when its single-line form is at most `threshold` bytes |
| `EmitFieldComments(v bool)` | off | print the `comment=` tag option of a field above its line or header |
### `func ParseAs[T any](data []byte) (T, error)` ### `func ParseAs[T any](data []byte) (T, error)`
@@ -261,13 +286,13 @@ shape `json.MarshalAppend` has. A failed encoding leaves `buf` untouched.
When decoding into a struct, these values convert onto the destination's When decoding into a struct, these values convert onto the destination's
concrete types: any integer or unsigned width, floats, slices, nested structs concrete types: any integer or unsigned width, floats, slices, nested structs
and `map[string]T`. `Decoder.UseNumber` replaces the two numeric rows of the and `map[string]T`. `NumbersAsLiterals` replaces the two numeric rows of the
table with `Number`, which keeps the literal; see table with `Number`, which keeps the literal; see
[Numbers as literals](#numbers-as-literals). [Numbers as literals](#numbers-as-literals).
### Target constraints ### Target constraints
`Unmarshal` and `(*Decoder).Decode` write into a non-nil pointer: `Unmarshal`, `UnmarshalRead` and `UnmarshalContext` write into a non-nil pointer:
- `*struct`, matched per the field rules below - `*struct`, matched per the field rules below
- `*map[string]any` or `*map[string]T`, keys become map keys and values decode - `*map[string]any` or `*map[string]T`, keys become map keys and values decode
@@ -329,7 +354,7 @@ offending key or index, for example `p: interpres: integer 300 overflows uint8`.
### Numbers as literals ### Numbers as literals
`NewDecoder().UseNumber()` decodes every integer and float into `Number`, a `NumbersAsLiterals(true)` decodes every integer and float into `Number`, a
string type that carries the literal the document wrote: `0x1f`, `1_000`, string type that carries the literal the document wrote: `0x1f`, `1_000`,
`+1.0`, `inf`. The shape is validated as strictly as ever, so `01` and `1__0` `+1.0`, `inf`. The shape is validated as strictly as ever, so `01` and `1__0`
remain parse errors; only the evaluated value is replaced by the literal. A remain parse errors; only the evaluated value is replaced by the literal. A
@@ -338,7 +363,7 @@ default tree normalises `0x1f` to `31` and `+1.0` to `1.0`.
```go ```go
var tree map[string]any var tree map[string]any
err := interpres.NewDecoder().UseNumber().Decode(data, &tree) err := interpres.Unmarshal(data, &tree, interpres.NumbersAsLiterals(true))
lit := tree["rate"].(interpres.Number) // "1_000" lit := tree["rate"].(interpres.Number) // "1_000"
``` ```
@@ -366,7 +391,7 @@ error. The date-time types take a bare timestamp and never a quoted string, so a
document that writes a date-time with quotes does not decode into them, and document that writes a date-time with quotes does not decode into them, and
neither `encoding.TextUnmarshaler` nor the embedded `time.Time` changes that. neither `encoding.TextUnmarshaler` nor the embedded `time.Time` changes that.
`NewDecoder().LocalTimeLocation(loc)` lets a local date-time fill a plain `LocalTimeLocation(loc)` lets a local date-time fill a plain
`time.Time` destination as well: the wall-clock value is carried in the `time.Time` destination as well: the wall-clock value is carried in the
location given, relabelled rather than shifted, so `07:32` in the document is location given, relabelled rather than shifted, so `07:32` in the document is
`07:32` in the zone. Without the option the wrapper types are the only `07:32` in the zone. Without the option the wrapper types are the only
@@ -475,13 +500,11 @@ as an ordinary inline table, whose keys are sorted.
### Strict decoding ### Strict decoding
By default unknown keys are dropped silently. A `Decoder` built with By default unknown keys are dropped silently. The `RejectUnknownFields`
`DisallowUnknownFields` rejects them instead: option rejects them instead:
```go ```go
err := interpres.NewDecoder(). err := interpres.Unmarshal(data, &cfg, interpres.RejectUnknownFields(true))
DisallowUnknownFields().
Decode(data, &cfg)
``` ```
A typo such as `database_urls` then fails with A typo such as `database_urls` then fails with
@@ -494,7 +517,7 @@ one, so it does not depend on map iteration order.
### Direct decoding ### Direct decoding
For a struct destination whose type graph carries no untagged embedded map and For a struct destination whose type graph carries no untagged embedded map and
no custom decode hook, `Unmarshal` and `(*Decoder).Decode` parse straight into no custom decode hook, the decode parses straight into
the destination: the table skeleton is resolved against the struct schema while the destination: the table skeleton is resolved against the struct schema while
the document scans, and no intermediate value tree is kept. Values still flow the document scans, and no intermediate value tree is kept. Values still flow
through the ordinary assignment rules, so every conversion, hook and error the through the ordinary assignment rules, so every conversion, hook and error the
@@ -510,7 +533,7 @@ observable behaviour is always the tree path's, exactly. Nothing changes for
### Cancellation ### Cancellation
`ParseContext`, `UnmarshalContext` and `(*Decoder).DecodeContext` accept a `ParseContext`, `UnmarshalContext` and `MarshalContext` accept a
`context.Context`. An already-cancelled context short-circuits with `context.Context`. An already-cancelled context short-circuits with
`context.Canceled` before any work begins; afterwards the context is checked `context.Canceled` before any work begins; afterwards the context is checked
every 64 top-level statements, and inside a value too: an array, an inline every 64 top-level statements, and inside a value too: an array, an inline
@@ -537,7 +560,7 @@ sequenceDiagram
### Input constraints ### Input constraints
`Marshal` and `(*Encoder).Marshal` accept a `struct`, a `map[string]V`, or a `Marshal` and `MarshalWrite` accept a `struct`, a `map[string]V`, or a
non-nil pointer to one, where `V` is any value `Marshal` itself understands. A non-nil pointer to one, where `V` is any value `Marshal` itself understands. A
different top-level value fails: different top-level value fails:
@@ -587,7 +610,7 @@ its key whether the table it came from was written inline or under a header.
tables is an error under `inline`, because the inline form would re-parse tables is an error under `inline`, because the inline form would re-parse
as a value array and change the value's Go type. as a value array and change the value's Go type.
- `comment=text` carries a comment for the field, which - `comment=text` carries a comment for the field, which
`NewEncoder().EmitFieldComments()` prints above the field's line or `EmitFieldComments(true)` prints above the field's line or
header, each line of a multi-line text with its own `# ` marker. Go doc header, each line of a multi-line text with its own `# ` marker. Go doc
comments are not visible to reflection, so the tag is the channel that comments are not visible to reflection, so the tag is the channel that
carries the text; without the encoder option the tag is ignored. carries the text; without the encoder option the tag is ignored.
@@ -628,7 +651,7 @@ parsed as keys of the sub-table.
instead, emitting each header immediately before its content: instead, emitting each header immediately before its content:
```go ```go
out, err := interpres.NewEncoder().Layout(interpres.LayoutKindDeclaration).Marshal(cfg) out, err := interpres.Marshal(cfg, interpres.Layout(interpres.LayoutKindDeclaration))
``` ```
The output remains parseable, but a scalar declared after a sub-table lands The output remains parseable, but a scalar declared after a sub-table lands
@@ -724,7 +747,7 @@ switches strings that contain a newline and are at least `threshold` bytes long
to the literal `'''...'''` form, which carries the newlines verbatim: to the literal `'''...'''` form, which carries the newlines verbatim:
```go ```go
out, err := interpres.NewEncoder().LiteralMultiline(80).Marshal(cfg) out, err := interpres.Marshal(cfg, interpres.LiteralMultiline(80))
``` ```
Single-line strings keep the basic form regardless of the threshold, and a Single-line strings keep the basic form regardless of the threshold, and a
@@ -758,7 +781,7 @@ single-line rendering is at most `threshold` bytes, and as a table header
section when it is longer. A document of small tables therefore grows shorter: section when it is longer. A document of small tables therefore grows shorter:
```go ```go
out, err := interpres.NewEncoder().InlineTables(60).Marshal(cfg) out, err := interpres.Marshal(cfg, interpres.InlineTables(60))
``` ```
With `60` and a table of three short entries, the same value is written With `60` and a table of three short entries, the same value is written
@@ -777,7 +800,7 @@ header is not read back as part of that header's section.
### Cancellation ### Cancellation
`MarshalContext` and `(*Encoder).MarshalContext` accept a `context.Context`. The `MarshalContext` accepts a `context.Context`. The
context is checked before any work and every 64 fields during the reflection context is checked before any work and every 64 fields during the reflection
walk. walk.
@@ -814,24 +837,29 @@ sequenceDiagram
Marshal-->>Caller: bytes, error Marshal-->>Caller: bytes, error
``` ```
## Coming from encoding/json ## Coming from encoding/json and encoding/json/v2
The API follows the shapes encoding/json made familiar, with the differences The API follows the shapes encoding/json made familiar and the option style
TOML asks for: encoding/json/v2 made current, with the differences TOML asks for:
| encoding/json | interpres | Notes | | encoding/json or encoding/json/v2 | interpres | Notes |
|---|---|---| |---|---|---|
| `json.Unmarshal(data, v)` | `Unmarshal(data, v)` | the same shape; the value mapping is TOML's | | `json.Unmarshal(data, v)` | `Unmarshal(data, v)` | the same shape; the value mapping is TOML's |
| `json.Marshal(v)` | `Marshal(v)` | the same shape; the output is TOML 1.1 | | `json.Marshal(v)` | `Marshal(v)` | the same shape; the output is TOML 1.1 |
| `json.MarshalAppend(buf, v)` | `MarshalAppend(buf, v)` | the same shape | | `json.MarshalAppend(buf, v)` | `MarshalAppend(buf, v)` | the same shape, options included |
| `(*json.Decoder).DisallowUnknownFields` | `(*Decoder).DisallowUnknownFields` | the same effect; the one-shot form is `UnmarshalWithOptions` | | `json.MarshalWrite(w, v)` | `MarshalWrite(w, v)` | the same shape, options included |
| `json.Number`, `(*json.Decoder).UseNumber` | `Number`, `(*Decoder).UseNumber` | the TOML literal carries its radix and separators, so `0x1f` stays `0x1f` | | `json.UnmarshalRead(r, v)` | `UnmarshalRead(r, v)` | the same shape, options included |
| `json/v2 RejectUnknownMembers` | `RejectUnknownFields(true)` | the same effect under TOML vocabulary |
| `(*json.Decoder).DisallowUnknownFields` | `RejectUnknownFields(true)` | the variadic option replaces the stateful decoder |
| `json.Number`, `StringifyNumbers` | `Number`, `NumbersAsLiterals(true)` | the TOML literal carries its radix and separators, so `0x1f` stays `0x1f` |
| `json/v2 MarshalOptions` fields | `MarshalOption` values | `Layout`, `OmitEmptyArrays`, `LiteralMultiline`, `InlineTables`, `EmitFieldComments` |
| `json/v2 JoinOptions` | listing | options compose by listing them in the call |
| `json.MarshalIndent` | none | TOML is the presentation format; the `-json` mode of interpres-decode prints plain JSON | | `json.MarshalIndent` | none | TOML is the presentation format; the `-json` mode of interpres-decode prints plain JSON |
| tag `json:"name,omitempty"` | tag `toml:"name,omitempty"` | the empty-value rules match encoding/json as of 2.0 | | tag `json:"name,omitempty"` | tag `toml:"name,omitempty"` | the empty-value rules match encoding/json as of 2.0 |
| tag `json:"name,omitzero"` | tag `toml:"name,omitzero"` | the same, `IsZero()` honoured | | tag `json:"name,omitzero"` | tag `toml:"name,omitzero"` | the same, `IsZero()` honoured |
| tag `json:"name,inline"` (v2) | tag `toml:"name,inline"` | forces the inline table form on encode | | tag `json:"name,inline"` (v2) | tag `toml:"name,inline"` | forces the inline table form on encode |
| `json.Marshaler` (`MarshalJSON`) | `Marshaler` (`MarshalTOML`) | the TOML method returns a value the encoder renders, not bytes | | `json/v2 Marshalers` | `Marshaler` (`MarshalTOML`) | the TOML method returns a value the encoder renders, not bytes |
| `json.Unmarshaler` (`UnmarshalJSON`) | `Unmarshaler` (`UnmarshalTOML`) | the data arrives decoded, not as bytes | | `json/v2 Unmarshalers` | `Unmarshaler` (`UnmarshalTOML`) | the data arrives decoded, not as bytes |
| `encoding.TextMarshaler`, `TextUnmarshaler` | honoured, the same | a type that renders itself as text becomes a TOML string, both ways | | `encoding.TextMarshaler`, `TextUnmarshaler` | honoured, the same | a type that renders itself as text becomes a TOML string, both ways |
| `*json.UnmarshalTypeError` | `*DecodeError` | the path is segments with a `String()` renderer, not a dotted string | | `*json.UnmarshalTypeError` | `*DecodeError` | the path is segments with a `String()` renderer, not a dotted string |
| `*json.SyntaxError` | `*SyntaxError` | the TOML error adds the byte `Offset` and the `Column` to the line | | `*json.SyntaxError` | `*SyntaxError` | the TOML error adds the byte `Offset` and the `Column` to the line |
@@ -887,21 +915,40 @@ The path both error wrappers carry, one segment per level from the document
root. `String()` renders the TOML notation: keys join with dots, an index root. `String()` renders the TOML notation: keys join with dots, an index
attaches to the previous segment in brackets, `items[0].weight`. attaches to the previous segment in brackets, `items[0].weight`.
### `type Decoder` ### Options
Configurable strictness for decoding, constructed with `NewDecoder`. Set up The decode and encode entries take variadic options, the shape
with the chainable methods, then call `Decode` or `DecodeContext` any number encoding/json/v2 uses for its own. Each option is a stateless function value
of times. A configured `Decoder` holds no per-call state and is safe for over the private settings of one call; they compose by listing in the call,
concurrent use. For a single document, `UnmarshalWithOptions(data, v, and there is no stateful Decoder or Encoder to share or guard.
DecodeOptions{...})` sets the same options without the Decoder; its zero
value takes the defaults.
| Method | Default | Effect | Decode options:
| Option | Default | Effect |
|---|---|---| |---|---|---|
| `DisallowUnknownFields()` | off | a key with no matching struct field is an error | | `RejectUnknownFields(v bool)` | off | a key with no matching struct field is an error |
| `UseNumber()` | off | integers and floats decode into `Number`, which carries the literal; see [Numbers as literals](#numbers-as-literals) | | `NumbersAsLiterals(v bool)` | off | integers and floats decode into `Number`, which carries the literal; see [Numbers as literals](#numbers-as-literals) |
| `MaxDepth(depth int)` | `10000` | bound how deeply arrays and inline tables may nest | | `MaxNestingDepth(depth int)` | `10000` | bound how deeply arrays and inline tables may nest |
| `MaxInputSize(size int)` | no limit | bound the size of the document, in bytes | | `MaxInputSize(size int)` | no limit | bound the size of the document, in bytes |
| `LocalTimeLocation(loc)` | nil | the zone a local date-time is carried in when it decodes into a `time.Time` |
Encode options:
| Option | Default | Effect |
|---|---|---|
| `Layout(kind LayoutKind)` | `LayoutKindGrouped` | group entries as scalars, then sub-tables, then arrays of tables; `LayoutKindDeclaration` preserves declaration order |
| `OmitEmptyArrays(v bool)` | off | skip `key = []` for empty scalar arrays |
| `LiteralMultiline(threshold int)` | `0` | emit multi-line strings of at least `threshold` bytes as literal `'''...'''` |
| `InlineTables(threshold int)` | `0` | write a sub-table inline when its single-line form is at most `threshold` bytes |
| `EmitFieldComments(v bool)` | off | print the `comment=` tag option of a field above its line or header |
```go
out, err := interpres.MarshalContext(ctx, cfg,
interpres.Layout(interpres.LayoutKindDeclaration),
interpres.OmitEmptyArrays(true),
interpres.LiteralMultiline(80),
interpres.InlineTables(60))
```
The nesting limit protects the stack, because the parser is a recursive The nesting limit protects the stack, because the parser is a recursive
descent: a deeper document is rejected with a `SyntaxError` naming the limit descent: a deeper document is rejected with a `SyntaxError` naming the limit
@@ -910,33 +957,6 @@ default but take no options. The size limit is off by default, because the
caller already holds the bytes and the size is therefore a policy, not a caller already holds the bytes and the size is therefore a policy, not a
protection the library can impose on its own. protection the library can impose on its own.
### `type Encoder`
Configurable emission policy, constructed with `NewEncoder`. The option state
is private; set it with the chainable methods, each of which returns the
encoder:
| Method | Default | Effect |
|---|---|---|
| `Layout(kind LayoutKind)` | `LayoutKindGrouped` | group entries as scalars, then sub-tables, then arrays of tables; `LayoutKindDeclaration` preserves declaration order |
| `OmitEmptyArrays()` | off | skip `key = []` for empty scalar arrays |
| `LiteralMultiline(threshold int)` | `0` | emit multi-line strings of at least `threshold` bytes as literal `'''...'''` |
| `InlineTables(threshold int)` | `0` | write a sub-table inline when its single-line form is at most `threshold` bytes |
| `EmitFieldComments()` | off | print the `comment=` tag option of a field above its line or header |
```go
out, err := interpres.NewEncoder().
Layout(interpres.LayoutKindDeclaration).
OmitEmptyArrays().
LiteralMultiline(80).
InlineTables(60).
MarshalContext(ctx, cfg)
```
A configured `Encoder` holds no per-call state; each `Marshal` or
`MarshalContext` call copies the options and is safe for concurrent use, as
long as no setter races with a call.
### `type Document`, `type Table`, `type Entry` ### `type Document`, `type Table`, `type Entry`
See [Documents](#documents). A `Document` is what `Parse` returns, and See [Documents](#documents). A `Document` is what `Parse` returns, and
@@ -956,7 +976,7 @@ wins when a type implements both.
### `type Number string` ### `type Number string`
The literal a number was written with, what `UseNumber` decodes into and what The literal a number was written with, what `NumbersAsLiterals` decodes into and what
`Marshal` writes back as it is. See `Marshal` writes back as it is. See
[Numbers as literals](#numbers-as-literals). [Numbers as literals](#numbers-as-literals).
+2 -2
View File
@@ -11,9 +11,9 @@ The benchmarks live in `bench_test.go`, next to the code they measure:
|---|---| |---|---|
| `BenchmarkParse` | `ParseMap` over a representative configuration document | | `BenchmarkParse` | `ParseMap` over a representative configuration document |
| `BenchmarkMarshal` | `Marshal` of the tree `ParseMap` produced from the same document | | `BenchmarkMarshal` | `Marshal` of the tree `ParseMap` produced from the same document |
| `BenchmarkStrictDecode` | `Decode` into a struct under `DisallowUnknownFields` | | `BenchmarkStrictDecode` | `Unmarshal` into a struct under `RejectUnknownFields` (the targeted parse) |
| `BenchmarkParseLong` | `ParseMap` over a generated document with about 2000 array-of-tables entries | | `BenchmarkParseLong` | `ParseMap` over a generated document with about 2000 array-of-tables entries |
| `BenchmarkStrictDecodeLong` | `Decode` into a typed document under `DisallowUnknownFields`, over the same long document | | `BenchmarkStrictDecodeLong` | `Unmarshal` into a typed document under `RejectUnknownFields`, over the same long document |
| `BenchmarkMarshalLong` | `Marshal` of the tree `ParseMap` produced from the long document | | `BenchmarkMarshalLong` | `Marshal` of the tree `ParseMap` produced from the long document |
## Running ## Running
+12 -2
View File
@@ -111,12 +111,22 @@ func getEncoderBuf() *bytes.Buffer {
return b return b
} }
// encodeConfig carries the public emission options one Marshal call applies;
// the names and defaults are documented on the MarshalOption constructors.
type encodeConfig struct {
layout LayoutKind
omitEmptyArrays bool
literalMultilineAt int
inlineTablesAt int
emitFieldComments bool
}
// encoder produces a TOML document from a Go value via a small intermediate // encoder produces a TOML document from a Go value via a small intermediate
// representation that preserves the order in which fields were declared. // representation that preserves the order in which fields were declared.
type encoder struct { type encoder struct {
buf *bytes.Buffer buf *bytes.Buffer
ctx context.Context ctx context.Context
opts Encoder opts encodeConfig
// inlineDepth is the nesting level inside inline tables, which decides // inlineDepth is the nesting level inside inline tables, which decides
// their indentation. // their indentation.
@@ -329,7 +339,7 @@ type entry struct {
type tomlDoc struct { type tomlDoc struct {
entries []entry entries []entry
ctx context.Context // inherited from encoder; nil-safe ctx context.Context // inherited from encoder; nil-safe
opts Encoder // inherited from encoder; options drive emit-time behaviour opts encodeConfig // inherited from encoder; options drive emit-time behaviour
depth int depth int
} }
+30 -34
View File
@@ -91,9 +91,6 @@ func TestMarshalContextHonoursCancellation(t *testing.T) {
if _, err := MarshalContext(ctx, C{A: 1}); !errors.Is(err, context.Canceled) { if _, err := MarshalContext(ctx, C{A: 1}); !errors.Is(err, context.Canceled) {
t.Fatalf("MarshalContext returned %v, want context.Canceled", err) t.Fatalf("MarshalContext returned %v, want context.Canceled", err)
} }
if _, err := NewEncoder().MarshalContext(ctx, C{A: 1}); !errors.Is(err, context.Canceled) {
t.Fatalf("Encoder.MarshalContext returned %v, want context.Canceled", err)
}
} }
func TestEncoderLayoutGroupedDefault(t *testing.T) { func TestEncoderLayoutGroupedDefault(t *testing.T) {
@@ -105,7 +102,7 @@ func TestEncoderLayoutGroupedDefault(t *testing.T) {
Host string `toml:"host"` Host string `toml:"host"`
} `toml:"s"` } `toml:"s"`
} }
out, err := NewEncoder().Marshal(Cfg{Name: "x", S: struct { out, err := Marshal(Cfg{Name: "x", S: struct {
Host string `toml:"host"` Host string `toml:"host"`
}{Host: "h"}}) }{Host: "h"}})
if err != nil { if err != nil {
@@ -131,7 +128,7 @@ func TestEncoderLayoutDeclarationPreservesOrder(t *testing.T) {
Server: Inner{Host: "h"}, Server: Inner{Host: "h"},
Debug: true, Debug: true,
} }
out, err := NewEncoder().Layout(LayoutKindDeclaration).Marshal(in) out, err := Marshal(in, Layout(LayoutKindDeclaration))
if err != nil { if err != nil {
t.Fatalf("marshal: %v", err) t.Fatalf("marshal: %v", err)
} }
@@ -161,7 +158,7 @@ func TestEncoderLayoutGroupedDefaultOrder(t *testing.T) {
Server: Inner{Host: "h"}, Server: Inner{Host: "h"},
Debug: true, Debug: true,
} }
out, err := NewEncoder().Marshal(in) out, err := Marshal(in)
if err != nil { if err != nil {
t.Fatalf("marshal: %v", err) t.Fatalf("marshal: %v", err)
} }
@@ -176,10 +173,10 @@ func TestEncoderOmitEmptyArrays(t *testing.T) {
Tags []string `toml:"tags"` Tags []string `toml:"tags"`
Secrets []string `toml:"secrets"` Secrets []string `toml:"secrets"`
} }
out, err := NewEncoder().OmitEmptyArrays().Marshal(Cfg{ out, err := Marshal(Cfg{
Tags: []string{"a", "b"}, Tags: []string{"a", "b"},
Secrets: []string{}, Secrets: []string{},
}) }, OmitEmptyArrays(true))
if err != nil { if err != nil {
t.Fatalf("marshal: %v", err) t.Fatalf("marshal: %v", err)
} }
@@ -193,7 +190,7 @@ func TestEncoderDefaultEmitsEmptyArray(t *testing.T) {
type Cfg struct { type Cfg struct {
Tags []string `toml:"tags"` Tags []string `toml:"tags"`
} }
out, err := NewEncoder().Marshal(Cfg{Tags: []string{}}) out, err := Marshal(Cfg{Tags: []string{}})
if err != nil { if err != nil {
t.Fatalf("marshal: %v", err) t.Fatalf("marshal: %v", err)
} }
@@ -211,10 +208,10 @@ func TestEncoderOmitEmptyArrayOfTablesStillSkipped(t *testing.T) {
Title string `toml:"title"` Title string `toml:"title"`
Items []Item `toml:"items"` Items []Item `toml:"items"`
} }
out, err := NewEncoder().OmitEmptyArrays().Marshal(Cfg{ out, err := Marshal(Cfg{
Title: "demo", Title: "demo",
Items: nil, Items: nil,
}) }, OmitEmptyArrays(true))
if err != nil { if err != nil {
t.Fatalf("marshal: %v", err) t.Fatalf("marshal: %v", err)
} }
@@ -229,7 +226,7 @@ func TestEncoderLiteralMultiline(t *testing.T) {
Long string `toml:"long"` Long string `toml:"long"`
} }
long := strings.Repeat("a", 50) + "\nline two\nline three" long := strings.Repeat("a", 50) + "\nline two\nline three"
out, err := NewEncoder().LiteralMultiline(20).Marshal(Cfg{Long: long}) out, err := Marshal(Cfg{Long: long}, LiteralMultiline(20))
if err != nil { if err != nil {
t.Fatalf("marshal: %v", err) t.Fatalf("marshal: %v", err)
} }
@@ -244,7 +241,7 @@ func TestEncoderLiteralMultilineBelowThreshold(t *testing.T) {
type Cfg struct { type Cfg struct {
Short string `toml:"short"` Short string `toml:"short"`
} }
out, err := NewEncoder().LiteralMultiline(1000).Marshal(Cfg{Short: "one\ntwo"}) out, err := Marshal(Cfg{Short: "one\ntwo"}, LiteralMultiline(1000))
if err != nil { if err != nil {
t.Fatalf("marshal: %v", err) t.Fatalf("marshal: %v", err)
} }
@@ -259,7 +256,7 @@ func TestEncoderLiteralMultilineThresholdZero(t *testing.T) {
type Cfg struct { type Cfg struct {
S string `toml:"s"` S string `toml:"s"`
} }
out, err := NewEncoder().LiteralMultiline(0).Marshal(Cfg{S: "a\nb\nc\nd"}) out, err := Marshal(Cfg{S: "a\nb\nc\nd"}, LiteralMultiline(0))
if err != nil { if err != nil {
t.Fatalf("marshal: %v", err) t.Fatalf("marshal: %v", err)
} }
@@ -282,7 +279,7 @@ func TestEncoderLiteralMultilineFallsBackWhenUnsafe(t *testing.T) {
{"lone carriage return", "first\rsecond\nthird"}, {"lone carriage return", "first\rsecond\nthird"},
} }
for _, c := range cases { for _, c := range cases {
out, err := NewEncoder().LiteralMultiline(5).Marshal(map[string]any{"s": c.in}) out, err := Marshal(map[string]any{"s": c.in}, LiteralMultiline(5))
if err != nil { if err != nil {
t.Fatalf("%s: marshal: %v", c.name, err) t.Fatalf("%s: marshal: %v", c.name, err)
} }
@@ -482,11 +479,10 @@ func TestEncoderChainedOptions(t *testing.T) {
I Inner `toml:"i"` I Inner `toml:"i"`
} }
long := strings.Repeat("x", 200) long := strings.Repeat("x", 200)
out, err := NewEncoder(). out, err := Marshal(Cfg{S: "short", I: Inner{V: long}},
Layout(LayoutKindDeclaration). Layout(LayoutKindDeclaration),
OmitEmptyArrays(). OmitEmptyArrays(true),
LiteralMultiline(50). LiteralMultiline(50))
Marshal(Cfg{S: "short", I: Inner{V: long}})
if err != nil { if err != nil {
t.Fatalf("marshal: %v", err) t.Fatalf("marshal: %v", err)
} }
@@ -1302,7 +1298,7 @@ func TestEncoderEquivalenceToMarshal(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("marshal: %v", err) t.Fatalf("marshal: %v", err)
} }
b, err := NewEncoder().Marshal(in) b, err := Marshal(in)
if err != nil { if err != nil {
t.Fatalf("encoder marshal: %v", err) t.Fatalf("encoder marshal: %v", err)
} }
@@ -1710,7 +1706,7 @@ func TestEncoderInlineTables(t *testing.T) {
// With the option both fit the threshold and become inline tables, nested // With the option both fit the threshold and become inline tables, nested
// ones included. // ones included.
out, err := NewEncoder().InlineTables(60).Marshal(cfg) out, err := Marshal(cfg, InlineTables(60))
if err != nil { if err != nil {
t.Fatalf("marshal: %v", err) t.Fatalf("marshal: %v", err)
} }
@@ -1720,7 +1716,7 @@ func TestEncoderInlineTables(t *testing.T) {
} }
// A threshold below the rendering keeps the header form. // A threshold below the rendering keeps the header form.
out, err = NewEncoder().InlineTables(10).Marshal(cfg) out, err = Marshal(cfg, InlineTables(10))
if err != nil { if err != nil {
t.Fatalf("marshal: %v", err) t.Fatalf("marshal: %v", err)
} }
@@ -1749,7 +1745,7 @@ func TestEncoderInlineTablesOrderAndRoundTrip(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("marshal: %v", err) t.Fatalf("marshal: %v", err)
} }
compact, err := NewEncoder().InlineTables(20).Marshal(cfg) compact, err := Marshal(cfg, InlineTables(20))
if err != nil { if err != nil {
t.Fatalf("marshal: %v", err) t.Fatalf("marshal: %v", err)
} }
@@ -1785,7 +1781,7 @@ func TestEncoderInlineTablesKeepsArraysOfTables(t *testing.T) {
Small inlineTLS `toml:"small"` Small inlineTLS `toml:"small"`
} }
cfg := Cfg{Items: []Item{{N: 1}}, Small: inlineTLS{On: true}} cfg := Cfg{Items: []Item{{N: 1}}, Small: inlineTLS{On: true}}
out, err := NewEncoder().InlineTables(60).Marshal(cfg) out, err := Marshal(cfg, InlineTables(60))
if err != nil { if err != nil {
t.Fatalf("marshal: %v", err) t.Fatalf("marshal: %v", err)
} }
@@ -1973,7 +1969,7 @@ func TestMarshalNumber(t *testing.T) {
t.Fatalf("output %q", out) t.Fatalf("output %q", out)
} }
var back map[string]any var back map[string]any
if err := NewDecoder().UseNumber().Decode(out, &back); err != nil { if err := Unmarshal(out, &back, NumbersAsLiterals(true)); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if got, ok := back["rate"].(Number); !ok || got != "1_000" { if got, ok := back["rate"].(Number); !ok || got != "1_000" {
@@ -2073,7 +2069,7 @@ func TestMarshalCyclicData(t *testing.T) {
}) })
} }
func TestUnmarshalWithOptions(t *testing.T) { func TestUnmarshalOptionsShape(t *testing.T) {
data := []byte("host = \"db\"\nextra = 1\n") data := []byte("host = \"db\"\nextra = 1\n")
type Config struct { type Config struct {
Host string `toml:"host,required"` Host string `toml:"host,required"`
@@ -2083,7 +2079,7 @@ func TestUnmarshalWithOptions(t *testing.T) {
Host string `toml:"host"` Host string `toml:"host"`
Extra int `toml:"extra"` Extra int `toml:"extra"`
} }
if err := UnmarshalWithOptions(data, &cfg, DecodeOptions{}); err != nil { if err := Unmarshal(data, &cfg); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if cfg.Host != "db" || cfg.Extra != 1 { if cfg.Host != "db" || cfg.Extra != 1 {
@@ -2091,7 +2087,7 @@ func TestUnmarshalWithOptions(t *testing.T) {
} }
}) })
t.Run("strict and required work in one call", func(t *testing.T) { t.Run("strict and required work in one call", func(t *testing.T) {
err := UnmarshalWithOptions(data, &Config{}, DecodeOptions{DisallowUnknownFields: true}) err := Unmarshal(data, &Config{}, RejectUnknownFields(true))
want := `interpres: unknown field "extra" for interpres.Config` want := `interpres: unknown field "extra" for interpres.Config`
if err == nil || err.Error() != want { if err == nil || err.Error() != want {
t.Errorf("err = %v, want %q", err, want) t.Errorf("err = %v, want %q", err, want)
@@ -2100,7 +2096,7 @@ func TestUnmarshalWithOptions(t *testing.T) {
t.Run("UseNumber keeps the literal", func(t *testing.T) { t.Run("UseNumber keeps the literal", func(t *testing.T) {
var tree map[string]any var tree map[string]any
in := []byte("n = 1_000\n") in := []byte("n = 1_000\n")
if err := UnmarshalWithOptions(in, &tree, DecodeOptions{UseNumber: true}); err != nil { if err := Unmarshal(in, &tree, NumbersAsLiterals(true)); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if got, ok := tree["n"].(Number); !ok || got != "1_000" { if got, ok := tree["n"].(Number); !ok || got != "1_000" {
@@ -2118,10 +2114,10 @@ func TestUnmarshalWithOptions(t *testing.T) {
nested.WriteString("]") nested.WriteString("]")
} }
var tree map[string]any var tree map[string]any
if err := UnmarshalWithOptions([]byte(nested.String()), &tree, DecodeOptions{MaxDepth: 10}); err == nil { if err := Unmarshal([]byte(nested.String()), &tree, MaxNestingDepth(10)); err == nil {
t.Error("a document over MaxDepth decoded, want an error") t.Error("a document over MaxDepth decoded, want an error")
} }
if err := UnmarshalWithOptions([]byte("a = 1\n"), &tree, DecodeOptions{MaxInputSize: 2}); err == nil { if err := Unmarshal([]byte("a = 1\n"), &tree, MaxInputSize(2)); err == nil {
t.Error("a document over MaxInputSize decoded, want an error") t.Error("a document over MaxInputSize decoded, want an error")
} }
}) })
@@ -2253,7 +2249,7 @@ func TestEmitFieldComments(t *testing.T) {
} }
}) })
t.Run("on, the comments print above their lines", func(t *testing.T) { t.Run("on, the comments print above their lines", func(t *testing.T) {
out, err := NewEncoder().EmitFieldComments().Marshal(cfg) out, err := Marshal(cfg, EmitFieldComments(true))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -2278,7 +2274,7 @@ func TestEmitFieldComments(t *testing.T) {
type Nested struct { type Nested struct {
Inner Inner `toml:"inner,comment=The inner table"` Inner Inner `toml:"inner,comment=The inner table"`
} }
out, err := NewEncoder().EmitFieldComments().Marshal(Nested{Inner: Inner{1}}) out, err := Marshal(Nested{Inner: Inner{1}}, EmitFieldComments(true))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
+9 -11
View File
@@ -70,12 +70,11 @@ func ExampleMarshal() {
// port = 9090 // port = 9090
} }
func ExampleDecoder() { func ExampleNumbersAsLiterals() {
var tree map[string]any var tree map[string]any
err := interpres.NewDecoder(). err := interpres.Unmarshal([]byte("rate = 1_000\n"), &tree,
DisallowUnknownFields(). interpres.RejectUnknownFields(true),
UseNumber(). interpres.NumbersAsLiterals(true))
Decode([]byte("rate = 1_000\n"), &tree)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@@ -83,16 +82,15 @@ func ExampleDecoder() {
// Output: 1_000 1_000 // Output: 1_000 1_000
} }
func ExampleEncoder() { func ExampleInlineTables() {
type Config struct { type Config struct {
Title string `toml:"title"` Title string `toml:"title"`
Extras map[string]string `toml:"extras,inline"` Extras map[string]string `toml:"extras,inline"`
} }
out, err := interpres.NewEncoder(). out, err := interpres.Marshal(Config{Title: "demo", Extras: map[string]string{"b": "two", "a": "one"}},
Layout(interpres.LayoutKindDeclaration). interpres.Layout(interpres.LayoutKindDeclaration),
LiteralMultiline(80). interpres.LiteralMultiline(80),
InlineTables(40). interpres.InlineTables(40))
Marshal(Config{Title: "demo", Extras: map[string]string{"b": "two", "a": "one"}})
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
+1 -1
View File
@@ -134,7 +134,7 @@ func Run(stdout, stderr io.Writer) int {
} }
fmt.Fprintf(stdout, "\n--- marshal (group by kind, default) ---\n%s", out) fmt.Fprintf(stdout, "\n--- marshal (group by kind, default) ---\n%s", out)
out2, err := interpres.NewEncoder().Layout(interpres.LayoutKindDeclaration).Marshal(cfg) out2, err := interpres.Marshal(cfg, interpres.Layout(interpres.LayoutKindDeclaration))
if err != nil { if err != nil {
fmt.Fprintln(stderr, "marshal:", err) fmt.Fprintln(stderr, "marshal:", err)
return 1 return 1
+1 -1
View File
@@ -109,7 +109,7 @@ func FuzzTargetedDecode(f *testing.F) {
f.Fuzz(func(t *testing.T, data []byte) { f.Fuzz(func(t *testing.T, data []byte) {
doc := fuzzDocument(data) doc := fuzzDocument(data)
var tgt fuzzDoc var tgt fuzzDoc
tgtErr := NewDecoder().Decode(doc, &tgt) tgtErr := Unmarshal(doc, &tgt)
if tgtErr != nil { if tgtErr != nil {
// A document with several decode-stage findings reports a different // A document with several decode-stage findings reports a different
// one per run (the tree decode walks its maps in random order), so the // one per run (the tree decode walks its maps in random order), so the
+169 -189
View File
@@ -17,7 +17,7 @@
// tree := doc.Map() // tree := doc.Map()
// //
// A Decoder allows strict decoding that rejects keys without a matching // A Decoder allows strict decoding that rejects keys without a matching
// struct field, mirroring (*json.Decoder).DisallowUnknownFields. // struct field, mirroring the RejectUnknownFields option of encoding/json/v2.
package interpres package interpres
import ( import (
@@ -259,6 +259,11 @@ func parseWithOptions(ctx context.Context, data []byte, opts parseOptions, wantD
// Unmarshal parses a TOML document and stores the result in the value pointed // Unmarshal parses a TOML document and stores the result in the value pointed
// to by v. v is typically a pointer to a struct or to a map[string]any. // to by v. v is typically a pointer to a struct or to a map[string]any.
// //
// Unmarshal parses a TOML document and stores the result in the value pointed
// to by v. v is typically a pointer to a struct or to a map[string]any.
// Options tune the call; with none, unknown keys are ignored, numbers are
// evaluated, and the nesting default applies.
//
// Struct fields are matched to TOML keys by the `toml:"name"` tag, or by a // Struct fields are matched to TOML keys by the `toml:"name"` tag, or by a
// case-insensitive match on the field name when no tag is present. A tag of // case-insensitive match on the field name when no tag is present. A tag of
// "-" skips the field. // "-" skips the field.
@@ -269,8 +274,8 @@ func parseWithOptions(ctx context.Context, data []byte, opts parseOptions, wantD
// bare integer as its nanosecond count. // bare integer as its nanosecond count.
// //
// Unmarshal is equivalent to UnmarshalContext with context.Background. // Unmarshal is equivalent to UnmarshalContext with context.Background.
func Unmarshal(data []byte, v any) error { func Unmarshal(data []byte, v any, opts ...UnmarshalOption) error {
return UnmarshalContext(context.Background(), data, v) return UnmarshalContext(context.Background(), data, v, opts...)
} }
// ParseAs decodes a TOML document into T in one call, the generic shorthand // ParseAs decodes a TOML document into T in one call, the generic shorthand
@@ -304,120 +309,76 @@ func NewSchema[T any]() {
} }
// UnmarshalContext is the cancellable variant of Unmarshal. // UnmarshalContext is the cancellable variant of Unmarshal.
func UnmarshalContext(ctx context.Context, data []byte, v any) error { func UnmarshalContext(ctx context.Context, data []byte, v any, opts ...UnmarshalOption) error {
dec := newDecoder() return settingsFor(opts).decode(ctx, data, v)
dec.ctx = ctx
if canTargetDecode(v) {
// The targeted parse fills struct destinations without the
// intermediate tree; a document or destination it cannot model falls
// back to the tree path, whose contracts it keeps.
if err := parseIntoTargeted(ctx, data, dec, false, 0, v); err != errTargetFallback {
return err
}
}
// Only a destination that can reach an OrderedMap needs the node tree the
// written key order is read from; every other decode skips building it.
tree, doc, err := parseWithOptions(ctx, data, parseOptions{}, typeWantsOrder(reflect.TypeOf(v)))
if err != nil {
return err
}
dec.nodes = indexNodes(doc.Root())
return dec.decode(tree, v)
} }
// A Decoder decodes a TOML document into a Go value with configurable // UnmarshalRead reads the document from r and decodes it into v, the
// strictness and configurable limits on the parse it performs. // streaming-shaped entry the json/v2 vocabulary uses. The reader is
type Decoder struct { // consumed in full, because the parser scans its source in place; the
// options and the behaviour are Unmarshal's.
func UnmarshalRead(r io.Reader, v any, opts ...UnmarshalOption) error {
data, err := io.ReadAll(r)
if err != nil {
return fmt.Errorf("interpres: read: %w", err)
}
return Unmarshal(data, v, opts...)
}
// An UnmarshalOption configures one Unmarshal, UnmarshalContext,
// UnmarshalRead or ParseAs call. Options are function values over the
// private decode settings, the shape encoding/json/v2 uses for its own
// options, and compose by simple listing:
//
// err := interpres.Unmarshal(data, &cfg,
// interpres.RejectUnknownFields(true),
// interpres.NumbersAsLiterals(true))
//
// A destination that the direct skeleton cannot model falls back to the
// tree path, so every option means the same thing on every document.
type UnmarshalOption func(*decodeSettings)
// decodeSettings is the option carrier of one decode call.
type decodeSettings struct {
disallowUnknown bool disallowUnknown bool
useNumber bool useNumber bool
maxDepth int maxDepth int
maxInputSize int maxInputSize int
localLoc *time.Location localLoc *time.Location
ctx context.Context
} }
// NewDecoder returns a Decoder. func settingsFor(opts []UnmarshalOption) *decodeSettings {
func NewDecoder() *Decoder { return &Decoder{} } s := &decodeSettings{ctx: context.Background()}
for _, opt := range opts {
// DisallowUnknownFields causes Decode to return an error when the document opt(s)
// contains a key with no matching destination struct field. }
func (d *Decoder) DisallowUnknownFields() *Decoder { return s
d.disallowUnknown = true
return d
} }
// UseNumber causes the numbers of the document to reach the value tree as a // decode runs the decode the settings describe: the targeted parse when the
// Number carrying the literal the document wrote, so 0x1f, 1_000, +1.0 and // destination takes it, the tree path otherwise or on fallback.
// inf survive a round trip with their spelling intact. A destination of a func (s *decodeSettings) decode(ctx context.Context, data []byte, v any) error {
// concrete numeric kind still takes the evaluated value; the literal is kept
// only where a Number, or an any, receives it.
func (d *Decoder) UseNumber() *Decoder {
d.useNumber = true
return d
}
// LocalTimeLocation sets the zone a local date-time is placed in when it
// decodes into a time.Time destination. Without the option a local date-time
// fills only its own wrapper type (LocalDateTime, LocalDate, LocalTime),
// whose embedded time.Time is UTC; with the option, a time.Time destination
// takes the value too, carried in the location given. A nil location restores
// the default.
func (d *Decoder) LocalTimeLocation(loc *time.Location) *Decoder {
d.localLoc = loc
return d
}
// MaxDepth bounds how deeply arrays and inline tables may nest in a document
// this decoder accepts. The parser is a recursive descent, so a document that
// nests without bound would exhaust the stack; one that nests deeper than the
// limit is rejected with a SyntaxError naming it instead. Use 0 or any
// negative value for the default of 10000, which no hand-written document
// approaches.
func (d *Decoder) MaxDepth(depth int) *Decoder {
d.maxDepth = depth
return d
}
// MaxInputSize bounds the size of a document this decoder accepts, in bytes; a
// larger one is rejected before parsing starts. Use 0 or any negative value for
// no limit, which is the default: the caller already holds the bytes, so the
// size is a policy the caller sets rather than a protection the library
// imposes on its own. Parse and ParseContext take no limit beyond the nesting
// default.
func (d *Decoder) MaxInputSize(size int) *Decoder {
d.maxInputSize = size
return d
}
// Decode parses data and stores the result in the value pointed to by v,
// honouring the decoder's strictness settings.
//
// Decode is equivalent to DecodeContext with context.Background.
func (d *Decoder) Decode(data []byte, v any) error {
return d.DecodeContext(context.Background(), data, v)
}
// DecodeContext is the cancellable variant of Decode.
func (d *Decoder) DecodeContext(ctx context.Context, data []byte, v any) error {
dec := newDecoder() dec := newDecoder()
dec.disallowUnknown = d.disallowUnknown dec.disallowUnknown = s.disallowUnknown
dec.ctx = ctx dec.ctx = ctx
dec.loc = d.localLoc dec.loc = s.localLoc
if canTargetDecode(v) { if canTargetDecode(v) {
// The targeted parse fills struct destinations without the // The targeted parse fills struct destinations without the
// intermediate tree; a document or destination it cannot model falls // intermediate tree; a document or destination it cannot model falls
// back to the tree path, whose contracts it keeps. The size limit is // back to the tree path, whose contracts it keeps. The size limit is
// checked here, the targeted parse being the parse itself. // checked here, the targeted parse being the parse itself.
if d.maxInputSize > 0 && len(data) > d.maxInputSize { if s.maxInputSize > 0 && len(data) > s.maxInputSize {
return fmt.Errorf("interpres: input is %d bytes, over the limit of %d", len(data), d.maxInputSize) return fmt.Errorf("interpres: input is %d bytes, over the limit of %d", len(data), s.maxInputSize)
} }
if err := parseIntoTargeted(ctx, data, dec, d.useNumber, d.maxDepth, v); err != errTargetFallback { if err := parseIntoTargeted(ctx, data, dec, s.useNumber, s.maxDepth, v); err != errTargetFallback {
return err return err
} }
} }
opts := parseOptions{ opts := parseOptions{
maxDepth: d.maxDepth, maxDepth: s.maxDepth,
maxInputSize: d.maxInputSize, maxInputSize: s.maxInputSize,
useNumber: d.useNumber, useNumber: s.useNumber,
} }
tree, doc, err := parseWithOptions(ctx, data, opts, typeWantsOrder(reflect.TypeOf(v))) tree, doc, err := parseWithOptions(ctx, data, opts, typeWantsOrder(reflect.TypeOf(v)))
if err != nil { if err != nil {
@@ -427,33 +388,50 @@ func (d *Decoder) DecodeContext(ctx context.Context, data []byte, v any) error {
return dec.decode(tree, v) return dec.decode(tree, v)
} }
// DecodeOptions gathers the options a one-shot decode call can set, the // RejectUnknownFields makes the decode fail when the document contains a
// struct-shaped alternative to building a Decoder for a single document. The // key with no matching destination struct field. Off by default: unknown
// zero value decodes with the defaults: unknown keys ignored, numbers // keys are ignored.
// evaluated, and no limit beyond the nesting default. func RejectUnknownFields(v bool) UnmarshalOption {
type DecodeOptions struct { return func(s *decodeSettings) { s.disallowUnknown = v }
// DisallowUnknownFields rejects a key with no matching struct field.
DisallowUnknownFields bool
// UseNumber keeps the numbers of the document as Number literals.
UseNumber bool
// MaxDepth bounds how deeply arrays and inline tables may nest; 0 takes
// the default of 10000.
MaxDepth int
// MaxInputSize bounds the document size in bytes; 0 takes no limit.
MaxInputSize int
} }
// UnmarshalWithOptions decodes data into v with the options set, the one-shot // NumbersAsLiterals keeps the numbers of the document as a Number carrying
// form of building a Decoder. See DecodeOptions for the fields and their // the literal the document wrote, so 0x1f, 1_000, +1.0 and inf survive a
// defaults. // round trip with their spelling intact. A destination of a concrete numeric
func UnmarshalWithOptions(data []byte, v any, opts DecodeOptions) error { // kind still takes the evaluated value; the literal is kept only where a
dec := &Decoder{ // Number, or an any, receives it. Off by default: numbers evaluate to
disallowUnknown: opts.DisallowUnknownFields, // int64 and float64.
useNumber: opts.UseNumber, func NumbersAsLiterals(v bool) UnmarshalOption {
maxDepth: opts.MaxDepth, return func(s *decodeSettings) { s.useNumber = v }
maxInputSize: opts.MaxInputSize, }
}
return dec.DecodeContext(context.Background(), data, v) // LocalTimeLocation sets the zone a local date-time is placed in when it
// decodes into a time.Time destination. Without the option a local date-time
// fills only its own wrapper type (LocalDateTime, LocalDate, LocalTime),
// whose embedded time.Time is UTC; with the option, a time.Time destination
// takes the value too, carried in the location given. A nil location restores
// the default.
func LocalTimeLocation(loc *time.Location) UnmarshalOption {
return func(s *decodeSettings) { s.localLoc = loc }
}
// MaxNestingDepth bounds how deeply arrays and inline tables may nest in a
// document the decode accepts. The parser is a recursive descent, so a
// document that nests without bound would exhaust the stack; one that nests
// deeper than the limit is rejected with a SyntaxError naming it instead.
// Use 0 or any negative value for the default of 10000, which no
// hand-written document approaches.
func MaxNestingDepth(depth int) UnmarshalOption {
return func(s *decodeSettings) { s.maxDepth = depth }
}
// MaxInputSize bounds the size of a document the decode accepts, in bytes; a
// larger one is rejected before parsing starts. Use 0 or any negative value
// for no limit, which is the default: the caller already holds the bytes, so
// the size is a policy the caller sets rather than a protection the library
// imposes on its own.
func MaxInputSize(size int) UnmarshalOption {
return func(s *decodeSettings) { s.maxInputSize = size }
} }
// Marshaler is the interface implemented by types that can produce a custom // Marshaler is the interface implemented by types that can produce a custom
@@ -543,9 +521,13 @@ type UnmarshalerContext interface {
// string quoting style, and the choice between `[table]` headers and inline // string quoting style, and the choice between `[table]` headers and inline
// tables are not preserved. // tables are not preserved.
// //
// Marshal returns the TOML encoding of v. Options tune the emission; with
// none, the layout groups entries by kind, empty arrays emit and sub-tables
// take the header form.
//
// Marshal is equivalent to MarshalContext with context.Background. // Marshal is equivalent to MarshalContext with context.Background.
func Marshal(v any) ([]byte, error) { func Marshal(v any, opts ...MarshalOption) ([]byte, error) {
return MarshalContext(context.Background(), v) return MarshalContext(context.Background(), v, opts...)
} }
// A Statement is one top-level statement of a document, what Statements // A Statement is one top-level statement of a document, what Statements
@@ -612,10 +594,10 @@ func Statements(r io.Reader) iter.Seq2[Statement, error] {
} }
// 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/v2's MarshalAppendTo and json's MarshalAppend have.
// untouched and comes back with a nil slice. // A failed encoding leaves buf untouched and comes back with a nil slice.
func MarshalAppend(buf []byte, v any) ([]byte, error) { func MarshalAppend(buf []byte, v any, opts ...MarshalOption) ([]byte, error) {
out, err := Marshal(v) out, err := Marshal(v, opts...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -623,11 +605,25 @@ func MarshalAppend(buf []byte, v any) ([]byte, error) {
} }
// 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, opts ...MarshalOption) ([]byte, error) {
if err := ctx.Err(); err != nil { if err := ctx.Err(); err != nil {
return nil, err return nil, err
} }
return NewEncoder().MarshalContext(ctx, v) return settingsForEncode(opts).marshal(ctx, v)
}
// MarshalWrite encodes v and writes the document to w, the streaming-shaped
// entry the json/v2 vocabulary uses. The options and the behaviour are
// Marshal's.
func MarshalWrite(w io.Writer, v any, opts ...MarshalOption) error {
out, err := Marshal(v, opts...)
if err != nil {
return err
}
if _, err := w.Write(out); err != nil {
return fmt.Errorf("interpres: write: %w", err)
}
return nil
} }
// A LayoutKind names the layout the encoder writes a document's entries in. // A LayoutKind names the layout the encoder writes a document's entries in.
@@ -642,48 +638,58 @@ const (
LayoutKindDeclaration LayoutKindDeclaration
) )
// An Encoder encodes Go values into TOML. // A MarshalOption configures one Marshal, MarshalContext, MarshalAppend or
// MarshalWrite call. Options are function values over the private encode
// settings, the shape encoding/json/v2 uses for its own, and compose by
// simple listing:
// //
// All options default to the behaviour that passes the toml-test compliance // out, err := interpres.Marshal(cfg,
// suite in both directions: // interpres.Layout(interpres.LayoutKindDeclaration),
// // interpres.InlineTables(60))
// Layout: LayoutKindGrouped (scalars first, then tables, then type MarshalOption func(*encodeSettings)
// arrays of tables)
// OmitEmptyArrays: false (a nil/empty []string slice emits [] as a value; // encodeSettings is the option carrier of one encode call.
// a nil/empty []Item struct slice is still skipped) type encodeSettings struct {
// LiteralMultilineAt: 0 (always emit the escaped basic form, never a ctx context.Context
// literal one) cfg encodeConfig
// InlineTablesAt: 0 (always emit a table header, never an inline
// table)
//
// Use the chainable option methods to opt out. The option state is private;
// callers that need the underlying knobs reach for the methods rather than
// reading or mutating fields.
type Encoder struct {
layout LayoutKind // default LayoutKindGrouped; set via (*Encoder).Layout
omitEmptyArrays bool // default false; set via (*Encoder).OmitEmptyArrays
literalMultilineAt int // default 0; set via (*Encoder).LiteralMultiline
inlineTablesAt int // default 0; set via (*Encoder).InlineTables
emitFieldComments bool // default false; set via (*Encoder).EmitFieldComments
} }
// NewEncoder returns an Encoder with default options. func settingsForEncode(opts []MarshalOption) *encodeSettings {
func NewEncoder() *Encoder { return &Encoder{layout: LayoutKindGrouped} } s := &encodeSettings{ctx: context.Background(), cfg: encodeConfig{layout: LayoutKindGrouped}}
for _, opt := range opts {
opt(s)
}
return s
}
// marshal runs the encode the settings describe.
func (s *encodeSettings) marshal(ctx context.Context, v any) ([]byte, error) {
enc := newEncoder()
enc.ctx = ctx
enc.opts = s.cfg
if err := enc.encode(v); err != nil {
enc.release()
return nil, err
}
// The output leaves the pooled buffer as a copy, so the next Marshal
// reuses the buffer without touching what the caller holds.
out := slices.Clone(enc.buf.Bytes())
enc.release()
return out, nil
}
// Layout sets the layout the encoder writes a document's entries in: // Layout sets the layout the encoder writes a document's entries in:
// LayoutKindGrouped, the default, reorders them scalars first, then tables, // LayoutKindGrouped, the default, reorders them scalars first, then tables,
// then arrays of tables; LayoutKindDeclaration preserves declaration order. // then arrays of tables; LayoutKindDeclaration preserves declaration order.
func (e *Encoder) Layout(kind LayoutKind) *Encoder { func Layout(kind LayoutKind) MarshalOption {
e.layout = kind return func(s *encodeSettings) { s.cfg.layout = kind }
return e
} }
// OmitEmptyArrays opts in to skipping empty (non-nil, length 0) TOML arrays // OmitEmptyArrays opts in to skipping empty (non-nil, length 0) TOML arrays
// of scalars. The default emits them as "key = []". Nil slices and empty // of scalars. The default emits them as "key = []". Nil slices and empty
// arrays of tables are already always omitted. // arrays of tables are already always omitted.
func (e *Encoder) OmitEmptyArrays() *Encoder { func OmitEmptyArrays(v bool) MarshalOption {
e.omitEmptyArrays = true return func(s *encodeSettings) { s.cfg.omitEmptyArrays = v }
return e
} }
// LiteralMultiline sets the length threshold at which a multi-line string // LiteralMultiline sets the length threshold at which a multi-line string
@@ -691,9 +697,8 @@ func (e *Encoder) OmitEmptyArrays() *Encoder {
// Use 0 or any negative value to disable (always escaped). The literal form // Use 0 or any negative value to disable (always escaped). The literal form
// is selected only when the value contains an internal newline; otherwise the // is selected only when the value contains an internal newline; otherwise the
// single-line basic form is used regardless of this setting. // single-line basic form is used regardless of this setting.
func (e *Encoder) LiteralMultiline(threshold int) *Encoder { func LiteralMultiline(threshold int) MarshalOption {
e.literalMultilineAt = threshold return func(s *encodeSettings) { s.cfg.literalMultilineAt = threshold }
return e
} }
// InlineTables sets the size limit, in bytes of the single-line rendering, at // InlineTables sets the size limit, in bytes of the single-line rendering, at
@@ -709,9 +714,8 @@ func (e *Encoder) LiteralMultiline(threshold int) *Encoder {
// With LayoutKindDeclaration the layout is already for presentation only, and an // With LayoutKindDeclaration the layout is already for presentation only, and an
// inlined table follows the same rule as any other value line: it lands in the // inlined table follows the same rule as any other value line: it lands in the
// section of the header that precedes it. // section of the header that precedes it.
func (e *Encoder) InlineTables(threshold int) *Encoder { func InlineTables(threshold int) MarshalOption {
e.inlineTablesAt = threshold return func(s *encodeSettings) { s.cfg.inlineTablesAt = threshold }
return e
} }
// EmitFieldComments turns on printing the comment a field's `toml` tag // EmitFieldComments turns on printing the comment a field's `toml` tag
@@ -724,30 +728,6 @@ func (e *Encoder) InlineTables(threshold int) *Encoder {
// that carries the text. Off by default, and a field without a `comment=` // that carries the text. Off by default, and a field without a `comment=`
// option prints none. Multi-line comments carry newlines in the tag, each // option prints none. Multi-line comments carry newlines in the tag, each
// line printed with its own "# " marker. // line printed with its own "# " marker.
func (e *Encoder) EmitFieldComments() *Encoder { func EmitFieldComments(v bool) MarshalOption {
e.emitFieldComments = true return func(s *encodeSettings) { s.cfg.emitFieldComments = v }
return e
}
// Marshal encodes v to TOML bytes. It is equivalent to calling Marshal with v.
//
// Marshal is equivalent to MarshalContext with context.Background.
func (e *Encoder) Marshal(v any) ([]byte, error) {
return e.MarshalContext(context.Background(), v)
}
// MarshalContext is the cancellable variant of Marshal.
func (e *Encoder) MarshalContext(ctx context.Context, v any) ([]byte, error) {
enc := newEncoder()
enc.ctx = ctx
enc.opts = *e
if err := enc.encode(v); err != nil {
enc.release()
return nil, err
}
// The output leaves the pooled buffer as a copy, so the next Marshal
// reuses the buffer without touching what the caller holds.
out := slices.Clone(enc.buf.Bytes())
enc.release()
return out, nil
} }
+2 -2
View File
@@ -317,7 +317,7 @@ func TestDisallowUnknownFields(t *testing.T) {
} }
var strict C var strict C
err := NewDecoder().DisallowUnknownFields().Decode(data, &strict) err := Unmarshal(data, &strict, RejectUnknownFields(true))
if err == nil { if err == nil {
t.Fatal("expected error for unknown field, got nil") t.Fatal("expected error for unknown field, got nil")
} }
@@ -333,7 +333,7 @@ func TestDisallowUnknownFieldsReportsSmallestKey(t *testing.T) {
data := []byte("known = \"x\"\nzeta = 1\nalpha = 2\nmu = 3\n") data := []byte("known = \"x\"\nzeta = 1\nalpha = 2\nmu = 3\n")
for range 20 { for range 20 {
var c C var c C
err := NewDecoder().DisallowUnknownFields().Decode(data, &c) err := Unmarshal(data, &c, RejectUnknownFields(true))
if err == nil { if err == nil {
t.Fatal("expected error for unknown fields") t.Fatal("expected error for unknown fields")
} }
+2 -1
View File
@@ -11,7 +11,8 @@ import (
) )
// A Number holds a TOML number as the literal the document wrote it with: // A Number holds a TOML number as the literal the document wrote it with:
// 0x1f, 1_000, +1.0, inf. Decoder.UseNumber decodes integers and floats into // 0x1f, 1_000, +1.0, inf. The NumbersAsLiterals option decodes integers and
// floats into
// it, so a round trip through the value tree keeps the spelling instead of a // it, so a round trip through the value tree keeps the spelling instead of a
// normalised one, and Marshal writes the literal back as it is. // normalised one, and Marshal writes the literal back as it is.
// //
+1 -1
View File
@@ -97,7 +97,7 @@ func TestMarshalOrderedMap(t *testing.T) {
m := NewOrderedMap() m := NewOrderedMap()
m.Set("zebra", int64(1)) m.Set("zebra", int64(1))
m.Set("alpha", int64(2)) m.Set("alpha", int64(2))
out, err := NewEncoder().InlineTables(60).Marshal(map[string]any{"t": m}) out, err := Marshal(map[string]any{"t": m}, InlineTables(60))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
+1 -1
View File
@@ -726,7 +726,7 @@ func (p *parser) parseAtom() (any, error) {
if err != nil { if err != nil {
return nil, p.errf("%s", err) return nil, p.errf("%s", err)
} }
// The token's shape is validated either way; UseNumber only keeps the // The token's shape is validated either way; NumbersAsLiterals only keeps the
// literal instead of the evaluated value. // literal instead of the evaluated value.
if p.useNumber { if p.useNumber {
return Number(tok), nil return Number(tok), nil
+20 -21
View File
@@ -54,7 +54,7 @@ func TestTargetedStrictFindings(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
var cfg targetCfg var cfg targetCfg
err := NewDecoder().DisallowUnknownFields().Decode([]byte(tt.doc), &cfg) err := Unmarshal([]byte(tt.doc), &cfg, RejectUnknownFields(true))
if err == nil { if err == nil {
t.Fatalf("no error, want %q", tt.want) t.Fatalf("no error, want %q", tt.want)
} }
@@ -102,7 +102,7 @@ func TestTargetedParseErrors(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
var cfg targetCfg var cfg targetCfg
err := NewDecoder().Decode([]byte(tt.doc), &cfg) err := Unmarshal([]byte(tt.doc), &cfg)
if err == nil { if err == nil {
t.Fatalf("no error, want %q", tt.want) t.Fatalf("no error, want %q", tt.want)
} }
@@ -121,7 +121,7 @@ func TestTargetedSilentShapes(t *testing.T) {
// sink's own x is a different key, the tree's shape exactly. // sink's own x is a different key, the tree's shape exactly.
var cfg, ref targetCfg var cfg, ref targetCfg
in := []byte("[tab]\nx = 1\n[tab.nested]\n") in := []byte("[tab]\nx = 1\n[tab.nested]\n")
if err := NewDecoder().Decode(in, &cfg); err != nil { if err := Unmarshal(in, &cfg); err != nil {
t.Fatalf("decode: %v", err) t.Fatalf("decode: %v", err)
} }
if err := treeDecodeInto(in, &ref); err != nil { if err := treeDecodeInto(in, &ref); err != nil {
@@ -137,7 +137,7 @@ func TestTargetedSilentShapes(t *testing.T) {
t.Run("unknown keys are ignored without strict", func(t *testing.T) { t.Run("unknown keys are ignored without strict", func(t *testing.T) {
var cfg, ref targetCfg var cfg, ref targetCfg
in := []byte("num = 5\nz1 = 1\n[zz]\nk = 1\n") in := []byte("num = 5\nz1 = 1\n[zz]\nk = 1\n")
if err := NewDecoder().Decode(in, &cfg); err != nil { if err := Unmarshal(in, &cfg); err != nil {
t.Fatalf("decode: %v", err) t.Fatalf("decode: %v", err)
} }
if err := treeDecodeInto(in, &ref); err != nil { if err := treeDecodeInto(in, &ref); err != nil {
@@ -153,7 +153,7 @@ func TestTargetedSilentShapes(t *testing.T) {
t.Run("an inline table into a map field", func(t *testing.T) { t.Run("an inline table into a map field", func(t *testing.T) {
var cfg targetCfg var cfg targetCfg
in := []byte("lims = { cpu = 4, deep = { a = true } }\n") in := []byte("lims = { cpu = 4, deep = { a = true } }\n")
if err := NewDecoder().Decode(in, &cfg); err != nil { if err := Unmarshal(in, &cfg); err != nil {
t.Fatalf("decode: %v", err) t.Fatalf("decode: %v", err)
} }
if cfg.Lims["cpu"] != int64(4) { if cfg.Lims["cpu"] != int64(4) {
@@ -162,7 +162,7 @@ func TestTargetedSilentShapes(t *testing.T) {
}) })
t.Run("an overflow falls back to the decode error", func(t *testing.T) { t.Run("an overflow falls back to the decode error", func(t *testing.T) {
var cfg targetCfg var cfg targetCfg
err := NewDecoder().Decode([]byte("small = 300\n"), &cfg) err := Unmarshal([]byte("small = 300\n"), &cfg)
want := "interpres: small: integer 300 overflows uint8" want := "interpres: small: integer 300 overflows uint8"
if err == nil || err.Error() != want { if err == nil || err.Error() != want {
t.Errorf("err = %v, want %q", err, want) t.Errorf("err = %v, want %q", err, want)
@@ -185,7 +185,7 @@ func TestTargetedSilentShapes(t *testing.T) {
var cfg struct { var cfg struct {
Rate Number `toml:"rate"` Rate Number `toml:"rate"`
} }
if err := NewDecoder().UseNumber().Decode([]byte("rate = 1_000\n"), &cfg); err != nil { if err := Unmarshal([]byte("rate = 1_000\n"), &cfg, NumbersAsLiterals(true)); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if cfg.Rate != "1_000" { if cfg.Rate != "1_000" {
@@ -195,7 +195,7 @@ func TestTargetedSilentShapes(t *testing.T) {
t.Run("dotted keys fill a map field", func(t *testing.T) { t.Run("dotted keys fill a map field", func(t *testing.T) {
var cfg targetCfg var cfg targetCfg
in := []byte("lims.a.b = true\nlims.c = 3\n") in := []byte("lims.a.b = true\nlims.c = 3\n")
if err := NewDecoder().Decode(in, &cfg); err != nil { if err := Unmarshal(in, &cfg); err != nil {
t.Fatalf("decode: %v", err) t.Fatalf("decode: %v", err)
} }
if cfg.Lims["c"] != int64(3) { if cfg.Lims["c"] != int64(3) {
@@ -205,7 +205,7 @@ func TestTargetedSilentShapes(t *testing.T) {
t.Run("an inline table cannot be extended", func(t *testing.T) { t.Run("an inline table cannot be extended", func(t *testing.T) {
var cfg targetCfg var cfg targetCfg
in := []byte("lims = { a = 1 }\n[lims.deep]\nb = 2\n") in := []byte("lims = { a = 1 }\n[lims.deep]\nb = 2\n")
err := NewDecoder().Decode(in, &cfg) err := Unmarshal(in, &cfg)
if err == nil || !strings.Contains(err.Error(), "cannot extend inline table") { if err == nil || !strings.Contains(err.Error(), "cannot extend inline table") {
t.Errorf("err = %v, want the inline-table extension error", err) t.Errorf("err = %v, want the inline-table extension error", err)
} }
@@ -233,8 +233,7 @@ func TestTargetedShapesMatrix(t *testing.T) {
for i, doc := range docs { for i, doc := range docs {
var ref, tgt targetCfg var ref, tgt targetCfg
refErr := treeDecodeInto([]byte(doc), &ref) refErr := treeDecodeInto([]byte(doc), &ref)
dec := NewDecoder() tgtErr := Unmarshal([]byte(doc), &tgt)
tgtErr := dec.Decode([]byte(doc), &tgt)
if (refErr == nil) != (tgtErr == nil) { if (refErr == nil) != (tgtErr == nil) {
t.Errorf("doc %d: error presence disagrees: tree %v, targeted %v", i, refErr, tgtErr) t.Errorf("doc %d: error presence disagrees: tree %v, targeted %v", i, refErr, tgtErr)
continue continue
@@ -283,7 +282,7 @@ func TestTargetedFallbackContracts(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
var cfg targetCfg var cfg targetCfg
err := NewDecoder().Decode([]byte(tt.doc), &cfg) err := Unmarshal([]byte(tt.doc), &cfg)
if err == nil || err.Error() != tt.want { if err == nil || err.Error() != tt.want {
t.Errorf("err = %v, want %q", err, tt.want) t.Errorf("err = %v, want %q", err, tt.want)
} }
@@ -297,7 +296,7 @@ func TestTargetedFallbackContracts(t *testing.T) {
func TestTargetedHeaderOnAssignedScalarArray(t *testing.T) { func TestTargetedHeaderOnAssignedScalarArray(t *testing.T) {
var cfg targetCfg var cfg targetCfg
in := []byte("arr = []\n[[arr]]\nx = 1\n") in := []byte("arr = []\n[[arr]]\nx = 1\n")
err := NewDecoder().Decode(in, &cfg) err := Unmarshal(in, &cfg)
want := "interpres: line 2: key \"arr\" is not an array of tables" want := "interpres: line 2: key \"arr\" is not an array of tables"
if err == nil || err.Error() != want { if err == nil || err.Error() != want {
t.Errorf("err = %v, want %q", err, want) t.Errorf("err = %v, want %q", err, want)
@@ -362,7 +361,7 @@ func TestTargetedBranchParity(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
var cfg targetCfg var cfg targetCfg
err := NewDecoder().DisallowUnknownFields().Decode([]byte(tt.doc), &cfg) err := Unmarshal([]byte(tt.doc), &cfg, RejectUnknownFields(true))
if tt.want == "" { if tt.want == "" {
if err != nil { if err != nil {
t.Fatalf("err = %v, want nil", err) t.Fatalf("err = %v, want nil", err)
@@ -386,7 +385,7 @@ func TestTargetedDecodeHookFields(t *testing.T) {
} }
var cfg Cfg var cfg Cfg
in := []byte("ip = \"192.0.2.1\"\ndur = \"1h30m\"\nunm = \"hello\"\n") in := []byte("ip = \"192.0.2.1\"\ndur = \"1h30m\"\nunm = \"hello\"\n")
if err := NewDecoder().Decode(in, &cfg); err != nil { if err := Unmarshal(in, &cfg); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if cfg.IP.String() != "192.0.2.1" { if cfg.IP.String() != "192.0.2.1" {
@@ -410,7 +409,7 @@ func TestTargetedOddShapes(t *testing.T) {
} }
var cfg, ref Cfg var cfg, ref Cfg
doc := []byte("when = 1979-05-27 07:32:00Z\n") doc := []byte("when = 1979-05-27 07:32:00Z\n")
if err := NewDecoder().Decode(doc, &cfg); err != nil { if err := Unmarshal(doc, &cfg); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if err := treeDecodeInto(doc, &ref); err != nil { if err := treeDecodeInto(doc, &ref); err != nil {
@@ -426,7 +425,7 @@ func TestTargetedOddShapes(t *testing.T) {
} }
var cfg, ref Cfg var cfg, ref Cfg
doc := []byte("m = { a = 1 }\n") doc := []byte("m = { a = 1 }\n")
err := NewDecoder().Decode(doc, &cfg) err := Unmarshal(doc, &cfg)
refErr := treeDecodeInto(doc, &ref) refErr := treeDecodeInto(doc, &ref)
if err == nil || refErr == nil { if err == nil || refErr == nil {
t.Fatalf("err = %v, refErr = %v, want both to fail", err, refErr) t.Fatalf("err = %v, refErr = %v, want both to fail", err, refErr)
@@ -438,7 +437,7 @@ func TestTargetedOddShapes(t *testing.T) {
t.Run("a repeated dotted map key is a duplicate", func(t *testing.T) { t.Run("a repeated dotted map key is a duplicate", func(t *testing.T) {
var cfg targetCfg var cfg targetCfg
doc := []byte("lims.a.b = 1\nlims.a.b = 2\n") doc := []byte("lims.a.b = 1\nlims.a.b = 2\n")
err := NewDecoder().Decode(doc, &cfg) err := Unmarshal(doc, &cfg)
want := "interpres: line 2: duplicate key \"b\"" want := "interpres: line 2: duplicate key \"b\""
if err == nil || err.Error() != want { if err == nil || err.Error() != want {
t.Errorf("err = %v, want %q", err, want) t.Errorf("err = %v, want %q", err, want)
@@ -447,7 +446,7 @@ func TestTargetedOddShapes(t *testing.T) {
t.Run("an underscored integer takes the token path", func(t *testing.T) { t.Run("an underscored integer takes the token path", func(t *testing.T) {
var cfg targetCfg var cfg targetCfg
doc := []byte("num = 1_000\n") doc := []byte("num = 1_000\n")
if err := NewDecoder().Decode(doc, &cfg); err != nil { if err := Unmarshal(doc, &cfg); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if cfg.Num != 1000 { if cfg.Num != 1000 {
@@ -459,7 +458,7 @@ func TestTargetedOddShapes(t *testing.T) {
Big int64 `toml:"big"` Big int64 `toml:"big"`
} }
doc := []byte("big = 999999999999999999\n") doc := []byte("big = 999999999999999999\n")
if err := NewDecoder().Decode(doc, &cfg); err != nil { if err := Unmarshal(doc, &cfg); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if cfg.Big != 999999999999999999 { if cfg.Big != 999999999999999999 {
@@ -506,7 +505,7 @@ func TestTargetedMapTableShapes(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
var cfg, ref targetCfg var cfg, ref targetCfg
err := NewDecoder().Decode([]byte(tt.doc), &cfg) err := Unmarshal([]byte(tt.doc), &cfg)
refErr := treeDecodeInto([]byte(tt.doc), &ref) refErr := treeDecodeInto([]byte(tt.doc), &ref)
if (err == nil) != (refErr == nil) { if (err == nil) != (refErr == nil) {
t.Fatalf("error presence disagrees: tree %v, targeted %v", refErr, err) t.Fatalf("error presence disagrees: tree %v, targeted %v", refErr, err)