Skip to content

Commit

Permalink
Add support for TOML tables to fftoml package (#48)
Browse files Browse the repository at this point in the history
* Add support for TOML tables to fftoml package

* Make fftoml flag name separator configurable

* Export fftoml config as ConfigFileParser and rename FlagSeparator as TableDelimeter

* Update fftoml/fftoml_test.go

Restructure call to ff.Parse

Co-Authored-By: Peter Bourgon <peterbourgon@users.noreply.github.com>

* Correct Delimeter -> Delimiter typo in fftoml package
  • Loading branch information
GeorgeMac authored and peterbourgon committed Dec 9, 2019
1 parent 20df174 commit c0f6605
Show file tree
Hide file tree
Showing 5 changed files with 154 additions and 15 deletions.
77 changes: 68 additions & 9 deletions fftoml/fftoml.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,86 @@ import (
"io"
"strconv"

"github.com/BurntSushi/toml"
"github.com/pelletier/go-toml"
"github.com/peterbourgon/ff"
)

// Parser is a parser for TOML file format. Flags and their values are read
// from the key/value pairs defined in the config file.
func Parser(r io.Reader, set func(name, value string) error) error {
var m map[string]interface{}
_, err := toml.DecodeReader(r, &m)
return New().Parse(r, set)
}

// ConfigFileParser is a parser for the TOML file format. Flags and their values
// are read from the key/value pairs defined in the config file.
// Nested tables and keys are concatenated with a delimiter to derive the
// relevant flag name.
type ConfigFileParser struct {
delimiter string
}

// New constructs and configures a ConfigFileParser using the provided options.
func New(opts ...Option) (c ConfigFileParser) {
c.delimiter = "."
for _, opt := range opts {
opt(&c)
}
return c
}

// Parse parses the provided io.Reader as a TOML file and uses the provided set function
// to set flag names derived from the tables names and their key/value pairs.
func (c ConfigFileParser) Parse(r io.Reader, set func(name, value string) error) error {
tree, err := toml.LoadReader(r)
if err != nil {
return ParseError{Inner: err}
}
for key, val := range m {
values, err := valsToStrs(val)
if err != nil {
return ParseError{Inner: err}

return parseTree(tree, "", c.delimiter, set)
}

// Option is a function which changes the behavior of the TOML config file parser.
type Option func(*ConfigFileParser)

// WithTableDelimiter is an option which configures a delimiter
// used to prefix table names onto keys when constructing
// their associated flag name.
// The default delimiter is "."
//
// For example, given the following TOML
//
// [section.subsection]
// value = 10
//
// Parse will match to a flag with the name `-section.subsection.value` by default.
// If the delimiter is "-", Parse will match to `-section-subsection-value` instead.
func WithTableDelimiter(d string) Option {
return func(c *ConfigFileParser) {
c.delimiter = d
}
}

func parseTree(tree *toml.Tree, parent, delimiter string, set func(name, value string) error) error {
for _, key := range tree.Keys() {
name := key
if parent != "" {
name = parent + delimiter + key
}
for _, value := range values {
if err = set(key, value); err != nil {
switch t := tree.Get(key).(type) {
case *toml.Tree:
if err := parseTree(t, name, delimiter, set); err != nil {
return err
}
case interface{}:
values, err := valsToStrs(t)
if err != nil {
return ParseError{Inner: err}
}
for _, value := range values {
if err = set(name, value); err != nil {
return err
}
}
}
}
return nil
Expand Down
73 changes: 71 additions & 2 deletions fftoml/fftoml_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package fftoml_test

import (
"flag"
"reflect"
"testing"
"time"

Expand All @@ -23,12 +25,19 @@ func TestParser(t *testing.T) {
{
name: "basic KV pairs",
file: "testdata/basic.toml",
want: fftest.Vars{S: "s", I: 10, F: 3.14e10, B: true, D: 5 * time.Second, X: []string{"1", "a", "👍"}},
want: fftest.Vars{
S: "s",
I: 10,
F: 3.14e10,
B: true,
D: 5 * time.Second,
X: []string{"1", "a", "👍"},
},
},
{
name: "bad TOML file",
file: "testdata/bad.toml",
want: fftest.Vars{WantParseErrorString: "bare keys cannot contain '{'"},
want: fftest.Vars{WantParseErrorString: "keys cannot contain { character"},
},
} {
t.Run(testcase.name, func(t *testing.T) {
Expand All @@ -43,3 +52,63 @@ func TestParser(t *testing.T) {
})
}
}

func TestParser_WithTables(t *testing.T) {
type fields struct {
String string
Float float64
Strings fftest.StringSlice
}

expected := fields{
String: "a string",
Float: 1.23,
Strings: fftest.StringSlice{"one", "two", "three"},
}

for _, testcase := range []struct {
name string
opts []fftoml.Option
// expectations
stringKey string
floatKey string
stringsKey string
}{
{
name: "defaults",
stringKey: "string.key",
floatKey: "float.nested.key",
stringsKey: "strings.nested.key",
},
{
name: "defaults",
opts: []fftoml.Option{fftoml.WithTableDelimiter("-")},
stringKey: "string-key",
floatKey: "float-nested-key",
stringsKey: "strings-nested-key",
},
} {
t.Run(testcase.name, func(t *testing.T) {
var (
found fields
fs = flag.NewFlagSet("fftest", flag.ContinueOnError)
)

fs.StringVar(&found.String, testcase.stringKey, "", "string")
fs.Float64Var(&found.Float, testcase.floatKey, 0, "float64")
fs.Var(&found.Strings, testcase.stringsKey, "string slice")

if err := ff.Parse(fs, []string{},
ff.WithConfigFile("testdata/table.toml"),
ff.WithConfigFileParser(fftoml.New(testcase.opts...).Parse),
); err != nil {
t.Fatal(err)
}

if !reflect.DeepEqual(expected, found) {
t.Errorf(`expected %v, to be %v`, found, expected)
}
})
}

}
7 changes: 7 additions & 0 deletions fftoml/testdata/table.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[string]
key = "a string"
[float]
[float.nested]
key = 1.23
[strings.nested]
key = ["one", "two", "three"]
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ module github.com/peterbourgon/ff
go 1.13

require (
github.com/BurntSushi/toml v0.3.1
github.com/mitchellh/go-wordwrap v1.0.0
github.com/pelletier/go-toml v1.6.0
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7
gopkg.in/yaml.v2 v2.2.2
gopkg.in/yaml.v2 v2.2.4
)
8 changes: 6 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4=
github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
github.com/pelletier/go-toml v1.6.0 h1:aetoXYr0Tv7xRU/V4B4IZJ2QcbtMUFoNb3ORp7TzIK4=
github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

0 comments on commit c0f6605

Please sign in to comment.