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
|
(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
|
||||||
rejected with a `SyntaxError` naming the limit.
|
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
|
### Changed
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ var timeType = reflect.TypeFor[time.Time]()
|
|||||||
var (
|
var (
|
||||||
unmarshalerType = reflect.TypeFor[Unmarshaler]()
|
unmarshalerType = reflect.TypeFor[Unmarshaler]()
|
||||||
textUnmarshalerType = reflect.TypeFor[encoding.TextUnmarshaler]()
|
textUnmarshalerType = reflect.TypeFor[encoding.TextUnmarshaler]()
|
||||||
|
numberType = reflect.TypeFor[Number]()
|
||||||
)
|
)
|
||||||
|
|
||||||
// The per-type flags record which interface lookups a decode into that type
|
// 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 setDuration(dst, v)
|
||||||
}
|
}
|
||||||
return setBasic(dst, reflect.ValueOf(v), "string")
|
return setBasic(dst, reflect.ValueOf(v), "string")
|
||||||
|
case Number:
|
||||||
|
return setNumber(dst, v)
|
||||||
case bool:
|
case bool:
|
||||||
return setBasic(dst, reflect.ValueOf(v), "bool")
|
return setBasic(dst, reflect.ValueOf(v), "bool")
|
||||||
case int64:
|
case int64:
|
||||||
@@ -389,6 +392,28 @@ func setDuration(dst reflect.Value, s string) error {
|
|||||||
return nil
|
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 {
|
func setInt(dst reflect.Value, v int64) error {
|
||||||
switch dst.Kind() {
|
switch dst.Kind() {
|
||||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
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)
|
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
|
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`.
|
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
|
### 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
|
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`.
|
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
|
### Date-time values
|
||||||
|
|
||||||
Offset date-times decode into `OffsetDateTime`, whose embedded `time.Time` is the
|
Offset date-times decode into `OffsetDateTime`, whose embedded `time.Time` is the
|
||||||
@@ -639,6 +664,7 @@ concurrent use.
|
|||||||
| Method | Default | Effect |
|
| Method | Default | Effect |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `DisallowUnknownFields()` | off | a key with no matching struct field is an error |
|
| `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 |
|
| `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 |
|
| `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).
|
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
|
### Date-time wrappers
|
||||||
|
|
||||||
```go
|
```go
|
||||||
|
|||||||
@@ -750,6 +750,11 @@ func normaliseValue(v reflect.Value) (any, error) {
|
|||||||
if v.Type() == durationType {
|
if v.Type() == durationType {
|
||||||
return time.Duration(v.Int()).String(), nil
|
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
|
// A type that renders itself as text becomes a TOML string, scalar kinds
|
||||||
// and structs alike.
|
// and structs alike.
|
||||||
s, isText, err := textValue(v)
|
s, isText, err := textValue(v)
|
||||||
@@ -1171,6 +1176,12 @@ func (e *encoder) writeValue(val any) error {
|
|||||||
case int64:
|
case int64:
|
||||||
e.buf.WriteString(strconv.FormatInt(v, 10))
|
e.buf.WriteString(strconv.FormatInt(v, 10))
|
||||||
return nil
|
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:
|
case float64:
|
||||||
return e.writeFloat(v)
|
return e.writeFloat(v)
|
||||||
case time.Time:
|
case time.Time:
|
||||||
|
|||||||
@@ -1946,3 +1946,55 @@ func TestMarshalerResultIsNormalised(t *testing.T) {
|
|||||||
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
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
|
return tree, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseOptions bound the work one parse may do. A zero field takes the
|
// parseOptions bound the work one parse may do and the shape it produces. A
|
||||||
// default.
|
// zero field takes the default.
|
||||||
type parseOptions struct {
|
type parseOptions struct {
|
||||||
maxDepth int
|
maxDepth int
|
||||||
maxInputSize int
|
maxInputSize int
|
||||||
|
useNumber bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseWithOptions parses data, building the node tree of a Document when
|
// 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
|
// The parser scans data in place; it only reads the buffer, and every
|
||||||
// string it stores in the tree is copied out of it.
|
// 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()
|
tree, err := p.parse()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
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.
|
// strictness and configurable limits on the parse it performs.
|
||||||
type Decoder struct {
|
type Decoder struct {
|
||||||
disallowUnknown bool
|
disallowUnknown bool
|
||||||
|
useNumber bool
|
||||||
maxDepth int
|
maxDepth int
|
||||||
maxInputSize int
|
maxInputSize int
|
||||||
}
|
}
|
||||||
@@ -207,6 +209,16 @@ func (d *Decoder) DisallowUnknownFields() *Decoder {
|
|||||||
return d
|
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
|
// 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
|
// 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
|
// 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{
|
tree, _, err := parseWithOptions(ctx, data, parseOptions{
|
||||||
maxDepth: d.maxDepth,
|
maxDepth: d.maxDepth,
|
||||||
maxInputSize: d.maxInputSize,
|
maxInputSize: d.maxInputSize,
|
||||||
|
useNumber: d.useNumber,
|
||||||
}, false)
|
}, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -10,6 +10,50 @@ import (
|
|||||||
"strings"
|
"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
|
// decodeNumber parses a bare numeric token under strict TOML rules: no leading
|
||||||
// zeros, underscores only between digits, prefixed radixes without a sign, and
|
// zeros, underscores only between digits, prefixed radixes without a sign, and
|
||||||
// floats with explicit fraction/exponent digits.
|
// floats with explicit fraction/exponent digits.
|
||||||
|
|||||||
@@ -36,6 +36,10 @@ type parser struct {
|
|||||||
maxDepth int
|
maxDepth int
|
||||||
depth 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
|
root map[string]any
|
||||||
current map[string]any
|
current map[string]any
|
||||||
headers map[string]bool
|
headers map[string]bool
|
||||||
@@ -721,6 +725,11 @@ 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
|
||||||
|
// literal instead of the evaluated value.
|
||||||
|
if p.useNumber {
|
||||||
|
return Number(tok), nil
|
||||||
|
}
|
||||||
return v, nil
|
return v, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user