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