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.
The compliance suite now runs the encoder as well as the decoder, 214
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
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.
@@ -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 tree's type, and `UnmarshalTOML` implementations that expect a
`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
(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
@@ -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]()`,
which precompiles the struct schema and the interface flags for a hot path
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
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
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
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.
@@ -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
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`.
- `UnmarshalWithOptions(data, v, opts)` decodes with a `DecodeOptions` struct
in one call, the options a `Decoder` sets without building one: unknown
keys, `Number` literals, and the parse limits.
- `Unmarshal(data, v, opts...)` and the other entries take variadic options,
the shape encoding/json/v2 uses: `RejectUnknownFields`,
`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:
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.
@@ -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
invalid byte in its message. The new fields are additive: a `SyntaxError`
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`,
`+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
@@ -136,11 +138,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- `Encoder.GroupByKind(bool)` is renamed to `Encoder.Layout(kind)` and takes
a `LayoutKind`: `LayoutKindGrouped`, the default, or
`LayoutKindDeclaration` for the declaration order. `UseLiteralMultiline`
is renamed to `LiteralMultiline`. The behaviour is unchanged; 2.0 is the
only chance a rename has, and the migrator updates the calls mechanically.
- The stateful `Decoder` and `Encoder` of 1.x are replaced by variadic
options on the entries, the shape encoding/json/v2 uses: `Layout(kind)`
with `LayoutKindGrouped` or `LayoutKindDeclaration`, `OmitEmptyArrays`,
`LiteralMultiline(threshold)`, `InlineTables(threshold)`,
`EmitFieldComments`, `RejectUnknownFields`, `NumbersAsLiterals`,
`MaxNestingDepth`, `MaxInputSize`, `LocalTimeLocation`.
- `DecodeError` and `EncodeError` carry one `Path` type, a list of segments
(`"items"`, `"[0]"`, `"weight"`) with a `String()` rendering the TOML
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.
The document is writable, and `Marshal` writes it back with its comments.
**Renamed API.** `Encoder.GroupByKind(bool)` is `Encoder.Layout(kind)` with
`LayoutKindGrouped` (the old default) and `LayoutKindDeclaration` (the old
`false`); `UseLiteralMultiline` is `LiteralMultiline`.
**Options instead of Decoder and Encoder.** The stateful types of 1.x are
gone; the entries take variadic options, the shape encoding/json/v2 uses.
`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
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
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
except where noted above.
@@ -442,7 +448,7 @@ uses only the standard library and passes the entire
nested structs, slices, and `map[string]T`.
- `toml:"name"` field tags, case-insensitive name fallback, and `toml:"-"` to
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.
- `Unmarshaler` interface (`UnmarshalTOML(data any) error`) for types that take
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.
- **Decoding and encoding**: `Parse` for an untyped tree, `Unmarshal` and
`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.
- **Custom types**: `Marshaler` and `Unmarshaler` let a type control its own
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
```go
err := interpres.NewDecoder().
DisallowUnknownFields().
Decode(data, &cfg)
err := interpres.Unmarshal(data, &cfg, interpres.RejectUnknownFields(true))
```
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;
`UnmarshalTOML` receives the parsed value verbatim.
### Encoder options
### Options
```go
out, err := interpres.NewEncoder().
Layout(interpres.LayoutKindDeclaration), // preserve declaration order
OmitEmptyArrays(). // skip empty scalar arrays
LiteralMultiline(80), // long multi-line strings as literal blocks
Marshal(cfg)
out, err := interpres.Marshal(cfg,
interpres.Layout(interpres.LayoutKindDeclaration), // preserve declaration order
interpres.OmitEmptyArrays(true), // skip empty scalar arrays
interpres.LiteralMultiline(80), // long multi-line strings as literal blocks
)
```
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
```go
@@ -151,8 +153,7 @@ defer cancel()
out, err := interpres.MarshalContext(ctx, cfg)
```
`ParseContext`, `UnmarshalContext`, `(*Decoder).DecodeContext` and
`(*Encoder).MarshalContext` follow the same pattern.
`ParseContext`, `UnmarshalContext` and `MarshalContext` follow the same pattern.
The full rules for field matching, numeric conversion and emission live in
[docs/API.md](docs/API.md).
+2 -4
View File
@@ -109,11 +109,10 @@ func BenchmarkMarshal(b *testing.B) {
}
func BenchmarkStrictDecode(b *testing.B) {
dec := NewDecoder().DisallowUnknownFields()
b.ReportAllocs()
for b.Loop() {
var cfg benchConfig
if err := dec.Decode(benchDoc, &cfg); err != nil {
if err := Unmarshal(benchDoc, &cfg, RejectUnknownFields(true)); err != nil {
b.Fatal(err)
}
}
@@ -145,12 +144,11 @@ type benchLongDoc struct {
}
func BenchmarkStrictDecodeLong(b *testing.B) {
dec := NewDecoder().DisallowUnknownFields()
b.ReportAllocs()
b.SetBytes(int64(len(longDoc)))
for b.Loop() {
var doc benchLongDoc
if err := dec.Decode(longDoc, &doc); err != nil {
if err := Unmarshal(longDoc, &doc, RejectUnknownFields(true)); err != nil {
b.Fatal(err)
}
}
+1 -1
View File
@@ -528,7 +528,7 @@ func setDuration(dst reflect.Value, s string) error {
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
// 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.
+17 -21
View File
@@ -169,7 +169,7 @@ func TestDecoderDecodeContextHonoursCancellation(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel()
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) {
t.Fatalf("DecodeContext returned %v, want context.Canceled", err)
}
@@ -194,7 +194,7 @@ count = 3
if out.Title != "x" || out.Count != 3 {
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)
}
}
@@ -695,8 +695,7 @@ func TestUnmarshalStrictEmbeddedMapStaysStrict(t *testing.T) {
RoundTripExtra
Name string `toml:"name"`
}
dec := NewDecoder().DisallowUnknownFields()
err := dec.Decode([]byte("name = \"n\"\nrogue = 1\n"), &Cfg{})
err := Unmarshal([]byte("name = \"n\"\nrogue = 1\n"), &Cfg{}, RejectUnknownFields(true))
if err == nil || !strings.Contains(err.Error(), "unknown field") {
t.Fatalf("expected unknown field error, got: %v", err)
}
@@ -980,10 +979,10 @@ func TestDecoderMaxDepth(t *testing.T) {
var cfg struct {
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)
}
err := NewDecoder().MaxDepth(4).Decode(deep(5), &cfg)
err := Unmarshal(deep(5), &cfg, MaxNestingDepth(4))
if err == nil {
t.Fatal("expected a nesting error")
}
@@ -997,10 +996,10 @@ func TestDecoderMaxInputSize(t *testing.T) {
var cfg struct {
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)
}
err := NewDecoder().MaxInputSize(len(doc)-1).Decode(doc, &cfg)
err := Unmarshal(doc, &cfg, MaxInputSize(len(doc)-1))
if err == nil {
t.Fatal("expected a size error")
}
@@ -1112,7 +1111,7 @@ frac = 2.5
`)
t.Run("the tree keeps the literal", func(t *testing.T) {
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)
}
for lit, key := range map[string]string{
@@ -1136,8 +1135,7 @@ frac = 2.5
Frac float64 `toml:"frac"`
Rate time.Duration
}
dec := NewDecoder().UseNumber()
if err := dec.Decode([]byte("hex = 0x1f\nplain = 42\nfrac = 2.5\nRate = 1_000\n"), &cfg); err != nil {
if err := Unmarshal([]byte("hex = 0x1f\nplain = 42\nfrac = 2.5\nRate = 1_000\n"), &cfg, NumbersAsLiterals(true)); err != nil {
t.Fatal(err)
}
if cfg.Hex != "0x1f" {
@@ -1156,14 +1154,14 @@ frac = 2.5
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"} {
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.Run("without UseNumber the tree holds the evaluated kinds", func(t *testing.T) {
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)
}
if v, ok := tree["hex"].(int64); !ok || v != 31 {
@@ -1507,9 +1505,8 @@ func TestLocalTimeLocation(t *testing.T) {
Date LocalDate `toml:"date"`
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")
if err := dec.Decode(in, &cfg); err != nil {
if err := Unmarshal(in, &cfg, LocalTimeLocation(zone)); err != nil {
t.Fatal(err)
}
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 {
Wall LocalDateTime `toml:"wall"`
}
dec := NewDecoder().LocalTimeLocation(zone)
if err := dec.Decode([]byte("wall = 1979-05-27T07:32:00\n"), &cfg); err != nil {
if err := Unmarshal([]byte("wall = 1979-05-27T07:32:00\n"), &cfg, LocalTimeLocation(zone)); err != nil {
t.Fatal(err)
}
if cfg.Wall.Hour() != 7 {
@@ -1567,7 +1563,7 @@ func TestCancelInsideValue(t *testing.T) {
b.WriteString("]\n")
ctx := &errAfterN{Context: context.Background(), n: 2}
var tree map[string]any
err := NewDecoder().DecodeContext(ctx, []byte(b.String()), &tree)
err := UnmarshalContext(ctx, []byte(b.String()), &tree)
if err == nil {
t.Fatal("a cancelled context did not stop the parse inside the value")
}
@@ -1585,7 +1581,7 @@ func TestCancellationSynctest(t *testing.T) {
cancel()
start := time.Now()
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) {
t.Errorf("err = %v, want context.Canceled", err)
}
@@ -1644,7 +1640,7 @@ func TestErrorMessagesGolden(t *testing.T) {
}
b.WriteString("\n")
var tree map[string]any
err := NewDecoder().MaxDepth(10).Decode([]byte(b.String()), &tree)
err := Unmarshal([]byte(b.String()), &tree, MaxNestingDepth(10))
return err
},
want: "interpres: line 1: nesting exceeds the limit of 10",
@@ -1663,7 +1659,7 @@ func TestErrorMessagesGolden(t *testing.T) {
var cfg struct {
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\"" }`,
},
+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
`NewDecoder` sets, in a `DecodeOptions` struct, applied to a single call.
The zero value takes the defaults: unknown keys ignored, numbers evaluated
as `int64` and `float64`, no size limit and the 10000-level nesting default.
The decode and encode calls take variadic options, the shape
encoding/json/v2 uses for its own. Each is a function value over the private
settings of one call, and they compose by listing:
```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)`
@@ -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
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
[Numbers as literals](#numbers-as-literals).
### 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
- `*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
`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`,
`+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
@@ -338,7 +363,7 @@ default tree normalises `0x1f` to `31` and `+1.0` to `1.0`.
```go
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"
```
@@ -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
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
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
@@ -475,13 +500,11 @@ as an ordinary inline table, whose keys are sorted.
### Strict decoding
By default unknown keys are dropped silently. A `Decoder` built with
`DisallowUnknownFields` rejects them instead:
By default unknown keys are dropped silently. The `RejectUnknownFields`
option rejects them instead:
```go
err := interpres.NewDecoder().
DisallowUnknownFields().
Decode(data, &cfg)
err := interpres.Unmarshal(data, &cfg, interpres.RejectUnknownFields(true))
```
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
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 document scans, and no intermediate value tree is kept. Values still flow
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
`ParseContext`, `UnmarshalContext` and `(*Decoder).DecodeContext` accept a
`ParseContext`, `UnmarshalContext` and `MarshalContext` accept a
`context.Context`. An already-cancelled context short-circuits with
`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
@@ -537,7 +560,7 @@ sequenceDiagram
### 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
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
as a value array and change the value's Go type.
- `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
comments are not visible to reflection, so the tag is the channel that
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:
```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
@@ -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:
```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
@@ -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:
```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
@@ -777,7 +800,7 @@ header is not read back as part of that header's section.
### 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
walk.
@@ -814,24 +837,29 @@ sequenceDiagram
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
TOML asks for:
The API follows the shapes encoding/json made familiar and the option style
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.Marshal(v)` | `Marshal(v)` | the same shape; the output is TOML 1.1 |
| `json.MarshalAppend(buf, v)` | `MarshalAppend(buf, v)` | the same shape |
| `(*json.Decoder).DisallowUnknownFields` | `(*Decoder).DisallowUnknownFields` | the same effect; the one-shot form is `UnmarshalWithOptions` |
| `json.Number`, `(*json.Decoder).UseNumber` | `Number`, `(*Decoder).UseNumber` | the TOML literal carries its radix and separators, so `0x1f` stays `0x1f` |
| `json.MarshalAppend(buf, v)` | `MarshalAppend(buf, v)` | the same shape, options included |
| `json.MarshalWrite(w, v)` | `MarshalWrite(w, v)` | the same shape, options included |
| `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 |
| 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,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.Unmarshaler` (`UnmarshalJSON`) | `Unmarshaler` (`UnmarshalTOML`) | the data arrives decoded, not as bytes |
| `json/v2 Marshalers` | `Marshaler` (`MarshalTOML`) | the TOML method returns a value the encoder renders, not 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 |
| `*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 |
@@ -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
attaches to the previous segment in brackets, `items[0].weight`.
### `type Decoder`
### Options
Configurable strictness for decoding, constructed with `NewDecoder`. Set up
with the chainable methods, then call `Decode` or `DecodeContext` any number
of times. A configured `Decoder` holds no per-call state and is safe for
concurrent use. For a single document, `UnmarshalWithOptions(data, v,
DecodeOptions{...})` sets the same options without the Decoder; its zero
value takes the defaults.
The decode and encode entries take variadic options, the shape
encoding/json/v2 uses for its own. Each option is a stateless function value
over the private settings of one call; they compose by listing in the call,
and there is no stateful Decoder or Encoder to share or guard.
| Method | Default | Effect |
Decode options:
| Option | Default | Effect |
|---|---|---|
| `DisallowUnknownFields()` | 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) |
| `MaxDepth(depth int)` | `10000` | bound how deeply arrays and inline tables may nest |
| `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 |
```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
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
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`
See [Documents](#documents). A `Document` is what `Parse` returns, and
@@ -956,7 +976,7 @@ wins when a type implements both.
### `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
[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 |
| `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 |
| `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 |
## Running
+12 -2
View File
@@ -111,12 +111,22 @@ func getEncoderBuf() *bytes.Buffer {
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
// representation that preserves the order in which fields were declared.
type encoder struct {
buf *bytes.Buffer
ctx context.Context
opts Encoder
opts encodeConfig
// inlineDepth is the nesting level inside inline tables, which decides
// their indentation.
@@ -329,7 +339,7 @@ type entry struct {
type tomlDoc struct {
entries []entry
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
}
+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) {
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) {
@@ -105,7 +102,7 @@ func TestEncoderLayoutGroupedDefault(t *testing.T) {
Host string `toml:"host"`
} `toml:"s"`
}
out, err := NewEncoder().Marshal(Cfg{Name: "x", S: struct {
out, err := Marshal(Cfg{Name: "x", S: struct {
Host string `toml:"host"`
}{Host: "h"}})
if err != nil {
@@ -131,7 +128,7 @@ func TestEncoderLayoutDeclarationPreservesOrder(t *testing.T) {
Server: Inner{Host: "h"},
Debug: true,
}
out, err := NewEncoder().Layout(LayoutKindDeclaration).Marshal(in)
out, err := Marshal(in, Layout(LayoutKindDeclaration))
if err != nil {
t.Fatalf("marshal: %v", err)
}
@@ -161,7 +158,7 @@ func TestEncoderLayoutGroupedDefaultOrder(t *testing.T) {
Server: Inner{Host: "h"},
Debug: true,
}
out, err := NewEncoder().Marshal(in)
out, err := Marshal(in)
if err != nil {
t.Fatalf("marshal: %v", err)
}
@@ -176,10 +173,10 @@ func TestEncoderOmitEmptyArrays(t *testing.T) {
Tags []string `toml:"tags"`
Secrets []string `toml:"secrets"`
}
out, err := NewEncoder().OmitEmptyArrays().Marshal(Cfg{
out, err := Marshal(Cfg{
Tags: []string{"a", "b"},
Secrets: []string{},
})
}, OmitEmptyArrays(true))
if err != nil {
t.Fatalf("marshal: %v", err)
}
@@ -193,7 +190,7 @@ func TestEncoderDefaultEmitsEmptyArray(t *testing.T) {
type Cfg struct {
Tags []string `toml:"tags"`
}
out, err := NewEncoder().Marshal(Cfg{Tags: []string{}})
out, err := Marshal(Cfg{Tags: []string{}})
if err != nil {
t.Fatalf("marshal: %v", err)
}
@@ -211,10 +208,10 @@ func TestEncoderOmitEmptyArrayOfTablesStillSkipped(t *testing.T) {
Title string `toml:"title"`
Items []Item `toml:"items"`
}
out, err := NewEncoder().OmitEmptyArrays().Marshal(Cfg{
out, err := Marshal(Cfg{
Title: "demo",
Items: nil,
})
}, OmitEmptyArrays(true))
if err != nil {
t.Fatalf("marshal: %v", err)
}
@@ -229,7 +226,7 @@ func TestEncoderLiteralMultiline(t *testing.T) {
Long string `toml:"long"`
}
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 {
t.Fatalf("marshal: %v", err)
}
@@ -244,7 +241,7 @@ func TestEncoderLiteralMultilineBelowThreshold(t *testing.T) {
type Cfg struct {
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 {
t.Fatalf("marshal: %v", err)
}
@@ -259,7 +256,7 @@ func TestEncoderLiteralMultilineThresholdZero(t *testing.T) {
type Cfg struct {
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 {
t.Fatalf("marshal: %v", err)
}
@@ -282,7 +279,7 @@ func TestEncoderLiteralMultilineFallsBackWhenUnsafe(t *testing.T) {
{"lone carriage return", "first\rsecond\nthird"},
}
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 {
t.Fatalf("%s: marshal: %v", c.name, err)
}
@@ -482,11 +479,10 @@ func TestEncoderChainedOptions(t *testing.T) {
I Inner `toml:"i"`
}
long := strings.Repeat("x", 200)
out, err := NewEncoder().
Layout(LayoutKindDeclaration).
OmitEmptyArrays().
LiteralMultiline(50).
Marshal(Cfg{S: "short", I: Inner{V: long}})
out, err := Marshal(Cfg{S: "short", I: Inner{V: long}},
Layout(LayoutKindDeclaration),
OmitEmptyArrays(true),
LiteralMultiline(50))
if err != nil {
t.Fatalf("marshal: %v", err)
}
@@ -1302,7 +1298,7 @@ func TestEncoderEquivalenceToMarshal(t *testing.T) {
if err != nil {
t.Fatalf("marshal: %v", err)
}
b, err := NewEncoder().Marshal(in)
b, err := Marshal(in)
if err != nil {
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
// ones included.
out, err := NewEncoder().InlineTables(60).Marshal(cfg)
out, err := Marshal(cfg, InlineTables(60))
if err != nil {
t.Fatalf("marshal: %v", err)
}
@@ -1720,7 +1716,7 @@ func TestEncoderInlineTables(t *testing.T) {
}
// 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 {
t.Fatalf("marshal: %v", err)
}
@@ -1749,7 +1745,7 @@ func TestEncoderInlineTablesOrderAndRoundTrip(t *testing.T) {
if err != nil {
t.Fatalf("marshal: %v", err)
}
compact, err := NewEncoder().InlineTables(20).Marshal(cfg)
compact, err := Marshal(cfg, InlineTables(20))
if err != nil {
t.Fatalf("marshal: %v", err)
}
@@ -1785,7 +1781,7 @@ func TestEncoderInlineTablesKeepsArraysOfTables(t *testing.T) {
Small inlineTLS `toml:"small"`
}
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 {
t.Fatalf("marshal: %v", err)
}
@@ -1973,7 +1969,7 @@ func TestMarshalNumber(t *testing.T) {
t.Fatalf("output %q", out)
}
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)
}
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")
type Config struct {
Host string `toml:"host,required"`
@@ -2083,7 +2079,7 @@ func TestUnmarshalWithOptions(t *testing.T) {
Host string `toml:"host"`
Extra int `toml:"extra"`
}
if err := UnmarshalWithOptions(data, &cfg, DecodeOptions{}); err != nil {
if err := Unmarshal(data, &cfg); err != nil {
t.Fatal(err)
}
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) {
err := UnmarshalWithOptions(data, &Config{}, DecodeOptions{DisallowUnknownFields: true})
err := Unmarshal(data, &Config{}, RejectUnknownFields(true))
want := `interpres: unknown field "extra" for interpres.Config`
if err == nil || err.Error() != 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) {
var tree map[string]any
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)
}
if got, ok := tree["n"].(Number); !ok || got != "1_000" {
@@ -2118,10 +2114,10 @@ func TestUnmarshalWithOptions(t *testing.T) {
nested.WriteString("]")
}
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")
}
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")
}
})
@@ -2253,7 +2249,7 @@ func TestEmitFieldComments(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 {
t.Fatal(err)
}
@@ -2278,7 +2274,7 @@ func TestEmitFieldComments(t *testing.T) {
type Nested struct {
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 {
t.Fatal(err)
}
+9 -11
View File
@@ -70,12 +70,11 @@ func ExampleMarshal() {
// port = 9090
}
func ExampleDecoder() {
func ExampleNumbersAsLiterals() {
var tree map[string]any
err := interpres.NewDecoder().
DisallowUnknownFields().
UseNumber().
Decode([]byte("rate = 1_000\n"), &tree)
err := interpres.Unmarshal([]byte("rate = 1_000\n"), &tree,
interpres.RejectUnknownFields(true),
interpres.NumbersAsLiterals(true))
if err != nil {
log.Fatal(err)
}
@@ -83,16 +82,15 @@ func ExampleDecoder() {
// Output: 1_000 1_000
}
func ExampleEncoder() {
func ExampleInlineTables() {
type Config struct {
Title string `toml:"title"`
Extras map[string]string `toml:"extras,inline"`
}
out, err := interpres.NewEncoder().
Layout(interpres.LayoutKindDeclaration).
LiteralMultiline(80).
InlineTables(40).
Marshal(Config{Title: "demo", Extras: map[string]string{"b": "two", "a": "one"}})
out, err := interpres.Marshal(Config{Title: "demo", Extras: map[string]string{"b": "two", "a": "one"}},
interpres.Layout(interpres.LayoutKindDeclaration),
interpres.LiteralMultiline(80),
interpres.InlineTables(40))
if err != nil {
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)
out2, err := interpres.NewEncoder().Layout(interpres.LayoutKindDeclaration).Marshal(cfg)
out2, err := interpres.Marshal(cfg, interpres.Layout(interpres.LayoutKindDeclaration))
if err != nil {
fmt.Fprintln(stderr, "marshal:", err)
return 1
+1 -1
View File
@@ -109,7 +109,7 @@ func FuzzTargetedDecode(f *testing.F) {
f.Fuzz(func(t *testing.T, data []byte) {
doc := fuzzDocument(data)
var tgt fuzzDoc
tgtErr := NewDecoder().Decode(doc, &tgt)
tgtErr := Unmarshal(doc, &tgt)
if tgtErr != nil {
// A document with several decode-stage findings reports a different
// 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()
//
// 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
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
// 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
// case-insensitive match on the field name when no tag is present. A tag of
// "-" skips the field.
@@ -269,8 +274,8 @@ func parseWithOptions(ctx context.Context, data []byte, opts parseOptions, wantD
// bare integer as its nanosecond count.
//
// Unmarshal is equivalent to UnmarshalContext with context.Background.
func Unmarshal(data []byte, v any) error {
return UnmarshalContext(context.Background(), data, v)
func Unmarshal(data []byte, v any, opts ...UnmarshalOption) error {
return UnmarshalContext(context.Background(), data, v, opts...)
}
// 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.
func UnmarshalContext(ctx context.Context, data []byte, v any) error {
dec := newDecoder()
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)
func UnmarshalContext(ctx context.Context, data []byte, v any, opts ...UnmarshalOption) error {
return settingsFor(opts).decode(ctx, data, v)
}
// A Decoder decodes a TOML document into a Go value with configurable
// strictness and configurable limits on the parse it performs.
type Decoder struct {
// UnmarshalRead reads the document from r and decodes it into v, the
// streaming-shaped entry the json/v2 vocabulary uses. The reader is
// 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
useNumber bool
maxDepth int
maxInputSize int
localLoc *time.Location
ctx context.Context
}
// NewDecoder returns a Decoder.
func NewDecoder() *Decoder { return &Decoder{} }
// DisallowUnknownFields causes Decode to return an error when the document
// contains a key with no matching destination struct field.
func (d *Decoder) DisallowUnknownFields() *Decoder {
d.disallowUnknown = true
return d
func settingsFor(opts []UnmarshalOption) *decodeSettings {
s := &decodeSettings{ctx: context.Background()}
for _, opt := range opts {
opt(s)
}
return s
}
// UseNumber causes the numbers of the document to reach the value tree as a
// Number carrying the literal the document wrote, so 0x1f, 1_000, +1.0 and
// inf survive a round trip with their spelling intact. A destination of a
// 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 {
// decode runs the decode the settings describe: the targeted parse when the
// destination takes it, the tree path otherwise or on fallback.
func (s *decodeSettings) decode(ctx context.Context, data []byte, v any) error {
dec := newDecoder()
dec.disallowUnknown = d.disallowUnknown
dec.disallowUnknown = s.disallowUnknown
dec.ctx = ctx
dec.loc = d.localLoc
dec.loc = s.localLoc
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. The size limit is
// checked here, the targeted parse being the parse itself.
if d.maxInputSize > 0 && len(data) > d.maxInputSize {
return fmt.Errorf("interpres: input is %d bytes, over the limit of %d", 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), 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
}
}
opts := parseOptions{
maxDepth: d.maxDepth,
maxInputSize: d.maxInputSize,
useNumber: d.useNumber,
maxDepth: s.maxDepth,
maxInputSize: s.maxInputSize,
useNumber: s.useNumber,
}
tree, doc, err := parseWithOptions(ctx, data, opts, typeWantsOrder(reflect.TypeOf(v)))
if err != nil {
@@ -427,33 +388,50 @@ func (d *Decoder) DecodeContext(ctx context.Context, data []byte, v any) error {
return dec.decode(tree, v)
}
// DecodeOptions gathers the options a one-shot decode call can set, the
// struct-shaped alternative to building a Decoder for a single document. The
// zero value decodes with the defaults: unknown keys ignored, numbers
// evaluated, and no limit beyond the nesting default.
type DecodeOptions struct {
// 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
// RejectUnknownFields makes the decode fail when the document contains a
// key with no matching destination struct field. Off by default: unknown
// keys are ignored.
func RejectUnknownFields(v bool) UnmarshalOption {
return func(s *decodeSettings) { s.disallowUnknown = v }
}
// UnmarshalWithOptions decodes data into v with the options set, the one-shot
// form of building a Decoder. See DecodeOptions for the fields and their
// defaults.
func UnmarshalWithOptions(data []byte, v any, opts DecodeOptions) error {
dec := &Decoder{
disallowUnknown: opts.DisallowUnknownFields,
useNumber: opts.UseNumber,
maxDepth: opts.MaxDepth,
maxInputSize: opts.MaxInputSize,
}
return dec.DecodeContext(context.Background(), data, v)
// NumbersAsLiterals keeps the numbers of the document as a Number carrying
// the literal the document wrote, so 0x1f, 1_000, +1.0 and inf survive a
// round trip with their spelling intact. A destination of a concrete numeric
// kind still takes the evaluated value; the literal is kept only where a
// Number, or an any, receives it. Off by default: numbers evaluate to
// int64 and float64.
func NumbersAsLiterals(v bool) UnmarshalOption {
return func(s *decodeSettings) { s.useNumber = 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
@@ -543,9 +521,13 @@ type UnmarshalerContext interface {
// string quoting style, and the choice between `[table]` headers and inline
// 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.
func Marshal(v any) ([]byte, error) {
return MarshalContext(context.Background(), v)
func Marshal(v any, opts ...MarshalOption) ([]byte, error) {
return MarshalContext(context.Background(), v, opts...)
}
// 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
// buffer, the shape json.MarshalAppend has. A failed encoding leaves buf
// untouched and comes back with a nil slice.
func MarshalAppend(buf []byte, v any) ([]byte, error) {
out, err := Marshal(v)
// buffer, the shape json/v2's MarshalAppendTo and json's MarshalAppend have.
// A failed encoding leaves buf untouched and comes back with a nil slice.
func MarshalAppend(buf []byte, v any, opts ...MarshalOption) ([]byte, error) {
out, err := Marshal(v, opts...)
if err != nil {
return nil, err
}
@@ -623,11 +605,25 @@ func MarshalAppend(buf []byte, v any) ([]byte, error) {
}
// 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 {
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.
@@ -642,48 +638,58 @@ const (
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
// suite in both directions:
//
// Layout: LayoutKindGrouped (scalars first, then tables, then
// arrays of tables)
// OmitEmptyArrays: false (a nil/empty []string slice emits [] as a value;
// a nil/empty []Item struct slice is still skipped)
// LiteralMultilineAt: 0 (always emit the escaped basic form, never a
// literal one)
// 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
// out, err := interpres.Marshal(cfg,
// interpres.Layout(interpres.LayoutKindDeclaration),
// interpres.InlineTables(60))
type MarshalOption func(*encodeSettings)
// encodeSettings is the option carrier of one encode call.
type encodeSettings struct {
ctx context.Context
cfg encodeConfig
}
// NewEncoder returns an Encoder with default options.
func NewEncoder() *Encoder { return &Encoder{layout: LayoutKindGrouped} }
func settingsForEncode(opts []MarshalOption) *encodeSettings {
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:
// LayoutKindGrouped, the default, reorders them scalars first, then tables,
// then arrays of tables; LayoutKindDeclaration preserves declaration order.
func (e *Encoder) Layout(kind LayoutKind) *Encoder {
e.layout = kind
return e
func Layout(kind LayoutKind) MarshalOption {
return func(s *encodeSettings) { s.cfg.layout = kind }
}
// OmitEmptyArrays opts in to skipping empty (non-nil, length 0) TOML arrays
// of scalars. The default emits them as "key = []". Nil slices and empty
// arrays of tables are already always omitted.
func (e *Encoder) OmitEmptyArrays() *Encoder {
e.omitEmptyArrays = true
return e
func OmitEmptyArrays(v bool) MarshalOption {
return func(s *encodeSettings) { s.cfg.omitEmptyArrays = v }
}
// 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
// is selected only when the value contains an internal newline; otherwise the
// single-line basic form is used regardless of this setting.
func (e *Encoder) LiteralMultiline(threshold int) *Encoder {
e.literalMultilineAt = threshold
return e
func LiteralMultiline(threshold int) MarshalOption {
return func(s *encodeSettings) { s.cfg.literalMultilineAt = threshold }
}
// 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
// inlined table follows the same rule as any other value line: it lands in the
// section of the header that precedes it.
func (e *Encoder) InlineTables(threshold int) *Encoder {
e.inlineTablesAt = threshold
return e
func InlineTables(threshold int) MarshalOption {
return func(s *encodeSettings) { s.cfg.inlineTablesAt = threshold }
}
// 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=`
// option prints none. Multi-line comments carry newlines in the tag, each
// line printed with its own "# " marker.
func (e *Encoder) EmitFieldComments() *Encoder {
e.emitFieldComments = true
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
func EmitFieldComments(v bool) MarshalOption {
return func(s *encodeSettings) { s.cfg.emitFieldComments = v }
}
+2 -2
View File
@@ -317,7 +317,7 @@ func TestDisallowUnknownFields(t *testing.T) {
}
var strict C
err := NewDecoder().DisallowUnknownFields().Decode(data, &strict)
err := Unmarshal(data, &strict, RejectUnknownFields(true))
if err == 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")
for range 20 {
var c C
err := NewDecoder().DisallowUnknownFields().Decode(data, &c)
err := Unmarshal(data, &c, RejectUnknownFields(true))
if err == nil {
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:
// 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
// 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.Set("zebra", int64(1))
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 {
t.Fatal(err)
}
+1 -1
View File
@@ -726,7 +726,7 @@ func (p *parser) parseAtom() (any, error) {
if err != nil {
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.
if p.useNumber {
return Number(tok), nil
+20 -21
View File
@@ -54,7 +54,7 @@ func TestTargetedStrictFindings(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var cfg targetCfg
err := NewDecoder().DisallowUnknownFields().Decode([]byte(tt.doc), &cfg)
err := Unmarshal([]byte(tt.doc), &cfg, RejectUnknownFields(true))
if err == nil {
t.Fatalf("no error, want %q", tt.want)
}
@@ -102,7 +102,7 @@ func TestTargetedParseErrors(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var cfg targetCfg
err := NewDecoder().Decode([]byte(tt.doc), &cfg)
err := Unmarshal([]byte(tt.doc), &cfg)
if err == nil {
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.
var cfg, ref targetCfg
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)
}
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) {
var cfg, ref targetCfg
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)
}
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) {
var cfg targetCfg
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)
}
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) {
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"
if err == nil || err.Error() != want {
t.Errorf("err = %v, want %q", err, want)
@@ -185,7 +185,7 @@ func TestTargetedSilentShapes(t *testing.T) {
var cfg struct {
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)
}
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) {
var cfg targetCfg
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)
}
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) {
var cfg targetCfg
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") {
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 {
var ref, tgt targetCfg
refErr := treeDecodeInto([]byte(doc), &ref)
dec := NewDecoder()
tgtErr := dec.Decode([]byte(doc), &tgt)
tgtErr := Unmarshal([]byte(doc), &tgt)
if (refErr == nil) != (tgtErr == nil) {
t.Errorf("doc %d: error presence disagrees: tree %v, targeted %v", i, refErr, tgtErr)
continue
@@ -283,7 +282,7 @@ func TestTargetedFallbackContracts(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var cfg targetCfg
err := NewDecoder().Decode([]byte(tt.doc), &cfg)
err := Unmarshal([]byte(tt.doc), &cfg)
if err == nil || err.Error() != 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) {
var cfg targetCfg
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"
if err == nil || err.Error() != want {
t.Errorf("err = %v, want %q", err, want)
@@ -362,7 +361,7 @@ func TestTargetedBranchParity(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var cfg targetCfg
err := NewDecoder().DisallowUnknownFields().Decode([]byte(tt.doc), &cfg)
err := Unmarshal([]byte(tt.doc), &cfg, RejectUnknownFields(true))
if tt.want == "" {
if err != nil {
t.Fatalf("err = %v, want nil", err)
@@ -386,7 +385,7 @@ func TestTargetedDecodeHookFields(t *testing.T) {
}
var cfg Cfg
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)
}
if cfg.IP.String() != "192.0.2.1" {
@@ -410,7 +409,7 @@ func TestTargetedOddShapes(t *testing.T) {
}
var cfg, ref Cfg
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)
}
if err := treeDecodeInto(doc, &ref); err != nil {
@@ -426,7 +425,7 @@ func TestTargetedOddShapes(t *testing.T) {
}
var cfg, ref Cfg
doc := []byte("m = { a = 1 }\n")
err := NewDecoder().Decode(doc, &cfg)
err := Unmarshal(doc, &cfg)
refErr := treeDecodeInto(doc, &ref)
if err == nil || refErr == nil {
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) {
var cfg targetCfg
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\""
if err == nil || err.Error() != 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) {
var cfg targetCfg
doc := []byte("num = 1_000\n")
if err := NewDecoder().Decode(doc, &cfg); err != nil {
if err := Unmarshal(doc, &cfg); err != nil {
t.Fatal(err)
}
if cfg.Num != 1000 {
@@ -459,7 +458,7 @@ func TestTargetedOddShapes(t *testing.T) {
Big int64 `toml:"big"`
}
doc := []byte("big = 999999999999999999\n")
if err := NewDecoder().Decode(doc, &cfg); err != nil {
if err := Unmarshal(doc, &cfg); err != nil {
t.Fatal(err)
}
if cfg.Big != 999999999999999999 {
@@ -506,7 +505,7 @@ func TestTargetedMapTableShapes(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var cfg, ref targetCfg
err := NewDecoder().Decode([]byte(tt.doc), &cfg)
err := Unmarshal([]byte(tt.doc), &cfg)
refErr := treeDecodeInto([]byte(tt.doc), &ref)
if (err == nil) != (refErr == nil) {
t.Fatalf("error presence disagrees: tree %v, targeted %v", refErr, err)