Assisted-by: GLM 5.3 Flash
This commit is contained in:
@@ -46,6 +46,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
(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
|
||||
rejected with a `SyntaxError` naming the limit.
|
||||
- `Decoder.UseNumber()` decodes the integers and floats of the document into
|
||||
`Number`, which carries the literal the document wrote, so `0x1f`, `1_000`,
|
||||
`+1.0` and `inf` survive a round trip with their spelling instead of the
|
||||
normalised `31`, `1000` and `1.0`. Typed destinations take the evaluated
|
||||
value as before, a `Number` field takes the literal, and `Marshal` writes a
|
||||
`Number` back as its bare literal, rejecting one that is not a valid TOML
|
||||
number.
|
||||
|
||||
### Changed
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ var timeType = reflect.TypeFor[time.Time]()
|
||||
var (
|
||||
unmarshalerType = reflect.TypeFor[Unmarshaler]()
|
||||
textUnmarshalerType = reflect.TypeFor[encoding.TextUnmarshaler]()
|
||||
numberType = reflect.TypeFor[Number]()
|
||||
)
|
||||
|
||||
// The per-type flags record which interface lookups a decode into that type
|
||||
@@ -181,6 +182,8 @@ func (d *decoder) assign(data any, dst reflect.Value) error {
|
||||
return setDuration(dst, v)
|
||||
}
|
||||
return setBasic(dst, reflect.ValueOf(v), "string")
|
||||
case Number:
|
||||
return setNumber(dst, v)
|
||||
case bool:
|
||||
return setBasic(dst, reflect.ValueOf(v), "bool")
|
||||
case int64:
|
||||
@@ -389,6 +392,28 @@ func setDuration(dst reflect.Value, s string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// setNumber stores a Number, the literal UseNumber keeps. A Number destination
|
||||
// takes the literal as it is; every other destination takes the evaluated
|
||||
// value through the ordinary rules, so an integer field, a float field and a
|
||||
// duration field all read a Number the way they read the evaluated kind.
|
||||
func setNumber(dst reflect.Value, n Number) error {
|
||||
if dst.Type() == numberType {
|
||||
dst.SetString(string(n))
|
||||
return nil
|
||||
}
|
||||
v, err := decodeNumber(string(n))
|
||||
if err != nil {
|
||||
return fmt.Errorf("interpres: %w", err)
|
||||
}
|
||||
switch v := v.(type) {
|
||||
case int64:
|
||||
return setInt(dst, v)
|
||||
case float64:
|
||||
return setFloat(dst, v)
|
||||
}
|
||||
return fmt.Errorf("interpres: cannot assign number to %s", dst.Type())
|
||||
}
|
||||
|
||||
func setInt(dst reflect.Value, v int64) error {
|
||||
switch dst.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
|
||||
+123
@@ -1097,3 +1097,126 @@ func TestUnmarshalerReceivesOffsetDateTime(t *testing.T) {
|
||||
t.Errorf("local kind = %q, want interpres.LocalDateTime", cfg.L.Kind)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecoderUseNumber(t *testing.T) {
|
||||
data := []byte(`hex = 0x1f
|
||||
sep = 1_000
|
||||
signed = +1.0
|
||||
exp = 1e6
|
||||
posinf = inf
|
||||
negzero = -0.0
|
||||
plain = 42
|
||||
frac = 2.5
|
||||
`)
|
||||
t.Run("the tree keeps the literal", func(t *testing.T) {
|
||||
var tree map[string]any
|
||||
if err := NewDecoder().UseNumber().Decode(data, &tree); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for lit, key := range map[string]string{
|
||||
"0x1f": "hex", "1_000": "sep", "+1.0": "signed", "1e6": "exp",
|
||||
"inf": "posinf", "-0.0": "negzero", "42": "plain", "2.5": "frac",
|
||||
} {
|
||||
got, ok := tree[key].(Number)
|
||||
if !ok {
|
||||
t.Errorf("%s = %T, want Number", key, tree[key])
|
||||
continue
|
||||
}
|
||||
if string(got) != lit {
|
||||
t.Errorf("%s = %q, want %q", key, got, lit)
|
||||
}
|
||||
}
|
||||
})
|
||||
t.Run("typed fields take the evaluated value", func(t *testing.T) {
|
||||
var cfg struct {
|
||||
Hex Number `toml:"hex"`
|
||||
Plain int64 `toml:"plain"`
|
||||
Frac float64 `toml:"frac"`
|
||||
Rate time.Duration
|
||||
}
|
||||
dec := NewDecoder().UseNumber()
|
||||
if err := dec.Decode([]byte("hex = 0x1f\nplain = 42\nfrac = 2.5\nRate = 1_000\n"), &cfg); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if cfg.Hex != "0x1f" {
|
||||
t.Errorf("hex = %q, want 0x1f", cfg.Hex)
|
||||
}
|
||||
if cfg.Plain != 42 {
|
||||
t.Errorf("plain = %d, want 42", cfg.Plain)
|
||||
}
|
||||
if cfg.Frac != 2.5 {
|
||||
t.Errorf("frac = %g, want 2.5", cfg.Frac)
|
||||
}
|
||||
if cfg.Rate != 1000 {
|
||||
t.Errorf("rate = %s, want 1µs", cfg.Rate)
|
||||
}
|
||||
})
|
||||
t.Run("invalid numbers are still parse errors", func(t *testing.T) {
|
||||
for _, in := range []string{"a = 01\n", "a = 1__0\n", "a = 1x\n"} {
|
||||
var tree map[string]any
|
||||
if err := NewDecoder().UseNumber().Decode([]byte(in), &tree); err == nil {
|
||||
t.Errorf("%q decoded without an error", in)
|
||||
}
|
||||
}
|
||||
})
|
||||
t.Run("without UseNumber the tree holds the evaluated kinds", func(t *testing.T) {
|
||||
var tree map[string]any
|
||||
if err := NewDecoder().Decode([]byte("hex = 0x1f\nfrac = 2.5\n"), &tree); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if v, ok := tree["hex"].(int64); !ok || v != 31 {
|
||||
t.Errorf("hex = %#v, want int64 31", tree["hex"])
|
||||
}
|
||||
if v, ok := tree["frac"].(float64); !ok || v != 2.5 {
|
||||
t.Errorf("frac = %#v, want float64 2.5", tree["frac"])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestNumberMethods(t *testing.T) {
|
||||
tests := []struct {
|
||||
lit Number
|
||||
wantI int64
|
||||
wantF float64
|
||||
intErr bool
|
||||
}{
|
||||
{lit: "42", wantI: 42, wantF: 42},
|
||||
{lit: "0x1f", wantI: 31, wantF: 31},
|
||||
{lit: "1_000", wantI: 1000, wantF: 1000},
|
||||
{lit: "+1.0", wantF: 1, intErr: true},
|
||||
{lit: "1e6", wantF: 1e6, intErr: true},
|
||||
{lit: "inf", wantF: math.Inf(1), intErr: true},
|
||||
{lit: "-2.5", wantF: -2.5, intErr: true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
i, err := tt.lit.Int64()
|
||||
if tt.intErr && err == nil {
|
||||
t.Errorf("%q.Int64() succeeded with %d, want an error", tt.lit, i)
|
||||
}
|
||||
if !tt.intErr {
|
||||
if err != nil {
|
||||
t.Errorf("%q.Int64() = %v", tt.lit, err)
|
||||
continue
|
||||
}
|
||||
if i != tt.wantI {
|
||||
t.Errorf("%q.Int64() = %d, want %d", tt.lit, i, tt.wantI)
|
||||
}
|
||||
}
|
||||
f, err := tt.lit.Float64()
|
||||
if err != nil {
|
||||
t.Errorf("%q.Float64() = %v", tt.lit, err)
|
||||
continue
|
||||
}
|
||||
if f != tt.wantF {
|
||||
t.Errorf("%q.Float64() = %g, want %g", tt.lit, f, tt.wantF)
|
||||
}
|
||||
}
|
||||
for _, lit := range []Number{"01", "1__0", "abc", ""} {
|
||||
if _, err := lit.Float64(); err == nil {
|
||||
t.Errorf("%q.Float64() succeeded, want an error", lit)
|
||||
}
|
||||
if _, err := lit.Int64(); err == nil {
|
||||
t.Errorf("%q.Int64() succeeded, want an error", lit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+33
-1
@@ -157,7 +157,9 @@ and every 64 fields during the reflection walk.
|
||||
|
||||
When decoding into a struct, these values convert onto the destination's
|
||||
concrete types: any integer or unsigned width, floats, slices, nested structs
|
||||
and `map[string]T`.
|
||||
and `map[string]T`. `Decoder.UseNumber` replaces the two numeric rows of the
|
||||
table with `Number`, which keeps the literal; see
|
||||
[Numbers as literals](#numbers-as-literals).
|
||||
|
||||
### Target constraints
|
||||
|
||||
@@ -211,6 +213,29 @@ The decoder converts to the destination type with explicit overflow checks:
|
||||
A conversion that the rules do not allow produces an error wrapped with the
|
||||
offending key or index, for example `p: interpres: integer 300 overflows uint8`.
|
||||
|
||||
### Numbers as literals
|
||||
|
||||
`NewDecoder().UseNumber()` decodes every integer and float into `Number`, a
|
||||
string type that carries the literal the document wrote: `0x1f`, `1_000`,
|
||||
`+1.0`, `inf`. The shape is validated as strictly as ever, so `01` and `1__0`
|
||||
remain parse errors; only the evaluated value is replaced by the literal. A
|
||||
round trip through the value tree and `Marshal` keeps the spelling, where the
|
||||
default tree normalises `0x1f` to `31` and `+1.0` to `1.0`.
|
||||
|
||||
```go
|
||||
var tree map[string]any
|
||||
err := interpres.NewDecoder().UseNumber().Decode(data, &tree)
|
||||
lit := tree["rate"].(interpres.Number) // "1_000"
|
||||
```
|
||||
|
||||
A destination of a concrete kind is unaffected: an `int64` field, a `float64`
|
||||
field and a `time.Duration` field take the evaluated value they always took,
|
||||
and a `Number` field takes the literal. `Number.Float64` and `Number.Int64`
|
||||
evaluate the literal on demand, with an error for a float asked as an integer
|
||||
and for a literal that is not a valid TOML number. `Marshal` writes a `Number`
|
||||
as its bare literal and rejects one that is not a valid TOML number, whether it
|
||||
stands alone or inside a value array.
|
||||
|
||||
### Date-time values
|
||||
|
||||
Offset date-times decode into `OffsetDateTime`, whose embedded `time.Time` is the
|
||||
@@ -639,6 +664,7 @@ concurrent use.
|
||||
| Method | Default | Effect |
|
||||
|---|---|---|
|
||||
| `DisallowUnknownFields()` | off | a key with no matching struct field is an error |
|
||||
| `UseNumber()` | off | integers and floats decode into `Number`, which carries the literal; see [Numbers as literals](#numbers-as-literals) |
|
||||
| `MaxDepth(depth int)` | `10000` | bound how deeply arrays and inline tables may nest |
|
||||
| `MaxInputSize(size int)` | no limit | bound the size of the document, in bytes |
|
||||
|
||||
@@ -688,6 +714,12 @@ See [Custom encoding](#custom-encoding-marshaler).
|
||||
|
||||
See [Custom decoding](#custom-decoding-unmarshaler).
|
||||
|
||||
### `type Number string`
|
||||
|
||||
The literal a number was written with, what `UseNumber` decodes into and what
|
||||
`Marshal` writes back as it is. See
|
||||
[Numbers as literals](#numbers-as-literals).
|
||||
|
||||
### Date-time wrappers
|
||||
|
||||
```go
|
||||
|
||||
@@ -750,6 +750,11 @@ func normaliseValue(v reflect.Value) (any, error) {
|
||||
if v.Type() == durationType {
|
||||
return time.Duration(v.Int()).String(), nil
|
||||
}
|
||||
// A Number goes out as the literal it carries, the one string-kind value
|
||||
// that is not written quoted.
|
||||
if v.Type() == numberType {
|
||||
return Number(v.String()), nil
|
||||
}
|
||||
// A type that renders itself as text becomes a TOML string, scalar kinds
|
||||
// and structs alike.
|
||||
s, isText, err := textValue(v)
|
||||
@@ -1171,6 +1176,12 @@ func (e *encoder) writeValue(val any) error {
|
||||
case int64:
|
||||
e.buf.WriteString(strconv.FormatInt(v, 10))
|
||||
return nil
|
||||
case Number:
|
||||
if _, err := decodeNumber(string(v)); err != nil {
|
||||
return fmt.Errorf("interpres: %w", err)
|
||||
}
|
||||
e.buf.WriteString(string(v))
|
||||
return nil
|
||||
case float64:
|
||||
return e.writeFloat(v)
|
||||
case time.Time:
|
||||
|
||||
@@ -1946,3 +1946,55 @@ func TestMarshalerResultIsNormalised(t *testing.T) {
|
||||
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarshalNumber(t *testing.T) {
|
||||
t.Run("the literal is written as it is", func(t *testing.T) {
|
||||
out, err := Marshal(map[string]any{
|
||||
"hex": Number("0x1f"), "sep": Number("1_000"),
|
||||
"signed": Number("+1.0"), "inf": Number("inf"),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := "hex = 0x1f\ninf = inf\nsep = 1_000\nsigned = +1.0\n"
|
||||
if string(out) != want {
|
||||
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
||||
}
|
||||
})
|
||||
t.Run("a Number field round-trips", func(t *testing.T) {
|
||||
type Cfg struct {
|
||||
Rate Number `toml:"rate"`
|
||||
}
|
||||
out, err := Marshal(Cfg{Rate: "1_000"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(out) != "rate = 1_000\n" {
|
||||
t.Fatalf("output %q", out)
|
||||
}
|
||||
var back map[string]any
|
||||
if err := NewDecoder().UseNumber().Decode(out, &back); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got, ok := back["rate"].(Number); !ok || got != "1_000" {
|
||||
t.Errorf("round trip = %#v, want Number(\"1_000\")", back["rate"])
|
||||
}
|
||||
})
|
||||
t.Run("a Number inside a value array", func(t *testing.T) {
|
||||
out, err := Marshal(map[string]any{"vals": []any{Number("0x1f"), "s", int64(2)}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := "vals = [0x1f, \"s\", 2]\n"
|
||||
if string(out) != want {
|
||||
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
||||
}
|
||||
})
|
||||
t.Run("an invalid literal is an error", func(t *testing.T) {
|
||||
for _, lit := range []Number{"01", "1__0", "abc", "1.2.3"} {
|
||||
if _, err := Marshal(map[string]any{"n": lit}); err == nil {
|
||||
t.Errorf("Number(%q) encoded without an error", lit)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
+16
-3
@@ -127,11 +127,12 @@ func ParseMapContext(ctx context.Context, data []byte) (map[string]any, error) {
|
||||
return tree, err
|
||||
}
|
||||
|
||||
// parseOptions bound the work one parse may do. A zero field takes the
|
||||
// default.
|
||||
// parseOptions bound the work one parse may do and the shape it produces. A
|
||||
// zero field takes the default.
|
||||
type parseOptions struct {
|
||||
maxDepth int
|
||||
maxInputSize int
|
||||
useNumber bool
|
||||
}
|
||||
|
||||
// parseWithOptions parses data, building the node tree of a Document when
|
||||
@@ -152,7 +153,7 @@ func parseWithOptions(ctx context.Context, data []byte, opts parseOptions, wantD
|
||||
}
|
||||
// The parser scans data in place; it only reads the buffer, and every
|
||||
// string it stores in the tree is copied out of it.
|
||||
p := &parser{src: data, line: 1, ctx: ctx, maxDepth: maxDepth, wantDoc: wantDoc}
|
||||
p := &parser{src: data, line: 1, ctx: ctx, maxDepth: maxDepth, wantDoc: wantDoc, useNumber: opts.useNumber}
|
||||
tree, err := p.parse()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
@@ -193,6 +194,7 @@ func UnmarshalContext(ctx context.Context, data []byte, v any) error {
|
||||
// strictness and configurable limits on the parse it performs.
|
||||
type Decoder struct {
|
||||
disallowUnknown bool
|
||||
useNumber bool
|
||||
maxDepth int
|
||||
maxInputSize int
|
||||
}
|
||||
@@ -207,6 +209,16 @@ func (d *Decoder) DisallowUnknownFields() *Decoder {
|
||||
return d
|
||||
}
|
||||
|
||||
// UseNumber causes the numbers of the document to reach the value tree as a
|
||||
// Number carrying the literal the document wrote, so 0x1f, 1_000, +1.0 and
|
||||
// inf survive a round trip with their spelling intact. A destination of a
|
||||
// concrete numeric kind still takes the evaluated value; the literal is kept
|
||||
// only where a Number, or an any, receives it.
|
||||
func (d *Decoder) UseNumber() *Decoder {
|
||||
d.useNumber = true
|
||||
return d
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -242,6 +254,7 @@ func (d *Decoder) DecodeContext(ctx context.Context, data []byte, v any) error {
|
||||
tree, _, err := parseWithOptions(ctx, data, parseOptions{
|
||||
maxDepth: d.maxDepth,
|
||||
maxInputSize: d.maxInputSize,
|
||||
useNumber: d.useNumber,
|
||||
}, false)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -10,6 +10,50 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// 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
|
||||
// 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.
|
||||
//
|
||||
// Number is a string type, the shape encoding/json.Number has: the literal is
|
||||
// carried, not evaluated. Float64 and Int64 evaluate it on demand, and a
|
||||
// destination of another numeric kind takes the evaluated value through the
|
||||
// ordinary conversion rules.
|
||||
type Number string
|
||||
|
||||
// Float64 returns the value as a float64. An integer or radix literal
|
||||
// converts; a literal that is not a valid TOML number is an error.
|
||||
func (n Number) Float64() (float64, error) {
|
||||
v, err := decodeNumber(string(n))
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("interpres: %w", err)
|
||||
}
|
||||
switch v := v.(type) {
|
||||
case float64:
|
||||
return v, nil
|
||||
case int64:
|
||||
return float64(v), nil
|
||||
}
|
||||
return 0, fmt.Errorf("interpres: %q is not a number", n)
|
||||
}
|
||||
|
||||
// Int64 returns the value as an int64. A float literal is an error, however
|
||||
// whole its value, and so is a literal that is not a valid TOML number.
|
||||
func (n Number) Int64() (int64, error) {
|
||||
v, err := decodeNumber(string(n))
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("interpres: %w", err)
|
||||
}
|
||||
i, ok := v.(int64)
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("interpres: %q is not an integer", n)
|
||||
}
|
||||
return i, nil
|
||||
}
|
||||
|
||||
// String returns the literal itself.
|
||||
func (n Number) String() string { return string(n) }
|
||||
|
||||
// decodeNumber parses a bare numeric token under strict TOML rules: no leading
|
||||
// zeros, underscores only between digits, prefixed radixes without a sign, and
|
||||
// floats with explicit fraction/exponent digits.
|
||||
|
||||
@@ -36,6 +36,10 @@ type parser struct {
|
||||
maxDepth int
|
||||
depth int
|
||||
|
||||
// useNumber leaves the numbers a Number carries the literal, instead of
|
||||
// the evaluated int64 or float64 the tree holds by default.
|
||||
useNumber bool
|
||||
|
||||
root map[string]any
|
||||
current map[string]any
|
||||
headers map[string]bool
|
||||
@@ -721,6 +725,11 @@ func (p *parser) parseAtom() (any, error) {
|
||||
if err != nil {
|
||||
return nil, p.errf("%s", err)
|
||||
}
|
||||
// The token's shape is validated either way; UseNumber only keeps the
|
||||
// literal instead of the evaluated value.
|
||||
if p.useNumber {
|
||||
return Number(tok), nil
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user