1package kvconf
2
3import (
4 "encoding"
5 "errors"
6 "fmt"
7 "math"
8 "reflect"
9 "slices"
10 "strconv"
11 "strings"
12 "sync"
13 "time"
14)
15
16// Struct tags understood by Decode and Marshal:
17//
18// kv:"name" the key; a nested struct prefixes its keys with "name."
19// kv:",inline" a nested struct whose keys are not prefixed
20// kv:"name,off" "off" sets the zero value (durations, numbers, strings)
21// kv:"name,nonempty" an empty value is rejected
22// kv:"bind.*" a map: bind.<name> = value fills map[name]. A map of
23// structs takes bind.<name>.<field>; names may hold dots
24// kv:"*" a []Entry or map[string]string that collects unknown keys
25// kv:"-" ignored, like untagged fields
26// min:"1" max:"100" bounds of a number or duration ("1s", "10m")
27// oneof:"a|b|c" the allowed values
28// sep:"|" the separator of a list (default ",")
29//
30// A value decodes into a string, a bool (on/off, true/false, yes/no, 1/0), an
31// integer, a float, a time.Duration, a slice of those, or any type whose
32// pointer implements encoding.TextUnmarshaler, such as [Path]. Named types
33// work too: type Mode string with oneof is an enum.
34
35// ValueError is an error message that is safe to show in a Warning: it
36// does not contain the value. Return one from UnmarshalText to explain a
37// rejected value; any other error is reported as "invalid <type>".
38type ValueError string
39
40func (e ValueError) Error() string { return string(e) }
41
42// Validator is implemented by a config that checks itself after decoding,
43// for rules that span several keys. Its error always fails the load.
44type Validator interface{ Validate() error }
45
46// Defaulter is implemented by a config, or by the element of a map of
47// structs, that fills in its defaults. Load calls it before decoding; a new
48// map element calls it before its first key is set.
49type Defaulter interface{ SetDefaults() }
50
51var (
52 durationType = reflect.TypeFor[time.Duration]()
53 unmarshalType = reflect.TypeFor[encoding.TextUnmarshaler]()
54 marshalType = reflect.TypeFor[encoding.TextMarshaler]()
55 entrySliceType = reflect.TypeFor[[]Entry]()
56 stringMapType = reflect.TypeFor[map[string]string]()
57 defaulterType = reflect.TypeFor[Defaulter]()
58 errTarget = errors.New("kvconf: target must be a non-nil pointer to a struct")
59)
60
61type field struct {
62 key string // full key; the prefix with its trailing dot for a map
63 index []int
64 typ reflect.Type
65 text bool // *typ implements encoding.TextUnmarshaler
66 marshal bool // typ or *typ implements encoding.TextMarshaler
67 duration bool
68 off bool
69 nonempty bool
70 hasMin bool
71 hasMax bool
72 min, max float64
73 minText string
74 maxText string
75 oneof []string
76 sep string
77 item *field // element of a slice
78 val *field // element of a map of values
79 elem *schema // element of a map of structs
80 defaults bool // map element implements Defaulter
81}
82
83type schema struct {
84 keys map[string]*field
85 order []*field // declaration order, for Marshal
86 prefixes []*field // maps, longest prefix first
87 catchAll *field
88}
89
90var schemas sync.Map // reflect.Type -> *schema or error
91
92func schemaOf(t reflect.Type) (*schema, error) {
93 if s, ok := schemas.Load(t); ok {
94 if err, bad := s.(error); bad {
95 return nil, err
96 }
97 return s.(*schema), nil
98 }
99 s, err := buildSchema(t)
100 if err != nil {
101 schemas.Store(t, err)
102 return nil, err
103 }
104 schemas.Store(t, s)
105 return s, nil
106}
107
108func buildSchema(t reflect.Type) (*schema, error) {
109 s := &schema{keys: map[string]*field{}}
110 if err := s.add(t, "", nil); err != nil {
111 return nil, err
112 }
113 slices.SortStableFunc(s.prefixes, func(a, b *field) int { return len(b.key) - len(a.key) })
114 return s, nil
115}
116
117func isText(t reflect.Type) bool { return reflect.PointerTo(t).Implements(unmarshalType) }
118
119func isLeaf(t reflect.Type) bool { return t.Kind() != reflect.Struct || isText(t) }
120
121func (s *schema) add(t reflect.Type, prefix string, index []int) error {
122 for i := range t.NumField() {
123 sf := t.Field(i)
124 tag, ok := sf.Tag.Lookup("kv")
125 if !ok || tag == "-" || !sf.IsExported() {
126 continue
127 }
128 where := t.String() + "." + sf.Name
129 name, flags, _ := strings.Cut(tag, ",")
130 idx := append(slices.Clip(index), i)
131 inline := false
132 var opts []string
133 for flag := range strings.SplitSeq(flags, ",") {
134 switch flag {
135 case "":
136 case "inline":
137 inline = true
138 case "off", "nonempty":
139 opts = append(opts, flag)
140 default:
141 return fmt.Errorf("kvconf: %s: unknown tag option %q", where, flag)
142 }
143 }
144 switch {
145 case name == "*":
146 if sf.Type != entrySliceType && sf.Type != stringMapType {
147 return fmt.Errorf("kvconf: %s: kv:\"*\" needs []kvconf.Entry or map[string]string", where)
148 }
149 if s.catchAll != nil {
150 return fmt.Errorf("kvconf: %s: a second kv:\"*\" field", where)
151 }
152 s.catchAll = &field{index: idx, typ: sf.Type}
153 case strings.HasSuffix(name, ".*"):
154 if sf.Type.Kind() != reflect.Map || sf.Type.Key().Kind() != reflect.String {
155 return fmt.Errorf("kvconf: %s: %q needs a map with string keys", where, name)
156 }
157 et := sf.Type.Elem()
158 f := &field{}
159 if isLeaf(et) {
160 val, err := newField(where, et, sf.Tag, opts)
161 if err != nil {
162 return err
163 }
164 f.val = val
165 } else {
166 elem, err := buildSchema(et)
167 if err != nil {
168 return err
169 }
170 if elem.catchAll != nil || len(elem.prefixes) > 0 {
171 return fmt.Errorf("kvconf: %s: a map element cannot hold maps or kv:\"*\"", where)
172 }
173 f = &field{elem: elem, defaults: reflect.PointerTo(et).Implements(defaulterType)}
174 }
175 f.key, f.index, f.typ = prefix+strings.TrimSuffix(name, "*"), idx, sf.Type
176 s.prefixes = append(s.prefixes, f)
177 s.order = append(s.order, f)
178 case !isLeaf(sf.Type):
179 p := prefix
180 if !inline {
181 if name == "" {
182 return fmt.Errorf("kvconf: %s: a nested struct needs a name or inline", where)
183 }
184 p += name + "."
185 }
186 if err := s.add(sf.Type, p, idx); err != nil {
187 return err
188 }
189 default:
190 if name == "" {
191 return fmt.Errorf("kvconf: %s: empty key", where)
192 }
193 f, err := newField(where, sf.Type, sf.Tag, opts)
194 if err != nil {
195 return err
196 }
197 f.key, f.index = prefix+name, idx
198 if _, dup := s.keys[f.key]; dup {
199 return fmt.Errorf("kvconf: %s: key %q declared twice", where, f.key)
200 }
201 s.keys[f.key] = f
202 s.order = append(s.order, f)
203 }
204 }
205 return nil
206}
207
208// newField checks that t can be decoded and precomputes how.
209func newField(where string, t reflect.Type, tag reflect.StructTag, opts []string) (*field, error) {
210 f := &field{typ: t, text: isText(t), duration: t == durationType, sep: tag.Get("sep"),
211 marshal: t.Implements(marshalType) || reflect.PointerTo(t).Implements(marshalType)}
212 f.off = slices.Contains(opts, "off")
213 f.nonempty = slices.Contains(opts, "nonempty")
214 if f.sep == "" {
215 f.sep = ","
216 }
217 if o := tag.Get("oneof"); o != "" {
218 f.oneof = strings.Split(o, "|")
219 }
220 kind := t.Kind()
221 if !f.text {
222 switch kind {
223 case reflect.String, reflect.Bool, reflect.Float32, reflect.Float64,
224 reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
225 reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
226 case reflect.Slice:
227 item, err := newField(where, t.Elem(), tag, nil)
228 if err != nil {
229 return nil, err
230 }
231 if item.item != nil {
232 return nil, fmt.Errorf("kvconf: %s: nested lists are not supported", where)
233 }
234 // The item reads oneof, min and max from the same tag and
235 // checks each element against them.
236 f.item, f.oneof = item, nil
237 return f, nil
238 default:
239 return nil, fmt.Errorf("kvconf: %s: unsupported type %s", where, t)
240 }
241 }
242 bound := func(s string) (float64, error) {
243 if f.duration {
244 d, err := time.ParseDuration(s)
245 return float64(d), err
246 }
247 return strconv.ParseFloat(s, 64)
248 }
249 var err error
250 if f.minText = tag.Get("min"); f.minText != "" {
251 f.hasMin = true
252 if f.min, err = bound(f.minText); err != nil {
253 return nil, fmt.Errorf("kvconf: %s: bad min %q", where, f.minText)
254 }
255 }
256 if f.maxText = tag.Get("max"); f.maxText != "" {
257 f.hasMax = true
258 if f.max, err = bound(f.maxText); err != nil {
259 return nil, fmt.Errorf("kvconf: %s: bad max %q", where, f.maxText)
260 }
261 }
262 return f, nil
263}
264
265// Unmarshal parses data and decodes it into v, a pointer to a struct. Fill v
266// with its defaults first: keys absent from data keep their value.
267func Unmarshal(data []byte, v any, opts Options) ([]Warning, error) {
268 entries, warnings, err := ParseBytes(data, opts)
269 if err != nil {
270 return warnings, err
271 }
272 more, err := Decode(entries, v, opts)
273 return append(warnings, more...), err
274}
275
276// Decode sets the fields of v, a pointer to a struct, from entries in order,
277// so a repeated key ends with its last valid value. An invalid value is
278// reported and leaves its field unchanged, or fails the decode in strict
279// mode. An unknown key is reported unless Options.AllowUnknown is set or a
280// kv:"*" field collects it. Validate runs last when v implements Validator.
281func Decode(entries []Entry, v any, opts Options) ([]Warning, error) {
282 rv := reflect.ValueOf(v)
283 if rv.Kind() != reflect.Pointer || rv.IsNil() || rv.Elem().Kind() != reflect.Struct {
284 return nil, errTarget
285 }
286 s, err := schemaOf(rv.Elem().Type())
287 if err != nil {
288 return nil, err
289 }
290 st := decodeState{root: rv.Elem()}
291 p := problems{strict: opts.Strict}
292 for i := range entries {
293 e := &entries[i]
294 msg, known := s.apply(&st, e)
295 if !known {
296 if opts.AllowUnknown {
297 continue
298 }
299 msg = "unknown key"
300 }
301 if msg != "" && !p.add(e.Line, e.Key, msg) {
302 return p.list, p.err()
303 }
304 }
305 st.flush()
306 if val, ok := v.(Validator); ok {
307 if err := val.Validate(); err != nil {
308 return p.list, err
309 }
310 }
311 return p.list, nil
312}
313
314// decodeState holds the scratch values of one Decode call, so a key of a
315// map does not allocate.
316type decodeState struct {
317 root reflect.Value
318 // scratch key and element per map field, reused for every entry.
319 keys, vals map[*field]reflect.Value
320 // pending struct elements, written back to their maps once at the end
321 // so each name is copied out of its map once, not once per key.
322 pending map[*field]map[string]*pendingElem
323}
324
325type pendingElem struct {
326 val reflect.Value
327 set bool // at least one key was accepted
328}
329
330func (st *decodeState) scratch(f *field) (key, val reflect.Value) {
331 if st.keys == nil {
332 st.keys, st.vals = map[*field]reflect.Value{}, map[*field]reflect.Value{}
333 }
334 key, ok := st.keys[f]
335 if !ok {
336 key = reflect.New(f.typ.Key()).Elem()
337 val = reflect.New(f.typ.Elem()).Elem()
338 st.keys[f], st.vals[f] = key, val
339 }
340 return key, st.vals[f]
341}
342
343// element returns the addressable pending element name of the map field f.
344func (st *decodeState) element(f *field, m reflect.Value, name string) *pendingElem {
345 if st.pending == nil {
346 st.pending = map[*field]map[string]*pendingElem{}
347 }
348 byName := st.pending[f]
349 if byName == nil {
350 byName = map[string]*pendingElem{}
351 st.pending[f] = byName
352 }
353 if p, ok := byName[name]; ok {
354 return p
355 }
356 p := &pendingElem{val: reflect.New(f.typ.Elem()).Elem()}
357 key, _ := st.scratch(f)
358 key.SetString(name)
359 if cur := m.MapIndex(key); cur.IsValid() {
360 p.val.Set(cur)
361 } else if f.defaults {
362 p.val.Addr().Interface().(Defaulter).SetDefaults()
363 }
364 byName[name] = p
365 return p
366}
367
368// flush writes the pending struct elements back to their maps.
369func (st *decodeState) flush() {
370 for f, byName := range st.pending {
371 m := st.root.FieldByIndex(f.index)
372 key, _ := st.scratch(f)
373 for name, p := range byName {
374 if p.set {
375 key.SetString(name)
376 m.SetMapIndex(key, p.val)
377 }
378 }
379 }
380}
381
382// apply sets the field of e. msg explains a rejected value; known is false
383// when no field takes the key.
384func (s *schema) apply(st *decodeState, e *Entry) (msg string, known bool) {
385 root := st.root
386 if f := s.keys[e.Key]; f != nil {
387 return f.set(root.FieldByIndex(f.index), e.Value), true
388 }
389 for _, f := range s.prefixes {
390 rest, ok := strings.CutPrefix(e.Key, f.key)
391 if !ok || rest == "" {
392 continue
393 }
394 name, sub := rest, (*field)(nil)
395 if f.elem != nil {
396 // Names may contain dots: the longest field suffix wins.
397 for _, ef := range f.elem.order {
398 n, ok := strings.CutSuffix(rest, ef.key)
399 if ok && len(n) > 1 && n[len(n)-1] == '.' && (sub == nil || len(ef.key) > len(sub.key)) {
400 name, sub = n[:len(n)-1], ef
401 }
402 }
403 if sub == nil {
404 continue
405 }
406 }
407 m := root.FieldByIndex(f.index)
408 if m.IsNil() {
409 m.Set(reflect.MakeMap(f.typ))
410 }
411 if sub != nil {
412 // A name whose every key is rejected is not created.
413 p := st.element(f, m, name)
414 if msg = sub.set(p.val.FieldByIndex(sub.index), e.Value); msg == "" {
415 p.set = true
416 }
417 return msg, true
418 }
419 if f.typ == stringMapType && f.val.plain() {
420 m.Interface().(map[string]string)[name] = e.Value
421 return "", true
422 }
423 key, val := st.scratch(f)
424 if msg = f.val.set(val, e.Value); msg == "" {
425 key.SetString(name)
426 m.SetMapIndex(key, val)
427 }
428 return msg, true
429 }
430 if f := s.catchAll; f != nil {
431 dst := root.FieldByIndex(f.index)
432 if f.typ == entrySliceType {
433 list := dst.Addr().Interface().(*[]Entry)
434 *list = append(*list, *e)
435 } else {
436 m := dst.Addr().Interface().(*map[string]string)
437 if *m == nil {
438 *m = map[string]string{}
439 }
440 (*m)[e.Key] = e.Value
441 }
442 return "", true
443 }
444 return "", false
445}
446
447// set decodes raw into dst, leaving dst unchanged on failure. The message
448// never contains raw: values may be secrets.
449func (f *field) set(dst reflect.Value, raw string) string {
450 if f.off && raw == "off" {
451 dst.SetZero()
452 return ""
453 }
454 if f.nonempty && raw == "" {
455 return "must not be empty"
456 }
457 if err := f.decode(dst, raw); err != nil {
458 return err.Error()
459 }
460 return ""
461}
462
463func (f *field) decode(dst reflect.Value, raw string) error {
464 if f.oneof != nil && !slices.Contains(f.oneof, raw) {
465 return errors.New("must be " + orList(f.oneof))
466 }
467 if f.text {
468 p := reflect.New(f.typ)
469 if err := p.Interface().(encoding.TextUnmarshaler).UnmarshalText([]byte(raw)); err != nil {
470 // Third-party errors often quote the input: show only ours.
471 var ve ValueError
472 if errors.As(err, &ve) {
473 return ve
474 }
475 return errors.New("invalid " + f.typ.String())
476 }
477 dst.Set(p.Elem())
478 return nil
479 }
480 switch f.typ.Kind() {
481 case reflect.String:
482 dst.SetString(raw)
483 case reflect.Bool:
484 b, err := ParseBool(raw)
485 if err != nil {
486 return err
487 }
488 dst.SetBool(b)
489 case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
490 var n int64
491 var err error
492 if f.duration {
493 var d time.Duration
494 if d, err = time.ParseDuration(raw); err != nil {
495 return errors.New("must be a duration such as 500ms, 5m or 1h30m")
496 }
497 n = int64(d)
498 } else if n, err = strconv.ParseInt(raw, 10, f.typ.Bits()); err != nil {
499 return errors.New("must be an integer")
500 }
501 if err := f.check(float64(n)); err != nil {
502 return err
503 }
504 dst.SetInt(n)
505 case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
506 n, err := strconv.ParseUint(raw, 10, f.typ.Bits())
507 if err != nil {
508 return errors.New("must be a non-negative integer")
509 }
510 if err := f.check(float64(n)); err != nil {
511 return err
512 }
513 dst.SetUint(n)
514 case reflect.Float32, reflect.Float64:
515 x, err := strconv.ParseFloat(raw, f.typ.Bits())
516 if err != nil {
517 return errors.New("must be a number")
518 }
519 if err := f.check(x); err != nil {
520 return err
521 }
522 dst.SetFloat(x)
523 case reflect.Slice:
524 n := 0
525 for part := range strings.SplitSeq(raw, f.sep) {
526 if strings.TrimSpace(part) != "" {
527 n++
528 }
529 }
530 list := reflect.MakeSlice(f.typ, n, n)
531 i := 0
532 for part := range strings.SplitSeq(raw, f.sep) {
533 if part = strings.TrimSpace(part); part == "" {
534 continue
535 }
536 if err := f.item.decode(list.Index(i), part); err != nil {
537 return fmt.Errorf("item %d: %w", i+1, err)
538 }
539 i++
540 }
541 dst.Set(list)
542 }
543 return nil
544}
545
546// plain reports a string field without any constraint.
547func (f *field) plain() bool {
548 return f.typ.Kind() == reflect.String && !f.text && !f.off && !f.nonempty && f.oneof == nil
549}
550
551func (f *field) check(x float64) error {
552 if x != x || (f.hasMin || f.hasMax) && math.IsInf(x, 0) {
553 return errors.New("must be a finite number")
554 }
555 if f.hasMin && x < f.min {
556 return errors.New("must be at least " + f.minText)
557 }
558 if f.hasMax && x > f.max {
559 return errors.New("must be at most " + f.maxText)
560 }
561 return nil
562}
563
564func orList(items []string) string {
565 if len(items) == 1 {
566 return items[0]
567 }
568 return strings.Join(items[:len(items)-1], ", ") + " or " + items[len(items)-1]
569}
570
571// ParseBool accepts on/off, true/false, yes/no and 1/0, in any case.
572func ParseBool(s string) (bool, error) {
573 switch {
574 case s == "on" || s == "1" || strings.EqualFold(s, "on") || strings.EqualFold(s, "true") || strings.EqualFold(s, "yes"):
575 return true, nil
576 case s == "off" || s == "0" || strings.EqualFold(s, "off") || strings.EqualFold(s, "false") || strings.EqualFold(s, "no"):
577 return false, nil
578 }
579 return false, errors.New("must be on or off")
580}