1package config
2
3import (
4 "bytes"
5 "encoding/json"
6 "fmt"
7 "os"
8 "path/filepath"
9
10 "github.com/tidwall/gjson"
11 "github.com/tidwall/sjson"
12)
13
14// Store handles reading and writing raw config JSON data to persistent
15// storage.
16type Store interface {
17 Read() ([]byte, error)
18 Write(data []byte) error
19}
20
21// FileStore is a Store backed by a file on disk.
22type FileStore struct {
23 path string
24}
25
26// NewFileStore creates a new FileStore at the given path.
27func NewFileStore(path string) *FileStore {
28 return &FileStore{path: path}
29}
30
31// Read returns the raw bytes from the backing file. If the file does not
32// exist an empty JSON object is returned.
33func (s *FileStore) Read() ([]byte, error) {
34 data, err := os.ReadFile(s.path)
35 if err != nil {
36 if os.IsNotExist(err) {
37 return []byte("{}"), nil
38 }
39 return nil, fmt.Errorf("failed to read config file: %w", err)
40 }
41 return data, nil
42}
43
44// Write persists raw bytes to the backing file, creating parent directories
45// as needed. The JSON is pretty-printed with two-space indentation before
46// writing.
47func (s *FileStore) Write(data []byte) error {
48 var buf bytes.Buffer
49 if err := json.Indent(&buf, data, "", " "); err != nil {
50 return fmt.Errorf("failed to format config JSON: %w", err)
51 }
52 buf.WriteByte('\n')
53 if err := os.MkdirAll(filepath.Dir(s.path), 0o755); err != nil {
54 return fmt.Errorf("failed to create config directory %q: %w", s.path, err)
55 }
56 if err := os.WriteFile(s.path, buf.Bytes(), 0o600); err != nil {
57 return fmt.Errorf("failed to write config file: %w", err)
58 }
59 return nil
60}
61
62// HasField returns true if the given dotted key path exists in the store.
63func HasField(s Store, key string) bool {
64 data, err := s.Read()
65 if err != nil {
66 return false
67 }
68 return gjson.Get(string(data), key).Exists()
69}
70
71// SetField sets a value at the given dotted key path and persists it.
72func SetField(s Store, key string, value any) error {
73 data, err := s.Read()
74 if err != nil {
75 return err
76 }
77
78 updated, err := sjson.Set(string(data), key, value)
79 if err != nil {
80 return fmt.Errorf("failed to set config field %s: %w", key, err)
81 }
82 return s.Write([]byte(updated))
83}
84
85// RemoveField deletes a value at the given dotted key path and persists it.
86func RemoveField(s Store, key string) error {
87 data, err := s.Read()
88 if err != nil {
89 return err
90 }
91
92 updated, err := sjson.Delete(string(data), key)
93 if err != nil {
94 return fmt.Errorf("failed to delete config field %s: %w", key, err)
95 }
96 return s.Write([]byte(updated))
97}