Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Marshaler interface #327

Merged
merged 1 commit into from
Nov 16, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
39 changes: 15 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
@@ -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).

Expand All @@ -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
Expand All @@ -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
Expand All @@ -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:

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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}`.

6 changes: 6 additions & 0 deletions decode.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
44 changes: 30 additions & 14 deletions encode.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
}

Expand Down
62 changes: 62 additions & 0 deletions encode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down