diff --git a/README.md b/README.md index d472eb52..cc13f866 100644 --- a/README.md +++ b/README.md @@ -208,4 +208,4 @@ type clients struct { Note that a case insensitive match will be tried if an exact match can't be found. -A working example of the above can be found in `_examples/example.{go,toml}`. +A working example of the above can be found in `_example/example.{go,toml}`. diff --git a/_example/example.go b/_example/example.go new file mode 100644 index 00000000..4800c3a0 --- /dev/null +++ b/_example/example.go @@ -0,0 +1,112 @@ +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 []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 + `"`) +} + +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) + } +} diff --git a/_example/example.toml b/_example/example.toml new file mode 100644 index 00000000..372c281d --- /dev/null +++ b/_example/example.toml @@ -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. +] + +# Custom Unmarshal. +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" diff --git a/_examples/example.go b/_examples/example.go deleted file mode 100644 index 79f31f27..00000000 --- a/_examples/example.go +++ /dev/null @@ -1,61 +0,0 @@ -package main - -import ( - "fmt" - "time" - - "github.com/BurntSushi/toml" -) - -type tomlConfig struct { - Title string - Owner ownerInfo - DB database `toml:"database"` - Servers map[string]server - Clients clients -} - -type ownerInfo struct { - Name string - Org string `toml:"organization"` - Bio string - DOB time.Time -} - -type database struct { - Server string - Ports []int - ConnMax int `toml:"connection_max"` - Enabled bool -} - -type server struct { - IP string - DC string -} - -type clients struct { - Data [][]interface{} - Hosts []string -} - -func main() { - var config tomlConfig - if _, err := toml.DecodeFile("example.toml", &config); err != nil { - fmt.Println(err) - return - } - - fmt.Printf("Title: %s\n", config.Title) - fmt.Printf("Owner: %s (%s, %s), Born: %s\n", - config.Owner.Name, config.Owner.Org, config.Owner.Bio, - config.Owner.DOB) - fmt.Printf("Database: %s %v (Max conn. %d), Enabled? %v\n", - config.DB.Server, config.DB.Ports, config.DB.ConnMax, - config.DB.Enabled) - for serverName, server := range config.Servers { - fmt.Printf("Server: %s (%s, %s)\n", serverName, server.IP, server.DC) - } - fmt.Printf("Client data: %v\n", config.Clients.Data) - fmt.Printf("Client hosts: %v\n", config.Clients.Hosts) -} diff --git a/_examples/example.toml b/_examples/example.toml deleted file mode 100644 index 32c7a4fa..00000000 --- a/_examples/example.toml +++ /dev/null @@ -1,35 +0,0 @@ -# This is a TOML document. Boom. - -title = "TOML Example" - -[owner] -name = "Tom Preston-Werner" -organization = "GitHub" -bio = "GitHub Cofounder & CEO\nLikes tater tots and beer." -dob = 1979-05-27T07:32:00Z # First class dates? Why not? - -[database] -server = "192.168.1.1" -ports = [ 8001, 8001, 8002 ] -connection_max = 5000 -enabled = true - -[servers] - - # You can indent as you please. Tabs or spaces. TOML don't care. - [servers.alpha] - ip = "10.0.0.1" - dc = "eqdc10" - - [servers.beta] - ip = "10.0.0.2" - dc = "eqdc10" - -[clients] -data = [ ["gamma", "delta"], [1, 2] ] # just an update to make sure parsers support it - -# Line breaks are OK when inside arrays -hosts = [ - "alpha", - "omega" -] diff --git a/_examples/hard.toml b/_examples/hard.toml deleted file mode 100644 index 26145d2b..00000000 --- a/_examples/hard.toml +++ /dev/null @@ -1,22 +0,0 @@ -# Test file for TOML -# Only this one tries to emulate a TOML file written by a user of the kind of parser writers probably hate -# This part you'll really hate - -[the] -test_string = "You'll hate me after this - #" # " Annoying, isn't it? - - [the.hard] - test_array = [ "] ", " # "] # ] There you go, parse this! - test_array2 = [ "Test #11 ]proved that", "Experiment #9 was a success" ] - # You didn't think it'd as easy as chucking out the last #, did you? - another_test_string = " Same thing, but with a string #" - harder_test_string = " And when \"'s are in the string, along with # \"" # "and comments are there too" - # Things will get harder - - [the.hard.bit#] - what? = "You don't think some user won't do that?" - multi_line_array = [ - "]", - # ] Oh yes I did - ] - diff --git a/_examples/implicit.toml b/_examples/implicit.toml deleted file mode 100644 index 1dea5ceb..00000000 --- a/_examples/implicit.toml +++ /dev/null @@ -1,4 +0,0 @@ -# [x] you -# [x.y] don't -# [x.y.z] need these -[x.y.z.w] # for this to work diff --git a/_examples/invalid-apples.toml b/_examples/invalid-apples.toml deleted file mode 100644 index 74e9e337..00000000 --- a/_examples/invalid-apples.toml +++ /dev/null @@ -1,6 +0,0 @@ -# DO NOT WANT -[fruit] -type = "apple" - -[fruit.type] -apple = "yes" diff --git a/_examples/invalid.toml b/_examples/invalid.toml deleted file mode 100644 index beb1dba5..00000000 --- a/_examples/invalid.toml +++ /dev/null @@ -1,35 +0,0 @@ -# This is an INVALID TOML document. Boom. -# Can you spot the error without help? - -title = "TOML Example" - -[owner] -name = "Tom Preston-Werner" -organization = "GitHub" -bio = "GitHub Cofounder & CEO\nLikes tater tots and beer." -dob = 1979-05-27T7:32:00Z # First class dates? Why not? - -[database] -server = "192.168.1.1" -ports = [ 8001, 8001, 8002 ] -connection_max = 5000 -enabled = true - -[servers] - # You can indent as you please. Tabs or spaces. TOML don't care. - [servers.alpha] - ip = "10.0.0.1" - dc = "eqdc10" - - [servers.beta] - ip = "10.0.0.2" - dc = "eqdc10" - -[clients] -data = [ ["gamma", "delta"], [1, 2] ] # just an update to make sure parsers support it - -# Line breaks are OK when inside arrays -hosts = [ - "alpha", - "omega" -] diff --git a/_examples/readme1.toml b/_examples/readme1.toml deleted file mode 100644 index 3e1261d4..00000000 --- a/_examples/readme1.toml +++ /dev/null @@ -1,5 +0,0 @@ -Age = 25 -Cats = [ "Cauchy", "Plato" ] -Pi = 3.14 -Perfection = [ 6, 28, 496, 8128 ] -DOB = 1987-07-05T05:45:00Z diff --git a/_examples/readme2.toml b/_examples/readme2.toml deleted file mode 100644 index b51cd934..00000000 --- a/_examples/readme2.toml +++ /dev/null @@ -1 +0,0 @@ -some_key_NAME = "wat" diff --git a/bench_test.go b/bench_test.go new file mode 100644 index 00000000..c3d1a508 --- /dev/null +++ b/bench_test.go @@ -0,0 +1,195 @@ +//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" + } + + 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 + `"`) +} diff --git a/decode_test.go b/decode_test.go index 9b003fd5..8a2affbb 100644 --- a/decode_test.go +++ b/decode_test.go @@ -662,56 +662,6 @@ func TestDecodePrimitive(t *testing.T) { } } -func BenchmarkDecode(b *testing.B) { - var testSimple = ` -age = 250 -andrew = "gallant" -kait = "brady" -now = 1987-07-05T05:45:00Z -nowEast = 2017-06-22T16:15:21+08:00 -nowWest = 2017-06-22T02:14:36-06:00 -yesOrNo = true -pi = 3.14 -colors = [ - ["red", "green", "blue"], - ["cyan", "magenta", "yellow", "black"], -] - -[My.Cats] -plato = "cat 1" -cauchy = """ cat 2 -""" -` - - type cats struct { - Plato string - Cauchy string - } - type simple struct { - Age int - Colors [][]string - Pi float64 - YesOrNo bool - Now time.Time - NowEast time.Time - NowWest time.Time - Andrew string - Kait string - My map[string]cats - } - - var val simple - _, err := Decode(testSimple, &val) - if err != nil { - b.Fatal(err) - } - - b.ResetTimer() - for n := 0; n < b.N; n++ { - Decode(testSimple, &val) - } -} - func TestDecodeDatetime(t *testing.T) { // Test here in addition to toml-test to ensure the TZs are correct. tz7 := time.FixedZone("", -3600*7)