Skip to content

Commit

Permalink
Merge pull request #307 from BurntSushi/local-datetime
Browse files Browse the repository at this point in the history
Deal with local datetimes, dates, and times better
  • Loading branch information
arp242 committed Jul 4, 2021
2 parents ccff24e + fa10b4c commit ebe1404
Show file tree
Hide file tree
Showing 16 changed files with 185 additions and 159 deletions.
7 changes: 0 additions & 7 deletions cmd/toml-test-decoder/README.md
Expand Up @@ -4,10 +4,3 @@ 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`.


Compatible with TOML version
[v0.4.0](https://github.com/toml-lang/toml/blob/master/versions/en/toml-v0.4.0.md)

Compatible with `toml-test` version
[v0.2.0](https://github.com/BurntSushi/toml-test/tree/v0.2.0)
2 changes: 1 addition & 1 deletion cmd/toml-test-decoder/main.go
Expand Up @@ -37,7 +37,7 @@ func main() {

j := json.NewEncoder(os.Stdout)
j.SetIndent("", " ")
if err := j.Encode(tag.Add(decoded)); err != nil {
if err := j.Encode(tag.Add("", decoded)); err != nil {
log.Fatalf("Error encoding JSON: %s", err)
}
}
7 changes: 0 additions & 7 deletions cmd/toml-test-encoder/README.md
Expand Up @@ -4,10 +4,3 @@ 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`.


