v0.1.0 3b1b6ef

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}
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}

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