Repository: BurntSushi/toml
Pull request #300 (2021-06-20 22:05:35)

Add Decoder struct and DecodeFS()
Refactor things a wee bit to add a decoder struct. This doesn't really
*do* anything right now, but will be needed to add options, such as
"strict mode" in #197 or "extended errors" in #299.

Also add DecodeFS() as a companion to DecodeFile() to read from a fs.FS,
and since using a reader is the default with NewDecoder() (and was
already easy to do before with strings.NewReader()) I marked
DecodeReader() as deprecated.


Patch:
diff --git a/decode.go b/decode.go
index 50a97fa..746a1f6 100644
--- a/decode.go
+++ b/decode.go
@@ -6,6 +6,7 @@ import (
 	"io"
 	"io/ioutil"
 	"math"
+	"os"
 	"reflect"
 	"strings"
 	"time"
@@ -57,8 +58,7 @@ func (md *MetaData) PrimitiveDecode(primValue Primitive, v interface{}) error {
 	return md.unify(primValue.undecoded, rvalue(v))
 }
 
-// Decode will decode the contents of `data` in TOML format into a pointer
-// `v`.
+// Decode TOML data.
 //
 // TOML hashes correspond to Go structs or maps. (Dealer's choice. They can be
 // used interchangeably.)
@@ -93,7 +93,17 @@ func (md *MetaData) PrimitiveDecode(primValue Primitive, v interface{}) error {
 //
 // This decoder will not handle cyclic types. If a cyclic type is passed,
 // `Decode` will not terminate.
-func Decode(data string, v interface{}) (MetaData, error) {
+type Decoder struct {
+	r io.Reader
+}
+
+// NewDecoder creates a new Decoder.
+func NewDecoder(r io.Reader) *Decoder {
+	return &Decoder{r: r}
+}
+
+// Decode TOML data in to the pointer `v`.
+func (dec *Decoder) Decode(v interface{}) (MetaData, error) {
 	rv := reflect.ValueOf(v)
 	if rv.Kind() != reflect.Ptr {
 		return MetaData{}, e("Decode of non-pointer %s", reflect.TypeOf(v))
@@ -101,7 +111,15 @@ func Decode(data string, v interface{}) (MetaData, error) {
 	if rv.IsNil() {
 		return MetaData{}, e("Decode of nil %s", reflect.TypeOf(v))
 	}
-	p, err := parse(data)
+
+	// TODO: have parser should read from io.Reader? Or at the very least, make
+	// it read from []byte rather than string
+	data, err := ioutil.ReadAll(dec.r)
+	if err != nil {
+		return MetaData{}, err
+	}
+
+	p, err := parse(string(data))
 	if err != nil {
 		return MetaData{}, err
 	}
@@ -112,24 +130,22 @@ func Decode(data string, v interface{}) (MetaData, error) {
 	return md, md.unify(p.mapping, indirect(rv))
 }
 
+// Decode the TOML data in to the pointer v.
+//
+// See Decoder for the full documentation.
+func Decode(data string, v interface{}) (MetaData, error) {
+	return NewDecoder(strings.NewReader(data)).Decode(v)
+}
+
 // DecodeFile is just like Decode, except it will automatically read the
 // contents of the file at `fpath` and decode it for you.
 func DecodeFile(fpath string, v interface{}) (MetaData, error) {
-	bs, err := ioutil.ReadFile(fpath)
-	if err != nil {
-		return MetaData{}, err
-	}
-	return Decode(string(bs), v)
-}
-
-// DecodeReader is just like Decode, except it will consume all bytes
-// from the reader and decode it for you.
-func DecodeReader(r io.Reader, v interface{}) (MetaData, error) {
-	bs, err := ioutil.ReadAll(r)
+	fp, err := os.Open(fpath)
 	if err != nil {
 		return MetaData{}, err
 	}
-	return Decode(string(bs), v)
+	defer fp.Close()
+	return NewDecoder(fp).Decode(v)
 }
 
 // unify performs a sort of type unification based on the structure of `rv`,
diff --git a/decode_go116.go b/decode_go116.go
new file mode 100644
index 0000000..8d5f73c
--- /dev/null
+++ b/decode_go116.go
@@ -0,0 +1,18 @@
+// +build go1.16
+
+package toml
+
+import (
+	"io/fs"
+)
+
+// DecodeFS is just like Decode, except it will automatically read the contents
+// of the file at `fpath` from a fs.FS instance.
+func DecodeFS(fsys fs.FS, fpath string, v interface{}) (MetaData, error) {
+	fp, err := fsys.Open(fpath)
+	if err != nil {
+		return MetaData{}, err
+	}
+	defer fp.Close()
+	return NewDecoder(fp).Decode(v)
+}
diff --git a/deprecated.go b/deprecated.go
index b4ed0f7..db89eac 100644
--- a/deprecated.go
+++ b/deprecated.go
@@ -1,6 +1,9 @@
 package toml
 
-import "encoding"
+import (
+	"encoding"
+	"io"
+)
 
 // DEPRECATED!
 //
@@ -21,3 +24,10 @@ func PrimitiveDecode(primValue Primitive, v interface{}) error {
 	md := MetaData{decoded: make(map[string]bool)}
 	return md.unify(primValue.undecoded, rvalue(v))
 }
+
+// DEPRECATED!
+//
+// Use NewDecoder(reader).Decode(&v) instead.
+func DecodeReader(r io.Reader, v interface{}) (MetaData, error) {
+	return NewDecoder(r).Decode(v)
+}


Test patch:
diff --git a/decode_go116_test.go b/decode_go116_test.go
new file mode 100644
index 0000000..5d36789
--- /dev/null
+++ b/decode_go116_test.go
@@ -0,0 +1,28 @@
+// +build go1.16
+
+package toml
+
+import (
+	"fmt"
+	"testing"
+	"testing/fstest"
+)
+
+func TestDecodeFS(t *testing.T) {
+	fsys := fstest.MapFS{
+		"test.toml": &fstest.MapFile{
+			Data: []byte("a = 42"),
+		},
+	}
+
+	var i struct{ A int }
+	meta, err := DecodeFS(fsys, "test.toml", &i)
+	if err != nil {
+		t.Fatal(err)
+	}
+	have := fmt.Sprintf("%v %v %v", i, meta.Keys(), meta.Type("a"))
+	want := "{42} [a] Integer"
+	if have != want {
+		t.Errorf("\nhave: %s\nwant: %s", have, want)
+	}
+}