store.go

 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}