/
fftoml.go
145 lines (129 loc) · 3.63 KB
/
fftoml.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
// Package fftoml provides a TOML config file paser.
package fftoml
import (
"fmt"
"io"
"strconv"
"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 {
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}
}
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
}
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
}
func valsToStrs(val interface{}) ([]string, error) {
if vals, ok := val.([]interface{}); ok {
ss := make([]string, len(vals))
for i := range vals {
s, err := valToStr(vals[i])
if err != nil {
return nil, err
}
ss[i] = s
}
return ss, nil
}
s, err := valToStr(val)
if err != nil {
return nil, err
}
return []string{s}, nil
}
func valToStr(val interface{}) (string, error) {
switch v := val.(type) {
case string:
return v, nil
case bool:
return strconv.FormatBool(v), nil
case uint64:
return strconv.FormatUint(v, 10), nil
case int64:
return strconv.FormatInt(v, 10), nil
case float64:
return strconv.FormatFloat(v, 'g', -1, 64), nil
default:
return "", ff.StringConversionError{Value: val}
}
}
// ParseError wraps all errors originating from the TOML parser.
type ParseError struct {
Inner error
}
// Error implenents the error interface.
func (e ParseError) Error() string {
return fmt.Sprintf("error parsing TOML config: %v", e.Inner)
}
// Unwrap implements the xerrors.Wrapper interface, allowing
// xerrors.Is and xerrors.As to work with ParseErrors.
func (e ParseError) Unwrap() error {
return e.Inner
}