cli/pkg/config/config.go

188 lines
5.6 KiB
Go

package config
import (
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"reflect"
"strings"
"github.com/Snider/Core/pkg/core"
"github.com/adrg/xdg"
)
const appName = "lethean"
const configFileName = "config.json"
// Options holds configuration for the config service.
type Options struct{}
// Service provides access to the application's configuration.
// It handles loading, saving, and providing access to configuration values.
type Service struct {
*core.Runtime[Options] `json:"-"`
// Persistent fields, saved to config.json.
ConfigPath string `json:"configPath,omitempty"`
UserHomeDir string `json:"userHomeDir,omitempty"`
RootDir string `json:"rootDir,omitempty"`
CacheDir string `json:"cacheDir,omitempty"`
ConfigDir string `json:"configDir,omitempty"`
DataDir string `json:"dataDir,omitempty"`
WorkspaceDir string `json:"workspaceDir,omitempty"`
DefaultRoute string `json:"default_route"`
Features []string `json:"features"`
Language string `json:"language"`
}
// createServiceInstance contains the common logic for initializing a Service struct.
func createServiceInstance() (*Service, error) {
// --- Path and Directory Setup ---
homeDir, err := os.UserHomeDir()
if err != nil {
return nil, fmt.Errorf("could not resolve user home directory: %w", err)
}
userHomeDir := filepath.Join(homeDir, appName)
rootDir, err := xdg.DataFile(appName)
if err != nil {
return nil, fmt.Errorf("could not resolve data directory: %w", err)
}
cacheDir, err := xdg.CacheFile(appName)
if err != nil {
return nil, fmt.Errorf("could not resolve cache directory: %w", err)
}
s := &Service{
UserHomeDir: userHomeDir,
RootDir: rootDir,
CacheDir: cacheDir,
ConfigDir: filepath.Join(userHomeDir, "config"),
DataDir: filepath.Join(userHomeDir, "data"),
WorkspaceDir: filepath.Join(userHomeDir, "workspace"),
DefaultRoute: "/",
Features: []string{},
Language: "en",
}
s.ConfigPath = filepath.Join(s.ConfigDir, configFileName)
dirs := []string{s.RootDir, s.ConfigDir, s.DataDir, s.CacheDir, s.WorkspaceDir, s.UserHomeDir}
for _, dir := range dirs {
if err := os.MkdirAll(dir, os.ModePerm); err != nil {
return nil, fmt.Errorf("could not create directory %s: %w", dir, err)
}
}
// --- Load or Create Configuration ---
if data, err := os.ReadFile(s.ConfigPath); err == nil {
// Config file exists, load it.
if err := json.Unmarshal(data, s); err != nil {
return nil, fmt.Errorf("failed to unmarshal config: %w", err)
}
} else if os.IsNotExist(err) {
// Config file does not exist, create it with default values.
if err := s.Save(); err != nil {
return nil, fmt.Errorf("failed to create default config file: %w", err)
}
} else {
// Another error occurred reading the file.
return nil, fmt.Errorf("failed to read config file: %w", err)
}
return s, nil
}
// New is the constructor for static dependency injection.
// It creates a Service instance without initializing the core.Runtime field.
func New() (*Service, error) {
return createServiceInstance()
}
// Register is the constructor for dynamic dependency injection (used with core.WithService).
// It creates a Service instance and initializes its core.Runtime field.
func Register(c *core.Core) (any, error) {
s, err := createServiceInstance()
if err != nil {
return nil, err
}
// Defensive check: createServiceInstance should not return nil service with nil error
if s == nil {
return nil, errors.New("config: createServiceInstance returned a nil service instance with no error")
}
s.Runtime = core.NewRuntime(c, Options{})
return s, nil
}
// Save writes the current configuration to config.json.
func (s *Service) Save() error {
data, err := json.MarshalIndent(s, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal config: %w", err)
}
if err := os.WriteFile(s.ConfigPath, data, 0644); err != nil {
return fmt.Errorf("failed to write config file: %w", err)
}
return nil
}
// Get retrieves a configuration value by its key.
func (s *Service) Get(key string, out any) error {
val := reflect.ValueOf(s).Elem()
typ := val.Type()
for i := 0; i < val.NumField(); i++ {
field := typ.Field(i)
jsonTag := field.Tag.Get("json")
if jsonTag != "" && jsonTag != "-" {
jsonName := strings.Split(jsonTag, ",")[0]
if strings.EqualFold(jsonName, key) {
outVal := reflect.ValueOf(out)
if outVal.Kind() != reflect.Ptr || outVal.IsNil() {
return errors.New("output argument must be a non-nil pointer")
}
targetVal := outVal.Elem()
srcVal := val.Field(i)
if !srcVal.Type().AssignableTo(targetVal.Type()) {
return fmt.Errorf("cannot assign config value of type %s to output of type %s", srcVal.Type(), targetVal.Type())
}
targetVal.Set(srcVal)
return nil
}
}
}
return fmt.Errorf("key '%s' not found in config", key)
}
// Set updates a configuration value and saves the config.
func (s *Service) Set(key string, v any) error {
val := reflect.ValueOf(s).Elem()
typ := val.Type()
for i := 0; i < val.NumField(); i++ {
field := typ.Field(i)
jsonTag := field.Tag.Get("json")
if jsonTag != "" && jsonTag != "-" {
jsonName := strings.Split(jsonTag, ",")[0]
if strings.EqualFold(jsonName, key) {
fieldVal := val.Field(i)
if !fieldVal.CanSet() {
return fmt.Errorf("cannot set config field for key '%s'", key)
}
newVal := reflect.ValueOf(v)
if !newVal.Type().AssignableTo(fieldVal.Type()) {
return fmt.Errorf("type mismatch for key '%s': expected %s, got %s", key, fieldVal.Type(), newVal.Type())
}
fieldVal.Set(newVal)
return s.Save()
}
}
}
return fmt.Errorf("key '%s' not found in config", key)
}