-
Notifications
You must be signed in to change notification settings - Fork 10
/
load.go
307 lines (256 loc) · 7.12 KB
/
load.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
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
package conf
import (
"bytes"
"context"
"errors"
"flag"
"fmt"
"os"
"path/filepath"
"reflect"
"sort"
"strings"
"gopkg.in/go-playground/mold.v2/modifiers"
validator "gopkg.in/validator.v2"
// Load all default adapters of the objconv package.
_ "github.com/segmentio/objconv/adapters"
"github.com/segmentio/objconv/yaml"
)
var (
// Modifier is the default modification lib using the "mod" tag; it is
// exposed to allow registering of custom modifiers and aliases or to
// be set to a more central instance located in another repo.
Modifier = modifiers.New()
)
// Load the program's configuration into cfg, and returns the list of leftover
// arguments.
//
// The cfg argument is expected to be a pointer to a struct type where exported
// fields or fields with a "conf" tag will be used to load the program
// configuration.
// The function panics if cfg is not a pointer to struct, or if it's a nil
// pointer.
//
// The configuration is loaded from the command line, environment and optional
// configuration file if the -config-file option is present in the program
// arguments.
//
// Values found in the program arguments take precedence over those found in
// the environment, which takes precedence over the configuration file.
//
// If an error is detected with the configurable the function print the usage
// message to stderr and exit with status code 1.
func Load(cfg interface{}) (args []string) {
_, args = LoadWith(cfg, DefaultLoader)
return
}
// LoadWith behaves like Load but uses ld as a loader to parse the program
// configuration.
//
// The function panics if cfg is not a pointer to struct, or if it's a nil
// pointer and no commands were set.
func LoadWith(cfg interface{}, ld Loader) (cmd string, args []string) {
var err error
switch cmd, args, err = ld.Load(cfg); err {
case nil:
case flag.ErrHelp:
ld.PrintHelp(cfg)
os.Exit(0)
default:
ld.PrintHelp(cfg)
ld.PrintError(err)
os.Exit(1)
}
return
}
// A Command represents a command supported by a configuration loader.
type Command struct {
Name string // name of the command
Help string // help message describing what the command does
}
// A Loader exposes an API for customizing how a configuration is loaded and
// where it's loaded from.
type Loader struct {
Name string // program name
Usage string // program usage
Args []string // list of arguments
Commands []Command // list of commands
Sources []Source // list of sources to load configuration from.
}
// Load uses the loader ld to load the program configuration into cfg, and
// returns the list of program arguments that were not used.
//
// The function returns flag.ErrHelp when the list of arguments contained -h,
// -help, or --help.
//
// The cfg argument is expected to be a pointer to a struct type where exported
// fields or fields with a "conf" tag will be used to load the program
// configuration.
// The function panics if cfg is not a pointer to struct, or if it's a nil
// pointer and no commands were set.
func (ld Loader) Load(cfg interface{}) (cmd string, args []string, err error) {
var v reflect.Value
if cfg == nil {
v = reflect.ValueOf(&struct{}{})
} else {
v = reflect.ValueOf(cfg)
}
if v.Kind() != reflect.Ptr {
panic(fmt.Sprintf("cannot load configuration into non-pointer type: %T", cfg))
}
if v.IsNil() {
panic(fmt.Sprintf("cannot load configuration into nil pointer of type: %T", cfg))
}
if v = v.Elem(); v.Kind() != reflect.Struct {
panic(fmt.Sprintf("cannot load configuration into non-struct pointer: %T", cfg))
}
if len(ld.Commands) != 0 {
if len(ld.Args) == 0 {
err = errors.New("missing command")
return
}
found := false
for _, c := range ld.Commands {
if c.Name == ld.Args[0] {
found, cmd, ld.Args = true, ld.Args[0], ld.Args[1:]
break
}
}
if !found {
err = errors.New("unknown command: " + ld.Args[0])
return
}
if cfg == nil {
args = ld.Args
return
}
}
if args, err = ld.load(v); err != nil {
return
}
if err = Modifier.Struct(context.Background(), cfg); err != nil {
return
}
if err = validator.Validate(v.Interface()); err != nil {
err = makeValidationError(err, v.Type())
}
return
}
func (ld Loader) load(cfg reflect.Value) (args []string, err error) {
node := makeNodeStruct(cfg, cfg.Type())
set := newFlagSet(node, ld.Name, ld.Sources...)
// Parse the arguments a first time so the sources that implement the
// FlagSource interface get their values loaded.
if err = set.Parse(ld.Args); err != nil {
return
}
// Load the configuration from the sources that have been configured on the
// loader.
// Order is important here because the values will get overwritten by each
// source that loads the configuration.
for _, source := range ld.Sources {
if err = source.Load(node); err != nil {
return
}
}
// Parse the arguments a second time to overwrite values loaded by sources
// which were also passed to the program arguments.
if err = set.Parse(ld.Args); err != nil {
return
}
args = set.Args()
return
}
var DefaultLoader Loader
func init() {
env := os.Environ()
args := os.Args
DefaultLoader = defaultLoader(args, env)
}
func defaultLoader(args []string, env []string) Loader {
var name = filepath.Base(args[0])
return Loader{
Name: name,
Args: args[1:],
Sources: []Source{
NewFileSource("config-file", makeEnvVars(env), os.ReadFile, yaml.Unmarshal),
NewEnvSource(name, env...),
},
}
}
func makeEnvVars(env []string) (vars map[string]string) {
vars = make(map[string]string)
for _, e := range env {
var k string
var v string
if off := strings.IndexByte(e, '='); off >= 0 {
k, v = e[:off], e[off+1:]
} else {
k = e
}
vars[k] = v
}
return vars
}
func makeValidationError(err error, typ reflect.Type) error {
if errmap, ok := err.(validator.ErrorMap); ok {
errkeys := make([]string, 0, len(errmap))
errlist := make(errorList, 0, len(errmap))
for errkey := range errmap {
errkeys = append(errkeys, errkey)
}
sort.Strings(errkeys)
for _, errkey := range errkeys {
path := fieldPath(typ, errkey)
if len(errmap[errkey]) == 1 {
errlist = append(errlist, fmt.Errorf("invalid value passed to %s: %s", path, errmap[errkey][0]))
} else {
buf := &bytes.Buffer{}
fmt.Fprintf(buf, "invalid value passed to %s: ", path)
for i, errval := range errmap[errkey] {
if i != 0 {
buf.WriteString("; ")
}
buf.WriteString(errval.Error())
}
errlist = append(errlist, errors.New(buf.String()))
}
}
err = errlist
}
return err
}
type errorList []error
func (err errorList) Error() string {
if len(err) > 0 {
return err[0].Error()
}
return ""
}
func fieldPath(typ reflect.Type, path string) string {
var name string
if sep := strings.IndexByte(path, '.'); sep >= 0 {
name, path = path[:sep], path[sep+1:]
} else {
name, path = path, ""
}
if field, ok := typ.FieldByName(name); ok {
name = field.Tag.Get("conf")
if len(name) == 0 {
name = field.Name
} else if name == "_" {
name = ""
}
if len(path) != 0 {
path = fieldPath(field.Type, path)
}
}
if len(path) != 0 {
if len(name) == 0 {
name = path
} else {
name += "." + path
}
}
return name
}