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 item.oneof, f.oneof = f.oneof, nil
235 f.item = item
236 default:
237 return nil, fmt.Errorf("kvconf: %s: unsupported type %s", where, t)
238 }
239 }
240 bound := func(s string) (float64, error) {
241 if f.duration || f.item != nil && f.item.duration {
242 d, err := time.ParseDuration(s)
243 return float64(d), err
244 }
245 return strconv.ParseFloat(s, 64)
246 }
247 var err error
248 if f.minText = tag.Get("min"); f.minText != "" {
249 f.hasMin = true
250 if f.min, err = bound(f.minText); err != nil {
251 return nil, fmt.Errorf("kvconf: %s: bad min %q", where, f.minText)
252 }
253 }
254 if f.maxText = tag.Get("max"); f.maxText != "" {
255 f.hasMax = true
256 if f.max, err = bound(f.maxText); err != nil {
257 return nil, fmt.Errorf("kvconf: %s: bad max %q", where, f.maxText)
258 }
259 }
260 if f.item != nil {
261 f.item.hasMin, f.item.min, f.item.minText = f.hasMin, f.min, f.minText
262 f.item.hasMax, f.item.max, f.item.maxText = f.hasMax, f.max, f.maxText
263 f.hasMin, f.hasMax = false, false
264 }
265 return f, nil
266}
267
268// Unmarshal parses data and decodes it into v, a pointer to a struct. Fill v
269// with its defaults first: keys absent from data keep their value.
270func Unmarshal(data []byte, v any, opts Options) ([]Warning, error) {
271 entries, warnings, err := ParseBytes(data, opts)
272 if err != nil {
273 return warnings, err
274 }
275 more, err := Decode(entries, v, opts)
276 return append(warnings, more...), err
277}
278
279// Decode sets the fields of v, a pointer to a struct, from entries in order,
280// so a repeated key ends with its last valid value. An invalid value is
281// reported and leaves its field unchanged, or fails the decode in strict
282// mode. An unknown key is reported unless Options.AllowUnknown is set or a
283// kv:"*" field collects it. Validate runs last when v implements Validator.
284func Decode(entries []Entry, v any, opts Options) ([]Warning, error) {
285 rv := reflect.ValueOf(v)
286 if rv.Kind() != reflect.Pointer || rv.IsNil() || rv.Elem().Kind() != reflect.Struct {
287 return nil, errTarget
288 }
289 s, err := schemaOf(rv.Elem().Type())
290 if err != nil {
291 return nil, err
292 }
293 st := decodeState{root: rv.Elem()}
294 p := problems{strict: opts.Strict}
295 for i := range entries {
296 e := &entries[i]
297 msg, known := s.apply(&st, e)
298 if !known {
299 if opts.AllowUnknown {
300 continue
301 }
302 msg = "unknown key"
303 }
304 if msg != "" && !p.add(e.Line, e.Key, msg) {
305 return p.list, p.err()
306 }
307 }
308 st.flush()
309 if val, ok := v.(Validator); ok {
310 if err := val.Validate(); err != nil {
311 return p.list, err
312 }
313 }
314 return p.list, nil
315}
316
317// decodeState holds the scratch values of one Decode call, so a key of a
318// map does not allocate.
319type decodeState struct {
320 root reflect.Value
321 // scratch key and element per map field, reused for every entry.
322 keys, vals map[*field]reflect.Value
323 // pending struct elements, written back to their maps once at the end
324 // so each name is copied out of its map once, not once per key.
325 pending map[*field]map[string]*pendingElem
326}
327
328type pendingElem struct {
329 val reflect.Value
330 set bool // at least one key was accepted
331}
332
333func (st *decodeState) scratch(f *field) (key, val reflect.Value) {
334 if st.keys == nil {
335 st.keys, st.vals = map[*field]reflect.Value{}, map[*field]reflect.Value{}
336 }
337 key, ok := st.keys[f]
338 if !ok {
339 key = reflect.New(f.typ.Key()).Elem()
340 val = reflect.New(f.typ.Elem()).Elem()
341 st.keys[f], st.vals[f] = key, val
342 }
343 return key, st.vals[f]
344}
345
346// element returns the addressable pending element name of the map field f.
347func (st *decodeState) element(f *field, m reflect.Value, name string) *pendingElem {
348 if st.pending == nil {
349 st.pending = map[*field]map[string]*pendingElem{}
350 }
351 byName := st.pending[f]
352 if byName == nil {
353 byName = map[string]*pendingElem{}
354 st.pending[f] = byName
355 }
356 if p, ok := byName[name]; ok {
357 return p
358 }
359 p := &pendingElem{val: reflect.New(f.typ.Elem()).Elem()}
360 key, _ := st.scratch(f)
361 key.SetString(name)
362 if cur := m.MapIndex(key); cur.IsValid() {
363 p.val.Set(cur)
364 } else if f.defaults {
365 p.val.Addr().Interface().(Defaulter).SetDefaults()
366 }
367 byName[name] = p
368 return p
369}
370
371// flush writes the pending struct elements back to their maps.
372func (st *decodeState) flush() {
373 for f, byName := range st.pending {
374 m := st.root.FieldByIndex(f.index)
375 key, _ := st.scratch(f)
376 for name, p := range byName {
377 if p.set {
378 key.SetString(name)
379 m.SetMapIndex(key, p.val)
380 }
381 }
382 }
383}
384
385// apply sets the field of e. msg explains a rejected value; known is false
386// when no field takes the key.
387func (s *schema) apply(st *decodeState, e *Entry) (msg string, known bool) {
388 root := st.root
389 if f := s.keys[e.Key]; f != nil {
390 return f.set(root.FieldByIndex(f.index), e.Value), true
391 }
392 for _, f := range s.prefixes {
393 rest, ok := strings.CutPrefix(e.Key, f.key)
394 if !ok || rest == "" {
395 continue
396 }
397 name, sub := rest, (*field)(nil)
398 if f.elem != nil {
399 // Names may contain dots: the longest field suffix wins.
400 for _, ef := range f.elem.order {
401 n, ok := strings.CutSuffix(rest, ef.key)
402 if ok && len(n) > 1 && n[len(n)-1] == '.' && (sub == nil || len(ef.key) > len(sub.key)) {
403 name, sub = n[:len(n)-1], ef
404 }
405 }
406 if sub == nil {
407 continue
408 }
409 }
410 m := root.FieldByIndex(f.index)
411 if m.IsNil() {
412 m.Set(reflect.MakeMap(f.typ))
413 }
414 if sub != nil {
415 // A name whose every key is rejected is not created.
416 p := st.element(f, m, name)
417 if msg = sub.set(p.val.FieldByIndex(sub.index), e.Value); msg == "" {
418 p.set = true
419 }
420 return msg, true
421 }
422 if f.typ == stringMapType && f.val.plain() {
423 (*(*map[string]string)(m.Addr().UnsafePointer()))[name] = e.Value
424 return "", true
425 }
426 key, val := st.scratch(f)
427 if msg = f.val.set(val, e.Value); msg == "" {
428 key.SetString(name)
429 m.SetMapIndex(key, val)
430 }
431 return msg, true
432 }
433 if f := s.catchAll; f != nil {
434 dst := root.FieldByIndex(f.index)
435 if f.typ == entrySliceType {
436 list := dst.Addr().Interface().(*[]Entry)
437 *list = append(*list, *e)
438 } else {
439 m := dst.Addr().Interface().(*map[string]string)
440 if *m == nil {
441 *m = map[string]string{}
442 }
443 (*m)[e.Key] = e.Value
444 }
445 return "", true
446 }
447 return "", false
448}
449
450// set decodes raw into dst, leaving dst unchanged on failure. The message
451// never contains raw: values may be secrets.
452func (f *field) set(dst reflect.Value, raw string) string {
453 if f.off && raw == "off" {
454 dst.SetZero()
455 return ""
456 }
457 if f.nonempty && raw == "" {
458 return "must not be empty"
459 }
460 if err := f.decode(dst, raw); err != nil {
461 return err.Error()
462 }
463 return ""
464}
465
466func (f *field) decode(dst reflect.Value, raw string) error {
467 if f.oneof != nil && !slices.Contains(f.oneof, raw) {
468 return errors.New("must be " + orList(f.oneof))
469 }
470 if f.text {
471 p := reflect.New(f.typ)
472 if err := p.Interface().(encoding.TextUnmarshaler).UnmarshalText([]byte(raw)); err != nil {
473 // Third-party errors often quote the input: show only ours.
474 var ve ValueError
475 if errors.As(err, &ve) {
476 return ve
477 }
478 return errors.New("invalid " + f.typ.String())
479 }
480 dst.Set(p.Elem())
481 return nil
482 }
483 switch f.typ.Kind() {
484 case reflect.String:
485 dst.SetString(raw)
486 case reflect.Bool:
487 b, err := ParseBool(raw)
488 if err != nil {
489 return err
490 }
491 dst.SetBool(b)
492 case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
493 var n int64
494 var err error
495 if f.duration {
496 var d time.Duration
497 if d, err = time.ParseDuration(raw); err != nil {
498 return errors.New("must be a duration such as 500ms, 5m or 1h30m")
499 }
500 n = int64(d)
501 } else if n, err = strconv.ParseInt(raw, 10, f.typ.Bits()); err != nil {
502 return errors.New("must be an integer")
503 }
504 if err := f.check(float64(n)); err != nil {
505 return err
506 }
507 dst.SetInt(n)
508 case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
509 n, err := strconv.ParseUint(raw, 10, f.typ.Bits())
510 if err != nil {
511 return errors.New("must be a non-negative integer")
512 }
513 if err := f.check(float64(n)); err != nil {
514 return err
515 }
516 dst.SetUint(n)
517 case reflect.Float32, reflect.Float64:
518 x, err := strconv.ParseFloat(raw, f.typ.Bits())
519 if err != nil {
520 return errors.New("must be a number")
521 }
522 if err := f.check(x); err != nil {
523 return err
524 }
525 dst.SetFloat(x)
526 case reflect.Slice:
527 n := 0
528 for part := range strings.SplitSeq(raw, f.sep) {
529 if strings.TrimSpace(part) != "" {
530 n++
531 }
532 }
533 list := reflect.MakeSlice(f.typ, n, n)
534 i := 0
535 for part := range strings.SplitSeq(raw, f.sep) {
536 if part = strings.TrimSpace(part); part == "" {
537 continue
538 }
539 if err := f.item.decode(list.Index(i), part); err != nil {
540 return fmt.Errorf("item %d: %w", i+1, err)
541 }
542 i++
543 }
544 dst.Set(list)
545 }
546 return nil
547}
548
549// plain reports a string field without any constraint.
550func (f *field) plain() bool {
551 return f.typ.Kind() == reflect.String && !f.text && !f.off && !f.nonempty && f.oneof == nil
552}
553
554func (f *field) check(x float64) error {
555 if x != x || (f.hasMin || f.hasMax) && math.IsInf(x, 0) {
556 return errors.New("must be a finite number")
557 }
558 if f.hasMin && x < f.min {
559 return errors.New("must be at least " + f.minText)
560 }
561 if f.hasMax && x > f.max {
562 return errors.New("must be at most " + f.maxText)
563 }
564 return nil
565}
566
567func orList(items []string) string {
568 if len(items) == 1 {
569 return items[0]
570 }
571 return strings.Join(items[:len(items)-1], ", ") + " or " + items[len(items)-1]
572}
573
574// ParseBool accepts on/off, true/false, yes/no and 1/0, in any case.
575func ParseBool(s string) (bool, error) {
576 switch {
577 case s == "on" || s == "1" || strings.EqualFold(s, "on") || strings.EqualFold(s, "true") || strings.EqualFold(s, "yes"):
578 return true, nil
579 case s == "off" || s == "0" || strings.EqualFold(s, "off") || strings.EqualFold(s, "false") || strings.EqualFold(s, "no"):
580 return false, nil
581 }
582 return false, errors.New("must be on or off")
583}