1package kvconf
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "io/fs"
8 "slices"
9 "sync/atomic"
10)
11
12// Load reads the config file at path into a new T: it starts from the
13// defaults (SetDefaults when *T implements Defaulter), then applies the file.
14// A missing file gives the defaults, unless opts.RequireFile is set.
15func Load[T any](path string, opts Options) (*T, []Warning, error) {
16 cfg, _, warnings, err := load[T](path, opts)
17 return cfg, warnings, err
18}
19
20// load returns nil entries, not an empty slice, when the file is missing.
21func load[T any](path string, opts Options) (*T, []Entry, []Warning, error) {
22 data, err := ReadFile(path, opts)
23 if err != nil {
24 if isNotExist(err) && !opts.RequireFile {
25 // No entries: the defaults, then Validate.
26 cfg := newDefault[T]()
27 if _, err := Decode(nil, cfg, opts); err != nil {
28 return nil, nil, nil, err
29 }
30 return cfg, nil, nil, nil
31 }
32 return nil, nil, nil, err
33 }
34 cfg, entries, warnings, err := decodeFile[T](path, data, opts)
35 if err == nil && entries == nil {
36 entries = []Entry{}
37 }
38 return cfg, entries, warnings, err
39}
40
41func decodeFile[T any](path string, data []byte, opts Options) (*T, []Entry, []Warning, error) {
42 cfg := newDefault[T]()
43 entries, warnings, err := ParseBytes(data, opts)
44 if err != nil {
45 return nil, nil, warnings, fmt.Errorf("%s: %w", path, err)
46 }
47 more, err := Decode(entries, cfg, opts)
48 warnings = append(warnings, more...)
49 if err != nil {
50 return nil, nil, warnings, fmt.Errorf("%s: %w", path, err)
51 }
52 return cfg, entries, warnings, nil
53}
54
55func newDefault[T any]() *T {
56 cfg := new(T)
57 if d, ok := any(cfg).(Defaulter); ok {
58 d.SetDefaults()
59 }
60 return cfg
61}
62
63// Change describes a reload.
64type Change[T any] struct {
65 Old, New *T
66 // Keys lists, sorted, the keys added, removed or changed.
67 Keys []string
68 Warnings []Warning
69}
70
71// LiveOptions controls a Live config.
72type LiveOptions[T any] struct {
73 Options
74 Watch WatchOptions
75 // OnChange is called from Run after each reload that changed a value.
76 OnChange func(Change[T])
77 // OnError is called from Run when a reload fails; the current config
78 // stays in place. A removed file reports an error wrapping
79 // fs.ErrNotExist.
80 OnError func(error)
81}
82
83// Live is a config that reloads itself when its file changes. Get is safe
84// for concurrent use; a config it returned is never modified, a reload
85// replaces it.
86type Live[T any] struct {
87 path string
88 opts LiveOptions[T]
89 cur atomic.Pointer[T]
90 entries []Entry // owned by Run
91 exists bool // whether the file was there at the last load; owned by Run
92 running atomic.Bool
93}
94
95// Open loads the config file at path and returns it with the warnings of
96// the first load. Call Run to follow its changes.
97func Open[T any](path string, opts LiveOptions[T]) (*Live[T], []Warning, error) {
98 cfg, entries, warnings, err := load[T](path, opts.Options)
99 if err != nil {
100 return nil, warnings, err
101 }
102 l := &Live[T]{path: path, opts: opts, entries: entries, exists: entries != nil}
103 l.cur.Store(cfg)
104 return l, warnings, nil
105}
106
107// Get returns the current config. Do not modify it.
108func (l *Live[T]) Get() *T { return l.cur.Load() }
109
110// Path returns the path of the config file.
111func (l *Live[T]) Path() string { return l.path }
112
113// Run watches the file and reloads it until ctx ends. A reload that fails,
114// or a removed file, keeps the current config. A save that changes no value
115// (a comment, spacing, the same file written again) reloads nothing. Run
116// returns nil when ctx ends, or the error of the watcher. Call it once.
117func (l *Live[T]) Run(ctx context.Context) error {
118 if !l.running.CompareAndSwap(false, true) {
119 return errors.New("kvconf: Live.Run called twice")
120 }
121 changed := make(chan struct{}, 1)
122 ready := make(chan struct{})
123 wopts := l.opts.Watch
124 userReady := wopts.Ready
125 wopts.Ready = ready
126 errc := make(chan error, 1)
127 go func() { errc <- Watch(ctx, l.path, wopts, changed) }()
128 select {
129 case <-ready:
130 // A save between Open and the watch would go unseen: check once.
131 l.reload()
132 if userReady != nil {
133 close(userReady)
134 }
135 case err := <-errc:
136 return err
137 }
138 for {
139 select {
140 case <-changed:
141 l.reload()
142 case err := <-errc:
143 return err
144 }
145 }
146}
147
148func (l *Live[T]) reload() {
149 data, err := ReadFile(l.path, l.opts.Options)
150 if isNotExist(err) {
151 // Keep the current config: a removed file is often about to be
152 // replaced, and an empty config is rarely what the user meant.
153 if l.exists {
154 l.exists = false
155 l.report(fmt.Errorf("%s: %w", l.path, fs.ErrNotExist))
156 }
157 return
158 }
159 if err != nil {
160 l.report(err)
161 return
162 }
163 cfg, entries, warnings, err := decodeFile[T](l.path, data, l.opts.Options)
164 if err != nil {
165 l.report(err)
166 return
167 }
168 l.exists = true
169 // Compare every assignment, not the last one: a repeated key whose
170 // last value is invalid decodes to an earlier one.
171 keys := changedKeys(l.entries, entries)
172 l.entries = entries
173 if len(keys) == 0 {
174 return
175 }
176 old := l.cur.Swap(cfg)
177 if l.opts.OnChange != nil {
178 l.opts.OnChange(Change[T]{Old: old, New: cfg, Keys: keys, Warnings: warnings})
179 }
180}
181
182// changedKeys lists, sorted, the keys whose assignments differ.
183func changedKeys(old, cur []Entry) []string {
184 group := func(es []Entry) map[string][]string {
185 m := make(map[string][]string, len(es))
186 for _, e := range es {
187 m[e.Key] = append(m[e.Key], e.Value)
188 }
189 return m
190 }
191 a, b := group(old), group(cur)
192 var keys []string
193 for k, v := range b {
194 if !slices.Equal(a[k], v) {
195 keys = append(keys, k)
196 }
197 }
198 for k := range a {
199 if _, ok := b[k]; !ok {
200 keys = append(keys, k)
201 }
202 }
203 slices.Sort(keys)
204 return keys
205}
206
207func (l *Live[T]) report(err error) {
208 if l.opts.OnError != nil {
209 l.opts.OnError(err)
210 }
211}