1// Package fftoml provides a TOML config file paser. 2package fftoml 3 4import ( 5 "fmt" 6 "io" 7 "strconv" 8 9 "github.com/pelletier/go-toml" 10 "github.com/peterbourgon/ff/v3" 11) 12 13// Parser is a parser for TOML file format. Flags and their values are read 14// from the key/value pairs defined in the config file. 15func Parser(r io.Reader, set func(name, value string) error) error { 16 return New().Parse(r, set) 17} 18 19// ConfigFileParser is a parser for the TOML file format. Flags and their values 20// are read from the key/value pairs defined in the config file. 21// Nested tables and keys are concatenated with a delimiter to derive the 22// relevant flag name. 23type ConfigFileParser struct { 24 delimiter string 25} 26 27// New constructs and configures a ConfigFileParser using the provided options. 28func New(opts ...Option) (c ConfigFileParser) { 29 c.delimiter = "." 30 for _, opt := range opts { 31 opt(&c) 32 } 33 return c 34} 35 36// Parse parses the provided io.Reader as a TOML file and uses the provided set function 37// to set flag names derived from the tables names and their key/value pairs. 38func (c ConfigFileParser) Parse(r io.Reader, set func(name, value string) error) error { 39 tree, err := toml.LoadReader(r) 40 if err != nil { 41 return ParseError{Inner: err} 42 } 43 44 return parseTree(tree, "", c.delimiter, set) 45} 46 47// Option is a function which changes the behavior of the TOML config file parser. 48type Option func(*ConfigFileParser) 49 50// WithTableDelimiter is an option which configures a delimiter 51// used to prefix table names onto keys when constructing 52// their associated flag name. 53// The default delimiter is "." 54// 55// For example, given the following TOML 56// 57// [section.subsection] 58// value = 10 59// 60// Parse will match to a flag with the name `-section.subsection.value` by default. 61// If the delimiter is "-", Parse will match to `-section-subsection-value` instead. 62func WithTableDelimiter(d string) Option { 63 return func(c *ConfigFileParser) { 64 c.delimiter = d 65 } 66} 67 68func parseTree(tree *toml.Tree, parent, delimiter string, set func(name, value string) error) error { 69 for _, key := range tree.Keys() { 70 name := key 71 if parent != "" { 72 name = parent + delimiter + key 73 } 74 switch t := tree.Get(key).(type) { 75 case *toml.Tree: 76 if err := parseTree(t, name, delimiter, set); err != nil { 77 return err 78 } 79 case interface{}: 80 values, err := valsToStrs(t) 81 if err != nil { 82 return ParseError{Inner: err} 83 } 84 for _, value := range values { 85 if err = set(name, value); err != nil { 86 return err 87 } 88 } 89 } 90 } 91 return nil 92} 93 94func valsToStrs(val interface{}) ([]string, error) { 95 if vals, ok := val.([]interface{}); ok { 96 ss := make([]string, len(vals)) 97 for i := range vals { 98 s, err := valToStr(vals[i]) 99 if err != nil { 100 return nil, err 101 } 102 ss[i] = s 103 } 104 return ss, nil 105 } 106 s, err := valToStr(val) 107 if err != nil { 108 return nil, err 109 } 110 return []string{s}, nil 111 112} 113 114func valToStr(val interface{}) (string, error) { 115 switch v := val.(type) { 116 case string: 117 return v, nil 118 case bool: 119 return strconv.FormatBool(v), nil 120 case uint64: 121 return strconv.FormatUint(v, 10), nil 122 case int64: 123 return strconv.FormatInt(v, 10), nil 124 case float64: 125 return strconv.FormatFloat(v, 'g', -1, 64), nil 126 default: 127 return "", ff.StringConversionError{Value: val} 128 } 129} 130 131// ParseError wraps all errors originating from the TOML parser. 132type ParseError struct { 133 Inner error 134} 135 136// Error implenents the error interface. 137func (e ParseError) Error() string { 138 return fmt.Sprintf("error parsing TOML config: %v", e.Inner) 139} 140 141// Unwrap implements the errors.Wrapper interface, allowing errors.Is and 142// errors.As to work with ParseErrors. 143func (e ParseError) Unwrap() error { 144 return e.Inner 145} 146