Compatible with TOML version
[v0.4.0](https://github.com/toml-lang/toml/blob/master/versions/en/toml-v0.4.0.md)

Compatible with `toml-test` version
[v0.2.0](https://github.com/BurntSushi/toml-test/tree/v0.2.0)
13 changes: 3 additions & 10 deletions cmd/tomlv/README.md
Expand Up @@ -2,20 +2,13 @@

If Go is installed, it's simple to try it out:

```bash
go get github.com/BurntSushi/toml/cmd/tomlv
tomlv some-toml-file.toml
```
$ 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:

```bash
tomlv -types some-toml-file.toml
```
$ 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`.

Compatible with TOML version
[v0.4.0](https://github.com/toml-lang/toml/blob/master/versions/en/toml-v0.4.0.md)
9 changes: 2 additions & 7 deletions cmd/tomlv/main.go
Expand Up @@ -19,19 +19,14 @@ var (

func init() {
log.SetFlags(0)

flag.BoolVar(&flagTypes, "types", flagTypes,
"When set, the types of every defined key will be shown.")

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]))
log.Printf("Usage: %s toml-file [ toml-file ... ]\n", path.Base(os.Args[0]))
flag.PrintDefaults()

os.Exit(1)
}

Expand Down
43 changes: 22 additions & 21 deletions decode.go
Expand Up @@ -60,39 +60,40 @@ func (md *MetaData) PrimitiveDecode(primValue Primitive, v interface{}) error {

// Decode TOML data.
//
// TOML hashes correspond to Go structs or maps. (Dealer's choice. They can be
// used interchangeably.)
// TOML tables correspond to Go structs or maps (dealer's choice – they can be
// used interchangeably).
//
// TOML arrays of tables correspond to either a slice of structs or a slice
// of maps.
// TOML table arrays correspond to either a slice of structs or a slice of maps.
//
// TOML datetimes correspond to Go `time.Time` values.
// TOML datetimes correspond to Go `time.Time` values. Local datetimes are
// parsed in the local timezone and have the Location set to toml.LocalDatetime.
// Local dates and times have the Location set to toml.LocalDate and
// toml.LocalTime.
//
// All other TOML types (float, string, int, bool and array) correspond
// to the obvious Go types.
// 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
// encoding.TextUnmarshaler interface. In this case, any primitive TOML value
// (floats, strings, integers, booleans and datetimes) will be converted to
// a byte string and given to the value's UnmarshalText method. See the
// (floats, strings, integers, booleans and datetimes) will be converted to a
// byte string and given to the value's UnmarshalText method. See the
// Unmarshaler example for a demonstration with time duration strings.
//
// 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 may 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.
// 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.
// 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 will not handle cyclic types. If a cyclic type is passed,
// `Decode` will not terminate.
// This decoder does not handle cyclic types. Decode will not terminate if a
// cyclic type is passed.
type Decoder struct {
r io.Reader
}
Expand Down
42 changes: 42 additions & 0 deletions decode_test.go
Expand Up @@ -666,6 +666,48 @@ cauchy = """ cat 2
}
}

func TestDecodeDatetime(t *testing.T) {
// Test here in addition to toml-test to ensure the TZs are correct.
tz7 := time.FixedZone("", -3600*7)

for _, tt := range []struct {
in string
want time.Time
}{
// Offset datetime
{"1979-05-27T07:32:00Z", time.Date(1979, 05, 27, 07, 32, 0, 0, time.UTC)},
{"1979-05-27T07:32:00.999999Z", time.Date(1979, 05, 27, 07, 32, 0, 999999000, time.UTC)},
{"1979-05-27T00:32:00-07:00", time.Date(1979, 05, 27, 00, 32, 0, 0, tz7)},
{"1979-05-27T00:32:00.999999-07:00", time.Date(1979, 05, 27, 00, 32, 0, 999999000, tz7)},
{"1979-05-27T00:32:00.24-07:00", time.Date(1979, 05, 27, 00, 32, 0, 240000000, tz7)},
{"1979-05-27 07:32:00Z", time.Date(1979, 05, 27, 07, 32, 0, 0, time.UTC)},
{"1979-05-27t07:32:00z", time.Date(1979, 05, 27, 07, 32, 0, 0, time.UTC)},

// Make sure the space between the datetime and "#" isn't lexed.
{"1979-05-27T07:32:12-07:00 # c", time.Date(1979, 05, 27, 07, 32, 12, 0, tz7)},

// Local times.
{"1979-05-27T07:32:00", time.Date(1979, 05, 27, 07, 32, 0, 0, LocalDatetime)},
{"1979-05-27T07:32:00.999999", time.Date(1979, 05, 27, 07, 32, 0, 999999000, LocalDatetime)},
{"1979-05-27T07:32:00.25", time.Date(1979, 05, 27, 07, 32, 0, 250000000, LocalDatetime)},
{"1979-05-27", time.Date(1979, 05, 27, 0, 0, 0, 0, LocalDate)},
{"07:32:00", time.Date(0, 1, 1, 07, 32, 0, 0, LocalTime)},
{"07:32:00.999999", time.Date(0, 1, 1, 07, 32, 0, 999999000, LocalTime)},
} {
t.Run(tt.in, func(t *testing.T) {
var x struct{ D time.Time }
input := "d = " + tt.in
if _, err := Decode(input, &x); err != nil {
t.Fatalf("got error: %s", err)
}

if h, w := x.D.Format(time.RFC3339Nano), tt.want.Format(time.RFC3339Nano); h != w {
t.Errorf("\nhave: %s\nwant: %s", h, w)
}
})
}
}

func TestParseError(t *testing.T) {
file :=
`a = "a"
Expand Down
28 changes: 7 additions & 21 deletions doc.go
@@ -1,27 +1,13 @@
/*
Package toml provides facilities for decoding and encoding TOML configuration
files via reflection. 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.
Package toml implements decoding and encoding of TOML files.
The specification implemented: https://github.com/toml-lang/toml
This pakcage supports TOML v1.0.0, as listed on https://toml.io
The sub-command github.com/BurntSushi/toml/cmd/tomlv can be used to verify
whether a file is a valid TOML document. It can also be used to print the
type of each key in a TOML document.
Testing
There are two important types of tests used for this package. The first is
contained inside '*_test.go' files and uses the standard Go unit testing
framework. These tests are primarily devoted to holistically testing the
decoder and encoder.
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 second type of testing is used to verify the implementation's adherence
to the TOML specification. These tests have been factored into their own
project: https://github.com/BurntSushi/toml-test
The reason the tests are in a separate project is so that they can be used by
any implementation of TOML. Namely, it is language agnostic.
The sub-command github.com/BurntSushi/toml/cmd/tomlv can be used to verify
whether a file is a valid TOML document. It can also be used to print the type
of each key in a TOML document.
*/
package toml
19 changes: 16 additions & 3 deletions encode.go
Expand Up @@ -186,9 +186,22 @@ func (enc *Encoder) encode(key Key, rv reflect.Value) {
// 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.
enc.wf(v.Format(time.RFC3339Nano))
case time.Time: // Using TextMarshaler adds extra quotes, which we don't want.
format := time.RFC3339Nano
switch v.Location() {
case LocalDatetime:
format = "2006-01-02T15:04:05.999999999"
case LocalDate:
format = "2006-01-02"
case LocalTime:
format = "15:04:05.999999999"
}
switch v.Location() {
default:
enc.wf(v.Format(format))
case LocalDatetime, LocalDate, LocalTime:
enc.wf(v.In(time.UTC).Format(format))
}
return
case encoding.TextMarshaler:
// Use text marshaler if it's available for this value.
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Expand Up @@ -2,4 +2,4 @@ module github.com/BurntSushi/toml

go 1.13

require github.com/BurntSushi/toml-test v0.1.1-0.20210704062846-269931e74e3f
require github.com/BurntSushi/toml-test v0.1.1-0.20210704114940-e6948edce1c5
4 changes: 3 additions & 1 deletion go.sum
@@ -1,8 +1,10 @@
github.com/BurntSushi/toml v0.3.2-0.20210614224209-34d990aa228d/go.mod h1:2QZjSXA5e+XyFeCAxxtL8Z4StYUsTquL8ODGPR3C3MA=
github.com/BurntSushi/toml v0.3.2-0.20210621044154-20a94d639b8e/go.mod h1:t4zg8TkHfP16Vb3x4WKIw7zVYMit5QFtPEO8lOWxzTg=
github.com/BurntSushi/toml v0.3.2-0.20210624061728-01bfc69d1057/go.mod h1:NMj2lD5LfMqcE0w8tnqOsH6944oaqpI1974lrIwerfE=
github.com/BurntSushi/toml v0.3.2-0.20210704081116-ccff24ee4463/go.mod h1:EkRrMiQQmfxK6kIldz3QbPlhmVkrjW1RDJUnbDqGYvc=
github.com/BurntSushi/toml-test v0.1.1-0.20210620192437-de01089bbf76/go.mod h1:P/PrhmZ37t5llHfDuiouWXtFgqOoQ12SAh9j6EjrBR4=
github.com/BurntSushi/toml-test v0.1.1-0.20210624055653-1f6389604dc6/go.mod h1:UAIt+Eo8itMZAAgImXkPGDMYsT1SsJkVdB5TuONl86A=
github.com/BurntSushi/toml-test v0.1.1-0.20210704062846-269931e74e3f h1:2bJvwBZX/Ajv19zGY3hvuHDInegqjxsz9ht9Smlr7Rk=
github.com/BurntSushi/toml-test v0.1.1-0.20210704062846-269931e74e3f/go.mod h1:fnFWrIwqgHsEjVsW3RYCJmDo86oq9eiJ9u6bnqhtm2g=
github.com/BurntSushi/toml-test v0.1.1-0.20210704114940-e6948edce1c5 h1:pkhJ7YiuikhNSX/HnPKMahEWoWiQbsAZ3djE6vVF2I0=
github.com/BurntSushi/toml-test v0.1.1-0.20210704114940-e6948edce1c5/go.mod h1:ve9Q/RRu2vHi42LocPLNvagxuUJh993/95b18bw/Nws=
zgo.at/zli v0.0.0-20210619044753-e7020a328e59/go.mod h1:HLAc12TjNGT+VRXr76JnsNE3pbooQtwKWhX+RlDjQ2Y=
37 changes: 30 additions & 7 deletions internal/tag/add.go
Expand Up @@ -4,44 +4,67 @@ import (
"fmt"
"math"
"time"

"github.com/BurntSushi/toml"
)

func Add(tomlData interface{}) interface{} {
// 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(v)
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{})
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)
typed[i] = Add("", v)
}
return typed

// Datetime: tag as datetime.
case time.Time:
return tag("datetime", orig.Format("2006-01-02T15:04:05.999999999Z07:00"))
switch orig.Location() {
default:
return tag("datetime", orig.Format("2006-01-02T15:04:05.999999999Z07:00"))
case toml.LocalDatetime:
return tag("datetime-local", orig.Format("2006-01-02T15:04:05.999999999"))
case toml.LocalDate:
return tag("date-local", orig.Format("2006-01-02"))
case toml.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))
case string:
return tag("string", orig)
}
}

Expand Down

0 comments on commit ebe1404

Please sign in to comment.