feat!: rework the public API to json/v2-style variadic options
Test / test (push) Successful in 1m49s
Test / test (push) Successful in 1m49s
Assisted-by: GLM 5.3 Flash
This commit is contained in:
+91
-71
@@ -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).
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user