From 2c9d689ce191604842d9697080c38664f51b7181 Mon Sep 17 00:00:00 2001 From: Martin Tournoij Date: Tue, 16 Nov 2021 11:23:57 +0100 Subject: [PATCH] Add Marshaler interface (#327) Ass Marshaler interface with the MarhsalTOML method to specifically target TOML. This is similar to e.g. json.Marshaler, and takes precedence over encoding.TextMarshaler. Fixes #76 --- README.md | 39 ++++++++++++------------------- decode.go | 6 +++++ encode.go | 44 +++++++++++++++++++++++------------ encode_test.go | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 113 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index 64410cf7..d472eb52 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,6 @@ -## TOML parser and encoder for Go with reflection - 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. This package also supports the `encoding.TextUnmarshaler` and -`encoding.TextMarshaler` interfaces so that you can define custom data -representations. (There is an example of this below.) +packages. Compatible with TOML version [v1.0.0](https://toml.io/en/v1.0.0). @@ -16,26 +12,25 @@ v0.4.0`). This library requires Go 1.13 or newer; install it with: - $ go get github.com/BurntSushi/toml + % go get github.com/BurntSushi/toml@latest It also comes with a TOML validator CLI tool: - $ go get github.com/BurntSushi/toml/cmd/tomlv - $ tomlv some-toml-file.toml + % 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. -This package passes all tests in -[toml-test](https://github.com/BurntSushi/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. -This package works similarly 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: +For the simplest example, consider some TOML file as just a list of keys and +values: ```toml Age = 25 @@ -61,9 +56,8 @@ And then decoded with: ```go var conf Config -if _, err := toml.Decode(tomlData, &conf); err != nil { - // handle error -} +err := toml.Decode(tomlData, &conf) +// handle error ``` You can also use struct tags if your struct field name doesn't map to a TOML @@ -75,15 +69,14 @@ some_key_NAME = "wat" ```go type TOML struct { - ObscureKey string `toml:"some_key_NAME"` + ObscureKey string `toml:"some_key_NAME"` } ``` Beware that like other most other decoders **only exported fields** are considered when encoding and decoding; private fields are silently ignored. -### Using the `encoding.TextUnmarshaler` interface - +### Using the `Marshaler` and `encoding.TextUnmarshaler` interfaces Here's an example that automatically parses duration strings into `time.Duration` values: @@ -136,7 +129,6 @@ 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 @@ -217,4 +209,3 @@ 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}`. - diff --git a/decode.go b/decode.go index c4b6287b..25ae13f0 100644 --- a/decode.go +++ b/decode.go @@ -431,6 +431,12 @@ func (md *MetaData) unifyAnything(data interface{}, rv reflect.Value) error { 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 TextMarshaler: text, err := sdata.MarshalText() if err != nil { diff --git a/encode.go b/encode.go index 0ea6e9a1..da5b337d 100644 --- a/encode.go +++ b/encode.go @@ -64,13 +64,22 @@ var quotedReplacer = strings.NewReplacer( "\x7f", `\u007f`, ) +// 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 the Decode* functions. Similarly, the TextMarshaler interface is -// supported by encoding the resulting bytes as strings. 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. +// for the Decode* functions. +// +// The toml.Marshaler and encoder.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. @@ -83,7 +92,7 @@ var quotedReplacer = strings.NewReplacer( // 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 +// NOTE: only exported keys are encoded due to the use of reflection. Unexported // keys are silently discarded. type Encoder struct { // The string to use for a single indentation level. The default is two @@ -130,12 +139,13 @@ func (enc *Encoder) safeEncode(key Key, rv reflect.Value) (err error) { } func (enc *Encoder) encode(key Key, rv reflect.Value) { - // Special case. Time needs to be in ISO8601 format. - // Special case. If we can marshal the type to text, then we used that. - // Basically, this prevents the encoder for handling these types as - // generic structs (or whatever the underlying type of a TextMarshaler is). + // Special case: time needs to be in ISO8601 format. + // + // Special case: if we can marshal the type to text, then we used that. This + // prevents the encoder for handling these types as generic structs (or + // whatever the underlying type of a TextMarshaler is). switch t := rv.Interface().(type) { - case time.Time, encoding.TextMarshaler: + case time.Time, encoding.TextMarshaler, Marshaler: enc.writeKeyValue(key, rv, false) return // TODO: #76 would make this superfluous after implemented. @@ -200,13 +210,19 @@ func (enc *Encoder) eElement(rv reflect.Value) { enc.wf(v.In(time.UTC).Format(format)) } return + case Marshaler: + s, err := v.MarshalTOML() + if err != nil { + encPanic(err) + } + enc.writeQuoted(string(s)) + return case encoding.TextMarshaler: - // Use text marshaler if it's available for this value. - if s, err := v.MarshalText(); err != nil { + s, err := v.MarshalText() + if err != nil { encPanic(err) - } else { - enc.writeQuoted(string(s)) } + enc.writeQuoted(string(s)) return } diff --git a/encode_test.go b/encode_test.go index 02bcef20..1a2ac830 100644 --- a/encode_test.go +++ b/encode_test.go @@ -336,6 +336,11 @@ type ( food struct{ F []string } fun func() cplx complex128 + + sound2 struct{ S string } + food2 struct{ F []string } + fun2 func() + cplx2 complex128 ) // This is intentionally wrong (pointer receiver) @@ -347,6 +352,14 @@ func (c cplx) MarshalText() ([]byte, error) { return []byte(fmt.Sprintf("(%f+%fi)", real(cplx), imag(cplx))), nil } +func (s *sound2) MarshalTOML() ([]byte, error) { return []byte(s.S), nil } +func (f food2) MarshalTOML() ([]byte, error) { return []byte(strings.Join(f.F, ", ")), nil } +func (f fun2) MarshalTOML() ([]byte, error) { return []byte("why would you do this?"), nil } +func (c cplx2) MarshalTOML() ([]byte, error) { + cplx := complex128(c) + return []byte(fmt.Sprintf("(%f+%fi)", real(cplx), imag(cplx))), nil +} + func TestEncodeTextMarshaler(t *testing.T) { x := struct { Name string @@ -396,6 +409,55 @@ Fun = "why would you do this?" } } +func TestEncodeTOMLMarshaler(t *testing.T) { + x := struct { + Name string + Labels map[string]string + Sound sound + Sound2 *sound + Food food + Food2 *food + Complex cplx + Fun fun + }{ + Name: "Goblok", + Sound: sound{"miauw"}, + Sound2: &sound{"miauw"}, + Labels: map[string]string{ + "type": "cat", + "color": "black", + }, + Food: food{[]string{"chicken", "fish"}}, + Food2: &food{[]string{"chicken", "fish"}}, + Complex: complex(42, 666), + Fun: func() { panic("x") }, + } + + var buf bytes.Buffer + if err := NewEncoder(&buf).Encode(x); err != nil { + t.Fatal(err) + } + + want := `Name = "Goblok" +Sound2 = "miauw" +Food = "chicken, fish" +Food2 = "chicken, fish" +Complex = "(42.000000+666.000000i)" +Fun = "why would you do this?" + +[Labels] + color = "black" + type = "cat" + +[Sound] + S = "miauw" +` + + if buf.String() != want { + t.Error("\n" + buf.String()) + } +} + // Would previously fail on 32bit architectures; can test with: // GOARCH=386 go test -c && ./toml.test // GOARCH=arm GOARM=7 go test -c && qemu-arm ./toml.test