From f5486d590f7ffd345be34728e38b3089eaac5fb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Gra=C3=B1a?= Date: Thu, 25 Jan 2024 20:21:02 -0300 Subject: [PATCH] Support encoding json.Number type (#923) Co-authored-by: Thomas Pelletier --- README.md | 105 +++++++++++++++++++------------------- cmd/jsontoml/main.go | 13 ++++- cmd/jsontoml/main_test.go | 23 +++++++-- marshaler.go | 33 ++++++++++-- marshaler_test.go | 23 +++++++++ 5 files changed, 136 insertions(+), 61 deletions(-) diff --git a/README.md b/README.md index b10f97f..b0918c6 100644 --- a/README.md +++ b/README.md @@ -98,9 +98,9 @@ Given the following struct, let's see how to read it and write it as TOML: ```go type MyConfig struct { - Version int - Name string - Tags []string + Version int + Name string + Tags []string } ``` @@ -119,7 +119,7 @@ tags = ["go", "toml"] var cfg MyConfig err := toml.Unmarshal([]byte(doc), &cfg) if err != nil { - panic(err) + panic(err) } fmt.Println("version:", cfg.Version) fmt.Println("name:", cfg.Name) @@ -140,14 +140,14 @@ as a TOML document: ```go cfg := MyConfig{ - Version: 2, - Name: "go-toml", - Tags: []string{"go", "toml"}, + Version: 2, + Name: "go-toml", + Tags: []string{"go", "toml"}, } b, err := toml.Marshal(cfg) if err != nil { - panic(err) + panic(err) } 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: - - - - - - - - - - - + + + + + + + + + + +
Benchmarkgo-toml v1BurntSushi/toml
Marshal/HugoFrontMatter-21.9x2.2x
Marshal/ReferenceFile/map-21.7x2.1x
Marshal/ReferenceFile/struct-22.2x3.0x
Unmarshal/HugoFrontMatter-22.9x2.7x
Unmarshal/ReferenceFile/map-22.6x2.7x
Unmarshal/ReferenceFile/struct-24.6x5.1x
Benchmarkgo-toml v1BurntSushi/toml
Marshal/HugoFrontMatter-21.9x2.2x
Marshal/ReferenceFile/map-21.7x2.1x
Marshal/ReferenceFile/struct-22.2x3.0x
Unmarshal/HugoFrontMatter-22.9x2.7x
Unmarshal/ReferenceFile/map-22.6x2.7x
Unmarshal/ReferenceFile/struct-24.6x5.1x
See more

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.

- - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + +
Benchmarkgo-toml v1BurntSushi/toml
Marshal/SimpleDocument/map-21.8x2.7x
Marshal/SimpleDocument/struct-22.7x3.8x
Unmarshal/SimpleDocument/map-23.8x3.0x
Unmarshal/SimpleDocument/struct-25.6x4.1x
UnmarshalDataset/example-23.0x3.2x
UnmarshalDataset/code-22.3x2.9x
UnmarshalDataset/twitter-22.6x2.7x
UnmarshalDataset/citm_catalog-22.2x2.3x
UnmarshalDataset/canada-21.8x1.5x
UnmarshalDataset/config-24.1x2.9x
geomean2.7x2.8x
Benchmarkgo-toml v1BurntSushi/toml
Marshal/SimpleDocument/map-21.8x2.7x
Marshal/SimpleDocument/struct-22.7x3.8x
Unmarshal/SimpleDocument/map-23.8x3.0x
Unmarshal/SimpleDocument/struct-25.6x4.1x
UnmarshalDataset/example-23.0x3.2x
UnmarshalDataset/code-22.3x2.9x
UnmarshalDataset/twitter-22.6x2.7x
UnmarshalDataset/citm_catalog-22.2x2.3x
UnmarshalDataset/canada-21.8x1.5x
UnmarshalDataset/config-24.1x2.9x
geomean2.7x2.8x

This table can be generated with ./ci.sh benchmark -a -html.

@@ -233,24 +233,24 @@ Go-toml provides three handy command line tools: * `tomljson`: Reads a TOML file and outputs its JSON representation. - ``` - $ go install github.com/pelletier/go-toml/v2/cmd/tomljson@latest - $ tomljson --help - ``` + ``` + $ go install github.com/pelletier/go-toml/v2/cmd/tomljson@latest + $ tomljson --help + ``` * `jsontoml`: Reads a JSON file and outputs a TOML representation. - ``` - $ go install github.com/pelletier/go-toml/v2/cmd/jsontoml@latest - $ jsontoml --help - ``` + ``` + $ go install github.com/pelletier/go-toml/v2/cmd/jsontoml@latest + $ jsontoml --help + ``` * `tomll`: Lints and reformats a TOML file. - ``` - $ go install github.com/pelletier/go-toml/v2/cmd/tomll@latest - $ tomll --help - ``` + ``` + $ go install github.com/pelletier/go-toml/v2/cmd/tomll@latest + $ tomll --help + ``` ### Docker image @@ -301,7 +301,7 @@ type doc struct { d := doc{ A: inner{ - B: "Before", + B: "Before", }, } @@ -565,10 +565,11 @@ complete solutions exist out there. ## Versioning -Go-toml follows [Semantic Versioning](https://semver.org). The supported version -of [TOML](https://github.com/toml-lang/toml) is indicated at the beginning of -this document. The last two major versions of Go are supported -(see [Go Release Policy](https://golang.org/doc/devel/release.html#policy)). +Expect for parts explicitely marked otherwise, go-toml follows [Semantic +Versioning](https://semver.org). The supported version of +[TOML](https://github.com/toml-lang/toml) is indicated at the beginning of this +document. The last two major versions of Go are supported (see [Go Release +Policy](https://golang.org/doc/devel/release.html#policy)). ## License diff --git a/cmd/jsontoml/main.go b/cmd/jsontoml/main.go index 5a1fbe4..a7bc709 100644 --- a/cmd/jsontoml/main.go +++ b/cmd/jsontoml/main.go @@ -19,6 +19,7 @@ package main import ( "encoding/json" + "flag" "io" "github.com/pelletier/go-toml/v2" @@ -33,7 +34,11 @@ Reading from a file: jsontoml file.json > file.toml ` +var useJsonNumber bool + func main() { + flag.BoolVar(&useJsonNumber, "use-json-number", false, "unmarshal numbers into `json.Number` type instead of as `float64`") + p := cli.Program{ Usage: usage, Fn: convert, @@ -45,11 +50,17 @@ func convert(r io.Reader, w io.Writer) error { var v interface{} d := json.NewDecoder(r) + e := toml.NewEncoder(w) + + if useJsonNumber { + d.UseNumber() + e.SetMarshalJsonNumbers(true) + } + err := d.Decode(&v) if err != nil { return err } - e := toml.NewEncoder(w) return e.Encode(v) } diff --git a/cmd/jsontoml/main_test.go b/cmd/jsontoml/main_test.go index 4a4ad07..81c187a 100644 --- a/cmd/jsontoml/main_test.go +++ b/cmd/jsontoml/main_test.go @@ -11,10 +11,11 @@ import ( func TestConvert(t *testing.T) { examples := []struct { - name string - input string - expected string - errors bool + name string + input string + expected string + errors bool + useJsonNumber bool }{ { name: "valid json", @@ -26,6 +27,19 @@ func TestConvert(t *testing.T) { }`, expected: `[mytoml] 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 { b := new(bytes.Buffer) + useJsonNumber = e.useJsonNumber err := convert(strings.NewReader(e.input), b) if e.errors { require.Error(t, err) diff --git a/marshaler.go b/marshaler.go index 5aaff3b..306f79c 100644 --- a/marshaler.go +++ b/marshaler.go @@ -3,6 +3,7 @@ package toml import ( "bytes" "encoding" + "encoding/json" "fmt" "io" "math" @@ -37,10 +38,11 @@ type Encoder struct { w io.Writer // global settings - tablesInline bool - arraysMultiline bool - indentSymbol string - indentTables bool + tablesInline bool + arraysMultiline bool + indentSymbol string + indentTables bool + marshalJsonNumbers bool } // NewEncoder returns a new Encoder that writes to w. @@ -87,6 +89,17 @@ func (enc *Encoder) SetIndentTables(indent bool) *Encoder { 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. // // 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 case LocalDateTime: 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) diff --git a/marshaler_test.go b/marshaler_test.go index d984010..23b0efc 100644 --- a/marshaler_test.go +++ b/marshaler_test.go @@ -948,6 +948,29 @@ func TestEncoderSetIndentSymbol(t *testing.T) { 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) { type doc struct { String string `toml:",omitempty,multiline"`