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:
+24
-18
@@ -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.
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
+168
-188
@@ -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,
|
||||
// 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 }
|
||||
}
|
||||
return dec.DecodeContext(context.Background(), data, v)
|
||||
|
||||
// LocalTimeLocation sets the zone a local date-time is placed in when it
|
||||
// decodes into a time.Time destination. Without the option a local date-time
|
||||
// fills only its own wrapper type (LocalDateTime, LocalDate, LocalTime),
|
||||
// whose embedded time.Time is UTC; with the option, a time.Time destination
|
||||
// takes the value too, carried in the location given. A nil location restores
|
||||
// the default.
|
||||
func LocalTimeLocation(loc *time.Location) UnmarshalOption {
|
||||
return func(s *decodeSettings) { s.localLoc = loc }
|
||||
}
|
||||
|
||||
// MaxNestingDepth bounds how deeply arrays and inline tables may nest in a
|
||||
// document the decode accepts. The parser is a recursive descent, so a
|
||||
// document that nests without bound would exhaust the stack; one that nests
|
||||
// deeper than the limit is rejected with a SyntaxError naming it instead.
|
||||
// Use 0 or any negative value for the default of 10000, which no
|
||||
// hand-written document approaches.
|
||||
func MaxNestingDepth(depth int) UnmarshalOption {
|
||||
return func(s *decodeSettings) { s.maxDepth = depth }
|
||||
}
|
||||
|
||||
// MaxInputSize bounds the size of a document the decode accepts, in bytes; a
|
||||
// larger one is rejected before parsing starts. Use 0 or any negative value
|
||||
// for no limit, which is the default: the caller already holds the bytes, so
|
||||
// the size is a policy the caller sets rather than a protection the library
|
||||
// imposes on its own.
|
||||
func MaxInputSize(size int) UnmarshalOption {
|
||||
return func(s *decodeSettings) { s.maxInputSize = size }
|
||||
}
|
||||
|
||||
// Marshaler is the interface implemented by types that can produce a custom
|
||||
@@ -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
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user