Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b730b2be5d | |||
| a437caafe5 | |||
| be6c57be30 | |||
| d55304782e | |||
| 0977c05dd5 | |||
| bccd6e48f4 | |||
| 9b890cf9c5 | |||
| a3d5a0bb53 | |||
| d00d2cca6e | |||
| 86608d7fca | |||
| 4a1877957a | |||
| 3021d6d033 | |||
| 32788f26f8 | |||
| 8ed6d131eb | |||
| 7dad87762a | |||
| 2b69615b5d | |||
| 06fb30bf2e | |||
| 2e087bdf5f | |||
| caeb9f9631 | |||
| e7223fb40e | |||
| 05bedf36d8 | |||
| f5486d590f | |||
| 2ca21fb7b4 |
@@ -19,7 +19,7 @@ jobs:
|
||||
dry-run: false
|
||||
language: go
|
||||
- name: Upload Crash
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
if: failure() && steps.build.outcome == 'success'
|
||||
with:
|
||||
name: artifacts
|
||||
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@@ -50,7 +50,7 @@ jobs:
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
@@ -64,4 +64,4 @@ jobs:
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
uses: github/codeql-action/analyze@v3
|
||||
|
||||
@@ -13,8 +13,8 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup go
|
||||
uses: actions/setup-go@v4
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.21"
|
||||
go-version: "1.23"
|
||||
- name: Run tests with coverage
|
||||
run: ./ci.sh coverage -d "${GITHUB_BASE_REF-HEAD}"
|
||||
|
||||
@@ -20,9 +20,9 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v4
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.21"
|
||||
go-version: "1.23"
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
@@ -30,10 +30,10 @@ jobs:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v3
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
distribution: goreleaser
|
||||
version: latest
|
||||
args: release ${{ inputs.args }} --rm-dist
|
||||
version: '~> v2'
|
||||
args: release ${{ inputs.args }} --clean
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -11,8 +11,8 @@ jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ 'ubuntu-latest', 'windows-latest', 'macos-latest' ]
|
||||
go: [ '1.20', '1.21' ]
|
||||
os: [ 'ubuntu-latest', 'windows-latest', 'macos-latest', 'macos-14' ]
|
||||
go: [ '1.22', '1.23' ]
|
||||
runs-on: ${{ matrix.os }}
|
||||
name: ${{ matrix.go }}/${{ matrix.os }}
|
||||
steps:
|
||||
@@ -20,13 +20,13 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup go ${{ matrix.go }}
|
||||
uses: actions/setup-go@v4
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
- name: Run unit tests
|
||||
run: go test -race ./...
|
||||
release-check:
|
||||
if: ${{ github.ref != 'refs/heads/v2' }}
|
||||
uses: pelletier/go-toml/.github/workflows/release.yml@v2
|
||||
uses: ./.github/workflows/release.yml
|
||||
with:
|
||||
args: --snapshot
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
version: 2
|
||||
before:
|
||||
hooks:
|
||||
- go mod tidy
|
||||
|
||||
+14
-17
@@ -165,25 +165,22 @@ Checklist:
|
||||
|
||||
### New release
|
||||
|
||||
1. Decide on the next version number. Use semver.
|
||||
2. Generate release notes using [`gh`][gh]. Example:
|
||||
1. Decide on the next version number. Use semver. Review commits since last
|
||||
version to assess.
|
||||
2. Tag release. For example:
|
||||
```
|
||||
$ gh api -X POST \
|
||||
-F tag_name='v2.0.0-beta.5' \
|
||||
-F target_commitish='v2' \
|
||||
-F previous_tag_name='v2.0.0-beta.4' \
|
||||
--jq '.body' \
|
||||
repos/pelletier/go-toml/releases/generate-notes
|
||||
git checkout v2
|
||||
git pull
|
||||
git tag v2.2.0
|
||||
git push --tags
|
||||
```
|
||||
3. Look for "Other changes". That would indicate a pull request not labeled
|
||||
properly. Tweak labels and pull request titles until changelog looks good for
|
||||
users.
|
||||
4. [Draft new release][new-release].
|
||||
5. Fill tag and target with the same value used to generate the changelog.
|
||||
6. Set title to the new tag value.
|
||||
7. Paste the generated changelog.
|
||||
8. Check "create discussion", in the "Releases" category.
|
||||
9. Check pre-release if new version is an alpha or beta.
|
||||
3. CI automatically builds a draft Github release. Review it and edit as
|
||||
necessary. Look for "Other changes". That would indicate a pull request not
|
||||
labeled properly. Tweak labels and pull request titles until changelog looks
|
||||
good for users.
|
||||
4. Check "create discussion" box, in the "Releases" category.
|
||||
5. If new version is an alpha or beta only, check pre-release box.
|
||||
|
||||
|
||||
[issues-tracker]: https://github.com/pelletier/go-toml/issues
|
||||
[bug-report]: https://github.com/pelletier/go-toml/issues/new?template=bug_report.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:
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Benchmark</th><th>go-toml v1</th><th>BurntSushi/toml</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<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/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/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>
|
||||
</tbody>
|
||||
<thead>
|
||||
<tr><th>Benchmark</th><th>go-toml v1</th><th>BurntSushi/toml</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<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/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/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>
|
||||
</tbody>
|
||||
</table>
|
||||
<details><summary>See more</summary>
|
||||
<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>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Benchmark</th><th>go-toml v1</th><th>BurntSushi/toml</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<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>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>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/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/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>geomean</td><td>2.7x</td><td>2.8x</td></tr>
|
||||
</tbody>
|
||||
<thead>
|
||||
<tr><th>Benchmark</th><th>go-toml v1</th><th>BurntSushi/toml</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<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>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>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/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/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>geomean</td><td>2.7x</td><td>2.8x</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>This table can be generated with <code>./ci.sh benchmark -a -html</code>.</p>
|
||||
</details>
|
||||
@@ -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
|
||||
|
||||
@@ -261,7 +261,7 @@ Those tools are also available as a [Docker image][docker]. For example, to use
|
||||
docker run -i ghcr.io/pelletier/go-toml:v2 tomljson < example.toml
|
||||
```
|
||||
|
||||
Multiple versions are availble on [ghcr.io][docker].
|
||||
Multiple versions are available on [ghcr.io][docker].
|
||||
|
||||
[docker]: https://github.com/pelletier/go-toml/pkgs/container/go-toml
|
||||
|
||||
@@ -293,16 +293,16 @@ element in the interface to decode the object. For example:
|
||||
|
||||
```go
|
||||
type inner struct {
|
||||
B interface{}
|
||||
B interface{}
|
||||
}
|
||||
type doc struct {
|
||||
A interface{}
|
||||
A interface{}
|
||||
}
|
||||
|
||||
d := doc{
|
||||
A: inner{
|
||||
B: "Before",
|
||||
},
|
||||
A: inner{
|
||||
B: "Before",
|
||||
},
|
||||
}
|
||||
|
||||
data := `
|
||||
@@ -341,7 +341,7 @@ contained in the doc is superior to the capacity of the array. For example:
|
||||
|
||||
```go
|
||||
type doc struct {
|
||||
A [2]string
|
||||
A [2]string
|
||||
}
|
||||
d := doc{}
|
||||
err := toml.Unmarshal([]byte(`A = ["one", "two", "many"]`), &d)
|
||||
@@ -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 explicitly 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
|
||||
|
||||
|
||||
+12
-1
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
//go:build go1.18 || go1.19 || go1.20 || go1.21
|
||||
// +build go1.18 go1.19 go1.20 go1.21
|
||||
|
||||
package toml_test
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
module github.com/pelletier/go-toml/v2
|
||||
|
||||
go 1.16
|
||||
go 1.21.0
|
||||
|
||||
require github.com/stretchr/testify v1.8.4
|
||||
require github.com/stretchr/testify v1.9.0
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -1,17 +1,10 @@
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
+38
-36
@@ -57,7 +57,11 @@ type SeenTracker struct {
|
||||
currentIdx int
|
||||
}
|
||||
|
||||
var pool sync.Pool
|
||||
var pool = sync.Pool{
|
||||
New: func() interface{} {
|
||||
return &SeenTracker{}
|
||||
},
|
||||
}
|
||||
|
||||
func (s *SeenTracker) reset() {
|
||||
// Always contains a root element at index 0.
|
||||
@@ -149,8 +153,9 @@ func (s *SeenTracker) setExplicitFlag(parentIdx int) {
|
||||
|
||||
// CheckExpression takes a top-level node and checks that it does not contain
|
||||
// keys that have been seen in previous calls, and validates that types are
|
||||
// consistent.
|
||||
func (s *SeenTracker) CheckExpression(node *unstable.Node) error {
|
||||
// consistent. It returns true if it is the first time this node's key is seen.
|
||||
// Useful to clear array tables on first use.
|
||||
func (s *SeenTracker) CheckExpression(node *unstable.Node) (bool, error) {
|
||||
if s.entries == nil {
|
||||
s.reset()
|
||||
}
|
||||
@@ -166,7 +171,7 @@ func (s *SeenTracker) CheckExpression(node *unstable.Node) error {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SeenTracker) checkTable(node *unstable.Node) error {
|
||||
func (s *SeenTracker) checkTable(node *unstable.Node) (bool, error) {
|
||||
if s.currentIdx >= 0 {
|
||||
s.setExplicitFlag(s.currentIdx)
|
||||
}
|
||||
@@ -192,7 +197,7 @@ func (s *SeenTracker) checkTable(node *unstable.Node) error {
|
||||
} else {
|
||||
entry := s.entries[idx]
|
||||
if entry.kind == valueKind {
|
||||
return fmt.Errorf("toml: expected %s to be a table, not a %s", string(k), entry.kind)
|
||||
return false, fmt.Errorf("toml: expected %s to be a table, not a %s", string(k), entry.kind)
|
||||
}
|
||||
}
|
||||
parentIdx = idx
|
||||
@@ -201,25 +206,27 @@ func (s *SeenTracker) checkTable(node *unstable.Node) error {
|
||||
k := it.Node().Data
|
||||
idx := s.find(parentIdx, k)
|
||||
|
||||
first := false
|
||||
if idx >= 0 {
|
||||
kind := s.entries[idx].kind
|
||||
if kind != tableKind {
|
||||
return fmt.Errorf("toml: key %s should be a table, not a %s", string(k), kind)
|
||||
return false, fmt.Errorf("toml: key %s should be a table, not a %s", string(k), kind)
|
||||
}
|
||||
if s.entries[idx].explicit {
|
||||
return fmt.Errorf("toml: table %s already exists", string(k))
|
||||
return false, fmt.Errorf("toml: table %s already exists", string(k))
|
||||
}
|
||||
s.entries[idx].explicit = true
|
||||
} else {
|
||||
idx = s.create(parentIdx, k, tableKind, true, false)
|
||||
first = true
|
||||
}
|
||||
|
||||
s.currentIdx = idx
|
||||
|
||||
return nil
|
||||
return first, nil
|
||||
}
|
||||
|
||||
func (s *SeenTracker) checkArrayTable(node *unstable.Node) error {
|
||||
func (s *SeenTracker) checkArrayTable(node *unstable.Node) (bool, error) {
|
||||
if s.currentIdx >= 0 {
|
||||
s.setExplicitFlag(s.currentIdx)
|
||||
}
|
||||
@@ -242,7 +249,7 @@ func (s *SeenTracker) checkArrayTable(node *unstable.Node) error {
|
||||
} else {
|
||||
entry := s.entries[idx]
|
||||
if entry.kind == valueKind {
|
||||
return fmt.Errorf("toml: expected %s to be a table, not a %s", string(k), entry.kind)
|
||||
return false, fmt.Errorf("toml: expected %s to be a table, not a %s", string(k), entry.kind)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -252,22 +259,23 @@ func (s *SeenTracker) checkArrayTable(node *unstable.Node) error {
|
||||
k := it.Node().Data
|
||||
idx := s.find(parentIdx, k)
|
||||
|
||||
if idx >= 0 {
|
||||
firstTime := idx < 0
|
||||
if firstTime {
|
||||
idx = s.create(parentIdx, k, arrayTableKind, true, false)
|
||||
} else {
|
||||
kind := s.entries[idx].kind
|
||||
if kind != arrayTableKind {
|
||||
return fmt.Errorf("toml: key %s already exists as a %s, but should be an array table", kind, string(k))
|
||||
return false, fmt.Errorf("toml: key %s already exists as a %s, but should be an array table", kind, string(k))
|
||||
}
|
||||
s.clear(idx)
|
||||
} else {
|
||||
idx = s.create(parentIdx, k, arrayTableKind, true, false)
|
||||
}
|
||||
|
||||
s.currentIdx = idx
|
||||
|
||||
return nil
|
||||
return firstTime, nil
|
||||
}
|
||||
|
||||
func (s *SeenTracker) checkKeyValue(node *unstable.Node) error {
|
||||
func (s *SeenTracker) checkKeyValue(node *unstable.Node) (bool, error) {
|
||||
parentIdx := s.currentIdx
|
||||
it := node.Key()
|
||||
|
||||
@@ -281,11 +289,11 @@ func (s *SeenTracker) checkKeyValue(node *unstable.Node) error {
|
||||
} else {
|
||||
entry := s.entries[idx]
|
||||
if it.IsLast() {
|
||||
return fmt.Errorf("toml: key %s is already defined", string(k))
|
||||
return false, fmt.Errorf("toml: key %s is already defined", string(k))
|
||||
} else if entry.kind != tableKind {
|
||||
return fmt.Errorf("toml: expected %s to be a table, not a %s", string(k), entry.kind)
|
||||
return false, fmt.Errorf("toml: expected %s to be a table, not a %s", string(k), entry.kind)
|
||||
} else if entry.explicit {
|
||||
return fmt.Errorf("toml: cannot redefine table %s that has already been explicitly defined", string(k))
|
||||
return false, fmt.Errorf("toml: cannot redefine table %s that has already been explicitly defined", string(k))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -303,45 +311,39 @@ func (s *SeenTracker) checkKeyValue(node *unstable.Node) error {
|
||||
return s.checkArray(value)
|
||||
}
|
||||
|
||||
return nil
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (s *SeenTracker) checkArray(node *unstable.Node) error {
|
||||
func (s *SeenTracker) checkArray(node *unstable.Node) (first bool, err error) {
|
||||
it := node.Children()
|
||||
for it.Next() {
|
||||
n := it.Node()
|
||||
switch n.Kind {
|
||||
case unstable.InlineTable:
|
||||
err := s.checkInlineTable(n)
|
||||
first, err = s.checkInlineTable(n)
|
||||
if err != nil {
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
case unstable.Array:
|
||||
err := s.checkArray(n)
|
||||
first, err = s.checkArray(n)
|
||||
if err != nil {
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return first, nil
|
||||
}
|
||||
|
||||
func (s *SeenTracker) checkInlineTable(node *unstable.Node) error {
|
||||
if pool.New == nil {
|
||||
pool.New = func() interface{} {
|
||||
return &SeenTracker{}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SeenTracker) checkInlineTable(node *unstable.Node) (first bool, err error) {
|
||||
s = pool.Get().(*SeenTracker)
|
||||
s.reset()
|
||||
|
||||
it := node.Children()
|
||||
for it.Next() {
|
||||
n := it.Node()
|
||||
err := s.checkKeyValue(n)
|
||||
first, err = s.checkKeyValue(n)
|
||||
if err != nil {
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -352,5 +354,5 @@ func (s *SeenTracker) checkInlineTable(node *unstable.Node) error {
|
||||
// redefinition of its keys: check* functions cannot walk into
|
||||
// a value.
|
||||
pool.Put(s)
|
||||
return nil
|
||||
return first, nil
|
||||
}
|
||||
|
||||
+52
-9
@@ -3,11 +3,12 @@ package toml
|
||||
import (
|
||||
"bytes"
|
||||
"encoding"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"reflect"
|
||||
"sort"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -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 compatibility guarantees 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,10 +265,22 @@ 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)
|
||||
if hasTextMarshaler || (v.CanAddr() && reflect.PtrTo(v.Type()).Implements(textMarshalerType)) {
|
||||
if hasTextMarshaler || (v.CanAddr() && reflect.PointerTo(v.Type()).Implements(textMarshalerType)) {
|
||||
if !hasTextMarshaler {
|
||||
v = v.Addr()
|
||||
}
|
||||
@@ -606,6 +631,18 @@ func (enc *Encoder) keyToString(k reflect.Value) (string, error) {
|
||||
return "", fmt.Errorf("toml: error marshalling key %v from text: %w", k, err)
|
||||
}
|
||||
return string(keyB), nil
|
||||
|
||||
case keyType.Kind() == reflect.Int || keyType.Kind() == reflect.Int8 || keyType.Kind() == reflect.Int16 || keyType.Kind() == reflect.Int32 || keyType.Kind() == reflect.Int64:
|
||||
return strconv.FormatInt(k.Int(), 10), nil
|
||||
|
||||
case keyType.Kind() == reflect.Uint || keyType.Kind() == reflect.Uint8 || keyType.Kind() == reflect.Uint16 || keyType.Kind() == reflect.Uint32 || keyType.Kind() == reflect.Uint64:
|
||||
return strconv.FormatUint(k.Uint(), 10), nil
|
||||
|
||||
case keyType.Kind() == reflect.Float32:
|
||||
return strconv.FormatFloat(k.Float(), 'f', -1, 32), nil
|
||||
|
||||
case keyType.Kind() == reflect.Float64:
|
||||
return strconv.FormatFloat(k.Float(), 'f', -1, 64), nil
|
||||
}
|
||||
return "", fmt.Errorf("toml: type %s is not supported as a map key", keyType.Kind())
|
||||
}
|
||||
@@ -643,8 +680,8 @@ func (enc *Encoder) encodeMap(b []byte, ctx encoderCtx, v reflect.Value) ([]byte
|
||||
}
|
||||
|
||||
func sortEntriesByKey(e []entry) {
|
||||
sort.Slice(e, func(i, j int) bool {
|
||||
return e[i].Key < e[j].Key
|
||||
slices.SortFunc(e, func(a, b entry) int {
|
||||
return strings.Compare(a.Key, b.Key)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -707,6 +744,8 @@ func walkStruct(ctx encoderCtx, t *table, v reflect.Value) {
|
||||
if fieldType.Anonymous {
|
||||
if fieldType.Type.Kind() == reflect.Struct {
|
||||
walkStruct(ctx, t, f)
|
||||
} else if fieldType.Type.Kind() == reflect.Ptr && !f.IsNil() && f.Elem().Kind() == reflect.Struct {
|
||||
walkStruct(ctx, t, f.Elem())
|
||||
}
|
||||
continue
|
||||
} else {
|
||||
@@ -924,7 +963,7 @@ func willConvertToTable(ctx encoderCtx, v reflect.Value) bool {
|
||||
if !v.IsValid() {
|
||||
return false
|
||||
}
|
||||
if v.Type() == timeType || v.Type().Implements(textMarshalerType) || (v.Kind() != reflect.Ptr && v.CanAddr() && reflect.PtrTo(v.Type()).Implements(textMarshalerType)) {
|
||||
if v.Type() == timeType || v.Type().Implements(textMarshalerType) || (v.Kind() != reflect.Ptr && v.CanAddr() && reflect.PointerTo(v.Type()).Implements(textMarshalerType)) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -998,6 +1037,10 @@ func (enc *Encoder) encodeSliceAsArrayTable(b []byte, ctx encoderCtx, v reflect.
|
||||
|
||||
scratch = enc.commented(ctx.commented, scratch)
|
||||
|
||||
if enc.indentTables {
|
||||
scratch = enc.indent(ctx.indent, scratch)
|
||||
}
|
||||
|
||||
scratch = append(scratch, "[["...)
|
||||
|
||||
for i, k := range ctx.parentKey {
|
||||
|
||||
+150
-3
@@ -587,13 +587,69 @@ foo = 42
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "invalid map key",
|
||||
desc: "int map key",
|
||||
v: map[int]interface{}{1: "a"},
|
||||
expected: `1 = 'a'
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "int8 map key",
|
||||
v: map[int8]interface{}{1: "a"},
|
||||
expected: `1 = 'a'
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "int64 map key",
|
||||
v: map[int64]interface{}{1: "a"},
|
||||
expected: `1 = 'a'
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "uint map key",
|
||||
v: map[uint]interface{}{1: "a"},
|
||||
expected: `1 = 'a'
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "uint8 map key",
|
||||
v: map[uint8]interface{}{1: "a"},
|
||||
expected: `1 = 'a'
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "uint64 map key",
|
||||
v: map[uint64]interface{}{1: "a"},
|
||||
expected: `1 = 'a'
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "float32 map key",
|
||||
v: map[float32]interface{}{
|
||||
1.1: "a",
|
||||
1.0020: "b",
|
||||
},
|
||||
expected: `'1.002' = 'b'
|
||||
'1.1' = 'a'
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "float64 map key",
|
||||
v: map[float64]interface{}{
|
||||
1.1: "a",
|
||||
1.0020: "b",
|
||||
},
|
||||
expected: `'1.002' = 'b'
|
||||
'1.1' = 'a'
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "invalid map key",
|
||||
v: map[struct{ int }]interface{}{{1}: "a"},
|
||||
err: true,
|
||||
},
|
||||
{
|
||||
desc: "invalid map key but empty",
|
||||
v: map[int]interface{}{},
|
||||
v: map[struct{ int }]interface{}{},
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
@@ -948,6 +1004,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"`
|
||||
@@ -1304,6 +1383,38 @@ value = ''
|
||||
require.Equal(t, expected, string(result))
|
||||
}
|
||||
|
||||
func TestMarshalNestedAnonymousStructs_PointerEmbedded(t *testing.T) {
|
||||
type Embedded struct {
|
||||
Value string `toml:"value" json:"value"`
|
||||
Omitted string `toml:"omitted,omitempty"`
|
||||
Ptr *string `toml:"ptr"`
|
||||
}
|
||||
|
||||
type Named struct {
|
||||
Value string `toml:"value" json:"value"`
|
||||
}
|
||||
|
||||
type Doc struct {
|
||||
*Embedded
|
||||
*Named `toml:"named" json:"named"`
|
||||
Anonymous struct {
|
||||
*Embedded
|
||||
Value *string `toml:"value" json:"value"`
|
||||
} `toml:"anonymous,omitempty" json:"anonymous,omitempty"`
|
||||
}
|
||||
|
||||
doc := &Doc{
|
||||
Embedded: &Embedded{Value: "foo"},
|
||||
}
|
||||
|
||||
expected := `value = 'foo'
|
||||
`
|
||||
|
||||
result, err := toml.Marshal(doc)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expected, string(result))
|
||||
}
|
||||
|
||||
func TestLocalTime(t *testing.T) {
|
||||
v := map[string]toml.LocalTime{
|
||||
"a": {
|
||||
@@ -1444,6 +1555,43 @@ func TestMarshalCommented(t *testing.T) {
|
||||
require.Equal(t, expected, string(out))
|
||||
}
|
||||
|
||||
func TestMarshalIndentedCustomTypeArray(t *testing.T) {
|
||||
c := struct {
|
||||
Nested struct {
|
||||
NestedArray []struct {
|
||||
Value int
|
||||
}
|
||||
}
|
||||
}{
|
||||
Nested: struct {
|
||||
NestedArray []struct {
|
||||
Value int
|
||||
}
|
||||
}{
|
||||
NestedArray: []struct {
|
||||
Value int
|
||||
}{
|
||||
{Value: 1},
|
||||
{Value: 2},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
expected := `[Nested]
|
||||
[[Nested.NestedArray]]
|
||||
Value = 1
|
||||
|
||||
[[Nested.NestedArray]]
|
||||
Value = 2
|
||||
`
|
||||
|
||||
var buf bytes.Buffer
|
||||
enc := toml.NewEncoder(&buf)
|
||||
enc.SetIndentTables(true)
|
||||
require.NoError(t, enc.Encode(c))
|
||||
require.Equal(t, expected, buf.String())
|
||||
}
|
||||
|
||||
func ExampleMarshal() {
|
||||
type MyConfig struct {
|
||||
Version int
|
||||
@@ -1473,7 +1621,6 @@ func ExampleMarshal() {
|
||||
// configuration file that has commented out sections (example from
|
||||
// go-graphite/graphite-clickhouse).
|
||||
func ExampleMarshal_commented() {
|
||||
|
||||
type Common struct {
|
||||
Listen string `toml:"listen" comment:"general listener"`
|
||||
PprofListen string `toml:"pprof-listen" comment:"listener to serve /debug/pprof requests. '-pprof' argument overrides it"`
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
//go:build go1.18 || go1.19 || go1.20 || go1.21
|
||||
// +build go1.18 go1.19 go1.20 go1.21
|
||||
|
||||
package ossfuzz
|
||||
|
||||
import (
|
||||
|
||||
+83
-13
@@ -5,9 +5,9 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"math"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
@@ -21,10 +21,8 @@ import (
|
||||
//
|
||||
// It is a shortcut for Decoder.Decode() with the default options.
|
||||
func Unmarshal(data []byte, v interface{}) error {
|
||||
p := unstable.Parser{}
|
||||
p.Reset(data)
|
||||
d := decoder{p: &p}
|
||||
|
||||
d := decoder{}
|
||||
d.p.Reset(data)
|
||||
return d.FromParser(v)
|
||||
}
|
||||
|
||||
@@ -35,6 +33,9 @@ type Decoder struct {
|
||||
|
||||
// global settings
|
||||
strict bool
|
||||
|
||||
// toggles unmarshaler interface
|
||||
unmarshalerInterface bool
|
||||
}
|
||||
|
||||
// NewDecoder creates a new Decoder that will read from r.
|
||||
@@ -54,6 +55,24 @@ func (d *Decoder) DisallowUnknownFields() *Decoder {
|
||||
return d
|
||||
}
|
||||
|
||||
// EnableUnmarshalerInterface allows to enable unmarshaler interface.
|
||||
//
|
||||
// With this feature enabled, types implementing the unstable/Unmarshaler
|
||||
// interface can be decoded from any structure of the document. It allows types
|
||||
// that don't have a straightfoward TOML representation to provide their own
|
||||
// decoding logic.
|
||||
//
|
||||
// Currently, types can only decode from a single value. Tables and array tables
|
||||
// are not supported.
|
||||
//
|
||||
// *Unstable:* This method does not follow the compatibility guarantees of
|
||||
// semver. It can be changed or removed without a new major version being
|
||||
// issued.
|
||||
func (d *Decoder) EnableUnmarshalerInterface() *Decoder {
|
||||
d.unmarshalerInterface = true
|
||||
return d
|
||||
}
|
||||
|
||||
// Decode the whole content of r into v.
|
||||
//
|
||||
// By default, values in the document that don't exist in the target Go value
|
||||
@@ -96,26 +115,25 @@ func (d *Decoder) DisallowUnknownFields() *Decoder {
|
||||
// Inline Table -> same as Table
|
||||
// Array of Tables -> same as Array and Table
|
||||
func (d *Decoder) Decode(v interface{}) error {
|
||||
b, err := ioutil.ReadAll(d.r)
|
||||
b, err := io.ReadAll(d.r)
|
||||
if err != nil {
|
||||
return fmt.Errorf("toml: %w", err)
|
||||
}
|
||||
|
||||
p := unstable.Parser{}
|
||||
p.Reset(b)
|
||||
dec := decoder{
|
||||
p: &p,
|
||||
strict: strict{
|
||||
Enabled: d.strict,
|
||||
},
|
||||
unmarshalerInterface: d.unmarshalerInterface,
|
||||
}
|
||||
dec.p.Reset(b)
|
||||
|
||||
return dec.FromParser(v)
|
||||
}
|
||||
|
||||
type decoder struct {
|
||||
// Which parser instance in use for this decoding session.
|
||||
p *unstable.Parser
|
||||
p unstable.Parser
|
||||
|
||||
// Flag indicating that the current expression is stashed.
|
||||
// If set to true, calling nextExpr will not actually pull a new expression
|
||||
@@ -127,6 +145,10 @@ type decoder struct {
|
||||
// need to be skipped.
|
||||
skipUntilTable bool
|
||||
|
||||
// Flag indicating that the current array/slice table should be cleared because
|
||||
// it is the first encounter of an array table.
|
||||
clearArrayTable bool
|
||||
|
||||
// Tracks position in Go arrays.
|
||||
// This is used when decoding [[array tables]] into Go arrays. Given array
|
||||
// tables are separate TOML expression, we need to keep track of where we
|
||||
@@ -139,6 +161,9 @@ type decoder struct {
|
||||
// Strict mode
|
||||
strict strict
|
||||
|
||||
// Flag that enables/disables unmarshaler interface.
|
||||
unmarshalerInterface bool
|
||||
|
||||
// Current context for the error.
|
||||
errorContext *errorContext
|
||||
}
|
||||
@@ -246,9 +271,10 @@ Rules for the unmarshal code:
|
||||
func (d *decoder) handleRootExpression(expr *unstable.Node, v reflect.Value) error {
|
||||
var x reflect.Value
|
||||
var err error
|
||||
var first bool // used for to clear array tables on first use
|
||||
|
||||
if !(d.skipUntilTable && expr.Kind == unstable.KeyValue) {
|
||||
err = d.seen.CheckExpression(expr)
|
||||
first, err = d.seen.CheckExpression(expr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -267,6 +293,7 @@ func (d *decoder) handleRootExpression(expr *unstable.Node, v reflect.Value) err
|
||||
case unstable.ArrayTable:
|
||||
d.skipUntilTable = false
|
||||
d.strict.EnterArrayTable(expr)
|
||||
d.clearArrayTable = first
|
||||
x, err = d.handleArrayTable(expr.Key(), v)
|
||||
default:
|
||||
panic(fmt.Errorf("parser should not permit expression of kind %s at document root", expr.Kind))
|
||||
@@ -307,6 +334,10 @@ func (d *decoder) handleArrayTableCollectionLast(key unstable.Iterator, v reflec
|
||||
reflect.Copy(nelem, elem)
|
||||
elem = nelem
|
||||
}
|
||||
if d.clearArrayTable && elem.Len() > 0 {
|
||||
elem.SetLen(0)
|
||||
d.clearArrayTable = false
|
||||
}
|
||||
}
|
||||
return d.handleArrayTableCollectionLast(key, elem)
|
||||
case reflect.Ptr:
|
||||
@@ -325,6 +356,10 @@ func (d *decoder) handleArrayTableCollectionLast(key unstable.Iterator, v reflec
|
||||
|
||||
return v, nil
|
||||
case reflect.Slice:
|
||||
if d.clearArrayTable && v.Len() > 0 {
|
||||
v.SetLen(0)
|
||||
d.clearArrayTable = false
|
||||
}
|
||||
elemType := v.Type().Elem()
|
||||
var elem reflect.Value
|
||||
if elemType.Kind() == reflect.Interface {
|
||||
@@ -576,7 +611,7 @@ func (d *decoder) handleKeyValues(v reflect.Value) (reflect.Value, error) {
|
||||
break
|
||||
}
|
||||
|
||||
err := d.seen.CheckExpression(expr)
|
||||
_, err := d.seen.CheckExpression(expr)
|
||||
if err != nil {
|
||||
return reflect.Value{}, err
|
||||
}
|
||||
@@ -634,6 +669,14 @@ func (d *decoder) handleValue(value *unstable.Node, v reflect.Value) error {
|
||||
v = initAndDereferencePointer(v)
|
||||
}
|
||||
|
||||
if d.unmarshalerInterface {
|
||||
if v.CanAddr() && v.Addr().CanInterface() {
|
||||
if outi, ok := v.Addr().Interface().(unstable.Unmarshaler); ok {
|
||||
return outi.UnmarshalTOML(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ok, err := d.tryTextUnmarshaler(value, v)
|
||||
if ok || err != nil {
|
||||
return err
|
||||
@@ -1031,12 +1074,39 @@ func (d *decoder) keyFromData(keyType reflect.Type, data []byte) (reflect.Value,
|
||||
}
|
||||
return mk, nil
|
||||
|
||||
case reflect.PtrTo(keyType).Implements(textUnmarshalerType):
|
||||
case reflect.PointerTo(keyType).Implements(textUnmarshalerType):
|
||||
mk := reflect.New(keyType)
|
||||
if err := mk.Interface().(encoding.TextUnmarshaler).UnmarshalText(data); err != nil {
|
||||
return reflect.Value{}, fmt.Errorf("toml: error unmarshalling key type %s from text: %w", stringType, err)
|
||||
}
|
||||
return mk.Elem(), nil
|
||||
|
||||
case keyType.Kind() == reflect.Int || keyType.Kind() == reflect.Int8 || keyType.Kind() == reflect.Int16 || keyType.Kind() == reflect.Int32 || keyType.Kind() == reflect.Int64:
|
||||
key, err := strconv.ParseInt(string(data), 10, 64)
|
||||
if err != nil {
|
||||
return reflect.Value{}, fmt.Errorf("toml: error parsing key of type %s from integer: %w", stringType, err)
|
||||
}
|
||||
return reflect.ValueOf(key).Convert(keyType), nil
|
||||
case keyType.Kind() == reflect.Uint || keyType.Kind() == reflect.Uint8 || keyType.Kind() == reflect.Uint16 || keyType.Kind() == reflect.Uint32 || keyType.Kind() == reflect.Uint64:
|
||||
key, err := strconv.ParseUint(string(data), 10, 64)
|
||||
if err != nil {
|
||||
return reflect.Value{}, fmt.Errorf("toml: error parsing key of type %s from unsigned integer: %w", stringType, err)
|
||||
}
|
||||
return reflect.ValueOf(key).Convert(keyType), nil
|
||||
|
||||
case keyType.Kind() == reflect.Float32:
|
||||
key, err := strconv.ParseFloat(string(data), 32)
|
||||
if err != nil {
|
||||
return reflect.Value{}, fmt.Errorf("toml: error parsing key of type %s from float: %w", stringType, err)
|
||||
}
|
||||
return reflect.ValueOf(float32(key)), nil
|
||||
|
||||
case keyType.Kind() == reflect.Float64:
|
||||
key, err := strconv.ParseFloat(string(data), 64)
|
||||
if err != nil {
|
||||
return reflect.Value{}, fmt.Errorf("toml: error parsing key of type %s from float: %w", stringType, err)
|
||||
}
|
||||
return reflect.ValueOf(float64(key)), nil
|
||||
}
|
||||
return reflect.Value{}, fmt.Errorf("toml: cannot convert map key of type %s to expected type %s", stringType, keyType)
|
||||
}
|
||||
|
||||
+315
-18
@@ -12,6 +12,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
"github.com/pelletier/go-toml/v2/unstable"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -204,7 +205,6 @@ func TestUnmarshal_Floats(t *testing.T) {
|
||||
testFn func(t *testing.T, v float64)
|
||||
err bool
|
||||
}{
|
||||
|
||||
{
|
||||
desc: "float pi",
|
||||
input: `3.1415`,
|
||||
@@ -839,8 +839,10 @@ huey = 'dewey'
|
||||
|
||||
return test{
|
||||
target: &doc{},
|
||||
expected: &doc{A: []interface{}{"0", "1", "2", "3", "4", "5", "6",
|
||||
"7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17"}},
|
||||
expected: &doc{A: []interface{}{
|
||||
"0", "1", "2", "3", "4", "5", "6",
|
||||
"7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17",
|
||||
}},
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -1695,16 +1697,6 @@ B = "data"`,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "empty map into map with invalid key type",
|
||||
input: ``,
|
||||
gen: func() test {
|
||||
return test{
|
||||
target: &map[int]string{},
|
||||
expected: &map[int]string{},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "into map with convertible key type",
|
||||
input: `A = "hello"`,
|
||||
@@ -1941,6 +1933,150 @@ B = "data"`,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "into map of int to string",
|
||||
input: `1 = "a"`,
|
||||
gen: func() test {
|
||||
return test{
|
||||
target: &map[int]string{},
|
||||
expected: &map[int]string{1: "a"},
|
||||
assert: func(t *testing.T, test test) {
|
||||
assert.Equal(t, test.expected, test.target)
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "into map of int8 to string",
|
||||
input: `1 = "a"`,
|
||||
gen: func() test {
|
||||
return test{
|
||||
target: &map[int8]string{},
|
||||
expected: &map[int8]string{1: "a"},
|
||||
assert: func(t *testing.T, test test) {
|
||||
assert.Equal(t, test.expected, test.target)
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "into map of int64 to string",
|
||||
input: `1 = "a"`,
|
||||
gen: func() test {
|
||||
return test{
|
||||
target: &map[int64]string{},
|
||||
expected: &map[int64]string{1: "a"},
|
||||
assert: func(t *testing.T, test test) {
|
||||
assert.Equal(t, test.expected, test.target)
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "into map of uint to string",
|
||||
input: `1 = "a"`,
|
||||
gen: func() test {
|
||||
return test{
|
||||
target: &map[uint]string{},
|
||||
expected: &map[uint]string{1: "a"},
|
||||
assert: func(t *testing.T, test test) {
|
||||
assert.Equal(t, test.expected, test.target)
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "into map of uint8 to string",
|
||||
input: `1 = "a"`,
|
||||
gen: func() test {
|
||||
return test{
|
||||
target: &map[uint8]string{},
|
||||
expected: &map[uint8]string{1: "a"},
|
||||
assert: func(t *testing.T, test test) {
|
||||
assert.Equal(t, test.expected, test.target)
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "into map of uint64 to string",
|
||||
input: `1 = "a"`,
|
||||
gen: func() test {
|
||||
return test{
|
||||
target: &map[uint64]string{},
|
||||
expected: &map[uint64]string{1: "a"},
|
||||
assert: func(t *testing.T, test test) {
|
||||
assert.Equal(t, test.expected, test.target)
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "into map of uint with invalid key",
|
||||
input: `-1 = "a"`,
|
||||
gen: func() test {
|
||||
return test{
|
||||
target: &map[uint]string{},
|
||||
err: true,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "into map of float64 to string",
|
||||
input: `'1.01' = "a"`,
|
||||
gen: func() test {
|
||||
return test{
|
||||
target: &map[float64]string{},
|
||||
expected: &map[float64]string{1.01: "a"},
|
||||
assert: func(t *testing.T, test test) {
|
||||
assert.Equal(t, test.expected, test.target)
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "into map of float64 with invalid key",
|
||||
input: `key = "a"`,
|
||||
gen: func() test {
|
||||
return test{
|
||||
target: &map[float64]string{},
|
||||
err: true,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "into map of float32 to string",
|
||||
input: `'1.01' = "a"`,
|
||||
gen: func() test {
|
||||
return test{
|
||||
target: &map[float32]string{},
|
||||
expected: &map[float32]string{1.01: "a"},
|
||||
assert: func(t *testing.T, test test) {
|
||||
assert.Equal(t, test.expected, test.target)
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "into map of float32 with invalid key",
|
||||
input: `key = "a"`,
|
||||
gen: func() test {
|
||||
return test{
|
||||
target: &map[float32]string{},
|
||||
err: true,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "invalid map key type",
|
||||
input: `1 = "a"`,
|
||||
gen: func() test {
|
||||
return test{
|
||||
target: &map[struct{ int }]string{},
|
||||
err: true,
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, e := range examples {
|
||||
@@ -2652,7 +2788,7 @@ func TestIssue772(t *testing.T) {
|
||||
FileHandling `toml:"filehandling"`
|
||||
}
|
||||
|
||||
var defaultConfigFile = []byte(`
|
||||
defaultConfigFile := []byte(`
|
||||
[filehandling]
|
||||
pattern = "reach-masterdev-"`)
|
||||
|
||||
@@ -2749,7 +2885,7 @@ func TestIssue866(t *testing.T) {
|
||||
PipelineMapping map[string]*Pipeline `toml:"pipelines"`
|
||||
}
|
||||
|
||||
var badToml = `
|
||||
badToml := `
|
||||
[pipelines.register]
|
||||
mapping.inst.req = [
|
||||
["param1", "value1"],
|
||||
@@ -2767,7 +2903,7 @@ mapping.inst.res = [
|
||||
t.Fatal("unmarshal failed with mismatch value")
|
||||
}
|
||||
|
||||
var goodTooToml = `
|
||||
goodTooToml := `
|
||||
[pipelines.register]
|
||||
mapping.inst.req = [
|
||||
["param1", "value1"],
|
||||
@@ -2782,7 +2918,7 @@ mapping.inst.req = [
|
||||
t.Fatal("unmarshal failed with mismatch value")
|
||||
}
|
||||
|
||||
var goodToml = `
|
||||
goodToml := `
|
||||
[pipelines.register.mapping.inst]
|
||||
req = [
|
||||
["param1", "value1"],
|
||||
@@ -2823,6 +2959,76 @@ blah.a = "def"`)
|
||||
require.Equal(t, "def", cfg.A)
|
||||
}
|
||||
|
||||
func TestIssue931(t *testing.T) {
|
||||
type item struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
type items struct {
|
||||
Slice []item
|
||||
}
|
||||
|
||||
its := items{[]item{{"a"}, {"b"}}}
|
||||
|
||||
b := []byte(`
|
||||
[[Slice]]
|
||||
Name = 'c'
|
||||
|
||||
[[Slice]]
|
||||
Name = 'd'
|
||||
`)
|
||||
|
||||
toml.Unmarshal(b, &its)
|
||||
require.Equal(t, items{[]item{{"c"}, {"d"}}}, its)
|
||||
}
|
||||
|
||||
func TestIssue931Interface(t *testing.T) {
|
||||
type items struct {
|
||||
Slice interface{}
|
||||
}
|
||||
|
||||
type item = map[string]interface{}
|
||||
|
||||
its := items{[]interface{}{item{"Name": "a"}, item{"Name": "b"}}}
|
||||
|
||||
b := []byte(`
|
||||
[[Slice]]
|
||||
Name = 'c'
|
||||
|
||||
[[Slice]]
|
||||
Name = 'd'
|
||||
`)
|
||||
|
||||
toml.Unmarshal(b, &its)
|
||||
require.Equal(t, items{[]interface{}{item{"Name": "c"}, item{"Name": "d"}}}, its)
|
||||
}
|
||||
|
||||
func TestIssue931SliceInterface(t *testing.T) {
|
||||
type items struct {
|
||||
Slice []interface{}
|
||||
}
|
||||
|
||||
type item = map[string]interface{}
|
||||
|
||||
its := items{
|
||||
[]interface{}{
|
||||
item{"Name": "a"},
|
||||
item{"Name": "b"},
|
||||
},
|
||||
}
|
||||
|
||||
b := []byte(`
|
||||
[[Slice]]
|
||||
Name = 'c'
|
||||
|
||||
[[Slice]]
|
||||
Name = 'd'
|
||||
`)
|
||||
|
||||
toml.Unmarshal(b, &its)
|
||||
require.Equal(t, items{[]interface{}{item{"Name": "c"}, item{"Name": "d"}}}, its)
|
||||
}
|
||||
|
||||
func TestUnmarshalDecodeErrors(t *testing.T) {
|
||||
examples := []struct {
|
||||
desc string
|
||||
@@ -3291,7 +3497,7 @@ func TestOmitEmpty(t *testing.T) {
|
||||
X []elem `toml:",inline"`
|
||||
}
|
||||
|
||||
d := doc{X: []elem{elem{
|
||||
d := doc{X: []elem{{
|
||||
Foo: "test",
|
||||
Inner: inner{
|
||||
V: "alue",
|
||||
@@ -3702,3 +3908,94 @@ func TestUnmarshal_Nil(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type CustomUnmarshalerKey struct {
|
||||
A int64
|
||||
}
|
||||
|
||||
func (k *CustomUnmarshalerKey) UnmarshalTOML(value *unstable.Node) error {
|
||||
item, err := strconv.ParseInt(string(value.Data), 10, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error converting to int64, %v", err)
|
||||
}
|
||||
k.A = item
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestUnmarshal_CustomUnmarshaler(t *testing.T) {
|
||||
type MyConfig struct {
|
||||
Unmarshalers []CustomUnmarshalerKey `toml:"unmarshalers"`
|
||||
Foo *string `toml:"foo,omitempty"`
|
||||
}
|
||||
|
||||
examples := []struct {
|
||||
desc string
|
||||
disableUnmarshalerInterface bool
|
||||
input string
|
||||
expected MyConfig
|
||||
err bool
|
||||
}{
|
||||
{
|
||||
desc: "empty",
|
||||
input: ``,
|
||||
expected: MyConfig{Unmarshalers: []CustomUnmarshalerKey{}, Foo: nil},
|
||||
},
|
||||
{
|
||||
desc: "simple",
|
||||
input: `unmarshalers = [1,2,3]`,
|
||||
expected: MyConfig{
|
||||
Unmarshalers: []CustomUnmarshalerKey{
|
||||
{A: 1},
|
||||
{A: 2},
|
||||
{A: 3},
|
||||
},
|
||||
Foo: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "unmarshal string and custom unmarshaler",
|
||||
input: `unmarshalers = [1,2,3]
|
||||
foo = "bar"`,
|
||||
expected: MyConfig{
|
||||
Unmarshalers: []CustomUnmarshalerKey{
|
||||
{A: 1},
|
||||
{A: 2},
|
||||
{A: 3},
|
||||
},
|
||||
Foo: func(v string) *string {
|
||||
return &v
|
||||
}("bar"),
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "simple example, but unmarshaler interface disabled",
|
||||
disableUnmarshalerInterface: true,
|
||||
input: `unmarshalers = [1,2,3]`,
|
||||
err: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, ex := range examples {
|
||||
e := ex
|
||||
t.Run(e.desc, func(t *testing.T) {
|
||||
foo := MyConfig{}
|
||||
|
||||
decoder := toml.NewDecoder(bytes.NewReader([]byte(e.input)))
|
||||
if !ex.disableUnmarshalerInterface {
|
||||
decoder.EnableUnmarshalerInterface()
|
||||
}
|
||||
err := decoder.Decode(&foo)
|
||||
|
||||
if e.err {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, len(foo.Unmarshalers), len(e.expected.Unmarshalers))
|
||||
for i := 0; i < len(foo.Unmarshalers); i++ {
|
||||
require.Equal(t, foo.Unmarshalers[i], e.expected.Unmarshalers[i])
|
||||
}
|
||||
require.Equal(t, foo.Foo, e.expected.Foo)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package unstable
|
||||
|
||||
// The Unmarshaler interface may be implemented by types to customize their
|
||||
// behavior when being unmarshaled from a TOML document.
|
||||
type Unmarshaler interface {
|
||||
UnmarshalTOML(value *Node) error
|
||||
}
|
||||
Reference in New Issue
Block a user