v0.1.2 1e7884d

decode.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 3

↑↓ moveenter openesc close