whatcanGOwrong
This commit is contained in:
@@ -0,0 +1 @@
|
||||
github: arp242
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "go test",
|
||||
"on": ["push", "pull_request"],
|
||||
"jobs": {
|
||||
"test": {
|
||||
"strategy": {
|
||||
"matrix": {
|
||||
"go-version": ["1.13.x", "1.14.x", "1.15.x", "1.16.x", "1.17.x", "1.18.x", "1.19.x"],
|
||||
"os": ["ubuntu-latest", "macos-latest", "windows-latest"]
|
||||
}
|
||||
},
|
||||
"runs-on": "${{ matrix.os }}",
|
||||
"env": {"GOPROXY": "direct"},
|
||||
"steps": [
|
||||
{
|
||||
"name": "Install Go",
|
||||
"uses": "actions/setup-go@v2",
|
||||
"with": {"go-version": "${{ matrix.go-version }}"}
|
||||
},
|
||||
{
|
||||
"name": "Checkout code",
|
||||
"uses": "actions/checkout@v2"
|
||||
},
|
||||
{
|
||||
"name": "Test",
|
||||
"run": "go test -v ./..."
|
||||
},
|
||||
{
|
||||
"name": "Test on 32bit",
|
||||
"if": "runner.os == 'Linux'",
|
||||
"run": "GOARCH=386 go test -v ./..."
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
/toml.test
|
||||
/toml-test
|
||||
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013 TOML authors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
@@ -0,0 +1,120 @@
|
||||
TOML stands for Tom's Obvious, Minimal Language. This Go package provides a
|
||||
reflection interface similar to Go's standard library `json` and `xml` packages.
|
||||
|
||||
Compatible with TOML version [v1.0.0](https://toml.io/en/v1.0.0).
|
||||
|
||||
Documentation: https://godocs.io/github.com/BurntSushi/toml
|
||||
|
||||
See the [releases page](https://github.com/BurntSushi/toml/releases) for a
|
||||
changelog; this information is also in the git tag annotations (e.g. `git show
|
||||
v0.4.0`).
|
||||
|
||||
This library requires Go 1.13 or newer; add it to your go.mod with:
|
||||
|
||||
% go get github.com/BurntSushi/toml@latest
|
||||
|
||||
It also comes with a TOML validator CLI tool:
|
||||
|
||||
% go install github.com/BurntSushi/toml/cmd/tomlv@latest
|
||||
% tomlv some-toml-file.toml
|
||||
|
||||
### Examples
|
||||
For the simplest example, consider some TOML file as just a list of keys and
|
||||
values:
|
||||
|
||||
```toml
|
||||
Age = 25
|
||||
Cats = [ "Cauchy", "Plato" ]
|
||||
Pi = 3.14
|
||||
Perfection = [ 6, 28, 496, 8128 ]
|
||||
DOB = 1987-07-05T05:45:00Z
|
||||
```
|
||||
|
||||
Which can be decoded with:
|
||||
|
||||
```go
|
||||
type Config struct {
|
||||
Age int
|
||||
Cats []string
|
||||
Pi float64
|
||||
Perfection []int
|
||||
DOB time.Time
|
||||
}
|
||||
|
||||
var conf Config
|
||||
_, err := toml.Decode(tomlData, &conf)
|
||||
```
|
||||
|
||||
You can also use struct tags if your struct field name doesn't map to a TOML key
|
||||
value directly:
|
||||
|
||||
```toml
|
||||
some_key_NAME = "wat"
|
||||
```
|
||||
|
||||
```go
|
||||
type TOML struct {
|
||||
ObscureKey string `toml:"some_key_NAME"`
|
||||
}
|
||||
```
|
||||
|
||||
Beware that like other decoders **only exported fields** are considered when
|
||||
encoding and decoding; private fields are silently ignored.
|
||||
|
||||
### Using the `Marshaler` and `encoding.TextUnmarshaler` interfaces
|
||||
Here's an example that automatically parses values in a `mail.Address`:
|
||||
|
||||
```toml
|
||||
contacts = [
|
||||
"Donald Duck <donald@duckburg.com>",
|
||||
"Scrooge McDuck <scrooge@duckburg.com>",
|
||||
]
|
||||
```
|
||||
|
||||
Can be decoded with:
|
||||
|
||||
```go
|
||||
// Create address type which satisfies the encoding.TextUnmarshaler interface.
|
||||
type address struct {
|
||||
*mail.Address
|
||||
}
|
||||
|
||||
func (a *address) UnmarshalText(text []byte) error {
|
||||
var err error
|
||||
a.Address, err = mail.ParseAddress(string(text))
|
||||
return err
|
||||
}
|
||||
|
||||
// Decode it.
|
||||
func decode() {
|
||||
blob := `
|
||||
contacts = [
|
||||
"Donald Duck <donald@duckburg.com>",
|
||||
"Scrooge McDuck <scrooge@duckburg.com>",
|
||||
]
|
||||
`
|
||||
|
||||
var contacts struct {
|
||||
Contacts []address
|
||||
}
|
||||
|
||||
_, err := toml.Decode(blob, &contacts)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
for _, c := range contacts.Contacts {
|
||||
fmt.Printf("%#v\n", c.Address)
|
||||
}
|
||||
|
||||
// Output:
|
||||
// &mail.Address{Name:"Donald Duck", Address:"donald@duckburg.com"}
|
||||
// &mail.Address{Name:"Scrooge McDuck", Address:"scrooge@duckburg.com"}
|
||||
}
|
||||
```
|
||||
|
||||
To target TOML specifically you can implement `UnmarshalTOML` TOML interface in
|
||||
a similar way.
|
||||
|
||||
### More complex usage
|
||||
See the [`_example/`](/_example) directory for a more complex example.
|
||||
@@ -0,0 +1,106 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
)
|
||||
|
||||
type (
|
||||
example struct {
|
||||
Title string
|
||||
Desc string
|
||||
Integers []int
|
||||
Floats []float64
|
||||
Times []fmtTime
|
||||
Duration []time.Duration
|
||||
Distros []distro
|
||||
Servers map[string]server
|
||||
Characters map[string][]struct {
|
||||
Name string
|
||||
Rank string
|
||||
}
|
||||
}
|
||||
|
||||
server struct {
|
||||
IP string
|
||||
Hostname string
|
||||
Enabled bool
|
||||
}
|
||||
|
||||
distro struct {
|
||||
Name string
|
||||
Packages string
|
||||
}
|
||||
|
||||
fmtTime struct{ time.Time }
|
||||
)
|
||||
|
||||
func (t fmtTime) String() string {
|
||||
f := "2006-01-02 15:04:05.999999999"
|
||||
if t.Time.Hour() == 0 {
|
||||
f = "2006-01-02"
|
||||
}
|
||||
if t.Time.Year() == 0 {
|
||||
f = "15:04:05.999999999"
|
||||
}
|
||||
if t.Time.Location() == time.UTC {
|
||||
f += " UTC"
|
||||
} else {
|
||||
f += " -0700"
|
||||
}
|
||||
return t.Time.Format(`"` + f + `"`)
|
||||
}
|
||||
|
||||
func main() {
|
||||
f := "example.toml"
|
||||
if _, err := os.Stat(f); err != nil {
|
||||
f = "_example/example.toml"
|
||||
}
|
||||
|
||||
var config example
|
||||
meta, err := toml.DecodeFile(f, &config)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
indent := strings.Repeat(" ", 14)
|
||||
|
||||
fmt.Print("Decoded")
|
||||
typ, val := reflect.TypeOf(config), reflect.ValueOf(config)
|
||||
for i := 0; i < typ.NumField(); i++ {
|
||||
indent := indent
|
||||
if i == 0 {
|
||||
indent = strings.Repeat(" ", 7)
|
||||
}
|
||||
fmt.Printf("%s%-11s → %v\n", indent, typ.Field(i).Name, val.Field(i).Interface())
|
||||
}
|
||||
|
||||
fmt.Print("\nKeys")
|
||||
keys := meta.Keys()
|
||||
sort.Slice(keys, func(i, j int) bool { return keys[i].String() < keys[j].String() })
|
||||
for i, k := range keys {
|
||||
indent := indent
|
||||
if i == 0 {
|
||||
indent = strings.Repeat(" ", 10)
|
||||
}
|
||||
fmt.Printf("%s%-10s %s\n", indent, meta.Type(k...), k)
|
||||
}
|
||||
|
||||
fmt.Print("\nUndecoded")
|
||||
keys = meta.Undecoded()
|
||||
sort.Slice(keys, func(i, j int) bool { return keys[i].String() < keys[j].String() })
|
||||
for i, k := range keys {
|
||||
indent := indent
|
||||
if i == 0 {
|
||||
indent = strings.Repeat(" ", 5)
|
||||
}
|
||||
fmt.Printf("%s%-10s %s\n", indent, meta.Type(k...), k)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
# This is an example TOML document which shows most of its features.
|
||||
|
||||
# Simple key/value with a string.
|
||||
title = "TOML example \U0001F60A"
|
||||
|
||||
desc = """
|
||||
An example TOML document. \
|
||||
"""
|
||||
|
||||
# Array with integers and floats in the various allowed formats.
|
||||
integers = [42, 0x42, 0o42, 0b0110]
|
||||
floats = [1.42, 1e-02]
|
||||
|
||||
# Array with supported datetime formats.
|
||||
times = [
|
||||
2021-11-09T15:16:17+01:00, # datetime with timezone.
|
||||
2021-11-09T15:16:17Z, # UTC datetime.
|
||||
2021-11-09T15:16:17, # local datetime.
|
||||
2021-11-09, # local date.
|
||||
15:16:17, # local time.
|
||||
]
|
||||
|
||||
# Durations.
|
||||
duration = ["4m49s", "8m03s", "1231h15m55s"]
|
||||
|
||||
# Table with inline tables.
|
||||
distros = [
|
||||
{name = "Arch Linux", packages = "pacman"},
|
||||
{name = "Void Linux", packages = "xbps"},
|
||||
{name = "Debian", packages = "apt"},
|
||||
]
|
||||
|
||||
# Create new table; note the "servers" table is created implicitly.
|
||||
[servers.alpha]
|
||||
# You can indent as you please, tabs or spaces.
|
||||
ip = '10.0.0.1'
|
||||
hostname = 'server1'
|
||||
enabled = false
|
||||
[servers.beta]
|
||||
ip = '10.0.0.2'
|
||||
hostname = 'server2'
|
||||
enabled = true
|
||||
|
||||
# Start a new table array; note that the "characters" table is created implicitly.
|
||||
[[characters.star-trek]]
|
||||
name = "James Kirk"
|
||||
rank = "Captain"
|
||||
[[characters.star-trek]]
|
||||
name = "Spock"
|
||||
rank = "Science officer"
|
||||
|
||||
[undecoded] # To show the MetaData.Undecoded() feature.
|
||||
key = "This table intentionally left undecoded"
|
||||
@@ -0,0 +1,200 @@
|
||||
//go:build go1.16
|
||||
// +build go1.16
|
||||
|
||||
package toml_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/fs"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
tomltest "github.com/BurntSushi/toml/internal/toml-test"
|
||||
)
|
||||
|
||||
func BenchmarkDecode(b *testing.B) {
|
||||
files := make(map[string][]string)
|
||||
fs.WalkDir(tomltest.EmbeddedTests(), ".", func(path string, d fs.DirEntry, err error) error {
|
||||
if strings.HasPrefix(path, "valid/") && strings.HasSuffix(path, ".toml") {
|
||||
d, _ := fs.ReadFile(tomltest.EmbeddedTests(), path)
|
||||
g := filepath.Dir(path[6:])
|
||||
if g == "." {
|
||||
g = "top"
|
||||
}
|
||||
files[g] = append(files[g], string(d))
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
type test struct {
|
||||
group string
|
||||
toml []string
|
||||
}
|
||||
tests := make([]test, 0, len(files))
|
||||
for k, v := range files {
|
||||
tests = append(tests, test{group: k, toml: v})
|
||||
}
|
||||
sort.Slice(tests, func(i, j int) bool { return tests[i].group < tests[j].group })
|
||||
|
||||
b.ResetTimer()
|
||||
for _, tt := range tests {
|
||||
b.Run(tt.group, func(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for n := 0; n < b.N; n++ {
|
||||
for _, f := range tt.toml {
|
||||
var val map[string]interface{}
|
||||
toml.Decode(f, &val)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkEncode(b *testing.B) {
|
||||
files := make(map[string][]map[string]interface{})
|
||||
fs.WalkDir(tomltest.EmbeddedTests(), ".", func(path string, d fs.DirEntry, err error) error {
|
||||
if strings.HasPrefix(path, "valid/") && strings.HasSuffix(path, ".toml") {
|
||||
d, _ := fs.ReadFile(tomltest.EmbeddedTests(), path)
|
||||
g := filepath.Dir(path[6:])
|
||||
if g == "." {
|
||||
g = "top"
|
||||
}
|
||||
|
||||
// "next" version of TOML.
|
||||
if path == "valid/string/escape-esc.toml" {
|
||||
return nil
|
||||
}
|
||||
|
||||
var dec map[string]interface{}
|
||||
_, err := toml.Decode(string(d), &dec)
|
||||
if err != nil {
|
||||
b.Fatalf("decode %q: %s", path, err)
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
err = toml.NewEncoder(buf).Encode(dec)
|
||||
if err != nil {
|
||||
b.Logf("encode failed for %q (skipping): %s", path, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
files[g] = append(files[g], dec)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
type test struct {
|
||||
group string
|
||||
data []map[string]interface{}
|
||||
}
|
||||
tests := make([]test, 0, len(files))
|
||||
for k, v := range files {
|
||||
tests = append(tests, test{group: k, data: v})
|
||||
}
|
||||
sort.Slice(tests, func(i, j int) bool { return tests[i].group < tests[j].group })
|
||||
|
||||
b.ResetTimer()
|
||||
for _, tt := range tests {
|
||||
b.Run(tt.group, func(b *testing.B) {
|
||||
buf := new(bytes.Buffer)
|
||||
buf.Grow(1024 * 64)
|
||||
b.ResetTimer()
|
||||
for n := 0; n < b.N; n++ {
|
||||
for _, f := range tt.data {
|
||||
toml.NewEncoder(buf).Encode(f)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkExample(b *testing.B) {
|
||||
d, err := ioutil.ReadFile("_example/example.toml")
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
t := string(d)
|
||||
|
||||
var decoded example
|
||||
_, err = toml.Decode(t, &decoded)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
err = toml.NewEncoder(buf).Encode(decoded)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
b.Run("decode", func(b *testing.B) {
|
||||
for n := 0; n < b.N; n++ {
|
||||
var c example
|
||||
toml.Decode(t, &c)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("encode", func(b *testing.B) {
|
||||
for n := 0; n < b.N; n++ {
|
||||
buf.Reset()
|
||||
toml.NewEncoder(buf).Encode(decoded)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Copy from _example/example.go
|
||||
type (
|
||||
example struct {
|
||||
Title string
|
||||
Integers []int
|
||||
Times []fmtTime
|
||||
Duration []duration
|
||||
Distros []distro
|
||||
Servers map[string]server
|
||||
Characters map[string][]struct {
|
||||
Name string
|
||||
Rank string
|
||||
}
|
||||
}
|
||||
|
||||
server struct {
|
||||
IP string
|
||||
Hostname string
|
||||
Enabled bool
|
||||
}
|
||||
|
||||
distro struct {
|
||||
Name string
|
||||
Packages string
|
||||
}
|
||||
|
||||
duration struct{ time.Duration }
|
||||
fmtTime struct{ time.Time }
|
||||
)
|
||||
|
||||
func (d *duration) UnmarshalText(text []byte) (err error) {
|
||||
d.Duration, err = time.ParseDuration(string(text))
|
||||
return err
|
||||
}
|
||||
|
||||
func (t fmtTime) String() string {
|
||||
f := "2006-01-02 15:04:05.999999999"
|
||||
if t.Time.Hour() == 0 {
|
||||
f = "2006-01-02"
|
||||
}
|
||||
if t.Time.Year() == 0 {
|
||||
f = "15:04:05.999999999"
|
||||
}
|
||||
if t.Time.Location() == time.UTC {
|
||||
f += " UTC"
|
||||
} else {
|
||||
f += " -0700"
|
||||
}
|
||||
return t.Time.Format(`"` + f + `"`)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013 TOML authors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
@@ -0,0 +1,6 @@
|
||||
# Implements the TOML test suite interface
|
||||
|
||||
This is an implementation of the interface expected by
|
||||
[toml-test](https://github.com/BurntSushi/toml-test) for my
|
||||
[toml parser written in Go](https://github.com/BurntSushi/toml).
|
||||
In particular, it maps TOML data on `stdin` to a JSON format on `stdout`.
|
||||
@@ -0,0 +1,43 @@
|
||||
// Command toml-test-decoder satisfies the toml-test interface for testing TOML
|
||||
// decoders. Namely, it accepts TOML on stdin and outputs JSON on stdout.
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
"github.com/BurntSushi/toml/internal/tag"
|
||||
)
|
||||
|
||||
func init() {
|
||||
log.SetFlags(0)
|
||||
flag.Usage = usage
|
||||
flag.Parse()
|
||||
}
|
||||
|
||||
func usage() {
|
||||
log.Printf("Usage: %s < toml-file\n", path.Base(os.Args[0]))
|
||||
flag.PrintDefaults()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func main() {
|
||||
if flag.NArg() != 0 {
|
||||
flag.Usage()
|
||||
}
|
||||
|
||||
var decoded interface{}
|
||||
if _, err := toml.NewDecoder(os.Stdin).Decode(&decoded); err != nil {
|
||||
log.Fatalf("Error decoding TOML: %s", err)
|
||||
}
|
||||
|
||||
j := json.NewEncoder(os.Stdout)
|
||||
j.SetIndent("", " ")
|
||||
if err := j.Encode(tag.Add("", decoded)); err != nil {
|
||||
log.Fatalf("Error encoding JSON: %s", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013 TOML authors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
@@ -0,0 +1,6 @@
|
||||
# Implements the TOML test suite interface for TOML encoders
|
||||
|
||||
This is an implementation of the interface expected by
|
||||
[toml-test](https://github.com/BurntSushi/toml-test) for the
|
||||
[TOML encoder](https://github.com/BurntSushi/toml).
|
||||
In particular, it maps JSON data on `stdin` to a TOML format on `stdout`.
|
||||
@@ -0,0 +1,46 @@
|
||||
// Command toml-test-encoder satisfies the toml-test interface for testing TOML
|
||||
// encoders. Namely, it accepts JSON on stdin and outputs TOML on stdout.
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
"github.com/BurntSushi/toml/internal/tag"
|
||||
)
|
||||
|
||||
func init() {
|
||||
log.SetFlags(0)
|
||||
flag.Usage = usage
|
||||
flag.Parse()
|
||||
}
|
||||
|
||||
func usage() {
|
||||
log.Printf("Usage: %s < json-file\n", path.Base(os.Args[0]))
|
||||
flag.PrintDefaults()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func main() {
|
||||
if flag.NArg() != 0 {
|
||||
flag.Usage()
|
||||
}
|
||||
|
||||
var tmp interface{}
|
||||
if err := json.NewDecoder(os.Stdin).Decode(&tmp); err != nil {
|
||||
log.Fatalf("Error decoding JSON: %s", err)
|
||||
}
|
||||
|
||||
rm, err := tag.Remove(tmp)
|
||||
if err != nil {
|
||||
log.Fatalf("Error decoding JSON: %s", err)
|
||||
}
|
||||
|
||||
if err := toml.NewEncoder(os.Stdout).Encode(rm); err != nil {
|
||||
log.Fatalf("Error encoding TOML: %s", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013 TOML authors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
@@ -0,0 +1,14 @@
|
||||
# TOML Validator
|
||||
|
||||
If Go is installed, it's simple to try it out:
|
||||
|
||||
$ go install github.com/BurntSushi/toml/cmd/tomlv@master
|
||||
$ tomlv some-toml-file.toml
|
||||
|
||||
You can see the types of every key in a TOML file with:
|
||||
|
||||
$ tomlv -types some-toml-file.toml
|
||||
|
||||
At the moment, only one error message is reported at a time. Error messages
|
||||
include line numbers. No output means that the files given are valid TOML, or
|
||||
there is a bug in `tomlv`.
|
||||
@@ -0,0 +1,56 @@
|
||||
// Command tomlv validates TOML documents and prints each key's type.
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
)
|
||||
|
||||
var (
|
||||
flagTypes = false
|
||||
)
|
||||
|
||||
func init() {
|
||||
log.SetFlags(0)
|
||||
flag.BoolVar(&flagTypes, "types", flagTypes, "Show the types for every key.")
|
||||
flag.Usage = usage
|
||||
flag.Parse()
|
||||
}
|
||||
|
||||
func usage() {
|
||||
log.Printf("Usage: %s toml-file [ toml-file ... ]\n", path.Base(os.Args[0]))
|
||||
flag.PrintDefaults()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func main() {
|
||||
if flag.NArg() < 1 {
|
||||
flag.Usage()
|
||||
}
|
||||
for _, f := range flag.Args() {
|
||||
var tmp interface{}
|
||||
md, err := toml.DecodeFile(f, &tmp)
|
||||
if err != nil {
|
||||
log.Fatalf("Error in '%s': %s", f, err)
|
||||
}
|
||||
if flagTypes {
|
||||
printTypes(md)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func printTypes(md toml.MetaData) {
|
||||
tabw := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
for _, key := range md.Keys() {
|
||||
fmt.Fprintf(tabw, "%s%s\t%s\n",
|
||||
strings.Repeat(" ", len(key)-1), key, md.Type(key...))
|
||||
}
|
||||
tabw.Flush()
|
||||
}
|
||||
@@ -0,0 +1,602 @@
|
||||
package toml
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"math"
|
||||
"os"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Unmarshaler is the interface implemented by objects that can unmarshal a
|
||||
// TOML description of themselves.
|
||||
type Unmarshaler interface {
|
||||
UnmarshalTOML(interface{}) error
|
||||
}
|
||||
|
||||
// Unmarshal decodes the contents of data in TOML format into a pointer v.
|
||||
//
|
||||
// See [Decoder] for a description of the decoding process.
|
||||
func Unmarshal(data []byte, v interface{}) error {
|
||||
_, err := NewDecoder(bytes.NewReader(data)).Decode(v)
|
||||
return err
|
||||
}
|
||||
|
||||
// Decode the TOML data in to the pointer v.
|
||||
//
|
||||
// See [Decoder] for a description of the decoding process.
|
||||
func Decode(data string, v interface{}) (MetaData, error) {
|
||||
return NewDecoder(strings.NewReader(data)).Decode(v)
|
||||
}
|
||||
|
||||
// DecodeFile reads the contents of a file and decodes it with [Decode].
|
||||
func DecodeFile(path string, v interface{}) (MetaData, error) {
|
||||
fp, err := os.Open(path)
|
||||
if err != nil {
|
||||
return MetaData{}, err
|
||||
}
|
||||
defer fp.Close()
|
||||
return NewDecoder(fp).Decode(v)
|
||||
}
|
||||
|
||||
// Primitive is a TOML value that hasn't been decoded into a Go value.
|
||||
//
|
||||
// This type can be used for any value, which will cause decoding to be delayed.
|
||||
// You can use [PrimitiveDecode] to "manually" decode these values.
|
||||
//
|
||||
// NOTE: The underlying representation of a `Primitive` value is subject to
|
||||
// change. Do not rely on it.
|
||||
//
|
||||
// NOTE: Primitive values are still parsed, so using them will only avoid the
|
||||
// overhead of reflection. They can be useful when you don't know the exact type
|
||||
// of TOML data until runtime.
|
||||
type Primitive struct {
|
||||
undecoded interface{}
|
||||
context Key
|
||||
}
|
||||
|
||||
// The significand precision for float32 and float64 is 24 and 53 bits; this is
|
||||
// the range a natural number can be stored in a float without loss of data.
|
||||
const (
|
||||
maxSafeFloat32Int = 16777215 // 2^24-1
|
||||
maxSafeFloat64Int = int64(9007199254740991) // 2^53-1
|
||||
)
|
||||
|
||||
// Decoder decodes TOML data.
|
||||
//
|
||||
// TOML tables correspond to Go structs or maps; they can be used
|
||||
// interchangeably, but structs offer better type safety.
|
||||
//
|
||||
// TOML table arrays correspond to either a slice of structs or a slice of maps.
|
||||
//
|
||||
// TOML datetimes correspond to [time.Time]. Local datetimes are parsed in the
|
||||
// local timezone.
|
||||
//
|
||||
// [time.Duration] types are treated as nanoseconds if the TOML value is an
|
||||
// integer, or they're parsed with time.ParseDuration() if they're strings.
|
||||
//
|
||||
// All other TOML types (float, string, int, bool and array) correspond to the
|
||||
// obvious Go types.
|
||||
//
|
||||
// An exception to the above rules is if a type implements the TextUnmarshaler
|
||||
// interface, in which case any primitive TOML value (floats, strings, integers,
|
||||
// booleans, datetimes) will be converted to a []byte and given to the value's
|
||||
// UnmarshalText method. See the Unmarshaler example for a demonstration with
|
||||
// email addresses.
|
||||
//
|
||||
// ### Key mapping
|
||||
//
|
||||
// TOML keys can map to either keys in a Go map or field names in a Go struct.
|
||||
// The special `toml` struct tag can be used to map TOML keys to struct fields
|
||||
// that don't match the key name exactly (see the example). A case insensitive
|
||||
// match to struct names will be tried if an exact match can't be found.
|
||||
//
|
||||
// The mapping between TOML values and Go values is loose. That is, there may
|
||||
// exist TOML values that cannot be placed into your representation, and there
|
||||
// may be parts of your representation that do not correspond to TOML values.
|
||||
// This loose mapping can be made stricter by using the IsDefined and/or
|
||||
// Undecoded methods on the MetaData returned.
|
||||
//
|
||||
// This decoder does not handle cyclic types. Decode will not terminate if a
|
||||
// cyclic type is passed.
|
||||
type Decoder struct {
|
||||
r io.Reader
|
||||
}
|
||||
|
||||
// NewDecoder creates a new Decoder.
|
||||
func NewDecoder(r io.Reader) *Decoder {
|
||||
return &Decoder{r: r}
|
||||
}
|
||||
|
||||
var (
|
||||
unmarshalToml = reflect.TypeOf((*Unmarshaler)(nil)).Elem()
|
||||
unmarshalText = reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem()
|
||||
primitiveType = reflect.TypeOf((*Primitive)(nil)).Elem()
|
||||
)
|
||||
|
||||
// Decode TOML data in to the pointer `v`.
|
||||
func (dec *Decoder) Decode(v interface{}) (MetaData, error) {
|
||||
rv := reflect.ValueOf(v)
|
||||
if rv.Kind() != reflect.Ptr {
|
||||
s := "%q"
|
||||
if reflect.TypeOf(v) == nil {
|
||||
s = "%v"
|
||||
}
|
||||
|
||||
return MetaData{}, fmt.Errorf("toml: cannot decode to non-pointer "+s, reflect.TypeOf(v))
|
||||
}
|
||||
if rv.IsNil() {
|
||||
return MetaData{}, fmt.Errorf("toml: cannot decode to nil value of %q", reflect.TypeOf(v))
|
||||
}
|
||||
|
||||
// Check if this is a supported type: struct, map, interface{}, or something
|
||||
// that implements UnmarshalTOML or UnmarshalText.
|
||||
rv = indirect(rv)
|
||||
rt := rv.Type()
|
||||
if rv.Kind() != reflect.Struct && rv.Kind() != reflect.Map &&
|
||||
!(rv.Kind() == reflect.Interface && rv.NumMethod() == 0) &&
|
||||
!rt.Implements(unmarshalToml) && !rt.Implements(unmarshalText) {
|
||||
return MetaData{}, fmt.Errorf("toml: cannot decode to type %s", rt)
|
||||
}
|
||||
|
||||
// TODO: parser should read from io.Reader? Or at the very least, make it
|
||||
// read from []byte rather than string
|
||||
data, err := ioutil.ReadAll(dec.r)
|
||||
if err != nil {
|
||||
return MetaData{}, err
|
||||
}
|
||||
|
||||
p, err := parse(string(data))
|
||||
if err != nil {
|
||||
return MetaData{}, err
|
||||
}
|
||||
|
||||
md := MetaData{
|
||||
mapping: p.mapping,
|
||||
keyInfo: p.keyInfo,
|
||||
keys: p.ordered,
|
||||
decoded: make(map[string]struct{}, len(p.ordered)),
|
||||
context: nil,
|
||||
data: data,
|
||||
}
|
||||
return md, md.unify(p.mapping, rv)
|
||||
}
|
||||
|
||||
// PrimitiveDecode is just like the other Decode* functions, except it decodes a
|
||||
// TOML value that has already been parsed. Valid primitive values can *only* be
|
||||
// obtained from values filled by the decoder functions, including this method.
|
||||
// (i.e., v may contain more [Primitive] values.)
|
||||
//
|
||||
// Meta data for primitive values is included in the meta data returned by the
|
||||
// Decode* functions with one exception: keys returned by the Undecoded method
|
||||
// will only reflect keys that were decoded. Namely, any keys hidden behind a
|
||||
// Primitive will be considered undecoded. Executing this method will update the
|
||||
// undecoded keys in the meta data. (See the example.)
|
||||
func (md *MetaData) PrimitiveDecode(primValue Primitive, v interface{}) error {
|
||||
md.context = primValue.context
|
||||
defer func() { md.context = nil }()
|
||||
return md.unify(primValue.undecoded, rvalue(v))
|
||||
}
|
||||
|
||||
// unify performs a sort of type unification based on the structure of `rv`,
|
||||
// which is the client representation.
|
||||
//
|
||||
// Any type mismatch produces an error. Finding a type that we don't know
|
||||
// how to handle produces an unsupported type error.
|
||||
func (md *MetaData) unify(data interface{}, rv reflect.Value) error {
|
||||
// Special case. Look for a `Primitive` value.
|
||||
// TODO: #76 would make this superfluous after implemented.
|
||||
if rv.Type() == primitiveType {
|
||||
// Save the undecoded data and the key context into the primitive
|
||||
// value.
|
||||
context := make(Key, len(md.context))
|
||||
copy(context, md.context)
|
||||
rv.Set(reflect.ValueOf(Primitive{
|
||||
undecoded: data,
|
||||
context: context,
|
||||
}))
|
||||
return nil
|
||||
}
|
||||
|
||||
rvi := rv.Interface()
|
||||
if v, ok := rvi.(Unmarshaler); ok {
|
||||
return v.UnmarshalTOML(data)
|
||||
}
|
||||
if v, ok := rvi.(encoding.TextUnmarshaler); ok {
|
||||
return md.unifyText(data, v)
|
||||
}
|
||||
|
||||
// TODO:
|
||||
// The behavior here is incorrect whenever a Go type satisfies the
|
||||
// encoding.TextUnmarshaler interface but also corresponds to a TOML hash or
|
||||
// array. In particular, the unmarshaler should only be applied to primitive
|
||||
// TOML values. But at this point, it will be applied to all kinds of values
|
||||
// and produce an incorrect error whenever those values are hashes or arrays
|
||||
// (including arrays of tables).
|
||||
|
||||
k := rv.Kind()
|
||||
|
||||
if k >= reflect.Int && k <= reflect.Uint64 {
|
||||
return md.unifyInt(data, rv)
|
||||
}
|
||||
switch k {
|
||||
case reflect.Ptr:
|
||||
elem := reflect.New(rv.Type().Elem())
|
||||
err := md.unify(data, reflect.Indirect(elem))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rv.Set(elem)
|
||||
return nil
|
||||
case reflect.Struct:
|
||||
return md.unifyStruct(data, rv)
|
||||
case reflect.Map:
|
||||
return md.unifyMap(data, rv)
|
||||
case reflect.Array:
|
||||
return md.unifyArray(data, rv)
|
||||
case reflect.Slice:
|
||||
return md.unifySlice(data, rv)
|
||||
case reflect.String:
|
||||
return md.unifyString(data, rv)
|
||||
case reflect.Bool:
|
||||
return md.unifyBool(data, rv)
|
||||
case reflect.Interface:
|
||||
if rv.NumMethod() > 0 { // Only support empty interfaces are supported.
|
||||
return md.e("unsupported type %s", rv.Type())
|
||||
}
|
||||
return md.unifyAnything(data, rv)
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return md.unifyFloat64(data, rv)
|
||||
}
|
||||
return md.e("unsupported type %s", rv.Kind())
|
||||
}
|
||||
|
||||
func (md *MetaData) unifyStruct(mapping interface{}, rv reflect.Value) error {
|
||||
tmap, ok := mapping.(map[string]interface{})
|
||||
if !ok {
|
||||
if mapping == nil {
|
||||
return nil
|
||||
}
|
||||
return md.e("type mismatch for %s: expected table but found %T",
|
||||
rv.Type().String(), mapping)
|
||||
}
|
||||
|
||||
for key, datum := range tmap {
|
||||
var f *field
|
||||
fields := cachedTypeFields(rv.Type())
|
||||
for i := range fields {
|
||||
ff := &fields[i]
|
||||
if ff.name == key {
|
||||
f = ff
|
||||
break
|
||||
}
|
||||
if f == nil && strings.EqualFold(ff.name, key) {
|
||||
f = ff
|
||||
}
|
||||
}
|
||||
if f != nil {
|
||||
subv := rv
|
||||
for _, i := range f.index {
|
||||
subv = indirect(subv.Field(i))
|
||||
}
|
||||
|
||||
if isUnifiable(subv) {
|
||||
md.decoded[md.context.add(key).String()] = struct{}{}
|
||||
md.context = append(md.context, key)
|
||||
|
||||
err := md.unify(datum, subv)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
md.context = md.context[0 : len(md.context)-1]
|
||||
} else if f.name != "" {
|
||||
return md.e("cannot write unexported field %s.%s", rv.Type().String(), f.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (md *MetaData) unifyMap(mapping interface{}, rv reflect.Value) error {
|
||||
keyType := rv.Type().Key().Kind()
|
||||
if keyType != reflect.String && keyType != reflect.Interface {
|
||||
return fmt.Errorf("toml: cannot decode to a map with non-string key type (%s in %q)",
|
||||
keyType, rv.Type())
|
||||
}
|
||||
|
||||
tmap, ok := mapping.(map[string]interface{})
|
||||
if !ok {
|
||||
if tmap == nil {
|
||||
return nil
|
||||
}
|
||||
return md.badtype("map", mapping)
|
||||
}
|
||||
if rv.IsNil() {
|
||||
rv.Set(reflect.MakeMap(rv.Type()))
|
||||
}
|
||||
for k, v := range tmap {
|
||||
md.decoded[md.context.add(k).String()] = struct{}{}
|
||||
md.context = append(md.context, k)
|
||||
|
||||
rvval := reflect.Indirect(reflect.New(rv.Type().Elem()))
|
||||
|
||||
err := md.unify(v, indirect(rvval))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
md.context = md.context[0 : len(md.context)-1]
|
||||
|
||||
rvkey := indirect(reflect.New(rv.Type().Key()))
|
||||
|
||||
switch keyType {
|
||||
case reflect.Interface:
|
||||
rvkey.Set(reflect.ValueOf(k))
|
||||
case reflect.String:
|
||||
rvkey.SetString(k)
|
||||
}
|
||||
|
||||
rv.SetMapIndex(rvkey, rvval)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (md *MetaData) unifyArray(data interface{}, rv reflect.Value) error {
|
||||
datav := reflect.ValueOf(data)
|
||||
if datav.Kind() != reflect.Slice {
|
||||
if !datav.IsValid() {
|
||||
return nil
|
||||
}
|
||||
return md.badtype("slice", data)
|
||||
}
|
||||
if l := datav.Len(); l != rv.Len() {
|
||||
return md.e("expected array length %d; got TOML array of length %d", rv.Len(), l)
|
||||
}
|
||||
return md.unifySliceArray(datav, rv)
|
||||
}
|
||||
|
||||
func (md *MetaData) unifySlice(data interface{}, rv reflect.Value) error {
|
||||
datav := reflect.ValueOf(data)
|
||||
if datav.Kind() != reflect.Slice {
|
||||
if !datav.IsValid() {
|
||||
return nil
|
||||
}
|
||||
return md.badtype("slice", data)
|
||||
}
|
||||
n := datav.Len()
|
||||
if rv.IsNil() || rv.Cap() < n {
|
||||
rv.Set(reflect.MakeSlice(rv.Type(), n, n))
|
||||
}
|
||||
rv.SetLen(n)
|
||||
return md.unifySliceArray(datav, rv)
|
||||
}
|
||||
|
||||
func (md *MetaData) unifySliceArray(data, rv reflect.Value) error {
|
||||
l := data.Len()
|
||||
for i := 0; i < l; i++ {
|
||||
err := md.unify(data.Index(i).Interface(), indirect(rv.Index(i)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (md *MetaData) unifyString(data interface{}, rv reflect.Value) error {
|
||||
_, ok := rv.Interface().(json.Number)
|
||||
if ok {
|
||||
if i, ok := data.(int64); ok {
|
||||
rv.SetString(strconv.FormatInt(i, 10))
|
||||
} else if f, ok := data.(float64); ok {
|
||||
rv.SetString(strconv.FormatFloat(f, 'f', -1, 64))
|
||||
} else {
|
||||
return md.badtype("string", data)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if s, ok := data.(string); ok {
|
||||
rv.SetString(s)
|
||||
return nil
|
||||
}
|
||||
return md.badtype("string", data)
|
||||
}
|
||||
|
||||
func (md *MetaData) unifyFloat64(data interface{}, rv reflect.Value) error {
|
||||
rvk := rv.Kind()
|
||||
|
||||
if num, ok := data.(float64); ok {
|
||||
switch rvk {
|
||||
case reflect.Float32:
|
||||
if num < -math.MaxFloat32 || num > math.MaxFloat32 {
|
||||
return md.parseErr(errParseRange{i: num, size: rvk.String()})
|
||||
}
|
||||
fallthrough
|
||||
case reflect.Float64:
|
||||
rv.SetFloat(num)
|
||||
default:
|
||||
panic("bug")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if num, ok := data.(int64); ok {
|
||||
if (rvk == reflect.Float32 && (num < -maxSafeFloat32Int || num > maxSafeFloat32Int)) ||
|
||||
(rvk == reflect.Float64 && (num < -maxSafeFloat64Int || num > maxSafeFloat64Int)) {
|
||||
return md.parseErr(errParseRange{i: num, size: rvk.String()})
|
||||
}
|
||||
rv.SetFloat(float64(num))
|
||||
return nil
|
||||
}
|
||||
|
||||
return md.badtype("float", data)
|
||||
}
|
||||
|
||||
func (md *MetaData) unifyInt(data interface{}, rv reflect.Value) error {
|
||||
_, ok := rv.Interface().(time.Duration)
|
||||
if ok {
|
||||
// Parse as string duration, and fall back to regular integer parsing
|
||||
// (as nanosecond) if this is not a string.
|
||||
if s, ok := data.(string); ok {
|
||||
dur, err := time.ParseDuration(s)
|
||||
if err != nil {
|
||||
return md.parseErr(errParseDuration{s})
|
||||
}
|
||||
rv.SetInt(int64(dur))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
num, ok := data.(int64)
|
||||
if !ok {
|
||||
return md.badtype("integer", data)
|
||||
}
|
||||
|
||||
rvk := rv.Kind()
|
||||
switch {
|
||||
case rvk >= reflect.Int && rvk <= reflect.Int64:
|
||||
if (rvk == reflect.Int8 && (num < math.MinInt8 || num > math.MaxInt8)) ||
|
||||
(rvk == reflect.Int16 && (num < math.MinInt16 || num > math.MaxInt16)) ||
|
||||
(rvk == reflect.Int32 && (num < math.MinInt32 || num > math.MaxInt32)) {
|
||||
return md.parseErr(errParseRange{i: num, size: rvk.String()})
|
||||
}
|
||||
rv.SetInt(num)
|
||||
case rvk >= reflect.Uint && rvk <= reflect.Uint64:
|
||||
unum := uint64(num)
|
||||
if rvk == reflect.Uint8 && (num < 0 || unum > math.MaxUint8) ||
|
||||
rvk == reflect.Uint16 && (num < 0 || unum > math.MaxUint16) ||
|
||||
rvk == reflect.Uint32 && (num < 0 || unum > math.MaxUint32) {
|
||||
return md.parseErr(errParseRange{i: num, size: rvk.String()})
|
||||
}
|
||||
rv.SetUint(unum)
|
||||
default:
|
||||
panic("unreachable")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (md *MetaData) unifyBool(data interface{}, rv reflect.Value) error {
|
||||
if b, ok := data.(bool); ok {
|
||||
rv.SetBool(b)
|
||||
return nil
|
||||
}
|
||||
return md.badtype("boolean", data)
|
||||
}
|
||||
|
||||
func (md *MetaData) unifyAnything(data interface{}, rv reflect.Value) error {
|
||||
rv.Set(reflect.ValueOf(data))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (md *MetaData) unifyText(data interface{}, v encoding.TextUnmarshaler) error {
|
||||
var s string
|
||||
switch sdata := data.(type) {
|
||||
case Marshaler:
|
||||
text, err := sdata.MarshalTOML()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s = string(text)
|
||||
case encoding.TextMarshaler:
|
||||
text, err := sdata.MarshalText()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s = string(text)
|
||||
case fmt.Stringer:
|
||||
s = sdata.String()
|
||||
case string:
|
||||
s = sdata
|
||||
case bool:
|
||||
s = fmt.Sprintf("%v", sdata)
|
||||
case int64:
|
||||
s = fmt.Sprintf("%d", sdata)
|
||||
case float64:
|
||||
s = fmt.Sprintf("%f", sdata)
|
||||
default:
|
||||
return md.badtype("primitive (string-like)", data)
|
||||
}
|
||||
if err := v.UnmarshalText([]byte(s)); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (md *MetaData) badtype(dst string, data interface{}) error {
|
||||
return md.e("incompatible types: TOML value has type %T; destination has type %s", data, dst)
|
||||
}
|
||||
|
||||
func (md *MetaData) parseErr(err error) error {
|
||||
k := md.context.String()
|
||||
return ParseError{
|
||||
LastKey: k,
|
||||
Position: md.keyInfo[k].pos,
|
||||
Line: md.keyInfo[k].pos.Line,
|
||||
err: err,
|
||||
input: string(md.data),
|
||||
}
|
||||
}
|
||||
|
||||
func (md *MetaData) e(format string, args ...interface{}) error {
|
||||
f := "toml: "
|
||||
if len(md.context) > 0 {
|
||||
f = fmt.Sprintf("toml: (last key %q): ", md.context)
|
||||
p := md.keyInfo[md.context.String()].pos
|
||||
if p.Line > 0 {
|
||||
f = fmt.Sprintf("toml: line %d (last key %q): ", p.Line, md.context)
|
||||
}
|
||||
}
|
||||
return fmt.Errorf(f+format, args...)
|
||||
}
|
||||
|
||||
// rvalue returns a reflect.Value of `v`. All pointers are resolved.
|
||||
func rvalue(v interface{}) reflect.Value {
|
||||
return indirect(reflect.ValueOf(v))
|
||||
}
|
||||
|
||||
// indirect returns the value pointed to by a pointer.
|
||||
//
|
||||
// Pointers are followed until the value is not a pointer. New values are
|
||||
// allocated for each nil pointer.
|
||||
//
|
||||
// An exception to this rule is if the value satisfies an interface of interest
|
||||
// to us (like encoding.TextUnmarshaler).
|
||||
func indirect(v reflect.Value) reflect.Value {
|
||||
if v.Kind() != reflect.Ptr {
|
||||
if v.CanSet() {
|
||||
pv := v.Addr()
|
||||
pvi := pv.Interface()
|
||||
if _, ok := pvi.(encoding.TextUnmarshaler); ok {
|
||||
return pv
|
||||
}
|
||||
if _, ok := pvi.(Unmarshaler); ok {
|
||||
return pv
|
||||
}
|
||||
}
|
||||
return v
|
||||
}
|
||||
if v.IsNil() {
|
||||
v.Set(reflect.New(v.Type().Elem()))
|
||||
}
|
||||
return indirect(reflect.Indirect(v))
|
||||
}
|
||||
|
||||
func isUnifiable(rv reflect.Value) bool {
|
||||
if rv.CanSet() {
|
||||
return true
|
||||
}
|
||||
rvi := rv.Interface()
|
||||
if _, ok := rvi.(encoding.TextUnmarshaler); ok {
|
||||
return true
|
||||
}
|
||||
if _, ok := rvi.(Unmarshaler); ok {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
//go:build go1.16
|
||||
// +build go1.16
|
||||
|
||||
package toml
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
)
|
||||
|
||||
// DecodeFS reads the contents of a file from [fs.FS] and decodes it with
|
||||
// [Decode].
|
||||
func DecodeFS(fsys fs.FS, path string, v interface{}) (MetaData, error) {
|
||||
fp, err := fsys.Open(path)
|
||||
if err != nil {
|
||||
return MetaData{}, err
|
||||
}
|
||||
defer fp.Close()
|
||||
return NewDecoder(fp).Decode(v)
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
//go:build go1.16
|
||||
// +build go1.16
|
||||
|
||||
package toml
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
)
|
||||
|
||||
func TestDecodeFS(t *testing.T) {
|
||||
fsys := fstest.MapFS{
|
||||
"test.toml": &fstest.MapFile{
|
||||
Data: []byte("a = 42"),
|
||||
},
|
||||
}
|
||||
|
||||
var i struct{ A int }
|
||||
meta, err := DecodeFS(fsys, "test.toml", &i)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
have := fmt.Sprintf("%v %v %v", i, meta.Keys(), meta.Type("a"))
|
||||
want := "{42} [a] Integer"
|
||||
if have != want {
|
||||
t.Errorf("\nhave: %s\nwant: %s", have, want)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,21 @@
|
||||
package toml
|
||||
|
||||
import (
|
||||
"encoding"
|
||||
"io"
|
||||
)
|
||||
|
||||
// Deprecated: use encoding.TextMarshaler
|
||||
type TextMarshaler encoding.TextMarshaler
|
||||
|
||||
// Deprecated: use encoding.TextUnmarshaler
|
||||
type TextUnmarshaler encoding.TextUnmarshaler
|
||||
|
||||
// Deprecated: use MetaData.PrimitiveDecode.
|
||||
func PrimitiveDecode(primValue Primitive, v interface{}) error {
|
||||
md := MetaData{decoded: make(map[string]struct{})}
|
||||
return md.unify(primValue.undecoded, rvalue(v))
|
||||
}
|
||||
|
||||
// Deprecated: use NewDecoder(reader).Decode(&value).
|
||||
func DecodeReader(r io.Reader, v interface{}) (MetaData, error) { return NewDecoder(r).Decode(v) }
|
||||
@@ -0,0 +1,11 @@
|
||||
// Package toml implements decoding and encoding of TOML files.
|
||||
//
|
||||
// This package supports TOML v1.0.0, as specified at https://toml.io
|
||||
//
|
||||
// There is also support for delaying decoding with the Primitive type, and
|
||||
// querying the set of keys in a TOML document with the MetaData type.
|
||||
//
|
||||
// The github.com/BurntSushi/toml/cmd/tomlv package implements a TOML validator,
|
||||
// and can be used to verify if TOML document is valid. It can also be used to
|
||||
// print the type of each key.
|
||||
package toml
|
||||
@@ -0,0 +1,750 @@
|
||||
package toml
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/BurntSushi/toml/internal"
|
||||
)
|
||||
|
||||
type tomlEncodeError struct{ error }
|
||||
|
||||
var (
|
||||
errArrayNilElement = errors.New("toml: cannot encode array with nil element")
|
||||
errNonString = errors.New("toml: cannot encode a map with non-string key type")
|
||||
errNoKey = errors.New("toml: top-level values must be Go maps or structs")
|
||||
errAnything = errors.New("") // used in testing
|
||||
)
|
||||
|
||||
var dblQuotedReplacer = strings.NewReplacer(
|
||||
"\"", "\\\"",
|
||||
"\\", "\\\\",
|
||||
"\x00", `\u0000`,
|
||||
"\x01", `\u0001`,
|
||||
"\x02", `\u0002`,
|
||||
"\x03", `\u0003`,
|
||||
"\x04", `\u0004`,
|
||||
"\x05", `\u0005`,
|
||||
"\x06", `\u0006`,
|
||||
"\x07", `\u0007`,
|
||||
"\b", `\b`,
|
||||
"\t", `\t`,
|
||||
"\n", `\n`,
|
||||
"\x0b", `\u000b`,
|
||||
"\f", `\f`,
|
||||
"\r", `\r`,
|
||||
"\x0e", `\u000e`,
|
||||
"\x0f", `\u000f`,
|
||||
"\x10", `\u0010`,
|
||||
"\x11", `\u0011`,
|
||||
"\x12", `\u0012`,
|
||||
"\x13", `\u0013`,
|
||||
"\x14", `\u0014`,
|
||||
"\x15", `\u0015`,
|
||||
"\x16", `\u0016`,
|
||||
"\x17", `\u0017`,
|
||||
"\x18", `\u0018`,
|
||||
"\x19", `\u0019`,
|
||||
"\x1a", `\u001a`,
|
||||
"\x1b", `\u001b`,
|
||||
"\x1c", `\u001c`,
|
||||
"\x1d", `\u001d`,
|
||||
"\x1e", `\u001e`,
|
||||
"\x1f", `\u001f`,
|
||||
"\x7f", `\u007f`,
|
||||
)
|
||||
|
||||
var (
|
||||
marshalToml = reflect.TypeOf((*Marshaler)(nil)).Elem()
|
||||
marshalText = reflect.TypeOf((*encoding.TextMarshaler)(nil)).Elem()
|
||||
timeType = reflect.TypeOf((*time.Time)(nil)).Elem()
|
||||
)
|
||||
|
||||
// Marshaler is the interface implemented by types that can marshal themselves
|
||||
// into valid TOML.
|
||||
type Marshaler interface {
|
||||
MarshalTOML() ([]byte, error)
|
||||
}
|
||||
|
||||
// Encoder encodes a Go to a TOML document.
|
||||
//
|
||||
// The mapping between Go values and TOML values should be precisely the same as
|
||||
// for [Decode].
|
||||
//
|
||||
// time.Time is encoded as a RFC 3339 string, and time.Duration as its string
|
||||
// representation.
|
||||
//
|
||||
// The [Marshaler] and [encoding.TextMarshaler] interfaces are supported to
|
||||
// encoding the value as custom TOML.
|
||||
//
|
||||
// If you want to write arbitrary binary data then you will need to use
|
||||
// something like base64 since TOML does not have any binary types.
|
||||
//
|
||||
// When encoding TOML hashes (Go maps or structs), keys without any sub-hashes
|
||||
// are encoded first.
|
||||
//
|
||||
// Go maps will be sorted alphabetically by key for deterministic output.
|
||||
//
|
||||
// The toml struct tag can be used to provide the key name; if omitted the
|
||||
// struct field name will be used. If the "omitempty" option is present the
|
||||
// following value will be skipped:
|
||||
//
|
||||
// - arrays, slices, maps, and string with len of 0
|
||||
// - struct with all zero values
|
||||
// - bool false
|
||||
//
|
||||
// If omitzero is given all int and float types with a value of 0 will be
|
||||
// skipped.
|
||||
//
|
||||
// Encoding Go values without a corresponding TOML representation will return an
|
||||
// error. Examples of this includes maps with non-string keys, slices with nil
|
||||
// elements, embedded non-struct types, and nested slices containing maps or
|
||||
// structs. (e.g. [][]map[string]string is not allowed but []map[string]string
|
||||
// is okay, as is []map[string][]string).
|
||||
//
|
||||
// NOTE: only exported keys are encoded due to the use of reflection. Unexported
|
||||
// keys are silently discarded.
|
||||
type Encoder struct {
|
||||
// String to use for a single indentation level; default is two spaces.
|
||||
Indent string
|
||||
|
||||
w *bufio.Writer
|
||||
hasWritten bool // written any output to w yet?
|
||||
}
|
||||
|
||||
// NewEncoder create a new Encoder.
|
||||
func NewEncoder(w io.Writer) *Encoder {
|
||||
return &Encoder{
|
||||
w: bufio.NewWriter(w),
|
||||
Indent: " ",
|
||||
}
|
||||
}
|
||||
|
||||
// Encode writes a TOML representation of the Go value to the [Encoder]'s writer.
|
||||
//
|
||||
// An error is returned if the value given cannot be encoded to a valid TOML
|
||||
// document.
|
||||
func (enc *Encoder) Encode(v interface{}) error {
|
||||
rv := eindirect(reflect.ValueOf(v))
|
||||
if err := enc.safeEncode(Key([]string{}), rv); err != nil {
|
||||
return err
|
||||
}
|
||||
return enc.w.Flush()
|
||||
}
|
||||
|
||||
func (enc *Encoder) safeEncode(key Key, rv reflect.Value) (err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
if terr, ok := r.(tomlEncodeError); ok {
|
||||
err = terr.error
|
||||
return
|
||||
}
|
||||
panic(r)
|
||||
}
|
||||
}()
|
||||
enc.encode(key, rv)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (enc *Encoder) encode(key Key, rv reflect.Value) {
|
||||
// If we can marshal the type to text, then we use that. This prevents the
|
||||
// encoder for handling these types as generic structs (or whatever the
|
||||
// underlying type of a TextMarshaler is).
|
||||
switch {
|
||||
case isMarshaler(rv):
|
||||
enc.writeKeyValue(key, rv, false)
|
||||
return
|
||||
case rv.Type() == primitiveType: // TODO: #76 would make this superfluous after implemented.
|
||||
enc.encode(key, reflect.ValueOf(rv.Interface().(Primitive).undecoded))
|
||||
return
|
||||
}
|
||||
|
||||
k := rv.Kind()
|
||||
switch k {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32,
|
||||
reflect.Int64,
|
||||
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32,
|
||||
reflect.Uint64,
|
||||
reflect.Float32, reflect.Float64, reflect.String, reflect.Bool:
|
||||
enc.writeKeyValue(key, rv, false)
|
||||
case reflect.Array, reflect.Slice:
|
||||
if typeEqual(tomlArrayHash, tomlTypeOfGo(rv)) {
|
||||
enc.eArrayOfTables(key, rv)
|
||||
} else {
|
||||
enc.writeKeyValue(key, rv, false)
|
||||
}
|
||||
case reflect.Interface:
|
||||
if rv.IsNil() {
|
||||
return
|
||||
}
|
||||
enc.encode(key, rv.Elem())
|
||||
case reflect.Map:
|
||||
if rv.IsNil() {
|
||||
return
|
||||
}
|
||||
enc.eTable(key, rv)
|
||||
case reflect.Ptr:
|
||||
if rv.IsNil() {
|
||||
return
|
||||
}
|
||||
enc.encode(key, rv.Elem())
|
||||
case reflect.Struct:
|
||||
enc.eTable(key, rv)
|
||||
default:
|
||||
encPanic(fmt.Errorf("unsupported type for key '%s': %s", key, k))
|
||||
}
|
||||
}
|
||||
|
||||
// eElement encodes any value that can be an array element.
|
||||
func (enc *Encoder) eElement(rv reflect.Value) {
|
||||
switch v := rv.Interface().(type) {
|
||||
case time.Time: // Using TextMarshaler adds extra quotes, which we don't want.
|
||||
format := time.RFC3339Nano
|
||||
switch v.Location() {
|
||||
case internal.LocalDatetime:
|
||||
format = "2006-01-02T15:04:05.999999999"
|
||||
case internal.LocalDate:
|
||||
format = "2006-01-02"
|
||||
case internal.LocalTime:
|
||||
format = "15:04:05.999999999"
|
||||
}
|
||||
switch v.Location() {
|
||||
default:
|
||||
enc.wf(v.Format(format))
|
||||
case internal.LocalDatetime, internal.LocalDate, internal.LocalTime:
|
||||
enc.wf(v.In(time.UTC).Format(format))
|
||||
}
|
||||
return
|
||||
case Marshaler:
|
||||
s, err := v.MarshalTOML()
|
||||
if err != nil {
|
||||
encPanic(err)
|
||||
}
|
||||
if s == nil {
|
||||
encPanic(errors.New("MarshalTOML returned nil and no error"))
|
||||
}
|
||||
enc.w.Write(s)
|
||||
return
|
||||
case encoding.TextMarshaler:
|
||||
s, err := v.MarshalText()
|
||||
if err != nil {
|
||||
encPanic(err)
|
||||
}
|
||||
if s == nil {
|
||||
encPanic(errors.New("MarshalText returned nil and no error"))
|
||||
}
|
||||
enc.writeQuoted(string(s))
|
||||
return
|
||||
case time.Duration:
|
||||
enc.writeQuoted(v.String())
|
||||
return
|
||||
case json.Number:
|
||||
n, _ := rv.Interface().(json.Number)
|
||||
|
||||
if n == "" { /// Useful zero value.
|
||||
enc.w.WriteByte('0')
|
||||
return
|
||||
} else if v, err := n.Int64(); err == nil {
|
||||
enc.eElement(reflect.ValueOf(v))
|
||||
return
|
||||
} else if v, err := n.Float64(); err == nil {
|
||||
enc.eElement(reflect.ValueOf(v))
|
||||
return
|
||||
}
|
||||
encPanic(fmt.Errorf("unable to convert %q to int64 or float64", n))
|
||||
}
|
||||
|
||||
switch rv.Kind() {
|
||||
case reflect.Ptr:
|
||||
enc.eElement(rv.Elem())
|
||||
return
|
||||
case reflect.String:
|
||||
enc.writeQuoted(rv.String())
|
||||
case reflect.Bool:
|
||||
enc.wf(strconv.FormatBool(rv.Bool()))
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
enc.wf(strconv.FormatInt(rv.Int(), 10))
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
enc.wf(strconv.FormatUint(rv.Uint(), 10))
|
||||
case reflect.Float32:
|
||||
f := rv.Float()
|
||||
if math.IsNaN(f) {
|
||||
enc.wf("nan")
|
||||
} else if math.IsInf(f, 0) {
|
||||
enc.wf("%cinf", map[bool]byte{true: '-', false: '+'}[math.Signbit(f)])
|
||||
} else {
|
||||
enc.wf(floatAddDecimal(strconv.FormatFloat(f, 'f', -1, 32)))
|
||||
}
|
||||
case reflect.Float64:
|
||||
f := rv.Float()
|
||||
if math.IsNaN(f) {
|
||||
enc.wf("nan")
|
||||
} else if math.IsInf(f, 0) {
|
||||
enc.wf("%cinf", map[bool]byte{true: '-', false: '+'}[math.Signbit(f)])
|
||||
} else {
|
||||
enc.wf(floatAddDecimal(strconv.FormatFloat(f, 'f', -1, 64)))
|
||||
}
|
||||
case reflect.Array, reflect.Slice:
|
||||
enc.eArrayOrSliceElement(rv)
|
||||
case reflect.Struct:
|
||||
enc.eStruct(nil, rv, true)
|
||||
case reflect.Map:
|
||||
enc.eMap(nil, rv, true)
|
||||
case reflect.Interface:
|
||||
enc.eElement(rv.Elem())
|
||||
default:
|
||||
encPanic(fmt.Errorf("unexpected type: %T", rv.Interface()))
|
||||
}
|
||||
}
|
||||
|
||||
// By the TOML spec, all floats must have a decimal with at least one number on
|
||||
// either side.
|
||||
func floatAddDecimal(fstr string) string {
|
||||
if !strings.Contains(fstr, ".") {
|
||||
return fstr + ".0"
|
||||
}
|
||||
return fstr
|
||||
}
|
||||
|
||||
func (enc *Encoder) writeQuoted(s string) {
|
||||
enc.wf("\"%s\"", dblQuotedReplacer.Replace(s))
|
||||
}
|
||||
|
||||
func (enc *Encoder) eArrayOrSliceElement(rv reflect.Value) {
|
||||
length := rv.Len()
|
||||
enc.wf("[")
|
||||
for i := 0; i < length; i++ {
|
||||
elem := eindirect(rv.Index(i))
|
||||
enc.eElement(elem)
|
||||
if i != length-1 {
|
||||
enc.wf(", ")
|
||||
}
|
||||
}
|
||||
enc.wf("]")
|
||||
}
|
||||
|
||||
func (enc *Encoder) eArrayOfTables(key Key, rv reflect.Value) {
|
||||
if len(key) == 0 {
|
||||
encPanic(errNoKey)
|
||||
}
|
||||
for i := 0; i < rv.Len(); i++ {
|
||||
trv := eindirect(rv.Index(i))
|
||||
if isNil(trv) {
|
||||
continue
|
||||
}
|
||||
enc.newline()
|
||||
enc.wf("%s[[%s]]", enc.indentStr(key), key)
|
||||
enc.newline()
|
||||
enc.eMapOrStruct(key, trv, false)
|
||||
}
|
||||
}
|
||||
|
||||
func (enc *Encoder) eTable(key Key, rv reflect.Value) {
|
||||
if len(key) == 1 {
|
||||
// Output an extra newline between top-level tables.
|
||||
// (The newline isn't written if nothing else has been written though.)
|
||||
enc.newline()
|
||||
}
|
||||
if len(key) > 0 {
|
||||
enc.wf("%s[%s]", enc.indentStr(key), key)
|
||||
enc.newline()
|
||||
}
|
||||
enc.eMapOrStruct(key, rv, false)
|
||||
}
|
||||
|
||||
func (enc *Encoder) eMapOrStruct(key Key, rv reflect.Value, inline bool) {
|
||||
switch rv.Kind() {
|
||||
case reflect.Map:
|
||||
enc.eMap(key, rv, inline)
|
||||
case reflect.Struct:
|
||||
enc.eStruct(key, rv, inline)
|
||||
default:
|
||||
// Should never happen?
|
||||
panic("eTable: unhandled reflect.Value Kind: " + rv.Kind().String())
|
||||
}
|
||||
}
|
||||
|
||||
func (enc *Encoder) eMap(key Key, rv reflect.Value, inline bool) {
|
||||
rt := rv.Type()
|
||||
if rt.Key().Kind() != reflect.String {
|
||||
encPanic(errNonString)
|
||||
}
|
||||
|
||||
// Sort keys so that we have deterministic output. And write keys directly
|
||||
// underneath this key first, before writing sub-structs or sub-maps.
|
||||
var mapKeysDirect, mapKeysSub []string
|
||||
for _, mapKey := range rv.MapKeys() {
|
||||
k := mapKey.String()
|
||||
if typeIsTable(tomlTypeOfGo(eindirect(rv.MapIndex(mapKey)))) {
|
||||
mapKeysSub = append(mapKeysSub, k)
|
||||
} else {
|
||||
mapKeysDirect = append(mapKeysDirect, k)
|
||||
}
|
||||
}
|
||||
|
||||
var writeMapKeys = func(mapKeys []string, trailC bool) {
|
||||
sort.Strings(mapKeys)
|
||||
for i, mapKey := range mapKeys {
|
||||
val := eindirect(rv.MapIndex(reflect.ValueOf(mapKey)))
|
||||
if isNil(val) {
|
||||
continue
|
||||
}
|
||||
|
||||
if inline {
|
||||
enc.writeKeyValue(Key{mapKey}, val, true)
|
||||
if trailC || i != len(mapKeys)-1 {
|
||||
enc.wf(", ")
|
||||
}
|
||||
} else {
|
||||
enc.encode(key.add(mapKey), val)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if inline {
|
||||
enc.wf("{")
|
||||
}
|
||||
writeMapKeys(mapKeysDirect, len(mapKeysSub) > 0)
|
||||
writeMapKeys(mapKeysSub, false)
|
||||
if inline {
|
||||
enc.wf("}")
|
||||
}
|
||||
}
|
||||
|
||||
const is32Bit = (32 << (^uint(0) >> 63)) == 32
|
||||
|
||||
func pointerTo(t reflect.Type) reflect.Type {
|
||||
if t.Kind() == reflect.Ptr {
|
||||
return pointerTo(t.Elem())
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
func (enc *Encoder) eStruct(key Key, rv reflect.Value, inline bool) {
|
||||
// Write keys for fields directly under this key first, because if we write
|
||||
// a field that creates a new table then all keys under it will be in that
|
||||
// table (not the one we're writing here).
|
||||
//
|
||||
// Fields is a [][]int: for fieldsDirect this always has one entry (the
|
||||
// struct index). For fieldsSub it contains two entries: the parent field
|
||||
// index from tv, and the field indexes for the fields of the sub.
|
||||
var (
|
||||
rt = rv.Type()
|
||||
fieldsDirect, fieldsSub [][]int
|
||||
addFields func(rt reflect.Type, rv reflect.Value, start []int)
|
||||
)
|
||||
addFields = func(rt reflect.Type, rv reflect.Value, start []int) {
|
||||
for i := 0; i < rt.NumField(); i++ {
|
||||
f := rt.Field(i)
|
||||
isEmbed := f.Anonymous && pointerTo(f.Type).Kind() == reflect.Struct
|
||||
if f.PkgPath != "" && !isEmbed { /// Skip unexported fields.
|
||||
continue
|
||||
}
|
||||
opts := getOptions(f.Tag)
|
||||
if opts.skip {
|
||||
continue
|
||||
}
|
||||
|
||||
frv := eindirect(rv.Field(i))
|
||||
|
||||
// Treat anonymous struct fields with tag names as though they are
|
||||
// not anonymous, like encoding/json does.
|
||||
//
|
||||
// Non-struct anonymous fields use the normal encoding logic.
|
||||
if isEmbed {
|
||||
if getOptions(f.Tag).name == "" && frv.Kind() == reflect.Struct {
|
||||
addFields(frv.Type(), frv, append(start, f.Index...))
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if typeIsTable(tomlTypeOfGo(frv)) {
|
||||
fieldsSub = append(fieldsSub, append(start, f.Index...))
|
||||
} else {
|
||||
// Copy so it works correct on 32bit archs; not clear why this
|
||||
// is needed. See #314, and https://www.reddit.com/r/golang/comments/pnx8v4
|
||||
// This also works fine on 64bit, but 32bit archs are somewhat
|
||||
// rare and this is a wee bit faster.
|
||||
if is32Bit {
|
||||
copyStart := make([]int, len(start))
|
||||
copy(copyStart, start)
|
||||
fieldsDirect = append(fieldsDirect, append(copyStart, f.Index...))
|
||||
} else {
|
||||
fieldsDirect = append(fieldsDirect, append(start, f.Index...))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
addFields(rt, rv, nil)
|
||||
|
||||
writeFields := func(fields [][]int) {
|
||||
for _, fieldIndex := range fields {
|
||||
fieldType := rt.FieldByIndex(fieldIndex)
|
||||
fieldVal := eindirect(rv.FieldByIndex(fieldIndex))
|
||||
|
||||
if isNil(fieldVal) { /// Don't write anything for nil fields.
|
||||
continue
|
||||
}
|
||||
|
||||
opts := getOptions(fieldType.Tag)
|
||||
if opts.skip {
|
||||
continue
|
||||
}
|
||||
keyName := fieldType.Name
|
||||
if opts.name != "" {
|
||||
keyName = opts.name
|
||||
}
|
||||
|
||||
if opts.omitempty && enc.isEmpty(fieldVal) {
|
||||
continue
|
||||
}
|
||||
if opts.omitzero && isZero(fieldVal) {
|
||||
continue
|
||||
}
|
||||
|
||||
if inline {
|
||||
enc.writeKeyValue(Key{keyName}, fieldVal, true)
|
||||
if fieldIndex[0] != len(fields)-1 {
|
||||
enc.wf(", ")
|
||||
}
|
||||
} else {
|
||||
enc.encode(key.add(keyName), fieldVal)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if inline {
|
||||
enc.wf("{")
|
||||
}
|
||||
writeFields(fieldsDirect)
|
||||
writeFields(fieldsSub)
|
||||
if inline {
|
||||
enc.wf("}")
|
||||
}
|
||||
}
|
||||
|
||||
// tomlTypeOfGo returns the TOML type name of the Go value's type.
|
||||
//
|
||||
// It is used to determine whether the types of array elements are mixed (which
|
||||
// is forbidden). If the Go value is nil, then it is illegal for it to be an
|
||||
// array element, and valueIsNil is returned as true.
|
||||
//
|
||||
// The type may be `nil`, which means no concrete TOML type could be found.
|
||||
func tomlTypeOfGo(rv reflect.Value) tomlType {
|
||||
if isNil(rv) || !rv.IsValid() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if rv.Kind() == reflect.Struct {
|
||||
if rv.Type() == timeType {
|
||||
return tomlDatetime
|
||||
}
|
||||
if isMarshaler(rv) {
|
||||
return tomlString
|
||||
}
|
||||
return tomlHash
|
||||
}
|
||||
|
||||
if isMarshaler(rv) {
|
||||
return tomlString
|
||||
}
|
||||
|
||||
switch rv.Kind() {
|
||||
case reflect.Bool:
|
||||
return tomlBool
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32,
|
||||
reflect.Int64,
|
||||
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32,
|
||||
reflect.Uint64:
|
||||
return tomlInteger
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return tomlFloat
|
||||
case reflect.Array, reflect.Slice:
|
||||
if isTableArray(rv) {
|
||||
return tomlArrayHash
|
||||
}
|
||||
return tomlArray
|
||||
case reflect.Ptr, reflect.Interface:
|
||||
return tomlTypeOfGo(rv.Elem())
|
||||
case reflect.String:
|
||||
return tomlString
|
||||
case reflect.Map:
|
||||
return tomlHash
|
||||
default:
|
||||
encPanic(errors.New("unsupported type: " + rv.Kind().String()))
|
||||
panic("unreachable")
|
||||
}
|
||||
}
|
||||
|
||||
func isMarshaler(rv reflect.Value) bool {
|
||||
return rv.Type().Implements(marshalText) || rv.Type().Implements(marshalToml)
|
||||
}
|
||||
|
||||
// isTableArray reports if all entries in the array or slice are a table.
|
||||
func isTableArray(arr reflect.Value) bool {
|
||||
if isNil(arr) || !arr.IsValid() || arr.Len() == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
ret := true
|
||||
for i := 0; i < arr.Len(); i++ {
|
||||
tt := tomlTypeOfGo(eindirect(arr.Index(i)))
|
||||
// Don't allow nil.
|
||||
if tt == nil {
|
||||
encPanic(errArrayNilElement)
|
||||
}
|
||||
|
||||
if ret && !typeEqual(tomlHash, tt) {
|
||||
ret = false
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
type tagOptions struct {
|
||||
skip bool // "-"
|
||||
name string
|
||||
omitempty bool
|
||||
omitzero bool
|
||||
}
|
||||
|
||||
func getOptions(tag reflect.StructTag) tagOptions {
|
||||
t := tag.Get("toml")
|
||||
if t == "-" {
|
||||
return tagOptions{skip: true}
|
||||
}
|
||||
var opts tagOptions
|
||||
parts := strings.Split(t, ",")
|
||||
opts.name = parts[0]
|
||||
for _, s := range parts[1:] {
|
||||
switch s {
|
||||
case "omitempty":
|
||||
opts.omitempty = true
|
||||
case "omitzero":
|
||||
opts.omitzero = true
|
||||
}
|
||||
}
|
||||
return opts
|
||||
}
|
||||
|
||||
func isZero(rv reflect.Value) bool {
|
||||
switch rv.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return rv.Int() == 0
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
return rv.Uint() == 0
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return rv.Float() == 0.0
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (enc *Encoder) isEmpty(rv reflect.Value) bool {
|
||||
switch rv.Kind() {
|
||||
case reflect.Array, reflect.Slice, reflect.Map, reflect.String:
|
||||
return rv.Len() == 0
|
||||
case reflect.Struct:
|
||||
if rv.Type().Comparable() {
|
||||
return reflect.Zero(rv.Type()).Interface() == rv.Interface()
|
||||
}
|
||||
// Need to also check if all the fields are empty, otherwise something
|
||||
// like this with uncomparable types will always return true:
|
||||
//
|
||||
// type a struct{ field b }
|
||||
// type b struct{ s []string }
|
||||
// s := a{field: b{s: []string{"AAA"}}}
|
||||
for i := 0; i < rv.NumField(); i++ {
|
||||
if !enc.isEmpty(rv.Field(i)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
case reflect.Bool:
|
||||
return !rv.Bool()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (enc *Encoder) newline() {
|
||||
if enc.hasWritten {
|
||||
enc.wf("\n")
|
||||
}
|
||||
}
|
||||
|
||||
// Write a key/value pair:
|
||||
//
|
||||
// key = <any value>
|
||||
//
|
||||
// This is also used for "k = v" in inline tables; so something like this will
|
||||
// be written in three calls:
|
||||
//
|
||||
// ┌───────────────────┐
|
||||
// │ ┌───┐ ┌────┐│
|
||||
// v v v v vv
|
||||
// key = {k = 1, k2 = 2}
|
||||
func (enc *Encoder) writeKeyValue(key Key, val reflect.Value, inline bool) {
|
||||
if len(key) == 0 {
|
||||
encPanic(errNoKey)
|
||||
}
|
||||
enc.wf("%s%s = ", enc.indentStr(key), key.maybeQuoted(len(key)-1))
|
||||
enc.eElement(val)
|
||||
if !inline {
|
||||
enc.newline()
|
||||
}
|
||||
}
|
||||
|
||||
func (enc *Encoder) wf(format string, v ...interface{}) {
|
||||
_, err := fmt.Fprintf(enc.w, format, v...)
|
||||
if err != nil {
|
||||
encPanic(err)
|
||||
}
|
||||
enc.hasWritten = true
|
||||
}
|
||||
|
||||
func (enc *Encoder) indentStr(key Key) string {
|
||||
return strings.Repeat(enc.Indent, len(key)-1)
|
||||
}
|
||||
|
||||
func encPanic(err error) {
|
||||
panic(tomlEncodeError{err})
|
||||
}
|
||||
|
||||
// Resolve any level of pointers to the actual value (e.g. **string → string).
|
||||
func eindirect(v reflect.Value) reflect.Value {
|
||||
if v.Kind() != reflect.Ptr && v.Kind() != reflect.Interface {
|
||||
if isMarshaler(v) {
|
||||
return v
|
||||
}
|
||||
if v.CanAddr() { /// Special case for marshalers; see #358.
|
||||
if pv := v.Addr(); isMarshaler(pv) {
|
||||
return pv
|
||||
}
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
if v.IsNil() {
|
||||
return v
|
||||
}
|
||||
|
||||
return eindirect(v.Elem())
|
||||
}
|
||||
|
||||
func isNil(rv reflect.Value) bool {
|
||||
switch rv.Kind() {
|
||||
case reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice:
|
||||
return rv.IsNil()
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,279 @@
|
||||
package toml
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ParseError is returned when there is an error parsing the TOML syntax such as
|
||||
// invalid syntax, duplicate keys, etc.
|
||||
//
|
||||
// In addition to the error message itself, you can also print detailed location
|
||||
// information with context by using [ErrorWithPosition]:
|
||||
//
|
||||
// toml: error: Key 'fruit' was already created and cannot be used as an array.
|
||||
//
|
||||
// At line 4, column 2-7:
|
||||
//
|
||||
// 2 | fruit = []
|
||||
// 3 |
|
||||
// 4 | [[fruit]] # Not allowed
|
||||
// ^^^^^
|
||||
//
|
||||
// [ErrorWithUsage] can be used to print the above with some more detailed usage
|
||||
// guidance:
|
||||
//
|
||||
// toml: error: newlines not allowed within inline tables
|
||||
//
|
||||
// At line 1, column 18:
|
||||
//
|
||||
// 1 | x = [{ key = 42 #
|
||||
// ^
|
||||
//
|
||||
// Error help:
|
||||
//
|
||||
// Inline tables must always be on a single line:
|
||||
//
|
||||
// table = {key = 42, second = 43}
|
||||
//
|
||||
// It is invalid to split them over multiple lines like so:
|
||||
//
|
||||
// # INVALID
|
||||
// table = {
|
||||
// key = 42,
|
||||
// second = 43
|
||||
// }
|
||||
//
|
||||
// Use regular for this:
|
||||
//
|
||||
// [table]
|
||||
// key = 42
|
||||
// second = 43
|
||||
type ParseError struct {
|
||||
Message string // Short technical message.
|
||||
Usage string // Longer message with usage guidance; may be blank.
|
||||
Position Position // Position of the error
|
||||
LastKey string // Last parsed key, may be blank.
|
||||
|
||||
// Line the error occurred.
|
||||
//
|
||||
// Deprecated: use [Position].
|
||||
Line int
|
||||
|
||||
err error
|
||||
input string
|
||||
}
|
||||
|
||||
// Position of an error.
|
||||
type Position struct {
|
||||
Line int // Line number, starting at 1.
|
||||
Start int // Start of error, as byte offset starting at 0.
|
||||
Len int // Lenght in bytes.
|
||||
}
|
||||
|
||||
func (pe ParseError) Error() string {
|
||||
msg := pe.Message
|
||||
if msg == "" { // Error from errorf()
|
||||
msg = pe.err.Error()
|
||||
}
|
||||
|
||||
if pe.LastKey == "" {
|
||||
return fmt.Sprintf("toml: line %d: %s", pe.Position.Line, msg)
|
||||
}
|
||||
return fmt.Sprintf("toml: line %d (last key %q): %s",
|
||||
pe.Position.Line, pe.LastKey, msg)
|
||||
}
|
||||
|
||||
// ErrorWithUsage() returns the error with detailed location context.
|
||||
//
|
||||
// See the documentation on [ParseError].
|
||||
func (pe ParseError) ErrorWithPosition() string {
|
||||
if pe.input == "" { // Should never happen, but just in case.
|
||||
return pe.Error()
|
||||
}
|
||||
|
||||
var (
|
||||
lines = strings.Split(pe.input, "\n")
|
||||
col = pe.column(lines)
|
||||
b = new(strings.Builder)
|
||||
)
|
||||
|
||||
msg := pe.Message
|
||||
if msg == "" {
|
||||
msg = pe.err.Error()
|
||||
}
|
||||
|
||||
// TODO: don't show control characters as literals? This may not show up
|
||||
// well everywhere.
|
||||
|
||||
if pe.Position.Len == 1 {
|
||||
fmt.Fprintf(b, "toml: error: %s\n\nAt line %d, column %d:\n\n",
|
||||
msg, pe.Position.Line, col+1)
|
||||
} else {
|
||||
fmt.Fprintf(b, "toml: error: %s\n\nAt line %d, column %d-%d:\n\n",
|
||||
msg, pe.Position.Line, col, col+pe.Position.Len)
|
||||
}
|
||||
if pe.Position.Line > 2 {
|
||||
fmt.Fprintf(b, "% 7d | %s\n", pe.Position.Line-2, lines[pe.Position.Line-3])
|
||||
}
|
||||
if pe.Position.Line > 1 {
|
||||
fmt.Fprintf(b, "% 7d | %s\n", pe.Position.Line-1, lines[pe.Position.Line-2])
|
||||
}
|
||||
fmt.Fprintf(b, "% 7d | %s\n", pe.Position.Line, lines[pe.Position.Line-1])
|
||||
fmt.Fprintf(b, "% 10s%s%s\n", "", strings.Repeat(" ", col), strings.Repeat("^", pe.Position.Len))
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// ErrorWithUsage() returns the error with detailed location context and usage
|
||||
// guidance.
|
||||
//
|
||||
// See the documentation on [ParseError].
|
||||
func (pe ParseError) ErrorWithUsage() string {
|
||||
m := pe.ErrorWithPosition()
|
||||
if u, ok := pe.err.(interface{ Usage() string }); ok && u.Usage() != "" {
|
||||
lines := strings.Split(strings.TrimSpace(u.Usage()), "\n")
|
||||
for i := range lines {
|
||||
if lines[i] != "" {
|
||||
lines[i] = " " + lines[i]
|
||||
}
|
||||
}
|
||||
return m + "Error help:\n\n" + strings.Join(lines, "\n") + "\n"
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func (pe ParseError) column(lines []string) int {
|
||||
var pos, col int
|
||||
for i := range lines {
|
||||
ll := len(lines[i]) + 1 // +1 for the removed newline
|
||||
if pos+ll >= pe.Position.Start {
|
||||
col = pe.Position.Start - pos
|
||||
if col < 0 { // Should never happen, but just in case.
|
||||
col = 0
|
||||
}
|
||||
break
|
||||
}
|
||||
pos += ll
|
||||
}
|
||||
|
||||
return col
|
||||
}
|
||||
|
||||
type (
|
||||
errLexControl struct{ r rune }
|
||||
errLexEscape struct{ r rune }
|
||||
errLexUTF8 struct{ b byte }
|
||||
errLexInvalidNum struct{ v string }
|
||||
errLexInvalidDate struct{ v string }
|
||||
errLexInlineTableNL struct{}
|
||||
errLexStringNL struct{}
|
||||
errParseRange struct {
|
||||
i interface{} // int or float
|
||||
size string // "int64", "uint16", etc.
|
||||
}
|
||||
errParseDuration struct{ d string }
|
||||
)
|
||||
|
||||
func (e errLexControl) Error() string {
|
||||
return fmt.Sprintf("TOML files cannot contain control characters: '0x%02x'", e.r)
|
||||
}
|
||||
func (e errLexControl) Usage() string { return "" }
|
||||
|
||||
func (e errLexEscape) Error() string { return fmt.Sprintf(`invalid escape in string '\%c'`, e.r) }
|
||||
func (e errLexEscape) Usage() string { return usageEscape }
|
||||
func (e errLexUTF8) Error() string { return fmt.Sprintf("invalid UTF-8 byte: 0x%02x", e.b) }
|
||||
func (e errLexUTF8) Usage() string { return "" }
|
||||
func (e errLexInvalidNum) Error() string { return fmt.Sprintf("invalid number: %q", e.v) }
|
||||
func (e errLexInvalidNum) Usage() string { return "" }
|
||||
func (e errLexInvalidDate) Error() string { return fmt.Sprintf("invalid date: %q", e.v) }
|
||||
func (e errLexInvalidDate) Usage() string { return "" }
|
||||
func (e errLexInlineTableNL) Error() string { return "newlines not allowed within inline tables" }
|
||||
func (e errLexInlineTableNL) Usage() string { return usageInlineNewline }
|
||||
func (e errLexStringNL) Error() string { return "strings cannot contain newlines" }
|
||||
func (e errLexStringNL) Usage() string { return usageStringNewline }
|
||||
func (e errParseRange) Error() string { return fmt.Sprintf("%v is out of range for %s", e.i, e.size) }
|
||||
func (e errParseRange) Usage() string { return usageIntOverflow }
|
||||
func (e errParseDuration) Error() string { return fmt.Sprintf("invalid duration: %q", e.d) }
|
||||
func (e errParseDuration) Usage() string { return usageDuration }
|
||||
|
||||
const usageEscape = `
|
||||
A '\' inside a "-delimited string is interpreted as an escape character.
|
||||
|
||||
The following escape sequences are supported:
|
||||
\b, \t, \n, \f, \r, \", \\, \uXXXX, and \UXXXXXXXX
|
||||
|
||||
To prevent a '\' from being recognized as an escape character, use either:
|
||||
|
||||
- a ' or '''-delimited string; escape characters aren't processed in them; or
|
||||
- write two backslashes to get a single backslash: '\\'.
|
||||
|
||||
If you're trying to add a Windows path (e.g. "C:\Users\martin") then using '/'
|
||||
instead of '\' will usually also work: "C:/Users/martin".
|
||||
`
|
||||
|
||||
const usageInlineNewline = `
|
||||
Inline tables must always be on a single line:
|
||||
|
||||
table = {key = 42, second = 43}
|
||||
|
||||
It is invalid to split them over multiple lines like so:
|
||||
|
||||
# INVALID
|
||||
table = {
|
||||
key = 42,
|
||||
second = 43
|
||||
}
|
||||
|
||||
Use regular for this:
|
||||
|
||||
[table]
|
||||
key = 42
|
||||
second = 43
|
||||
`
|
||||
|
||||
const usageStringNewline = `
|
||||
Strings must always be on a single line, and cannot span more than one line:
|
||||
|
||||
# INVALID
|
||||
string = "Hello,
|
||||
world!"
|
||||
|
||||
Instead use """ or ''' to split strings over multiple lines:
|
||||
|
||||
string = """Hello,
|
||||
world!"""
|
||||
`
|
||||
|
||||
const usageIntOverflow = `
|
||||
This number is too large; this may be an error in the TOML, but it can also be a
|
||||
bug in the program that uses too small of an integer.
|
||||
|
||||
The maximum and minimum values are:
|
||||
|
||||
size │ lowest │ highest
|
||||
───────┼────────────────┼──────────
|
||||
int8 │ -128 │ 127
|
||||
int16 │ -32,768 │ 32,767
|
||||
int32 │ -2,147,483,648 │ 2,147,483,647
|
||||
int64 │ -9.2 × 10¹⁷ │ 9.2 × 10¹⁷
|
||||
uint8 │ 0 │ 255
|
||||
uint16 │ 0 │ 65535
|
||||
uint32 │ 0 │ 4294967295
|
||||
uint64 │ 0 │ 1.8 × 10¹⁸
|
||||
|
||||
int refers to int32 on 32-bit systems and int64 on 64-bit systems.
|
||||
`
|
||||
|
||||
const usageDuration = `
|
||||
A duration must be as "number<unit>", without any spaces. Valid units are:
|
||||
|
||||
ns nanoseconds (billionth of a second)
|
||||
us, µs microseconds (millionth of a second)
|
||||
ms milliseconds (thousands of a second)
|
||||
s seconds
|
||||
m minutes
|
||||
h hours
|
||||
|
||||
You can combine multiple units; for example "5m10s" for 5 minutes and 10
|
||||
seconds.
|
||||
`
|
||||
@@ -0,0 +1,245 @@
|
||||
//go:build go1.16
|
||||
// +build go1.16
|
||||
|
||||
package toml_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"math"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
tomltest "github.com/BurntSushi/toml/internal/toml-test"
|
||||
)
|
||||
|
||||
func TestErrorPosition(t *testing.T) {
|
||||
// Note: take care to use leading spaces (not tabs).
|
||||
tests := []struct {
|
||||
test, err string
|
||||
}{
|
||||
{"array/missing-separator.toml", `
|
||||
toml: error: expected a comma (',') or array terminator (']'), but got '2'
|
||||
|
||||
At line 1, column 13:
|
||||
|
||||
1 | wrong = [ 1 2 3 ]
|
||||
^`},
|
||||
|
||||
{"array/no-close-2.toml", `
|
||||
toml: error: expected a comma (',') or array terminator (']'), but got end of file
|
||||
|
||||
At line 1, column 10:
|
||||
|
||||
1 | x = [42 #
|
||||
^`},
|
||||
|
||||
{"array/tables-2.toml", `
|
||||
toml: error: Key 'fruit.variety' has already been defined.
|
||||
|
||||
At line 9, column 3-8:
|
||||
|
||||
7 |
|
||||
8 | # This table conflicts with the previous table
|
||||
9 | [fruit.variety]
|
||||
^^^^^`},
|
||||
{"datetime/trailing-t.toml", `
|
||||
toml: error: Invalid TOML Datetime: "2006-01-30T".
|
||||
|
||||
At line 2, column 4-15:
|
||||
|
||||
1 | # Date cannot end with trailing T
|
||||
2 | d = 2006-01-30T
|
||||
^^^^^^^^^^^`},
|
||||
}
|
||||
|
||||
fsys := tomltest.EmbeddedTests()
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.test, func(t *testing.T) {
|
||||
input, err := fs.ReadFile(fsys, "invalid/"+tt.test)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var x interface{}
|
||||
_, err = toml.Decode(string(input), &x)
|
||||
if err == nil {
|
||||
t.Fatal("err is nil")
|
||||
}
|
||||
|
||||
var pErr toml.ParseError
|
||||
if !errors.As(err, &pErr) {
|
||||
t.Errorf("err is not a ParseError: %T %[1]v", err)
|
||||
}
|
||||
|
||||
tt.err = tt.err[1:] + "\n" // Remove first newline, and add trailing.
|
||||
want := pErr.ErrorWithUsage()
|
||||
|
||||
if !strings.Contains(want, tt.err) {
|
||||
t.Fatalf("\nwant:\n%s\nhave:\n%s", tt.err, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseError(t *testing.T) {
|
||||
tests := []struct {
|
||||
in interface{}
|
||||
toml, err string
|
||||
}{
|
||||
{
|
||||
&struct{ Int int8 }{},
|
||||
"Int = 200",
|
||||
`| toml: error: 200 is out of range for int8
|
||||
|
|
||||
| At line 1, column 6-9:
|
||||
|
|
||||
| 1 | Int = 200
|
||||
| ^^^
|
||||
| Error help:
|
||||
|
|
||||
| This number is too large; this may be an error in the TOML, but it can also be a
|
||||
| bug in the program that uses too small of an integer.
|
||||
|
|
||||
| The maximum and minimum values are:
|
||||
|
|
||||
| size │ lowest │ highest
|
||||
| ───────┼────────────────┼──────────
|
||||
| int8 │ -128 │ 127
|
||||
| int16 │ -32,768 │ 32,767
|
||||
| int32 │ -2,147,483,648 │ 2,147,483,647
|
||||
| int64 │ -9.2 × 10¹⁷ │ 9.2 × 10¹⁷
|
||||
| uint8 │ 0 │ 255
|
||||
| uint16 │ 0 │ 65535
|
||||
| uint32 │ 0 │ 4294967295
|
||||
| uint64 │ 0 │ 1.8 × 10¹⁸
|
||||
|
|
||||
| int refers to int32 on 32-bit systems and int64 on 64-bit systems.
|
||||
`,
|
||||
},
|
||||
{
|
||||
&struct{ Int int }{},
|
||||
fmt.Sprintf("Int = %d", uint64(math.MaxInt64+1)),
|
||||
`| toml: error: 9223372036854775808 is out of range for int64
|
||||
|
|
||||
| At line 1, column 6-25:
|
||||
|
|
||||
| 1 | Int = 9223372036854775808
|
||||
| ^^^^^^^^^^^^^^^^^^^
|
||||
| Error help:
|
||||
|
|
||||
| This number is too large; this may be an error in the TOML, but it can also be a
|
||||
| bug in the program that uses too small of an integer.
|
||||
|
|
||||
| The maximum and minimum values are:
|
||||
|
|
||||
| size │ lowest │ highest
|
||||
| ───────┼────────────────┼──────────
|
||||
| int8 │ -128 │ 127
|
||||
| int16 │ -32,768 │ 32,767
|
||||
| int32 │ -2,147,483,648 │ 2,147,483,647
|
||||
| int64 │ -9.2 × 10¹⁷ │ 9.2 × 10¹⁷
|
||||
| uint8 │ 0 │ 255
|
||||
| uint16 │ 0 │ 65535
|
||||
| uint32 │ 0 │ 4294967295
|
||||
| uint64 │ 0 │ 1.8 × 10¹⁸
|
||||
|
|
||||
| int refers to int32 on 32-bit systems and int64 on 64-bit systems.
|
||||
`,
|
||||
},
|
||||
{
|
||||
&struct{ Float float32 }{},
|
||||
"Float = 1.1e99",
|
||||
`
|
||||
| toml: error: 1.1e+99 is out of range for float32
|
||||
|
|
||||
| At line 1, column 8-14:
|
||||
|
|
||||
| 1 | Float = 1.1e99
|
||||
| ^^^^^^
|
||||
| Error help:
|
||||
|
|
||||
| This number is too large; this may be an error in the TOML, but it can also be a
|
||||
| bug in the program that uses too small of an integer.
|
||||
|
|
||||
| The maximum and minimum values are:
|
||||
|
|
||||
| size │ lowest │ highest
|
||||
| ───────┼────────────────┼──────────
|
||||
| int8 │ -128 │ 127
|
||||
| int16 │ -32,768 │ 32,767
|
||||
| int32 │ -2,147,483,648 │ 2,147,483,647
|
||||
| int64 │ -9.2 × 10¹⁷ │ 9.2 × 10¹⁷
|
||||
| uint8 │ 0 │ 255
|
||||
| uint16 │ 0 │ 65535
|
||||
| uint32 │ 0 │ 4294967295
|
||||
| uint64 │ 0 │ 1.8 × 10¹⁸
|
||||
|
|
||||
| int refers to int32 on 32-bit systems and int64 on 64-bit systems.
|
||||
`,
|
||||
},
|
||||
|
||||
{
|
||||
&struct{ D time.Duration }{},
|
||||
`D = "99 bottles"`,
|
||||
`
|
||||
| toml: error: invalid duration: "99 bottles"
|
||||
|
|
||||
| At line 1, column 5-15:
|
||||
|
|
||||
| 1 | D = "99 bottles"
|
||||
| ^^^^^^^^^^
|
||||
| Error help:
|
||||
|
|
||||
| A duration must be as "number<unit>", without any spaces. Valid units are:
|
||||
|
|
||||
| ns nanoseconds (billionth of a second)
|
||||
| us, µs microseconds (millionth of a second)
|
||||
| ms milliseconds (thousands of a second)
|
||||
| s seconds
|
||||
| m minutes
|
||||
| h hours
|
||||
|
|
||||
| You can combine multiple units; for example "5m10s" for 5 minutes and 10
|
||||
| seconds.
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
prep := func(s string) string {
|
||||
lines := strings.Split(strings.TrimSpace(s), "\n")
|
||||
for i := range lines {
|
||||
if j := strings.IndexByte(lines[i], '|'); j >= 0 {
|
||||
lines[i] = lines[i][j+1:]
|
||||
lines[i] = strings.Replace(lines[i], " ", "", 1)
|
||||
}
|
||||
}
|
||||
return strings.Join(lines, "\n") + "\n"
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run("", func(t *testing.T) {
|
||||
_, err := toml.Decode(tt.toml, tt.in)
|
||||
if err == nil {
|
||||
t.Fatalf("err is nil; decoded: %#v", tt.in)
|
||||
}
|
||||
|
||||
var pErr toml.ParseError
|
||||
if !errors.As(err, &pErr) {
|
||||
t.Fatalf("err is not a ParseError: %#v", err)
|
||||
}
|
||||
|
||||
tt.err = prep(tt.err)
|
||||
have := pErr.ErrorWithUsage()
|
||||
|
||||
// have = strings.ReplaceAll(have, " ", "·")
|
||||
// tt.err = strings.ReplaceAll(tt.err, " ", "·")
|
||||
if have != tt.err {
|
||||
t.Fatalf("\nwant:\n%s\nhave:\n%s", tt.err, have)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,387 @@
|
||||
package toml_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/mail"
|
||||
"time"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
)
|
||||
|
||||
func ExampleEncoder_Encode() {
|
||||
var (
|
||||
date, _ = time.Parse(time.RFC822, "14 Mar 10 18:00 UTC")
|
||||
buf = new(bytes.Buffer)
|
||||
)
|
||||
err := toml.NewEncoder(buf).Encode(map[string]interface{}{
|
||||
"date": date,
|
||||
"counts": []int{1, 1, 2, 3, 5, 8},
|
||||
"hash": map[string]string{
|
||||
"key1": "val1",
|
||||
"key2": "val2",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Println(buf.String())
|
||||
|
||||
// Output:
|
||||
// counts = [1, 1, 2, 3, 5, 8]
|
||||
// date = 2010-03-14T18:00:00Z
|
||||
//
|
||||
// [hash]
|
||||
// key1 = "val1"
|
||||
// key2 = "val2"
|
||||
}
|
||||
|
||||
func ExampleMetaData_PrimitiveDecode() {
|
||||
tomlBlob := `
|
||||
ranking = ["Springsteen", "J Geils"]
|
||||
|
||||
[bands.Springsteen]
|
||||
started = 1973
|
||||
albums = ["Greetings", "WIESS", "Born to Run", "Darkness"]
|
||||
|
||||
[bands."J Geils"]
|
||||
started = 1970
|
||||
albums = ["The J. Geils Band", "Full House", "Blow Your Face Out"]
|
||||
`
|
||||
|
||||
type (
|
||||
band struct {
|
||||
Started int
|
||||
Albums []string
|
||||
}
|
||||
classics struct {
|
||||
Ranking []string
|
||||
Bands map[string]toml.Primitive
|
||||
}
|
||||
)
|
||||
|
||||
// Do the initial decode; reflection is delayed on Primitive values.
|
||||
var music classics
|
||||
md, err := toml.Decode(tomlBlob, &music)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// MetaData still includes information on Primitive values.
|
||||
fmt.Printf("Is `bands.Springsteen` defined? %v\n",
|
||||
md.IsDefined("bands", "Springsteen"))
|
||||
|
||||
// Decode primitive data into Go values.
|
||||
for _, artist := range music.Ranking {
|
||||
// A band is a primitive value, so we need to decode it to get a real
|
||||
// `band` value.
|
||||
primValue := music.Bands[artist]
|
||||
|
||||
var aBand band
|
||||
err = md.PrimitiveDecode(primValue, &aBand)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Printf("%s started in %d.\n", artist, aBand.Started)
|
||||
}
|
||||
|
||||
// Check to see if there were any fields left undecoded. Note that this
|
||||
// won't be empty before decoding the Primitive value!
|
||||
fmt.Printf("Undecoded: %q\n", md.Undecoded())
|
||||
|
||||
// Output:
|
||||
// Is `bands.Springsteen` defined? true
|
||||
// Springsteen started in 1973.
|
||||
// J Geils started in 1970.
|
||||
// Undecoded: []
|
||||
}
|
||||
|
||||
func ExampleDecode() {
|
||||
tomlBlob := `
|
||||
# Some comments.
|
||||
[alpha]
|
||||
ip = "10.0.0.1"
|
||||
|
||||
[alpha.config]
|
||||
Ports = [ 8001, 8002 ]
|
||||
Location = "Toronto"
|
||||
Created = 1987-07-05T05:45:00Z
|
||||
|
||||
[beta]
|
||||
ip = "10.0.0.2"
|
||||
|
||||
[beta.config]
|
||||
Ports = [ 9001, 9002 ]
|
||||
Location = "New Jersey"
|
||||
Created = 1887-01-05T05:55:00Z
|
||||
`
|
||||
|
||||
type (
|
||||
serverConfig struct {
|
||||
Ports []int
|
||||
Location string
|
||||
Created time.Time
|
||||
}
|
||||
server struct {
|
||||
IP string `toml:"ip,omitempty"`
|
||||
Config serverConfig `toml:"config"`
|
||||
}
|
||||
servers map[string]server
|
||||
)
|
||||
|
||||
var config servers
|
||||
_, err := toml.Decode(tomlBlob, &config)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
for _, name := range []string{"alpha", "beta"} {
|
||||
s := config[name]
|
||||
fmt.Printf("Server: %s (ip: %s) in %s created on %s\n",
|
||||
name, s.IP, s.Config.Location,
|
||||
s.Config.Created.Format("2006-01-02"))
|
||||
fmt.Printf("Ports: %v\n", s.Config.Ports)
|
||||
}
|
||||
|
||||
// Output:
|
||||
// Server: alpha (ip: 10.0.0.1) in Toronto created on 1987-07-05
|
||||
// Ports: [8001 8002]
|
||||
// Server: beta (ip: 10.0.0.2) in New Jersey created on 1887-01-05
|
||||
// Ports: [9001 9002]
|
||||
}
|
||||
|
||||
type address struct{ *mail.Address }
|
||||
|
||||
func (a *address) UnmarshalText(text []byte) error {
|
||||
var err error
|
||||
a.Address, err = mail.ParseAddress(string(text))
|
||||
return err
|
||||
}
|
||||
|
||||
// Example Unmarshaler shows how to decode TOML strings into your own
|
||||
// custom data type.
|
||||
func Example_unmarshaler() {
|
||||
blob := `
|
||||
contacts = [
|
||||
"Donald Duck <donald@duckburg.com>",
|
||||
"Scrooge McDuck <scrooge@duckburg.com>",
|
||||
]
|
||||
`
|
||||
|
||||
var contacts struct {
|
||||
// Implementation of the address type:
|
||||
//
|
||||
// type address struct{ *mail.Address }
|
||||
//
|
||||
// func (a *address) UnmarshalText(text []byte) error {
|
||||
// var err error
|
||||
// a.Address, err = mail.ParseAddress(string(text))
|
||||
// return err
|
||||
// }
|
||||
|
||||
Contacts []address
|
||||
}
|
||||
|
||||
_, err := toml.Decode(blob, &contacts)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
for _, c := range contacts.Contacts {
|
||||
fmt.Printf("%#v\n", c.Address)
|
||||
}
|
||||
|
||||
// Output:
|
||||
// &mail.Address{Name:"Donald Duck", Address:"donald@duckburg.com"}
|
||||
// &mail.Address{Name:"Scrooge McDuck", Address:"scrooge@duckburg.com"}
|
||||
}
|
||||
|
||||
// Example StrictDecoding shows how to detect if there are keys in the TOML
|
||||
// document that weren't decoded into the value given. This is useful for
|
||||
// returning an error to the user if they've included extraneous fields in their
|
||||
// configuration.
|
||||
func Example_strictDecoding() {
|
||||
var blob = `
|
||||
key1 = "value1"
|
||||
key2 = "value2"
|
||||
key3 = "value3"
|
||||
`
|
||||
|
||||
var conf struct {
|
||||
Key1 string
|
||||
Key3 string
|
||||
}
|
||||
md, err := toml.Decode(blob, &conf)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Printf("Undecoded keys: %q\n", md.Undecoded())
|
||||
// Output:
|
||||
// Undecoded keys: ["key2"]
|
||||
}
|
||||
|
||||
type order struct {
|
||||
// NOTE `order.parts` is a private slice of type `part` which is an
|
||||
// interface and may only be loaded from toml using the UnmarshalTOML()
|
||||
// method of the Umarshaler interface.
|
||||
parts parts
|
||||
}
|
||||
|
||||
type parts []part
|
||||
|
||||
type part interface {
|
||||
Name() string
|
||||
}
|
||||
|
||||
type valve struct {
|
||||
Type string
|
||||
ID string
|
||||
Size float32
|
||||
Rating int
|
||||
}
|
||||
|
||||
func (v *valve) Name() string {
|
||||
return fmt.Sprintf("VALVE: %s", v.ID)
|
||||
}
|
||||
|
||||
type pipe struct {
|
||||
Type string
|
||||
ID string
|
||||
Length float32
|
||||
Diameter int
|
||||
}
|
||||
|
||||
func (p *pipe) Name() string {
|
||||
return fmt.Sprintf("PIPE: %s", p.ID)
|
||||
}
|
||||
|
||||
type cable struct {
|
||||
Type string
|
||||
ID string
|
||||
Length int
|
||||
Rating float32
|
||||
}
|
||||
|
||||
func (c *cable) Name() string {
|
||||
return fmt.Sprintf("CABLE: %s", c.ID)
|
||||
}
|
||||
|
||||
func (o *order) UnmarshalTOML(data interface{}) error {
|
||||
// NOTE the example below contains detailed type casting to show how the
|
||||
// 'data' is retrieved. In operational use, a type cast wrapper may be
|
||||
// preferred e.g.
|
||||
//
|
||||
// func AsMap(v interface{}) (map[string]interface{}, error) {
|
||||
// return v.(map[string]interface{})
|
||||
// }
|
||||
//
|
||||
// resulting in:
|
||||
// d, _ := AsMap(data)
|
||||
//
|
||||
|
||||
d, _ := data.(map[string]interface{})
|
||||
parts, _ := d["parts"].([]map[string]interface{})
|
||||
|
||||
for _, p := range parts {
|
||||
|
||||
typ, _ := p["type"].(string)
|
||||
id, _ := p["id"].(string)
|
||||
|
||||
// detect the type of part and handle each case
|
||||
switch p["type"] {
|
||||
case "valve":
|
||||
|
||||
size := float32(p["size"].(float64))
|
||||
rating := int(p["rating"].(int64))
|
||||
|
||||
valve := &valve{
|
||||
Type: typ,
|
||||
ID: id,
|
||||
Size: size,
|
||||
Rating: rating,
|
||||
}
|
||||
|
||||
o.parts = append(o.parts, valve)
|
||||
|
||||
case "pipe":
|
||||
|
||||
length := float32(p["length"].(float64))
|
||||
diameter := int(p["diameter"].(int64))
|
||||
|
||||
pipe := &pipe{
|
||||
Type: typ,
|
||||
ID: id,
|
||||
Length: length,
|
||||
Diameter: diameter,
|
||||
}
|
||||
|
||||
o.parts = append(o.parts, pipe)
|
||||
|
||||
case "cable":
|
||||
|
||||
length := int(p["length"].(int64))
|
||||
rating := float32(p["rating"].(float64))
|
||||
|
||||
cable := &cable{
|
||||
Type: typ,
|
||||
ID: id,
|
||||
Length: length,
|
||||
Rating: rating,
|
||||
}
|
||||
|
||||
o.parts = append(o.parts, cable)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Example UnmarshalTOML shows how to implement a struct type that knows how to
|
||||
// unmarshal itself. The struct must take full responsibility for mapping the
|
||||
// values passed into the struct. The method may be used with interfaces in a
|
||||
// struct in cases where the actual type is not known until the data is
|
||||
// examined.
|
||||
func Example_unmarshalTOML() {
|
||||
blob := `
|
||||
[[parts]]
|
||||
type = "valve"
|
||||
id = "valve-1"
|
||||
size = 1.2
|
||||
rating = 4
|
||||
|
||||
[[parts]]
|
||||
type = "valve"
|
||||
id = "valve-2"
|
||||
size = 2.1
|
||||
rating = 5
|
||||
|
||||
[[parts]]
|
||||
type = "pipe"
|
||||
id = "pipe-1"
|
||||
length = 2.1
|
||||
diameter = 12
|
||||
|
||||
[[parts]]
|
||||
type = "cable"
|
||||
id = "cable-1"
|
||||
length = 12
|
||||
rating = 3.1
|
||||
`
|
||||
|
||||
// See example_test.go in the source for the implementation of the order
|
||||
// type.
|
||||
o := &order{}
|
||||
|
||||
err := toml.Unmarshal([]byte(blob), o)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Println(len(o.parts))
|
||||
for _, part := range o.parts {
|
||||
fmt.Println(part.Name())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
//go:build go1.18
|
||||
// +build go1.18
|
||||
|
||||
package toml
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func FuzzDecode(f *testing.F) {
|
||||
buf := make([]byte, 0, 2048)
|
||||
|
||||
f.Add(`
|
||||
# This is an example TOML document which shows most of its features.
|
||||
|
||||
# Simple key/value with a string.
|
||||
title = "TOML example \U0001F60A"
|
||||
|
||||
desc = """
|
||||
An example TOML document. \
|
||||
"""
|
||||
|
||||
# Array with integers and floats in the various allowed formats.
|
||||
integers = [42, 0x42, 0o42, 0b0110]
|
||||
floats = [1.42, 1e-02]
|
||||
|
||||
# Array with supported datetime formats.
|
||||
times = [
|
||||
2021-11-09T15:16:17+01:00, # datetime with timezone.
|
||||
2021-11-09T15:16:17Z, # UTC datetime.
|
||||
2021-11-09T15:16:17, # local datetime.
|
||||
2021-11-09, # local date.
|
||||
15:16:17, # local time.
|
||||
]
|
||||
|
||||
# Durations.
|
||||
duration = ["4m49s", "8m03s", "1231h15m55s"]
|
||||
|
||||
# Table with inline tables.
|
||||
distros = [
|
||||
{name = "Arch Linux", packages = "pacman"},
|
||||
{name = "Void Linux", packages = "xbps"},
|
||||
{name = "Debian", packages = "apt"},
|
||||
]
|
||||
|
||||
# Create new table; note the "servers" table is created implicitly.
|
||||
[servers.alpha]
|
||||
# You can indent as you please, tabs or spaces.
|
||||
ip = '10.0.0.1'
|
||||
hostname = 'server1'
|
||||
enabled = false
|
||||
[servers.beta]
|
||||
ip = '10.0.0.2'
|
||||
hostname = 'server2'
|
||||
enabled = true
|
||||
|
||||
# Start a new table array; note that the "characters" table is created implicitly.
|
||||
[[characters.star-trek]]
|
||||
name = "James Kirk"
|
||||
rank = "Captain\u0012 \t"
|
||||
[[characters.star-trek]]
|
||||
name = "Spock"
|
||||
rank = "Science officer"
|
||||
|
||||
[undecoded] # To show the MetaData.Undecoded() feature.
|
||||
key = "This table intentionally left undecoded"
|
||||
`)
|
||||
f.Fuzz(func(t *testing.T, file string) {
|
||||
var m map[string]interface{}
|
||||
_, err := Decode(file, &m)
|
||||
if err != nil {
|
||||
t.Skip()
|
||||
}
|
||||
|
||||
NewEncoder(bytes.NewBuffer(buf)).Encode(m)
|
||||
|
||||
// TODO: should check if the output is equal to the input, too, but some
|
||||
// information is lost when encoding.
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
module github.com/BurntSushi/toml
|
||||
|
||||
go 1.16
|
||||
@@ -0,0 +1,76 @@
|
||||
package tag
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"github.com/BurntSushi/toml/internal"
|
||||
)
|
||||
|
||||
// Add JSON tags to a data structure as expected by toml-test.
|
||||
func Add(key string, tomlData interface{}) interface{} {
|
||||
// Switch on the data type.
|
||||
switch orig := tomlData.(type) {
|
||||
default:
|
||||
panic(fmt.Sprintf("Unknown type: %T", tomlData))
|
||||
|
||||
// A table: we don't need to add any tags, just recurse for every table
|
||||
// entry.
|
||||
case map[string]interface{}:
|
||||
typed := make(map[string]interface{}, len(orig))
|
||||
for k, v := range orig {
|
||||
typed[k] = Add(k, v)
|
||||
}
|
||||
return typed
|
||||
|
||||
// An array: we don't need to add any tags, just recurse for every table
|
||||
// entry.
|
||||
case []map[string]interface{}:
|
||||
typed := make([]map[string]interface{}, len(orig))
|
||||
for i, v := range orig {
|
||||
typed[i] = Add("", v).(map[string]interface{})
|
||||
}
|
||||
return typed
|
||||
case []interface{}:
|
||||
typed := make([]interface{}, len(orig))
|
||||
for i, v := range orig {
|
||||
typed[i] = Add("", v)
|
||||
}
|
||||
return typed
|
||||
|
||||
// Datetime: tag as datetime.
|
||||
case time.Time:
|
||||
switch orig.Location() {
|
||||
default:
|
||||
return tag("datetime", orig.Format("2006-01-02T15:04:05.999999999Z07:00"))
|
||||
case internal.LocalDatetime:
|
||||
return tag("datetime-local", orig.Format("2006-01-02T15:04:05.999999999"))
|
||||
case internal.LocalDate:
|
||||
return tag("date-local", orig.Format("2006-01-02"))
|
||||
case internal.LocalTime:
|
||||
return tag("time-local", orig.Format("15:04:05.999999999"))
|
||||
}
|
||||
|
||||
// Tag primitive values: bool, string, int, and float64.
|
||||
case bool:
|
||||
return tag("bool", fmt.Sprintf("%v", orig))
|
||||
case string:
|
||||
return tag("string", orig)
|
||||
case int64:
|
||||
return tag("integer", fmt.Sprintf("%d", orig))
|
||||
case float64:
|
||||
// Special case for nan since NaN == NaN is false.
|
||||
if math.IsNaN(orig) {
|
||||
return tag("float", "nan")
|
||||
}
|
||||
return tag("float", fmt.Sprintf("%v", orig))
|
||||
}
|
||||
}
|
||||
|
||||
func tag(typeName string, data interface{}) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"type": typeName,
|
||||
"value": data,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package tag
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/BurntSushi/toml/internal"
|
||||
)
|
||||
|
||||
// Remove JSON tags to a data structure as returned by toml-test.
|
||||
func Remove(typedJson interface{}) (interface{}, error) {
|
||||
// Switch on the data type.
|
||||
switch v := typedJson.(type) {
|
||||
|
||||
// Object: this can either be a TOML table or a primitive with tags.
|
||||
case map[string]interface{}:
|
||||
// This value represents a primitive: remove the tags and return just
|
||||
// the primitive value.
|
||||
if len(v) == 2 && in("type", v) && in("value", v) {
|
||||
ut, err := untag(v)
|
||||
if err != nil {
|
||||
return ut, fmt.Errorf("tag.Remove: %w", err)
|
||||
}
|
||||
return ut, nil
|
||||
}
|
||||
|
||||
// Table: remove tags on all children.
|
||||
m := make(map[string]interface{}, len(v))
|
||||
for k, v2 := range v {
|
||||
var err error
|
||||
m[k], err = Remove(v2)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
|
||||
// Array: remove tags from all items.
|
||||
case []interface{}:
|
||||
a := make([]interface{}, len(v))
|
||||
for i := range v {
|
||||
var err error
|
||||
a[i], err = Remove(v[i])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return a, nil
|
||||
}
|
||||
|
||||
// The top level must be an object or array.
|
||||
return nil, fmt.Errorf("tag.Remove: unrecognized JSON format '%T'", typedJson)
|
||||
}
|
||||
|
||||
// Check if key is in the table m.
|
||||
func in(key string, m map[string]interface{}) bool {
|
||||
_, ok := m[key]
|
||||
return ok
|
||||
}
|
||||
|
||||
// Return a primitive: read the "type" and convert the "value" to that.
|
||||
func untag(typed map[string]interface{}) (interface{}, error) {
|
||||
t := typed["type"].(string)
|
||||
v := typed["value"].(string)
|
||||
switch t {
|
||||
case "string":
|
||||
return v, nil
|
||||
case "integer":
|
||||
n, err := strconv.ParseInt(v, 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("untag: %w", err)
|
||||
}
|
||||
return n, nil
|
||||
case "float":
|
||||
f, err := strconv.ParseFloat(v, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("untag: %w", err)
|
||||
}
|
||||
return f, nil
|
||||
case "datetime":
|
||||
return parseTime(v, "2006-01-02T15:04:05.999999999Z07:00", nil)
|
||||
case "datetime-local":
|
||||
return parseTime(v, "2006-01-02T15:04:05.999999999", internal.LocalDatetime)
|
||||
case "date-local":
|
||||
return parseTime(v, "2006-01-02", internal.LocalDate)
|
||||
case "time-local":
|
||||
return parseTime(v, "15:04:05.999999999", internal.LocalTime)
|
||||
case "bool":
|
||||
switch v {
|
||||
case "true":
|
||||
return true, nil
|
||||
case "false":
|
||||
return false, nil
|
||||
}
|
||||
return nil, fmt.Errorf("untag: could not parse %q as a boolean", v)
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("untag: unrecognized tag type %q", t)
|
||||
}
|
||||
|
||||
func parseTime(v, format string, l *time.Location) (time.Time, error) {
|
||||
t, err := time.Parse(format, v)
|
||||
if err != nil {
|
||||
return time.Time{}, fmt.Errorf("could not parse %q as a datetime: %w", v, err)
|
||||
}
|
||||
if l != nil {
|
||||
t = t.In(l)
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
build_flags="-trimpath -ldflags '-w -s -X \"zgo.at/zli.version=$tag$commit_info\" -X \"zgo.at/zli.progname=toml-test\"' ./cmd/toml-test"
|
||||
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018 TOML authors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
@@ -0,0 +1,271 @@
|
||||
`toml-test` is a language-agnostic test suite to verify the correctness of
|
||||
[TOML][t] parsers and writers.
|
||||
|
||||
Tests are divided into two groups: "invalid" and "valid". Decoders or encoders
|
||||
that reject "invalid" tests pass the tests, and decoders that accept "valid"
|
||||
tests and output precisely what is expected pass the tests. The output format is
|
||||
JSON, described below.
|
||||
|
||||
Both encoders and decoders share valid tests, except an encoder accepts JSON and
|
||||
outputs TOML rather than the reverse. The TOML representations are read with a
|
||||
blessed decoder and is compared. Encoders have their own set of invalid tests in
|
||||
the invalid-encoder directory. The JSON given to a TOML encoder is in the same
|
||||
format as the JSON that a TOML decoder should output.
|
||||
|
||||
Compatible with TOML version [v1.0.0][v1].
|
||||
|
||||
[t]: https://toml.io
|
||||
[v1]: https://toml.io/en/v1.0.0
|
||||
|
||||
Installation
|
||||
------------
|
||||
There are binaries on the [release page][r]; these are statically compiled and
|
||||
should run in most environments. It's recommended you use a binary, or a tagged
|
||||
release if you build from source especially in CI environments. This prevents
|
||||
your tests from breaking on changes to tests in this tool.
|
||||
|
||||
To compile from source you will need Go 1.16 or newer (older versions will *not*
|
||||
work):
|
||||
|
||||
$ git clone https://github.com/BurntSushi/toml-test.git
|
||||
$ cd toml-test
|
||||
$ go build ./cmd/toml-test
|
||||
|
||||
This will build a `./toml-test` binary.
|
||||
|
||||
[r]: https://github.com/BurntSushi/toml-test/releases
|
||||
|
||||
Usage
|
||||
-----
|
||||
`toml-test` accepts an encoder or decoder as the first positional argument, for
|
||||
example:
|
||||
|
||||
$ toml-test my-toml-decoder
|
||||
$ toml-test my-toml-encoder -encoder
|
||||
|
||||
The `-encoder` flag is used to signal that this is an encoder rather than a
|
||||
decoder.
|
||||
|
||||
For example, to run the tests against the Go TOML library:
|
||||
|
||||
# Install my parser
|
||||
$ go install github.com/BurntSushi/toml/cmd/toml-test-decoder@master
|
||||
$ go install github.com/BurntSushi/toml/cmd/toml-test-encoder@master
|
||||
|
||||
$ toml-test toml-test-decoder
|
||||
toml-test [toml-test-decoder]: using embeded tests: 278 passed
|
||||
|
||||
$ toml-test -encoder toml-test-encoder
|
||||
toml-test [toml-test-encoder]: using embeded tests: 94 passed, 0 failed
|
||||
|
||||
The default is to use the tests compiled in the binary; you can use `-testdir`
|
||||
to load tests from the filesystem. You can use `-run [name]` or `-skip [name]`
|
||||
to run or skip specific tests. Both flags can be given more than once and accept
|
||||
glob patterns: `-run 'valid/string/*'`.
|
||||
|
||||
See `toml-test -help` for detailed usage.
|
||||
|
||||
### Implementing a decoder
|
||||
For your decoder to be compatible with `toml-test` it **must** satisfy the
|
||||
expected interface:
|
||||
|
||||
- Your decoder **must** accept TOML data on `stdin` until EOF.
|
||||
- If the TOML data is invalid, your decoder **must** return with a non-zero
|
||||
exit, code indicating an error.
|
||||
- If the TOML data is valid, your decoder **must** output a JSON encoding of
|
||||
that data on `stdout` and return with a zero exit code indicating success.
|
||||
|
||||
An example in pseudocode:
|
||||
|
||||
toml_data = read_stdin()
|
||||
|
||||
parsed_toml = decode_toml(toml_data)
|
||||
|
||||
if error_parsing_toml():
|
||||
print_error_to_stderr()
|
||||
exit(1)
|
||||
|
||||
print_as_tagged_json(parsed_toml)
|
||||
exit(0)
|
||||
|
||||
Details on the tagged JSON is explained below in "JSON encoding".
|
||||
|
||||
### Implementing an encoder
|
||||
For your encoder to be compatible with `toml-test`, it **must** satisfy the
|
||||
expected interface:
|
||||
|
||||
- Your encoder **must** accept JSON data on `stdin` until EOF.
|
||||
- If the JSON data cannot be converted to a valid TOML representation, your
|
||||
encoder **must** return with a non-zero exit code indicating an error.
|
||||
- If the JSON data can be converted to a valid TOML representation, your encoder
|
||||
**must** output a TOML encoding of that data on `stdout` and return with a
|
||||
zero exit code indicating success.
|
||||
|
||||
An example in pseudocode:
|
||||
|
||||
json_data = read_stdin()
|
||||
|
||||
parsed_json_with_tags = decode_json(json_data)
|
||||
|
||||
if error_parsing_json():
|
||||
print_error_to_stderr()
|
||||
exit(1)
|
||||
|
||||
print_as_toml(parsed_json_with_tags)
|
||||
exit(0)
|
||||
|
||||
JSON encoding
|
||||
-------------
|
||||
The following JSON encoding applies equally to both encoders and decoders:
|
||||
|
||||
- TOML tables correspond to JSON objects.
|
||||
- TOML table arrays correspond to JSON arrays.
|
||||
- TOML values correspond to a special JSON object of the form:
|
||||
`{"type": "{TTYPE}", "value": {TVALUE}}`
|
||||
|
||||
In the above, `TTYPE` may be one of:
|
||||
|
||||
- string
|
||||
- integer
|
||||
- float
|
||||
- bool
|
||||
- datetime
|
||||
- datetime-local
|
||||
- date-local
|
||||
- time-local
|
||||
|
||||
`TVALUE` is always a JSON string.
|
||||
|
||||
Empty hashes correspond to empty JSON objects (`{}`) and empty arrays correspond
|
||||
to empty JSON arrays (`[]`).
|
||||
|
||||
Offset datetimes should be encoded in RFC 3339; Local datetimes should be
|
||||
encoded following RFC 3339 without the offset part. Local dates should be
|
||||
encoded as the date part of RFC 3339 and Local times as the time part.
|
||||
|
||||
Examples:
|
||||
|
||||
TOML JSON
|
||||
|
||||
a = 42 {"type": "integer": "value": "42}
|
||||
|
||||
---
|
||||
|
||||
[tbl] {"tbl": {
|
||||
a = 42 "a": {"type": "integer": "value": "42}
|
||||
}}
|
||||
|
||||
---
|
||||
|
||||
a = ["a", 2] {"a": [
|
||||
{"type": "string", "value": "1"},
|
||||
{"type: "integer": "value": "2"}
|
||||
]}
|
||||
|
||||
Or a more complex example:
|
||||
|
||||
```toml
|
||||
best-day-ever = 1987-07-05T17:45:00Z
|
||||
|
||||
[numtheory]
|
||||
boring = false
|
||||
perfection = [6, 28, 496]
|
||||
```
|
||||
|
||||
And the JSON encoding expected by `toml-test` is:
|
||||
|
||||
```json
|
||||
{
|
||||
"best-day-ever": {"type": "datetime", "value": "1987-07-05T17:45:00Z"},
|
||||
"numtheory": {
|
||||
"boring": {"type": "bool", "value": "false"},
|
||||
"perfection": [
|
||||
{"type": "integer", "value": "6"},
|
||||
{"type": "integer", "value": "28"},
|
||||
{"type": "integer", "value": "496"}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Note that the only JSON values ever used are objects, arrays and strings.
|
||||
|
||||
An example implementation can be found in the BurnSushi/toml:
|
||||
|
||||
- [Add tags](https://github.com/BurntSushi/toml/blob/master/internal/tag/add.go)
|
||||
- [Remove tags](https://github.com/BurntSushi/toml/blob/master/internal/tag/rm.go)
|
||||
|
||||
Implementation-defined behaviour
|
||||
--------------------------------
|
||||
This only tests behaviour that's should be true for every encoder implementing
|
||||
TOML; a few things are left up to implementations, and are not tested here.
|
||||
|
||||
- Millisecond precision (4 digits) is required for datetimes and times, and
|
||||
further precision is implementation-specific, and any greater precision than
|
||||
is supported must be truncated (not rounded).
|
||||
|
||||
This tests only millisecond precision, and not any further precision or the
|
||||
truncation of it.
|
||||
|
||||
|
||||
Assumptions of Truth
|
||||
--------------------
|
||||
The following are taken as ground truths by `toml-test`:
|
||||
|
||||
- All tests classified as `invalid` **are** invalid.
|
||||
- All tests classified as `valid` **are** valid.
|
||||
- All expected outputs in `valid/test-name.json` are exactly correct.
|
||||
- The Go standard library package `encoding/json` decodes JSON correctly.
|
||||
- When testing encoders, the TOML decoder at
|
||||
[BurntSushi/toml](https://github.com/BurntSushi/toml) is assumed to be
|
||||
correct. (Note that this assumption is not made when testing decoders!)
|
||||
|
||||
Of particular note is that **no TOML decoder** is taken as ground truth when
|
||||
testing decoders. This means that most changes to the spec will only require an
|
||||
update of the tests in `toml-test`. (Bigger changes may require an adjustment of
|
||||
how two things are considered equal. Particularly if a new type of data is
|
||||
added.) Obviously, this advantage does not apply to testing TOML encoders since
|
||||
there must exist a TOML decoder that conforms to the specification in order to
|
||||
read the output of a TOML encoder.
|
||||
|
||||
Adding tests
|
||||
------------
|
||||
`toml-test` was designed so that tests can be easily added and removed. As
|
||||
mentioned above, tests are split into two groups: invalid and valid tests.
|
||||
|
||||
Invalid tests **only check if a decoder rejects invalid TOML data**. Or, in the
|
||||
case of testing encoders, invalid tests **only check if an encoder rejects an
|
||||
invalid representation of TOML** (e.g., a hetergeneous array). Therefore, all
|
||||
invalid tests should try to **test one thing and one thing only**. Invalid tests
|
||||
should be named after the fault it is trying to expose. Invalid tests for
|
||||
decoders are in the `tests/invalid` directory while invalid tests for encoders
|
||||
are in the `tests/invalid-encoder` directory.
|
||||
|
||||
Valid tests check that a decoder accepts valid TOML data **and** that the parser
|
||||
has the correct representation of the TOML data. Therefore, valid tests need a
|
||||
JSON encoding in addition to the TOML data. The tests should be small enough
|
||||
that writing the JSON encoding by hand will not give you brain damage. The exact
|
||||
reverse is true when testing encoders.
|
||||
|
||||
A valid test without either a `.json` or `.toml` file will automatically fail.
|
||||
|
||||
If you have tests that you'd like to add, please submit a pull request.
|
||||
|
||||
Why JSON?
|
||||
---------
|
||||
In order for a language agnostic test suite to work, we need some kind of data
|
||||
exchange format. TOML cannot be used, as it would imply that a particular parser
|
||||
has a blessing of correctness.
|
||||
|
||||
My decision to use JSON was not a careful one. It was based on expediency. The
|
||||
Go standard library has an excellent `encoding/json` package built in, which
|
||||
made it easy to compare JSON data.
|
||||
|
||||
The problem with JSON is that the types in TOML are not in one-to-one
|
||||
correspondence with JSON. This is why every TOML value represented in JSON is
|
||||
tagged with a type annotation, as described above.
|
||||
|
||||
YAML may be closer in correspondence with TOML, but I don't believe we should
|
||||
rely on that correspondence. Making things explicit with JSON means that writing
|
||||
tests is a little more cumbersome, but it also reduces the number of assumptions
|
||||
we need to make.
|
||||
@@ -0,0 +1,258 @@
|
||||
//go:build go1.16
|
||||
// +build go1.16
|
||||
|
||||
package tomltest
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// CompareJSON compares the given arguments.
|
||||
//
|
||||
// The returned value is a copy of Test with Failure set to a (human-readable)
|
||||
// description of the first element that is unequal. If both arguments are
|
||||
// equal, Test is returned unchanged.
|
||||
//
|
||||
// reflect.DeepEqual could work here, but it won't tell us how the two
|
||||
// structures are different.
|
||||
func (r Test) CompareJSON(want, have interface{}) Test {
|
||||
switch w := want.(type) {
|
||||
case map[string]interface{}:
|
||||
return r.cmpJSONMaps(w, have)
|
||||
case []interface{}:
|
||||
return r.cmpJSONArrays(w, have)
|
||||
default:
|
||||
return r.fail(
|
||||
"Key '%s' in expected output should be a map or a list of maps, but it's a %T",
|
||||
r.Key, want)
|
||||
}
|
||||
}
|
||||
|
||||
func (r Test) cmpJSONMaps(want map[string]interface{}, have interface{}) Test {
|
||||
haveMap, ok := have.(map[string]interface{})
|
||||
if !ok {
|
||||
return r.mismatch("table", want, haveMap)
|
||||
}
|
||||
|
||||
// Check to make sure both or neither are values.
|
||||
if isValue(want) && !isValue(haveMap) {
|
||||
return r.fail(
|
||||
"Key '%s' is supposed to be a value, but the parser reports it as a table",
|
||||
r.Key)
|
||||
}
|
||||
if !isValue(want) && isValue(haveMap) {
|
||||
return r.fail(
|
||||
"Key '%s' is supposed to be a table, but the parser reports it as a value",
|
||||
r.Key)
|
||||
}
|
||||
if isValue(want) && isValue(haveMap) {
|
||||
return r.cmpJSONValues(want, haveMap)
|
||||
}
|
||||
|
||||
// Check that the keys of each map are equivalent.
|
||||
for k := range want {
|
||||
if _, ok := haveMap[k]; !ok {
|
||||
bunk := r.kjoin(k)
|
||||
return bunk.fail("Could not find key '%s' in parser output.",
|
||||
bunk.Key)
|
||||
}
|
||||
}
|
||||
for k := range haveMap {
|
||||
if _, ok := want[k]; !ok {
|
||||
bunk := r.kjoin(k)
|
||||
return bunk.fail("Could not find key '%s' in expected output.",
|
||||
bunk.Key)
|
||||
}
|
||||
}
|
||||
|
||||
// Okay, now make sure that each value is equivalent.
|
||||
for k := range want {
|
||||
if sub := r.kjoin(k).CompareJSON(want[k], haveMap[k]); sub.Failed() {
|
||||
return sub
|
||||
}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func (r Test) cmpJSONArrays(want, have interface{}) Test {
|
||||
wantSlice, ok := want.([]interface{})
|
||||
if !ok {
|
||||
return r.bug("'value' should be a JSON array when 'type=array', but it is a %T", want)
|
||||
}
|
||||
|
||||
haveSlice, ok := have.([]interface{})
|
||||
if !ok {
|
||||
return r.fail(
|
||||
"Malformed output from your encoder: 'value' is not a JSON array: %T", have)
|
||||
}
|
||||
|
||||
if len(wantSlice) != len(haveSlice) {
|
||||
return r.fail("Array lengths differ for key '%s':\n"+
|
||||
" Expected: %d\n"+
|
||||
" Your encoder: %d",
|
||||
r.Key, len(wantSlice), len(haveSlice))
|
||||
}
|
||||
for i := 0; i < len(wantSlice); i++ {
|
||||
if sub := r.CompareJSON(wantSlice[i], haveSlice[i]); sub.Failed() {
|
||||
return sub
|
||||
}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func (r Test) cmpJSONValues(want, have map[string]interface{}) Test {
|
||||
wantType, ok := want["type"].(string)
|
||||
if !ok {
|
||||
return r.bug("'type' should be a string, but it is a %T", want["type"])
|
||||
}
|
||||
|
||||
haveType, ok := have["type"].(string)
|
||||
if !ok {
|
||||
return r.fail("Malformed output from your encoder: 'type' is not a string: %T", have["type"])
|
||||
}
|
||||
|
||||
if wantType != haveType {
|
||||
return r.valMismatch(wantType, haveType, want, have)
|
||||
}
|
||||
|
||||
// If this is an array, then we've got to do some work to check equality.
|
||||
if wantType == "array" {
|
||||
return r.cmpJSONArrays(want, have)
|
||||
}
|
||||
|
||||
// Atomic values are always strings
|
||||
wantVal, ok := want["value"].(string)
|
||||
if !ok {
|
||||
return r.bug("'value' %v should be a string, but it is a %[1]T", want["value"])
|
||||
}
|
||||
|
||||
haveVal, ok := have["value"].(string)
|
||||
if !ok {
|
||||
return r.fail("Malformed output from your encoder: %T is not a string", have["value"])
|
||||
}
|
||||
|
||||
// Excepting floats and datetimes, other values can be compared as strings.
|
||||
switch wantType {
|
||||
case "float":
|
||||
return r.cmpFloats(wantVal, haveVal)
|
||||
case "datetime", "datetime-local", "date-local", "time-local":
|
||||
return r.cmpAsDatetimes(wantType, wantVal, haveVal)
|
||||
default:
|
||||
return r.cmpAsStrings(wantVal, haveVal)
|
||||
}
|
||||
}
|
||||
|
||||
func (r Test) cmpAsStrings(want, have string) Test {
|
||||
if want != have {
|
||||
return r.fail("Values for key '%s' don't match:\n"+
|
||||
" Expected: %s\n"+
|
||||
" Your encoder: %s",
|
||||
r.Key, want, have)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func (r Test) cmpFloats(want, have string) Test {
|
||||
// Special case for NaN, since NaN != NaN.
|
||||
if strings.HasSuffix(want, "nan") || strings.HasSuffix(have, "nan") {
|
||||
if want != have {
|
||||
return r.fail("Values for key '%s' don't match:\n"+
|
||||
" Expected: %v\n"+
|
||||
" Your encoder: %v",
|
||||
r.Key, want, have)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
wantF, err := strconv.ParseFloat(want, 64)
|
||||
if err != nil {
|
||||
return r.bug("Could not read '%s' as a float value for key '%s'", want, r.Key)
|
||||
}
|
||||
|
||||
haveF, err := strconv.ParseFloat(have, 64)
|
||||
if err != nil {
|
||||
return r.fail("Malformed output from your encoder: key '%s' is not a float: '%s'", r.Key, have)
|
||||
}
|
||||
|
||||
if wantF != haveF {
|
||||
return r.fail("Values for key '%s' don't match:\n"+
|
||||
" Expected: %v\n"+
|
||||
" Your encoder: %v",
|
||||
r.Key, wantF, haveF)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
var datetimeRepl = strings.NewReplacer(
|
||||
" ", "T",
|
||||
"t", "T",
|
||||
"z", "Z")
|
||||
|
||||
var layouts = map[string]string{
|
||||
"datetime": time.RFC3339Nano,
|
||||
"datetime-local": "2006-01-02T15:04:05.999999999",
|
||||
"date-local": "2006-01-02",
|
||||
"time-local": "15:04:05",
|
||||
}
|
||||
|
||||
func (r Test) cmpAsDatetimes(kind, want, have string) Test {
|
||||
layout, ok := layouts[kind]
|
||||
if !ok {
|
||||
panic("should never happen")
|
||||
}
|
||||
|
||||
wantT, err := time.Parse(layout, datetimeRepl.Replace(want))
|
||||
if err != nil {
|
||||
return r.bug("Could not read '%s' as a datetime value for key '%s'", want, r.Key)
|
||||
}
|
||||
|
||||
haveT, err := time.Parse(layout, datetimeRepl.Replace(want))
|
||||
if err != nil {
|
||||
return r.fail("Malformed output from your encoder: key '%s' is not a datetime: '%s'", r.Key, have)
|
||||
}
|
||||
if !wantT.Equal(haveT) {
|
||||
return r.fail("Values for key '%s' don't match:\n"+
|
||||
" Expected: %v\n"+
|
||||
" Your encoder: %v",
|
||||
r.Key, wantT, haveT)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func (r Test) kjoin(key string) Test {
|
||||
if len(r.Key) == 0 {
|
||||
r.Key = key
|
||||
} else {
|
||||
r.Key += "." + key
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func isValue(m map[string]interface{}) bool {
|
||||
if len(m) != 2 {
|
||||
return false
|
||||
}
|
||||
if _, ok := m["type"]; !ok {
|
||||
return false
|
||||
}
|
||||
if _, ok := m["value"]; !ok {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (r Test) mismatch(wantType string, want, have interface{}) Test {
|
||||
return r.fail("Key '%s' is not an %s but %[4]T:\n"+
|
||||
" Expected: %#[3]v\n"+
|
||||
" Your encoder: %#[4]v",
|
||||
r.Key, wantType, want, have)
|
||||
}
|
||||
|
||||
func (r Test) valMismatch(wantType, haveType string, want, have interface{}) Test {
|
||||
return r.fail("Key '%s' is not an %s but %s:\n"+
|
||||
" Expected: %#[3]v\n"+
|
||||
" Your encoder: %#[4]v",
|
||||
r.Key, wantType, want, have)
|
||||
}
|
||||
@@ -0,0 +1,427 @@
|
||||
//go:generate ./gen-multi.py
|
||||
|
||||
//go:build go1.16
|
||||
// +build go1.16
|
||||
|
||||
package tomltest
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
)
|
||||
|
||||
type testType uint8
|
||||
|
||||
const (
|
||||
TypeValid testType = iota
|
||||
TypeInvalid
|
||||
)
|
||||
|
||||
//go:embed tests/*
|
||||
var embeddedTests embed.FS
|
||||
|
||||
// EmbeddedTests are the tests embedded in toml-test, rooted to the "test/"
|
||||
// directory.
|
||||
func EmbeddedTests() fs.FS {
|
||||
f, err := fs.Sub(embeddedTests, "tests")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
// Runner runs a set of tests.
|
||||
//
|
||||
// The validity of the parameters is not checked extensively; the caller should
|
||||
// verify this if need be. See ./cmd/toml-test for an example.
|
||||
type Runner struct {
|
||||
Files fs.FS // Test files.
|
||||
Encoder bool // Are we testing an encoder?
|
||||
RunTests []string // Tests to run; run all if blank.
|
||||
SkipTests []string // Tests to skip.
|
||||
Parser Parser // Send data to a parser.
|
||||
Version string // TOML version to run tests for.
|
||||
}
|
||||
|
||||
// A Parser instance is used to call the TOML parser we test.
|
||||
//
|
||||
// By default this is done through an external command.
|
||||
type Parser interface {
|
||||
// Encode a JSON string to TOML.
|
||||
//
|
||||
// The output is the TOML string; if outputIsError is true then it's assumed
|
||||
// that an encoding error occurred.
|
||||
//
|
||||
// An error return should only be used in case an unrecoverable error
|
||||
// occurred; failing to encode to TOML is not an error, but the encoder
|
||||
// unexpectedly panicking is.
|
||||
Encode(jsonInput string) (output string, outputIsError bool, err error)
|
||||
|
||||
// Decode a TOML string to JSON. The same semantics as Encode apply.
|
||||
Decode(tomlInput string) (output string, outputIsError bool, err error)
|
||||
}
|
||||
|
||||
// CommandParser calls an external command.
|
||||
type CommandParser struct {
|
||||
fsys fs.FS
|
||||
cmd []string
|
||||
}
|
||||
|
||||
// Tests are tests to run.
|
||||
type Tests struct {
|
||||
Tests []Test
|
||||
|
||||
// Set when test are run.
|
||||
|
||||
Skipped, Passed, Failed int
|
||||
}
|
||||
|
||||
// Result is the result of a single test.
|
||||
type Test struct {
|
||||
Path string // Path of test, e.g. "valid/string-test"
|
||||
|
||||
// Set when a test is run.
|
||||
|
||||
Skipped bool // Skipped this test?
|
||||
Failure string // Failure message.
|
||||
Key string // TOML key the failure occured on; may be blank.
|
||||
Encoder bool // Encoder test?
|
||||
Input string // The test case that we sent to the external program.
|
||||
Output string // Output from the external program.
|
||||
Want string // The output we want.
|
||||
OutputFromStderr bool // The Output came from stderr, not stdout.
|
||||
}
|
||||
|
||||
// List all tests in Files for the current TOML version.
|
||||
func (r Runner) List() ([]string, error) {
|
||||
if r.Version == "" {
|
||||
r.Version = "1.0.0"
|
||||
}
|
||||
if _, ok := versions[r.Version]; !ok {
|
||||
v := make([]string, 0, len(versions))
|
||||
for k := range versions {
|
||||
v = append(v, k)
|
||||
}
|
||||
sort.Strings(v)
|
||||
return nil, fmt.Errorf("tomltest.Runner.Run: unknown version: %q (supported: \"%s\")",
|
||||
r.Version, strings.Join(v, `", "`))
|
||||
}
|
||||
|
||||
var (
|
||||
v = versions[r.Version]
|
||||
exclude = make([]string, 0, 8)
|
||||
)
|
||||
for {
|
||||
exclude = append(exclude, v.exclude...)
|
||||
if v.inherit == "" {
|
||||
break
|
||||
}
|
||||
v = versions[v.inherit]
|
||||
}
|
||||
|
||||
ls := make([]string, 0, 256)
|
||||
if err := r.findTOML("valid", &ls, exclude); err != nil {
|
||||
return nil, fmt.Errorf("reading 'valid/' dir: %w", err)
|
||||
}
|
||||
|
||||
d := "invalid" + map[bool]string{true: "-encoder", false: ""}[r.Encoder]
|
||||
if err := r.findTOML(d, &ls, exclude); err != nil {
|
||||
return nil, fmt.Errorf("reading %q dir: %w", d, err)
|
||||
}
|
||||
|
||||
return ls, nil
|
||||
}
|
||||
|
||||
// Run all tests listed in t.RunTests.
|
||||
//
|
||||
// TODO: give option to:
|
||||
// - Run all tests with \n replaced with \r\n
|
||||
// - Run all tests with EOL removed
|
||||
// - Run all tests with '# comment' appended to every line.
|
||||
func (r Runner) Run() (Tests, error) {
|
||||
skipped, err := r.findTests()
|
||||
if err != nil {
|
||||
return Tests{}, fmt.Errorf("tomltest.Runner.Run: %w", err)
|
||||
}
|
||||
|
||||
tests := Tests{Tests: make([]Test, 0, len(r.RunTests)), Skipped: skipped}
|
||||
for _, p := range r.RunTests {
|
||||
if r.hasSkip(p) {
|
||||
tests.Skipped++
|
||||
tests.Tests = append(tests.Tests, Test{Path: p, Skipped: true, Encoder: r.Encoder})
|
||||
continue
|
||||
}
|
||||
|
||||
t := Test{Path: p, Encoder: r.Encoder}.Run(r.Parser, r.Files)
|
||||
tests.Tests = append(tests.Tests, t)
|
||||
|
||||
if t.Failed() {
|
||||
tests.Failed++
|
||||
} else {
|
||||
tests.Passed++
|
||||
}
|
||||
}
|
||||
|
||||
return tests, nil
|
||||
}
|
||||
|
||||
// find all TOML files in 'path' relative to the test directory.
|
||||
func (r Runner) findTOML(path string, appendTo *[]string, exclude []string) error {
|
||||
// It's okay if the directory doesn't exist.
|
||||
if _, err := fs.Stat(r.Files, path); errors.Is(err, fs.ErrNotExist) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fs.WalkDir(r.Files, path, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if d.IsDir() || !strings.HasSuffix(path, ".toml") {
|
||||
return nil
|
||||
}
|
||||
|
||||
path = strings.TrimSuffix(path, ".toml")
|
||||
for _, e := range exclude {
|
||||
if ok, _ := filepath.Match(e, path); ok {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
*appendTo = append(*appendTo, path)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// Expand RunTest glob patterns, or return all tests if RunTests if empty.
|
||||
func (r *Runner) findTests() (int, error) {
|
||||
ls, err := r.List()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var skip int
|
||||
|
||||
if len(r.RunTests) == 0 {
|
||||
r.RunTests = ls
|
||||
} else {
|
||||
run := make([]string, 0, len(r.RunTests))
|
||||
for _, l := range ls {
|
||||
for _, r := range r.RunTests {
|
||||
if m, _ := filepath.Match(r, l); m {
|
||||
run = append(run, l)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
r.RunTests, skip = run, len(ls)-len(run)
|
||||
}
|
||||
|
||||
// Expand invalid tests ending in ".multi.toml"
|
||||
expanded := make([]string, 0, len(r.RunTests))
|
||||
for _, path := range r.RunTests {
|
||||
if !strings.HasSuffix(path, ".multi") {
|
||||
expanded = append(expanded, path)
|
||||
continue
|
||||
}
|
||||
|
||||
d, err := fs.ReadFile(r.Files, path+".toml")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
fmt.Println(string(d))
|
||||
}
|
||||
r.RunTests = expanded
|
||||
|
||||
return skip, nil
|
||||
}
|
||||
|
||||
func (r Runner) hasSkip(path string) bool {
|
||||
for _, s := range r.SkipTests {
|
||||
if m, _ := filepath.Match(s, path); m {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (c CommandParser) Encode(input string) (output string, outputIsError bool, err error) {
|
||||
stdout, stderr := new(bytes.Buffer), new(bytes.Buffer)
|
||||
cmd := exec.Command(c.cmd[0])
|
||||
cmd.Args = c.cmd
|
||||
cmd.Stdin, cmd.Stdout, cmd.Stderr = strings.NewReader(input), stdout, stderr
|
||||
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
eErr := &exec.ExitError{}
|
||||
if errors.As(err, &eErr) {
|
||||
fmt.Fprintf(stderr, "\nExit %d\n", eErr.ProcessState.ExitCode())
|
||||
err = nil
|
||||
}
|
||||
}
|
||||
|
||||
if stderr.Len() > 0 {
|
||||
return strings.TrimSpace(stderr.String()) + "\n", true, err
|
||||
}
|
||||
return strings.TrimSpace(stdout.String()) + "\n", false, err
|
||||
}
|
||||
func NewCommandParser(fsys fs.FS, cmd []string) CommandParser { return CommandParser{fsys, cmd} }
|
||||
func (c CommandParser) Decode(input string) (string, bool, error) { return c.Encode(input) }
|
||||
|
||||
// Run this test.
|
||||
func (t Test) Run(p Parser, fsys fs.FS) Test {
|
||||
if t.Type() == TypeInvalid {
|
||||
return t.runInvalid(p, fsys)
|
||||
}
|
||||
return t.runValid(p, fsys)
|
||||
}
|
||||
|
||||
func (t Test) runInvalid(p Parser, fsys fs.FS) Test {
|
||||
var err error
|
||||
_, t.Input, err = t.ReadInput(fsys)
|
||||
if err != nil {
|
||||
return t.bug(err.Error())
|
||||
}
|
||||
|
||||
if t.Encoder {
|
||||
t.Output, t.OutputFromStderr, err = p.Encode(t.Input)
|
||||
} else {
|
||||
t.Output, t.OutputFromStderr, err = p.Decode(t.Input)
|
||||
}
|
||||
if err != nil {
|
||||
return t.fail(err.Error())
|
||||
}
|
||||
if !t.OutputFromStderr {
|
||||
return t.fail("Expected an error, but no error was reported.")
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
func (t Test) runValid(p Parser, fsys fs.FS) Test {
|
||||
var err error
|
||||
_, t.Input, err = t.ReadInput(fsys)
|
||||
if err != nil {
|
||||
return t.bug(err.Error())
|
||||
}
|
||||
|
||||
if t.Encoder {
|
||||
t.Output, t.OutputFromStderr, err = p.Encode(t.Input)
|
||||
} else {
|
||||
t.Output, t.OutputFromStderr, err = p.Decode(t.Input)
|
||||
}
|
||||
if err != nil {
|
||||
return t.fail(err.Error())
|
||||
}
|
||||
if t.OutputFromStderr {
|
||||
return t.fail(t.Output)
|
||||
}
|
||||
if t.Output == "" {
|
||||
// Special case: we expect an empty output here.
|
||||
if t.Path != "valid/empty-file" {
|
||||
return t.fail("stdout is empty")
|
||||
}
|
||||
}
|
||||
|
||||
// Compare for encoder test
|
||||
if t.Encoder {
|
||||
want, err := t.ReadWantTOML(fsys)
|
||||
if err != nil {
|
||||
return t.bug(err.Error())
|
||||
}
|
||||
var have interface{}
|
||||
if _, err := toml.Decode(t.Output, &have); err != nil {
|
||||
//return t.fail("decode TOML from encoder %q:\n %s", cmd, err)
|
||||
return t.fail("decode TOML from encoder:\n %s", err)
|
||||
}
|
||||
return t.CompareTOML(want, have)
|
||||
}
|
||||
|
||||
// Compare for decoder test
|
||||
want, err := t.ReadWantJSON(fsys)
|
||||
if err != nil {
|
||||
return t.fail(err.Error())
|
||||
}
|
||||
|
||||
var have interface{}
|
||||
if err := json.Unmarshal([]byte(t.Output), &have); err != nil {
|
||||
return t.fail("decode JSON output from parser:\n %s", err)
|
||||
}
|
||||
|
||||
return t.CompareJSON(want, have)
|
||||
}
|
||||
|
||||
// ReadInput reads the file sent to the encoder.
|
||||
func (t Test) ReadInput(fsys fs.FS) (path, data string, err error) {
|
||||
path = t.Path + map[bool]string{true: ".json", false: ".toml"}[t.Encoder]
|
||||
d, err := fs.ReadFile(fsys, path)
|
||||
if err != nil {
|
||||
return path, "", err
|
||||
}
|
||||
return path, string(d), nil
|
||||
}
|
||||
|
||||
func (t Test) ReadWant(fsys fs.FS) (path, data string, err error) {
|
||||
if t.Type() == TypeInvalid {
|
||||
panic("testoml.Test.ReadWant: invalid tests do not have a 'correct' version")
|
||||
}
|
||||
|
||||
path = t.Path + map[bool]string{true: ".toml", false: ".json"}[t.Encoder]
|
||||
d, err := fs.ReadFile(fsys, path)
|
||||
if err != nil {
|
||||
return path, "", err
|
||||
}
|
||||
return path, string(d), nil
|
||||
}
|
||||
|
||||
func (t *Test) ReadWantJSON(fsys fs.FS) (v interface{}, err error) {
|
||||
var path string
|
||||
path, t.Want, err = t.ReadWant(fsys)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := json.Unmarshal([]byte(t.Want), &v); err != nil {
|
||||
return nil, fmt.Errorf("decode JSON file %q:\n %s", path, err)
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
func (t *Test) ReadWantTOML(fsys fs.FS) (v interface{}, err error) {
|
||||
var path string
|
||||
path, t.Want, err = t.ReadWant(fsys)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = toml.Decode(t.Want, &v)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not decode TOML file %q:\n %s", path, err)
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// Test type: "valid", "invalid"
|
||||
func (t Test) Type() testType {
|
||||
if strings.HasPrefix(t.Path, "invalid") {
|
||||
return TypeInvalid
|
||||
}
|
||||
return TypeValid
|
||||
}
|
||||
|
||||
func (t Test) fail(format string, v ...interface{}) Test {
|
||||
t.Failure = fmt.Sprintf(format, v...)
|
||||
return t
|
||||
}
|
||||
func (t Test) bug(format string, v ...interface{}) Test {
|
||||
return t.fail("BUG IN TEST CASE: "+format, v...)
|
||||
}
|
||||
|
||||
func (t Test) Failed() bool { return t.Failure != "" }
|
||||
@@ -0,0 +1 @@
|
||||
*.toml -text
|
||||
+1
@@ -0,0 +1 @@
|
||||
array = [1,,2]
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
array = [1,2,,]
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
a = [{ b = 1 }]
|
||||
|
||||
# Cannot extend tables within static arrays
|
||||
# https://github.com/toml-lang/toml/issues/908
|
||||
[a.c]
|
||||
foo = 1
|
||||
+1
@@ -0,0 +1 @@
|
||||
wrong = [ 1 2 3 ]
|
||||
+1
@@ -0,0 +1 @@
|
||||
x = [42 #
|
||||
+1
@@ -0,0 +1 @@
|
||||
x = [{ key = 42 #
|
||||
+1
@@ -0,0 +1 @@
|
||||
x = [{ key = 42
|
||||
+1
@@ -0,0 +1 @@
|
||||
long_array = [ 1, 2, 3
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
# INVALID TOML DOC
|
||||
fruit = []
|
||||
|
||||
[[fruit]] # Not allowed
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
# INVALID TOML DOC
|
||||
[[fruit]]
|
||||
name = "apple"
|
||||
|
||||
[[fruit.variety]]
|
||||
name = "red delicious"
|
||||
|
||||
# This table conflicts with the previous table
|
||||
[fruit.variety]
|
||||
name = "granny smith"
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
array = [
|
||||
"Is there life after an array separator?", No
|
||||
"Entry"
|
||||
]
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
array = [
|
||||
"Is there life before an array separator?" No,
|
||||
"Entry"
|
||||
]
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
array = [
|
||||
"Entry 1",
|
||||
I don't belong,
|
||||
"Entry 2",
|
||||
]
|
||||
+1
@@ -0,0 +1 @@
|
||||
a = falsify
|
||||
+1
@@ -0,0 +1 @@
|
||||
a = fals
|
||||
+1
@@ -0,0 +1 @@
|
||||
a = truthy
|
||||
+1
@@ -0,0 +1 @@
|
||||
a = tru
|
||||
+1
@@ -0,0 +1 @@
|
||||
a = f
|
||||
+1
@@ -0,0 +1 @@
|
||||
a = t
|
||||
+1
@@ -0,0 +1 @@
|
||||
valid = False
|
||||
+1
@@ -0,0 +1 @@
|
||||
a = falsey
|
||||
+1
@@ -0,0 +1 @@
|
||||
a = truer
|
||||
+1
@@ -0,0 +1 @@
|
||||
b = FALSE
|
||||
+1
@@ -0,0 +1 @@
|
||||
a = TRUE
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
# The following line contains a single carriage return control character
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
bare-formfeed =
|
||||
BIN
Binary file not shown.
+1
@@ -0,0 +1 @@
|
||||
bare-vertical-tab =
|
||||
+1
@@ -0,0 +1 @@
|
||||
comment-cr = "Carriage return in comment" #
a=1
|
||||
+1
@@ -0,0 +1 @@
|
||||
comment-del = "0x7f" #
|
||||
+1
@@ -0,0 +1 @@
|
||||
comment-lf = "ctrl-P" #
|
||||
BIN
Binary file not shown.
+1
@@ -0,0 +1 @@
|
||||
comment-us = "ctrl-_" #
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
# "\x.." sequences are replaced with literal control characters.
|
||||
|
||||
comment-null = "null" # \x00
|
||||
comment-lf = "ctrl-P" # \x10
|
||||
comment-us = "ctrl-_" # \x1f
|
||||
comment-del = "0x7f" # \x7f
|
||||
comment-cr = "Carriage return in comment" # \x0da=1
|
||||
|
||||
string-null = "null\x00"
|
||||
string-lf = "null\x10"
|
||||
string-us = "null\x1f"
|
||||
string-del = "null\x7f"
|
||||
|
||||
rawstring-null = 'null\x00'
|
||||
rawstring-lf = 'null\x10'
|
||||
rawstring-us = 'null\x1f'
|
||||
rawstring-del = 'null\x7f'
|
||||
|
||||
multi-null = """null\x00"""
|
||||
multi-lf = """null\x10"""
|
||||
multi-us = """null\x1f"""
|
||||
multi-del = """null\x7f"""
|
||||
|
||||
rawmulti-null = '''null\x00'''
|
||||
rawmulti-lf = '''null\x10'''
|
||||
rawmulti-us = '''null\x1f'''
|
||||
rawmulti-del = '''null\x7f'''
|
||||
|
||||
string-bs = "backspace\x08"
|
||||
|
||||
bare-null = "some value" \x00
|
||||
bare-formfeed = \x0c
|
||||
bare-vertical-tab = \x0b
|
||||
+1
@@ -0,0 +1 @@
|
||||
multi-del = """null"""
|
||||
+1
@@ -0,0 +1 @@
|
||||
multi-lf = """null"""
|
||||
BIN
Binary file not shown.
+1
@@ -0,0 +1 @@
|
||||
multi-us = """null"""
|
||||
+1
@@ -0,0 +1 @@
|
||||
rawmulti-del = '''null'''
|
||||
+1
@@ -0,0 +1 @@
|
||||
rawmulti-lf = '''null'''
|
||||
BIN
Binary file not shown.
+1
@@ -0,0 +1 @@
|
||||
rawmulti-us = '''null'''
|
||||
+1
@@ -0,0 +1 @@
|
||||
rawstring-del = 'null'
|
||||
+1
@@ -0,0 +1 @@
|
||||
rawstring-lf = 'null'
|
||||
BIN
Binary file not shown.
+1
@@ -0,0 +1 @@
|
||||
rawstring-us = 'null'
|
||||
+1
@@ -0,0 +1 @@
|
||||
string-bs = "backspace"
|
||||
+1
@@ -0,0 +1 @@
|
||||
string-del = "null"
|
||||
+1
@@ -0,0 +1 @@
|
||||
string-lf = "null"
|
||||
BIN
Binary file not shown.
+1
@@ -0,0 +1 @@
|
||||
string-us = "null"
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
# time-hour = 2DIGIT ; 00-23
|
||||
d = 2006-01-01T24:00:00-00:00
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
# date-mday = 2DIGIT ; 01-28, 01-29, 01-30, 01-31 based on
|
||||
# ; month/year
|
||||
d = 2006-01-32T00:00:00-00:00
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
# date-mday = 2DIGIT ; 01-28, 01-29, 01-30, 01-31 based on
|
||||
# ; month/year
|
||||
d = 2006-01-00T00:00:00-00:00
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
# time-minute = 2DIGIT ; 00-59
|
||||
d = 2006-01-01T00:60:00-00:00
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
# date-month = 2DIGIT ; 01-12
|
||||
d = 2006-13-01T00:00:00-00:00
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
# date-month = 2DIGIT ; 01-12
|
||||
d = 2007-00-01T00:00:00-00:00
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
# Day "5" instead of "05"; the leading zero is required.
|
||||
with-milli = 1987-07-5T17:45:00.12Z
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
# Month "7" instead of "07"; the leading zero is required.
|
||||
no-leads = 1987-7-05T17:45:00Z
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
# No seconds in time.
|
||||
no-secs = 1987-07-05T17:45Z
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
# No "t" or "T" between the date and time.
|
||||
no-t = 1987-07-0517:45:00Z
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
# time-second = 2DIGIT ; 00-58, 00-59, 00-60 based on leap second
|
||||
# ; rules
|
||||
d = 2006-01-01T00:00:61-00:00
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user