Mining/pkg/mining/repository.go
Claude 1ad014e71c
ax(mining): replace prose comments with usage examples in repository.go
Three comments on Path, Exists, and Delete restated what the signatures
already convey. Replaced with concrete call examples per AX Principle 2.

Co-Authored-By: Charon <charon@lethean.io>
2026-04-02 09:04:34 +01:00

160 lines
3.6 KiB
Go

package mining
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"sync"
)
// Repository defines a generic interface for data persistence.
// Implementations can store data in files, databases, etc.
type Repository[T any] interface {
// data, err := repo.Load()
Load() (T, error)
// repo.Save(updated)
Save(data T) error
// repo.Update(func(d *T) error { d.Field = value; return nil })
Update(fn func(*T) error) error
}
// FileRepository provides atomic file-based persistence for JSON data.
// It uses atomic writes (temp file + rename) to prevent corruption.
type FileRepository[T any] struct {
mu sync.RWMutex
path string
defaults func() T
}
// FileRepositoryOption configures a FileRepository.
type FileRepositoryOption[T any] func(*FileRepository[T])
// repo := NewFileRepository[MinersConfig](path, WithDefaults(defaultMinersConfig))
func WithDefaults[T any](fn func() T) FileRepositoryOption[T] {
return func(r *FileRepository[T]) {
r.defaults = fn
}
}
// repo := NewFileRepository[MinersConfig](path, WithDefaults(defaultMinersConfig))
func NewFileRepository[T any](path string, options ...FileRepositoryOption[T]) *FileRepository[T] {
r := &FileRepository[T]{
path: path,
}
for _, option := range options {
option(r)
}
return r
}
// data, err := repo.Load()
// if err != nil { return defaults, err }
func (r *FileRepository[T]) Load() (T, error) {
r.mu.RLock()
defer r.mu.RUnlock()
var result T
data, err := os.ReadFile(r.path)
if err != nil {
if os.IsNotExist(err) {
if r.defaults != nil {
return r.defaults(), nil
}
return result, nil
}
return result, fmt.Errorf("failed to read file: %w", err)
}
if err := json.Unmarshal(data, &result); err != nil {
return result, fmt.Errorf("failed to unmarshal data: %w", err)
}
return result, nil
}
// if err := repo.Save(updated); err != nil { return fmt.Errorf("save: %w", err) }
func (r *FileRepository[T]) Save(data T) error {
r.mu.Lock()
defer r.mu.Unlock()
return r.saveUnlocked(data)
}
// saveUnlocked saves data without acquiring the lock (caller must hold lock).
func (r *FileRepository[T]) saveUnlocked(data T) error {
dir := filepath.Dir(r.path)
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("failed to create directory: %w", err)
}
jsonData, err := json.MarshalIndent(data, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal data: %w", err)
}
return AtomicWriteFile(r.path, jsonData, 0600)
}
// repo.Update(func(cfg *MinersConfig) error {
// cfg.Miners = append(cfg.Miners, entry)
// return nil
// })
func (r *FileRepository[T]) Update(fn func(*T) error) error {
r.mu.Lock()
defer r.mu.Unlock()
// Load current data
var data T
fileData, err := os.ReadFile(r.path)
if err != nil {
if os.IsNotExist(err) {
if r.defaults != nil {
data = r.defaults()
}
} else {
return fmt.Errorf("failed to read file: %w", err)
}
} else {
if err := json.Unmarshal(fileData, &data); err != nil {
return fmt.Errorf("failed to unmarshal data: %w", err)
}
}
// Apply modification
if err := fn(&data); err != nil {
return err
}
// Save atomically
return r.saveUnlocked(data)
}
// path := repo.Path() // => "/home/user/.config/lethean-desktop/miners.json"
func (r *FileRepository[T]) Path() string {
return r.path
}
// if !repo.Exists() { return defaults, nil }
func (r *FileRepository[T]) Exists() bool {
r.mu.RLock()
defer r.mu.RUnlock()
_, err := os.Stat(r.path)
return err == nil
}
// if err := repo.Delete(); err != nil { return err }
func (r *FileRepository[T]) Delete() error {
r.mu.Lock()
defer r.mu.Unlock()
err := os.Remove(r.path)
if os.IsNotExist(err) {
return nil
}
return err
}