cli/pkg/config/formats.go
Snider 4e02d5bc97 refactor: bring external packages home and restructure
- Imported packages from separate repos:
  - github.com/Snider/config -> pkg/config
  - github.com/Snider/display -> pkg/display
  - github.com/Snider/help -> pkg/help
  - github.com/Snider/i18n -> pkg/i18n
  - github.com/Snider/updater -> pkg/updater
- Moved core code from root to pkg/core
- Flattened nested package structures
- Updated all import paths to github.com/Snider/Core/pkg/*
- Added Display interface to Core
- Updated go.work for workspace modules

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-15 15:30:43 +00:00

239 lines
7.1 KiB
Go

package config
import (
"encoding/json"
"encoding/xml"
"fmt"
"os"
"path/filepath"
"strings"
"gopkg.in/ini.v1"
"gopkg.in/yaml.v2"
)
// ConfigFormat defines an interface for loading and saving configuration data in
// various formats. Each format implementation is responsible for serializing and
// deserializing data between a file and a map of key-value pairs.
type ConfigFormat interface {
// Load reads data from the specified path and returns it as a map.
Load(path string) (map[string]interface{}, error)
// Save writes the provided data map to the specified path.
Save(path string, data map[string]interface{}) error
}
// JSONFormat implements the ConfigFormat interface for JSON files. It provides
// methods to read from and write to files in JSON format.
type JSONFormat struct{}
// Load reads a JSON file from the given path and decodes it into a map.
// The keys of the map are strings, and the values are of type interface{}.
func (f *JSONFormat) Load(path string) (map[string]interface{}, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var result map[string]interface{}
if err := json.Unmarshal(data, &result); err != nil {
return nil, err
}
return result, nil
}
// Save encodes the provided map into JSON format and writes it to the given
// path. The output is indented for readability.
func (f *JSONFormat) Save(path string, data map[string]interface{}) error {
jsonData, err := json.MarshalIndent(data, "", " ")
if err != nil {
return err
}
return os.WriteFile(path, jsonData, 0644)
}
// YAMLFormat implements the ConfigFormat interface for YAML files. It provides
// methods to read from and write to files in YAML format.
type YAMLFormat struct{}
// Load reads a YAML file from the given path and decodes it into a map.
func (f *YAMLFormat) Load(path string) (map[string]interface{}, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var result map[string]interface{}
if err := yaml.Unmarshal(data, &result); err != nil {
return nil, err
}
return result, nil
}
// Save encodes the provided map into YAML format and writes it to the given
// path.
func (f *YAMLFormat) Save(path string, data map[string]interface{}) error {
yamlData, err := yaml.Marshal(data)
if err != nil {
return err
}
return os.WriteFile(path, yamlData, 0644)
}
// INIFormat implements the ConfigFormat interface for INI files. It handles
// the structured format of INI files, including sections and keys.
type INIFormat struct{}
// Load reads an INI file and converts its sections and keys into a single map.
// Keys in the map are formed by concatenating the section name and key name with
// a dot (e.g., "section.key").
func (f *INIFormat) Load(path string) (map[string]interface{}, error) {
cfg, err := ini.Load(path)
if err != nil {
return nil, err
}
result := make(map[string]interface{})
for _, section := range cfg.Sections() {
for _, key := range section.Keys() {
result[section.Name()+"."+key.Name()] = key.Value()
}
}
return result, nil
}
// Save writes a map of key-value pairs to an INI file. Keys in the map are
// split by a dot to determine the section and key for the INI file.
func (f *INIFormat) Save(path string, data map[string]interface{}) error {
cfg := ini.Empty()
for key, value := range data {
parts := strings.SplitN(key, ".", 2)
section := ini.DefaultSection
keyName := parts[0]
if len(parts) > 1 {
section = parts[0]
keyName = parts[1]
}
if _, err := cfg.Section(section).NewKey(keyName, fmt.Sprintf("%v", value)); err != nil {
return err
}
}
return cfg.SaveTo(path)
}
// XMLFormat implements the ConfigFormat interface for XML files. It uses a
// simple structure with a root "config" element containing a series of "entry"
// elements, each with a "key" and "value".
type XMLFormat struct{}
// xmlEntry is a helper struct for marshaling and unmarshaling XML data.
type xmlEntry struct {
Key string `xml:"key"`
Value string `xml:"value"`
}
// Load reads an XML file and parses it into a map. It expects the XML to have
// a specific structure as defined by the xmlEntry struct.
func (f *XMLFormat) Load(path string) (map[string]interface{}, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var v struct {
XMLName xml.Name `xml:"config"`
Entries []xmlEntry `xml:"entry"`
}
if err := xml.Unmarshal(data, &v); err != nil {
return nil, err
}
result := make(map[string]interface{})
for _, entry := range v.Entries {
result[entry.Key] = entry.Value
}
return result, nil
}
// Save writes a map of key-value pairs to an XML file. The data is structured
// with a root "config" element and child "entry" elements.
func (f *XMLFormat) Save(path string, data map[string]interface{}) error {
var entries []xmlEntry
for key, value := range data {
entries = append(entries, xmlEntry{
Key: key,
Value: fmt.Sprintf("%v", value),
})
}
xmlData, err := xml.MarshalIndent(struct {
XMLName xml.Name `xml:"config"`
Entries []xmlEntry `xml:"entry"`
}{Entries: entries}, "", " ")
if err != nil {
return err
}
return os.WriteFile(path, xmlData, 0644)
}
// GetConfigFormat returns a ConfigFormat implementation based on the file
// extension of the provided path. This allows the config service to dynamically
// handle different file formats.
//
// Example:
//
// format, err := GetConfigFormat("settings.json")
// if err != nil {
// log.Fatal(err)
// }
// // format is now a JSONFormat
func GetConfigFormat(path string) (ConfigFormat, error) {
ext := strings.ToLower(filepath.Ext(path))
switch ext {
case ".json":
return &JSONFormat{}, nil
case ".yaml", ".yml":
return &YAMLFormat{}, nil
case ".ini":
return &INIFormat{}, nil
case ".xml":
return &XMLFormat{}, nil
default:
return nil, fmt.Errorf("unsupported config format: %s", ext)
}
}
// SaveKeyValues saves a map of key-value pairs to a file in the config
// directory. The file format is determined by the extension of the `key`
// parameter. This method is a convenient way to store structured data in a
// format of choice.
//
// Example:
//
// data := map[string]interface{}{"host": "localhost", "port": 8080}
// err := cfg.SaveKeyValues("database.yml", data)
// if err != nil {
// log.Printf("Error saving database config: %v", err)
// }
func (s *Service) SaveKeyValues(key string, data map[string]interface{}) error {
format, err := GetConfigFormat(key)
if err != nil {
return err
}
filePath := filepath.Join(s.ConfigDir, key)
return format.Save(filePath, data)
}
// LoadKeyValues loads a map of key-value pairs from a file in the config
// directory. The file format is determined by the extension of the `key`
// parameter. This allows for easy retrieval of data stored in various formats.
//
// Example:
//
// dbConfig, err := cfg.LoadKeyValues("database.yml")
// if err != nil {
// log.Printf("Error loading database config: %v", err)
// }
// port, ok := dbConfig["port"].(int)
// // ...
func (s *Service) LoadKeyValues(key string) (map[string]interface{}, error) {
format, err := GetConfigFormat(key)
if err != nil {
return nil, err
}
filePath := filepath.Join(s.ConfigDir, key)
return format.Load(filePath)
}