Skip to content

Commit

Permalink
Update README, errors, some documentation, examples
Browse files Browse the repository at this point in the history
  • Loading branch information
arp242 committed May 28, 2022
1 parent fa027bd commit d716584
Show file tree
Hide file tree
Showing 11 changed files with 337 additions and 485 deletions.
185 changes: 47 additions & 138 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
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.
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).

Expand All @@ -10,7 +9,7 @@ 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; install it with:
This library requires Go 1.13 or newer; add it to your go.mod with:

% go get github.com/BurntSushi/toml@latest

Expand All @@ -19,16 +18,7 @@ It also comes with a TOML validator CLI tool:
% go install github.com/BurntSushi/toml/cmd/tomlv@latest
% tomlv some-toml-file.toml

### Testing
This package passes all tests in [toml-test] for both the decoder and the
encoder.

[toml-test]: https://github.com/BurntSushi/toml-test

### Examples
This package works similar to how the Go standard library handles XML and JSON.
Namely, data is loaded into Go values via reflection.

For the simplest example, consider some TOML file as just a list of keys and
values:

Expand All @@ -40,28 +30,23 @@ Perfection = [ 6, 28, 496, 8128 ]
DOB = 1987-07-05T05:45:00Z
```

Which could be defined in Go as:
Which can be decoded with:

```go
type Config struct {
Age int
Cats []string
Pi float64
Perfection []int
DOB time.Time // requires `import time`
DOB time.Time
}
```

And then decoded with:

```go
var conf Config
_, err := toml.Decode(tomlData, &conf)
// handle error
```

You can also use struct tags if your struct field name doesn't map to a TOML
key value directly:
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"
Expand All @@ -73,139 +58,63 @@ type TOML struct {
}
```

Beware that like other most other decoders **only exported fields** are
considered when encoding and decoding; private fields are silently ignored.
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 duration strings into
`time.Duration` values:
Here's an example that automatically parses values in a `mail.Address`:

```toml
[[song]]
name = "Thunder Road"
duration = "4m49s"

[[song]]
name = "Stairway to Heaven"
duration = "8m03s"
```

Which can be decoded with:

```go
type song struct {
Name string
Duration duration
}
type songs struct {
Song []song
}
var favorites songs
if _, err := toml.Decode(blob, &favorites); err != nil {
log.Fatal(err)
}

for _, s := range favorites.Song {
fmt.Printf("%s (%s)\n", s.Name, s.Duration)
}
contacts = [
"Donald Duck <donald@duckburg.com>",
"Scrooge McDuck <scrooge@duckburg.com>",
]
```

And you'll also need a `duration` type that satisfies the
`encoding.TextUnmarshaler` interface:
Can be decoded with:

```go
type duration struct {
time.Duration
// Create address type which satisfies the encoding.TextUnmarshaler interface.
type address struct {
*mail.Address
}

func (d *duration) UnmarshalText(text []byte) error {
func (a *address) UnmarshalText(text []byte) error {
var err error
d.Duration, err = time.ParseDuration(string(text))
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
Here's an example of how to load the example from the official spec page:

```toml
# 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"
]
```

And the corresponding Go types are:

```go
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
}
```

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 `_example/example.{go,toml}`.
See the [`_example/`](/_example) directory for a more complex example.
10 changes: 2 additions & 8 deletions _example/example.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ type (
Integers []int
Floats []float64
Times []fmtTime
Duration []duration
Duration []time.Duration
Distros []distro
Servers map[string]server
Characters map[string][]struct {
Expand All @@ -38,15 +38,9 @@ type (
Packages string
}

duration struct{ time.Duration }
fmtTime struct{ time.Time }
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 {
Expand Down
2 changes: 1 addition & 1 deletion _example/example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ times = [
15:16:17, # local time.
]

# Custom Unmarshal.
# Durations.
duration = ["4m49s", "8m03s", "1231h15m55s"]

# Table with inline tables.
Expand Down
12 changes: 6 additions & 6 deletions bench_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,14 +169,14 @@ type (
Packages string
}

//duration struct{ time.Duration }
fmtTime struct{ time.Time }
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 (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"
Expand Down
22 changes: 14 additions & 8 deletions decode.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,14 +76,17 @@ const (
// TOML datetimes correspond to Go time.Time values. 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
// time duration strings.
// email addresses.
//
// Key mapping
//
Expand Down Expand Up @@ -413,22 +416,25 @@ func (md *MetaData) unifyFloat64(data interface{}, rv reflect.Value) error {
}

func (md *MetaData) unifyInt(data interface{}, rv reflect.Value) error {
num, ok := data.(int64)
if !ok {
return md.badtype("integer", data)
}

if _, ok := rv.Interface().(time.Duration); ok {
_, 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.e("value %q is not a valid duration: %w", s, err)
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:
Expand Down
Loading

0 comments on commit d716584

Please sign in to comment.