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 elems reflect.Type // []element of a map of structs
82 slot int // index of a map field in schema.prefixes
83}
84
85type schema struct {
86 keys map[string]*field
87 order []*field // declaration order, for Marshal
88 prefixes []*field // maps, longest prefix first
89 catchAll *field
90}
91
92var schemas sync.Map // reflect.Type -> *schema or error
93
94func schemaOf(t reflect.Type) (*schema, error) {
95 if s, ok := schemas.Load(t); ok {
96 if err, bad := s.(error); bad {
97 return nil, err
98 }
99 return s.(*schema), nil
100 }
101 s, err := buildSchema(t)
102 if err != nil {
103 schemas.Store(t, err)
104 return nil, err
105 }
106 schemas.Store(t, s)
107 return s, nil
108}
109
110func buildSchema(t reflect.Type) (*schema, error) {
111 s := &schema{keys: map[string]*field{}}
112 if err := s.add(t, "", nil); err != nil {
113 return nil, err
114 }
115 slices.SortStableFunc(s.prefixes, func(a, b *field) int { return len(b.key) - len(a.key) })
116 for i, f := range s.prefixes {
117 f.slot = i
118 }
119 return s, nil
120}
121
122func isText(t reflect.Type) bool { return reflect.PointerTo(t).Implements(unmarshalType) }
123
124func isLeaf(t reflect.Type) bool { return t.Kind() != reflect.Struct || isText(t) }
125
126func (s *schema) add(t reflect.Type, prefix string, index []int) error {
127 for i := range t.NumField() {
128 sf := t.Field(i)
129 tag, ok := sf.Tag.Lookup("kv")
130 if !ok || tag == "-" || !sf.IsExported() {
131 continue
132 }
133 where := t.String() + "." + sf.Name
134 name, flags, _ := strings.Cut(tag, ",")
135 idx := append(slices.Clip(index), i)
136 inline := false
137 var opts []string
138 for flag := range strings.SplitSeq(flags, ",") {
139 switch flag {
140 case "":
141 case "inline":
142 inline = true
143 case "off", "nonempty":
144 opts = append(opts, flag)
145 default:
146 return fmt.Errorf("kvconf: %s: unknown tag option %q", where, flag)
147 }
148 }
149 switch {
150 case name == "*":
151 if sf.Type != entrySliceType && sf.Type != stringMapType {
152 return fmt.Errorf("kvconf: %s: kv:\"*\" needs []kvconf.Entry or map[string]string", where)
153 }
154 if s.catchAll != nil {
155 return fmt.Errorf("kvconf: %s: a second kv:\"*\" field", where)
156 }
157 s.catchAll = &field{index: idx, typ: sf.Type}
158 case strings.HasSuffix(name, ".*"):
159 if sf.Type.Kind() != reflect.Map || sf.Type.Key().Kind() != reflect.String {
160 return fmt.Errorf("kvconf: %s: %q needs a map with string keys", where, name)
161 }
162 et := sf.Type.Elem()
163 f := &field{}
164 if isLeaf(et) {
165 val, err := newField(where, et, sf.Tag, opts)
166 if err != nil {
167 return err
168 }
169 f.val = val
170 } else {
171 elem, err := buildSchema(et)
172 if err != nil {
173 return err
174 }
175 if elem.catchAll != nil || len(elem.prefixes) > 0 {
176 return fmt.Errorf("kvconf: %s: a map element cannot hold maps or kv:\"*\"", where)
177 }
178 f = &field{elem: elem, elems: reflect.SliceOf(et), defaults: reflect.PointerTo(et).Implements(defaulterType)}
179 }
180 f.key, f.index, f.typ = prefix+strings.TrimSuffix(name, "*"), idx, sf.Type
181 s.prefixes = append(s.prefixes, f)
182 s.order = append(s.order, f)
183 case !isLeaf(sf.Type):
184 p := prefix
185 if !inline {
186 if name == "" {
187 return fmt.Errorf("kvconf: %s: a nested struct needs a name or inline", where)
188 }
189 p += name + "."
190 }
191 if err := s.add(sf.Type, p, idx); err != nil {
192 return err
193 }
194 default:
195 if name == "" {
196 return fmt.Errorf("kvconf: %s: empty key", where)
197 }
198 f, err := newField(where, sf.Type, sf.Tag, opts)
199 if err != nil {
200 return err
201 }
202 f.key, f.index = prefix+name, idx
203 if _, dup := s.keys[f.key]; dup {
204 return fmt.Errorf("kvconf: %s: key %q declared twice", where, f.key)
205 }
206 s.keys[f.key] = f
207 s.order = append(s.order, f)
208 }
209 }
210 return nil
211}
212
213// newField checks that t can be decoded and precomputes how.
214func newField(where string, t reflect.Type, tag reflect.StructTag, opts []string) (*field, error) {
215 f := &field{typ: t, text: isText(t), duration: t == durationType, sep: tag.Get("sep"),
216 marshal: t.Implements(marshalType) || reflect.PointerTo(t).Implements(marshalType)}
217 f.off = slices.Contains(opts, "off")
218 f.nonempty = slices.Contains(opts, "nonempty")
219 if f.sep == "" {
220 f.sep = ","
221 }
222 if o := tag.Get("oneof"); o != "" {
223 f.oneof = strings.Split(o, "|")
224 }
225 kind := t.Kind()
226 if !f.text {
227 switch kind {
228 case reflect.String, reflect.Bool, reflect.Float32, reflect.Float64,
229 reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
230 reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
231 case reflect.Slice:
232 item, err := newField(where, t.Elem(), tag, nil)
233 if err != nil {
234 return nil, err
235 }
236 if item.item != nil {
237 return nil, fmt.Errorf("kvconf: %s: nested lists are not supported", where)
238 }
239 // The item reads oneof, min and max from the same tag and
240 // checks each element against them.
241 f.item, f.oneof = item, nil
242 return f, nil
243 default:
244 return nil, fmt.Errorf("kvconf: %s: unsupported type %s", where, t)
245 }
246 }
247 bound := func(s string) (float64, error) {
248 if f.duration {
249 d, err := time.ParseDuration(s)
250 return float64(d), err
251 }
252 return strconv.ParseFloat(s, 64)
253 }
254 var err error
255 if f.minText = tag.Get("min"); f.minText != "" {
256 f.hasMin = true
257 if f.min, err = bound(f.minText); err != nil {
258 return nil, fmt.Errorf("kvconf: %s: bad min %q", where, f.minText)
259 }
260 }
261 if f.maxText = tag.Get("max"); f.maxText != "" {
262 f.hasMax = true
263 if f.max, err = bound(f.maxText); err != nil {
264 return nil, fmt.Errorf("kvconf: %s: bad max %q", where, f.maxText)
265 }
266 }
267 return f, nil
268}
269
270// Unmarshal parses data and decodes it into v, a pointer to a struct. Fill v
271// with its defaults first: keys absent from data keep their value.
272func Unmarshal(data []byte, v any, opts Options) ([]Warning, error) {
273 entries, warnings, err := ParseBytes(data, opts)
274 if err != nil {
275 return warnings, err
276 }
277 more, err := Decode(entries, v, opts)
278 return append(warnings, more...), err
279}
280
281// Decode sets the fields of v, a pointer to a struct, from entries in order,
282// so a repeated key ends with its last valid value. An invalid value is
283// reported and leaves its field unchanged, or fails the decode in strict
284// mode. An unknown key is reported unless Options.AllowUnknown is set or a
285// kv:"*" field collects it. Validate runs last when v implements Validator.
286func Decode(entries []Entry, v any, opts Options) ([]Warning, error) {
287 rv := reflect.ValueOf(v)
288 if rv.Kind() != reflect.Pointer || rv.IsNil() || rv.Elem().Kind() != reflect.Struct {
289 return nil, errTarget
290 }
291 s, err := schemaOf(rv.Elem().Type())
292 if err != nil {
293 return nil, err
294 }
295 st := decodeState{root: rv.Elem(), entries: entries}
296 p := problems{strict: opts.Strict}
297 for i := range entries {
298 st.next = i
299 e := &entries[i]
300 msg, known := s.apply(&st, e)
301 if !known {
302 if opts.AllowUnknown {
303 continue
304 }
305 msg = "unknown key"
306 }
307 if msg != "" && !p.add(e.Line, e.Key, msg) {
308 return p.list, p.err()
309 }
310 }
311 st.flush(s)
312 if val, ok := v.(Validator); ok {
313 if err := val.Validate(); err != nil {
314 return p.list, err
315 }
316 }
317 return p.list, nil
318}
319
320// decodeState holds the scratch values of one Decode call, so a key of a
321// map does not allocate.
322type decodeState struct {
323 root reflect.Value
324 entries []Entry
325 next int // index in entries of the entry being applied
326 maps []mapState // by field.slot; nil until a key reaches a map
327}
328
329// mapState decodes the keys of one kv:"x.*" field.
330type mapState struct {
331 m reflect.Value // the map field; a map of values is created on first use
332 plain map[string]string // m, for the map[string]string fast path
333 hint int // upper bound of the keys reaching it, to size its storage once
334 key, val reflect.Value // scratch, reused for every entry
335 // Struct elements are decoded in elems, then written back once by
336 // flush, so each name is copied out of the map once, not once per key.
337 elems reflect.Value // []element
338 names []pendingName // names[i] is elems[i]
339 index map[string]int
340}
341
342type pendingName struct {
343 name string
344 set bool // at least one key was accepted
345}
346
347const (
348 // linearNames is how many names are searched linearly before index
349 // is built.
350 linearNames = 16
351 // maxElemHint caps the elements of a map of structs allocated up front.
352 maxElemHint = 64
353)
354
355// mapFor returns the state of the map field f. The first call sets every
356// map's hint from the entries left, so a config without map keys never
357// counts. A key is counted for every prefix it starts with, so a key that
358// apply passes on to a shorter prefix is counted there.
359func (st *decodeState) mapFor(s *schema, f *field) *mapState {
360 if st.maps == nil {
361 st.maps = make([]mapState, len(s.prefixes))
362 for i := st.next; i < len(st.entries); i++ {
363 key := st.entries[i].Key
364 for _, p := range s.prefixes {
365 if len(key) > len(p.key) && key[:len(p.key)] == p.key {
366 st.maps[p.slot].hint++
367 }
368 }
369 }
370 }
371 ms := &st.maps[f.slot]
372 if !ms.m.IsValid() {
373 ms.m = st.root.FieldByIndex(f.index)
374 if f.elem != nil {
375 // A name sets some of its fields: guess half of them per name,
376 // capped, as elements are whole structs. elems grows if needed.
377 ms.hint = min(2*ms.hint/max(len(f.elem.order), 1)+1, maxElemHint)
378 }
379 if ms.m.IsNil() {
380 // Created even if every key is rejected, as before a strict error.
381 // A map of structs gets its names in flush: hint is only a guess.
382 size := ms.hint
383 if f.elem != nil {
384 size = 0
385 }
386 ms.m.Set(reflect.MakeMapWithSize(f.typ, size))
387 }
388 if f.elem == nil {
389 if f.typ == stringMapType && f.val.plain() {
390 ms.plain = ms.m.Interface().(map[string]string)
391 } else {
392 ms.val = reflect.New(f.typ.Elem()).Elem()
393 }
394 }
395 if ms.plain == nil {
396 ms.key = reflect.New(f.typ.Key()).Elem()
397 }
398 }
399 return ms
400}
401
402// element returns the index in ms.elems of the pending element name.
403func (ms *mapState) element(f *field, name string) int {
404 if ms.index != nil {
405 if i, ok := ms.index[name]; ok {
406 return i
407 }
408 } else {
409 for i := range ms.names {
410 if ms.names[i].name == name {
411 return i
412 }
413 }
414 }
415 i := len(ms.names)
416 if !ms.elems.IsValid() {
417 ms.elems = reflect.MakeSlice(f.elems, ms.hint, ms.hint)
418 ms.names = make([]pendingName, 0, ms.hint)
419 } else if i == ms.elems.Len() {
420 grown := reflect.MakeSlice(f.elems, 2*i, 2*i)
421 reflect.Copy(grown, ms.elems)
422 ms.elems = grown
423 }
424 ms.names = append(ms.names, pendingName{name: name})
425 if ms.index != nil {
426 ms.index[name] = i
427 } else if len(ms.names) > linearNames {
428 ms.index = make(map[string]int, ms.hint)
429 for j, p := range ms.names {
430 ms.index[p.name] = j
431 }
432 }
433 ev := ms.elems.Index(i)
434 ms.key.SetString(name)
435 if cur := ms.m.MapIndex(ms.key); cur.IsValid() {
436 ev.Set(cur)
437 } else if f.defaults {
438 ev.Addr().Interface().(Defaulter).SetDefaults()
439 }
440 return i
441}
442
443// flush writes the pending struct elements back to their maps.
444func (st *decodeState) flush(s *schema) {
445 for slot := range st.maps {
446 ms, f := &st.maps[slot], s.prefixes[slot]
447 if f.elem == nil || !ms.m.IsValid() {
448 continue
449 }
450 for i, p := range ms.names {
451 if p.set {
452 ms.key.SetString(p.name)
453 ms.m.SetMapIndex(ms.key, ms.elems.Index(i))
454 }
455 }
456 }
457}
458
459// apply sets the field of e. msg explains a rejected value; known is false
460// when no field takes the key.
461func (s *schema) apply(st *decodeState, e *Entry) (msg string, known bool) {
462 root := st.root
463 if f := s.keys[e.Key]; f != nil {
464 return f.set(root.FieldByIndex(f.index), e.Value), true
465 }
466 for _, f := range s.prefixes {
467 rest, ok := strings.CutPrefix(e.Key, f.key)
468 if !ok || rest == "" {
469 continue
470 }
471 name, sub := rest, (*field)(nil)
472 if f.elem != nil {
473 // Names may contain dots: the longest field suffix wins.
474 for _, ef := range f.elem.order {
475 n, ok := strings.CutSuffix(rest, ef.key)
476 if ok && len(n) > 1 && n[len(n)-1] == '.' && (sub == nil || len(ef.key) > len(sub.key)) {
477 name, sub = n[:len(n)-1], ef
478 }
479 }
480 if sub == nil {
481 continue
482 }
483 }
484 ms := st.mapFor(s, f)
485 if sub != nil {
486 // A name whose every key is rejected is not created.
487 i := ms.element(f, name)
488 if msg = sub.set(ms.elems.Index(i).FieldByIndex(sub.index), e.Value); msg == "" {
489 ms.names[i].set = true
490 }
491 return msg, true
492 }
493 if ms.plain != nil {
494 ms.plain[name] = e.Value
495 return "", true
496 }
497 if msg = f.val.set(ms.val, e.Value); msg == "" {
498 ms.key.SetString(name)
499 ms.m.SetMapIndex(ms.key, ms.val)
500 }
501 return msg, true
502 }
503 if f := s.catchAll; f != nil {
504 dst := root.FieldByIndex(f.index)
505 if f.typ == entrySliceType {
506 list := dst.Addr().Interface().(*[]Entry)
507 *list = append(*list, *e)
508 } else {
509 m := dst.Addr().Interface().(*map[string]string)
510 if *m == nil {
511 *m = map[string]string{}
512 }
513 (*m)[e.Key] = e.Value
514 }
515 return "", true
516 }
517 return "", false
518}
519
520// set decodes raw into dst, leaving dst unchanged on failure. The message
521// never contains raw: values may be secrets.
522func (f *field) set(dst reflect.Value, raw string) string {
523 if f.off && raw == "off" {
524 dst.SetZero()
525 return ""
526 }
527 if f.nonempty && raw == "" {
528 return "must not be empty"
529 }
530 if err := f.decode(dst, raw); err != nil {
531 return err.Error()
532 }
533 return ""
534}
535
536func (f *field) decode(dst reflect.Value, raw string) error {
537 if f.oneof != nil && !slices.Contains(f.oneof, raw) {
538 return errors.New("must be " + orList(f.oneof))
539 }
540 if f.text {
541 p := reflect.New(f.typ)
542 if err := p.Interface().(encoding.TextUnmarshaler).UnmarshalText([]byte(raw)); err != nil {
543 // Third-party errors often quote the input: show only ours.
544 var ve ValueError
545 if errors.As(err, &ve) {
546 return ve
547 }
548 return errors.New("invalid " + f.typ.String())
549 }
550 dst.Set(p.Elem())
551 return nil
552 }
553 switch f.typ.Kind() {
554 case reflect.String:
555 dst.SetString(raw)
556 case reflect.Bool:
557 b, err := ParseBool(raw)
558 if err != nil {
559 return err
560 }
561 dst.SetBool(b)
562 case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
563 var n int64
564 var err error
565 if f.duration {
566 var d time.Duration
567 if d, err = time.ParseDuration(raw); err != nil {
568 return errors.New("must be a duration such as 500ms, 5m or 1h30m")
569 }
570 n = int64(d)
571 } else if n, err = strconv.ParseInt(raw, 10, f.typ.Bits()); err != nil {
572 return errors.New("must be an integer")
573 }
574 if err := f.check(float64(n)); err != nil {
575 return err
576 }
577 dst.SetInt(n)
578 case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
579 n, err := strconv.ParseUint(raw, 10, f.typ.Bits())
580 if err != nil {
581 return errors.New("must be a non-negative integer")
582 }
583 if err := f.check(float64(n)); err != nil {
584 return err
585 }
586 dst.SetUint(n)
587 case reflect.Float32, reflect.Float64:
588 x, err := strconv.ParseFloat(raw, f.typ.Bits())
589 if err != nil {
590 return errors.New("must be a number")
591 }
592 if err := f.check(x); err != nil {
593 return err
594 }
595 dst.SetFloat(x)
596 case reflect.Slice:
597 n := 0
598 for part := range strings.SplitSeq(raw, f.sep) {
599 if strings.TrimSpace(part) != "" {
600 n++
601 }
602 }
603 list := reflect.MakeSlice(f.typ, n, n)
604 i := 0
605 for part := range strings.SplitSeq(raw, f.sep) {
606 if part = strings.TrimSpace(part); part == "" {
607 continue
608 }
609 if err := f.item.decode(list.Index(i), part); err != nil {
610 return fmt.Errorf("item %d: %w", i+1, err)
611 }
612 i++
613 }
614 dst.Set(list)
615 }
616 return nil
617}
618
619// plain reports a string field without any constraint.
620func (f *field) plain() bool {
621 return f.typ.Kind() == reflect.String && !f.text && !f.off && !f.nonempty && f.oneof == nil
622}
623
624func (f *field) check(x float64) error {
625 if x != x || (f.hasMin || f.hasMax) && math.IsInf(x, 0) {
626 return errors.New("must be a finite number")
627 }
628 if f.hasMin && x < f.min {
629 return errors.New("must be at least " + f.minText)
630 }
631 if f.hasMax && x > f.max {
632 return errors.New("must be at most " + f.maxText)
633 }
634 return nil
635}
636
637func orList(items []string) string {
638 if len(items) == 1 {
639 return items[0]
640 }
641 return strings.Join(items[:len(items)-1], ", ") + " or " + items[len(items)-1]
642}
643
644// ParseBool accepts on/off, true/false, yes/no and 1/0, in any case.
645func ParseBool(s string) (bool, error) {
646 switch {
647 case s == "on" || s == "1" || strings.EqualFold(s, "on") || strings.EqualFold(s, "true") || strings.EqualFold(s, "yes"):
648 return true, nil
649 case s == "off" || s == "0" || strings.EqualFold(s, "off") || strings.EqualFold(s, "false") || strings.EqualFold(s, "no"):
650 return false, nil
651 }
652 return false, errors.New("must be on or off")
653}