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