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