Support encoding json.Number type (#923)

Co-authored-by: Thomas Pelletier <thomas@pelletier.codes>
This commit is contained in:
Daniel Graña
2024-01-25 20:21:02 -03:00
committed by GitHub
parent 2ca21fb7b4
commit f5486d590f
5 changed files with 136 additions and 61 deletions
+53 -52
View File
@@ -98,9 +98,9 @@ Given the following struct, let's see how to read it and write it as TOML:
```go ```go
type MyConfig struct { type MyConfig struct {
Version int Version int
Name string Name string
Tags []string Tags []string
} }
``` ```
@@ -119,7 +119,7 @@ tags = ["go", "toml"]
var cfg MyConfig var cfg MyConfig
err := toml.Unmarshal([]byte(doc), &cfg) err := toml.Unmarshal([]byte(doc), &cfg)
if err != nil { if err != nil {
panic(err) panic(err)
} }
fmt.Println("version:", cfg.Version) fmt.Println("version:", cfg.Version)
fmt.Println("name:", cfg.Name) fmt.Println("name:", cfg.Name)
@@ -140,14 +140,14 @@ as a TOML document:
```go ```go
cfg := MyConfig{ cfg := MyConfig{
Version: 2, Version: 2,
Name: "go-toml", Name: "go-toml",
Tags: []string{"go", "toml"}, Tags: []string{"go", "toml"},
} }
b, err := toml.Marshal(cfg) b, err := toml.Marshal(cfg)
if err != nil { if err != nil {
panic(err) panic(err)
} }
fmt.Println(string(b)) fmt.Println(string(b))
@@ -175,17 +175,17 @@ the AST level. See https://pkg.go.dev/github.com/pelletier/go-toml/v2/unstable.
Execution time speedup compared to other Go TOML libraries: Execution time speedup compared to other Go TOML libraries:
<table> <table>
<thead> <thead>
<tr><th>Benchmark</th><th>go-toml v1</th><th>BurntSushi/toml</th></tr> <tr><th>Benchmark</th><th>go-toml v1</th><th>BurntSushi/toml</th></tr>
</thead> </thead>
<tbody> <tbody>
<tr><td>Marshal/HugoFrontMatter-2</td><td>1.9x</td><td>2.2x</td></tr> <tr><td>Marshal/HugoFrontMatter-2</td><td>1.9x</td><td>2.2x</td></tr>
<tr><td>Marshal/ReferenceFile/map-2</td><td>1.7x</td><td>2.1x</td></tr> <tr><td>Marshal/ReferenceFile/map-2</td><td>1.7x</td><td>2.1x</td></tr>
<tr><td>Marshal/ReferenceFile/struct-2</td><td>2.2x</td><td>3.0x</td></tr> <tr><td>Marshal/ReferenceFile/struct-2</td><td>2.2x</td><td>3.0x</td></tr>
<tr><td>Unmarshal/HugoFrontMatter-2</td><td>2.9x</td><td>2.7x</td></tr> <tr><td>Unmarshal/HugoFrontMatter-2</td><td>2.9x</td><td>2.7x</td></tr>
<tr><td>Unmarshal/ReferenceFile/map-2</td><td>2.6x</td><td>2.7x</td></tr> <tr><td>Unmarshal/ReferenceFile/map-2</td><td>2.6x</td><td>2.7x</td></tr>
<tr><td>Unmarshal/ReferenceFile/struct-2</td><td>4.6x</td><td>5.1x</td></tr> <tr><td>Unmarshal/ReferenceFile/struct-2</td><td>4.6x</td><td>5.1x</td></tr>
</tbody> </tbody>
</table> </table>
<details><summary>See more</summary> <details><summary>See more</summary>
<p>The table above has the results of the most common use-cases. The table below <p>The table above has the results of the most common use-cases. The table below
@@ -193,22 +193,22 @@ contains the results of all benchmarks, including unrealistic ones. It is
provided for completeness.</p> provided for completeness.</p>
<table> <table>
<thead> <thead>
<tr><th>Benchmark</th><th>go-toml v1</th><th>BurntSushi/toml</th></tr> <tr><th>Benchmark</th><th>go-toml v1</th><th>BurntSushi/toml</th></tr>
</thead> </thead>
<tbody> <tbody>
<tr><td>Marshal/SimpleDocument/map-2</td><td>1.8x</td><td>2.7x</td></tr> <tr><td>Marshal/SimpleDocument/map-2</td><td>1.8x</td><td>2.7x</td></tr>
<tr><td>Marshal/SimpleDocument/struct-2</td><td>2.7x</td><td>3.8x</td></tr> <tr><td>Marshal/SimpleDocument/struct-2</td><td>2.7x</td><td>3.8x</td></tr>
<tr><td>Unmarshal/SimpleDocument/map-2</td><td>3.8x</td><td>3.0x</td></tr> <tr><td>Unmarshal/SimpleDocument/map-2</td><td>3.8x</td><td>3.0x</td></tr>
<tr><td>Unmarshal/SimpleDocument/struct-2</td><td>5.6x</td><td>4.1x</td></tr> <tr><td>Unmarshal/SimpleDocument/struct-2</td><td>5.6x</td><td>4.1x</td></tr>
<tr><td>UnmarshalDataset/example-2</td><td>3.0x</td><td>3.2x</td></tr> <tr><td>UnmarshalDataset/example-2</td><td>3.0x</td><td>3.2x</td></tr>
<tr><td>UnmarshalDataset/code-2</td><td>2.3x</td><td>2.9x</td></tr> <tr><td>UnmarshalDataset/code-2</td><td>2.3x</td><td>2.9x</td></tr>
<tr><td>UnmarshalDataset/twitter-2</td><td>2.6x</td><td>2.7x</td></tr> <tr><td>UnmarshalDataset/twitter-2</td><td>2.6x</td><td>2.7x</td></tr>
<tr><td>UnmarshalDataset/citm_catalog-2</td><td>2.2x</td><td>2.3x</td></tr> <tr><td>UnmarshalDataset/citm_catalog-2</td><td>2.2x</td><td>2.3x</td></tr>
<tr><td>UnmarshalDataset/canada-2</td><td>1.8x</td><td>1.5x</td></tr> <tr><td>UnmarshalDataset/canada-2</td><td>1.8x</td><td>1.5x</td></tr>
<tr><td>UnmarshalDataset/config-2</td><td>4.1x</td><td>2.9x</td></tr> <tr><td>UnmarshalDataset/config-2</td><td>4.1x</td><td>2.9x</td></tr>
<tr><td>geomean</td><td>2.7x</td><td>2.8x</td></tr> <tr><td>geomean</td><td>2.7x</td><td>2.8x</td></tr>
</tbody> </tbody>
</table> </table>
<p>This table can be generated with <code>./ci.sh benchmark -a -html</code>.</p> <p>This table can be generated with <code>./ci.sh benchmark -a -html</code>.</p>
</details> </details>
@@ -233,24 +233,24 @@ Go-toml provides three handy command line tools:
* `tomljson`: Reads a TOML file and outputs its JSON representation. * `tomljson`: Reads a TOML file and outputs its JSON representation.
``` ```
$ go install github.com/pelletier/go-toml/v2/cmd/tomljson@latest $ go install github.com/pelletier/go-toml/v2/cmd/tomljson@latest
$ tomljson --help $ tomljson --help
``` ```
* `jsontoml`: Reads a JSON file and outputs a TOML representation. * `jsontoml`: Reads a JSON file and outputs a TOML representation.
``` ```
$ go install github.com/pelletier/go-toml/v2/cmd/jsontoml@latest $ go install github.com/pelletier/go-toml/v2/cmd/jsontoml@latest
$ jsontoml --help $ jsontoml --help
``` ```
* `tomll`: Lints and reformats a TOML file. * `tomll`: Lints and reformats a TOML file.
``` ```
$ go install github.com/pelletier/go-toml/v2/cmd/tomll@latest $ go install github.com/pelletier/go-toml/v2/cmd/tomll@latest
$ tomll --help $ tomll --help
``` ```
### Docker image ### Docker image
@@ -301,7 +301,7 @@ type doc struct {
d := doc{ d := doc{
A: inner{ A: inner{
B: "Before", B: "Before",
}, },
} }
@@ -565,10 +565,11 @@ complete solutions exist out there.
## Versioning ## Versioning
Go-toml follows [Semantic Versioning](https://semver.org). The supported version Expect for parts explicitely marked otherwise, go-toml follows [Semantic
of [TOML](https://github.com/toml-lang/toml) is indicated at the beginning of Versioning](https://semver.org). The supported version of
this document. The last two major versions of Go are supported [TOML](https://github.com/toml-lang/toml) is indicated at the beginning of this
(see [Go Release Policy](https://golang.org/doc/devel/release.html#policy)). document. The last two major versions of Go are supported (see [Go Release
Policy](https://golang.org/doc/devel/release.html#policy)).
## License ## License
+12 -1
View File
@@ -19,6 +19,7 @@ package main
import ( import (
"encoding/json" "encoding/json"
"flag"
"io" "io"
"github.com/pelletier/go-toml/v2" "github.com/pelletier/go-toml/v2"
@@ -33,7 +34,11 @@ Reading from a file:
jsontoml file.json > file.toml jsontoml file.json > file.toml
` `
var useJsonNumber bool
func main() { func main() {
flag.BoolVar(&useJsonNumber, "use-json-number", false, "unmarshal numbers into `json.Number` type instead of as `float64`")
p := cli.Program{ p := cli.Program{
Usage: usage, Usage: usage,
Fn: convert, Fn: convert,
@@ -45,11 +50,17 @@ func convert(r io.Reader, w io.Writer) error {
var v interface{} var v interface{}
d := json.NewDecoder(r) d := json.NewDecoder(r)
e := toml.NewEncoder(w)
if useJsonNumber {
d.UseNumber()
e.SetMarshalJsonNumbers(true)
}
err := d.Decode(&v) err := d.Decode(&v)
if err != nil { if err != nil {
return err return err
} }
e := toml.NewEncoder(w)
return e.Encode(v) return e.Encode(v)
} }
+19 -4
View File
@@ -11,10 +11,11 @@ import (
func TestConvert(t *testing.T) { func TestConvert(t *testing.T) {
examples := []struct { examples := []struct {
name string name string
input string input string
expected string expected string
errors bool errors bool
useJsonNumber bool
}{ }{
{ {
name: "valid json", name: "valid json",
@@ -26,6 +27,19 @@ func TestConvert(t *testing.T) {
}`, }`,
expected: `[mytoml] expected: `[mytoml]
a = 42.0 a = 42.0
`,
},
{
name: "use json number",
useJsonNumber: true,
input: `
{
"mytoml": {
"a": 42
}
}`,
expected: `[mytoml]
a = 42
`, `,
}, },
{ {
@@ -37,6 +51,7 @@ a = 42.0
for _, e := range examples { for _, e := range examples {
b := new(bytes.Buffer) b := new(bytes.Buffer)
useJsonNumber = e.useJsonNumber
err := convert(strings.NewReader(e.input), b) err := convert(strings.NewReader(e.input), b)
if e.errors { if e.errors {
require.Error(t, err) require.Error(t, err)
+29 -4
View File
@@ -3,6 +3,7 @@ package toml
import ( import (
"bytes" "bytes"
"encoding" "encoding"
"encoding/json"
"fmt" "fmt"
"io" "io"
"math" "math"
@@ -37,10 +38,11 @@ type Encoder struct {
w io.Writer w io.Writer
// global settings // global settings
tablesInline bool tablesInline bool
arraysMultiline bool arraysMultiline bool
indentSymbol string indentSymbol string
indentTables bool indentTables bool
marshalJsonNumbers bool
} }
// NewEncoder returns a new Encoder that writes to w. // NewEncoder returns a new Encoder that writes to w.
@@ -87,6 +89,17 @@ func (enc *Encoder) SetIndentTables(indent bool) *Encoder {
return enc return enc
} }
// SetMarshalJsonNumbers forces the encoder to serialize `json.Number` as a
// float or integer instead of relying on TextMarshaler to emit a string.
//
// *Unstable:* This method does not follow the compatiblity guarnatees of
// semver. It can be changed or removed without a new major version being
// issued.
func (enc *Encoder) SetMarshalJsonNumbers(indent bool) *Encoder {
enc.marshalJsonNumbers = indent
return enc
}
// Encode writes a TOML representation of v to the stream. // Encode writes a TOML representation of v to the stream.
// //
// If v cannot be represented to TOML it returns an error. // If v cannot be represented to TOML it returns an error.
@@ -252,6 +265,18 @@ func (enc *Encoder) encode(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, e
return append(b, x.String()...), nil return append(b, x.String()...), nil
case LocalDateTime: case LocalDateTime:
return append(b, x.String()...), nil return append(b, x.String()...), nil
case json.Number:
if enc.marshalJsonNumbers {
if x == "" { /// Useful zero value.
return append(b, "0"...), nil
} else if v, err := x.Int64(); err == nil {
return enc.encode(b, ctx, reflect.ValueOf(v))
} else if f, err := x.Float64(); err == nil {
return enc.encode(b, ctx, reflect.ValueOf(f))
} else {
return nil, fmt.Errorf("toml: unable to convert %q to int64 or float64", x)
}
}
} }
hasTextMarshaler := v.Type().Implements(textMarshalerType) hasTextMarshaler := v.Type().Implements(textMarshalerType)
+23
View File
@@ -948,6 +948,29 @@ func TestEncoderSetIndentSymbol(t *testing.T) {
assert.Equal(t, expected, w.String()) assert.Equal(t, expected, w.String())
} }
func TestEncoderSetMarshalJsonNumbers(t *testing.T) {
var w strings.Builder
enc := toml.NewEncoder(&w)
enc.SetMarshalJsonNumbers(true)
err := enc.Encode(map[string]interface{}{
"A": json.Number("1.1"),
"B": json.Number("42e-3"),
"C": json.Number("42"),
"D": json.Number("0"),
"E": json.Number("0.0"),
"F": json.Number(""),
})
require.NoError(t, err)
expected := `A = 1.1
B = 0.042
C = 42
D = 0
E = 0.0
F = 0
`
assert.Equal(t, expected, w.String())
}
func TestEncoderOmitempty(t *testing.T) { func TestEncoderOmitempty(t *testing.T) {
type doc struct { type doc struct {
String string `toml:",omitempty,multiline"` String string `toml:",omitempty,multiline"`