First commit of golang CLI.
This commit is contained in:
298
wild-cli/internal/config/manager.go
Normal file
298
wild-cli/internal/config/manager.go
Normal file
@@ -0,0 +1,298 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// Manager handles configuration and secrets files
|
||||
type Manager struct {
|
||||
configPath string
|
||||
secretsPath string
|
||||
}
|
||||
|
||||
// NewManager creates a new configuration manager
|
||||
func NewManager(configPath, secretsPath string) *Manager {
|
||||
return &Manager{
|
||||
configPath: configPath,
|
||||
secretsPath: secretsPath,
|
||||
}
|
||||
}
|
||||
|
||||
// Get retrieves a value from the config file using dot-notation path
|
||||
func (m *Manager) Get(path string) (interface{}, error) {
|
||||
return m.getValue(m.configPath, path)
|
||||
}
|
||||
|
||||
// Set sets a value in the config file using dot-notation path
|
||||
func (m *Manager) Set(path, value string) error {
|
||||
return m.setValue(m.configPath, path, value)
|
||||
}
|
||||
|
||||
// GetSecret retrieves a value from the secrets file using dot-notation path
|
||||
func (m *Manager) GetSecret(path string) (interface{}, error) {
|
||||
return m.getValue(m.secretsPath, path)
|
||||
}
|
||||
|
||||
// SetSecret sets a value in the secrets file using dot-notation path
|
||||
func (m *Manager) SetSecret(path, value string) error {
|
||||
return m.setValue(m.secretsPath, path, value)
|
||||
}
|
||||
|
||||
// getValue retrieves a value from a YAML file using dot-notation path
|
||||
func (m *Manager) getValue(filePath, path string) (interface{}, error) {
|
||||
// Check if file exists
|
||||
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("file not found: %s", filePath)
|
||||
}
|
||||
|
||||
// Read file
|
||||
data, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading file: %w", err)
|
||||
}
|
||||
|
||||
// Parse YAML
|
||||
var yamlData interface{}
|
||||
if err := yaml.Unmarshal(data, &yamlData); err != nil {
|
||||
return nil, fmt.Errorf("parsing YAML: %w", err)
|
||||
}
|
||||
|
||||
// Navigate to the specified path
|
||||
value, err := m.navigatePath(yamlData, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return value, nil
|
||||
}
|
||||
|
||||
// setValue sets a value in a YAML file using dot-notation path
|
||||
func (m *Manager) setValue(filePath, path, value string) error {
|
||||
// Ensure directory exists
|
||||
if err := os.MkdirAll(filepath.Dir(filePath), 0755); err != nil {
|
||||
return fmt.Errorf("creating directory: %w", err)
|
||||
}
|
||||
|
||||
// Read existing file or create empty structure
|
||||
var yamlData interface{}
|
||||
if data, err := os.ReadFile(filePath); err == nil {
|
||||
if err := yaml.Unmarshal(data, &yamlData); err != nil {
|
||||
return fmt.Errorf("parsing existing YAML: %w", err)
|
||||
}
|
||||
} else if !os.IsNotExist(err) {
|
||||
return fmt.Errorf("reading file: %w", err)
|
||||
}
|
||||
|
||||
// If no existing data, start with empty map
|
||||
if yamlData == nil {
|
||||
yamlData = make(map[string]interface{})
|
||||
}
|
||||
|
||||
// Parse the value as YAML to handle different types
|
||||
var parsedValue interface{}
|
||||
if err := yaml.Unmarshal([]byte(value), &parsedValue); err != nil {
|
||||
// If it fails to parse as YAML, treat as string
|
||||
parsedValue = value
|
||||
}
|
||||
|
||||
// Set the value at the specified path
|
||||
if err := m.setValueAtPath(yamlData, path, parsedValue); err != nil {
|
||||
return fmt.Errorf("setting value at path: %w", err)
|
||||
}
|
||||
|
||||
// Marshal back to YAML
|
||||
data, err := yaml.Marshal(yamlData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshaling YAML: %w", err)
|
||||
}
|
||||
|
||||
// Write file
|
||||
if err := os.WriteFile(filePath, data, 0600); err != nil {
|
||||
return fmt.Errorf("writing file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// navigatePath navigates through a nested data structure using dot-notation path
|
||||
func (m *Manager) navigatePath(data interface{}, path string) (interface{}, error) {
|
||||
if path == "" {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
parts := m.parsePath(path)
|
||||
current := data
|
||||
|
||||
for _, part := range parts {
|
||||
if part.isArray {
|
||||
// Handle array access like "items[0]"
|
||||
slice, ok := current.([]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("path component %s is not an array", part.key)
|
||||
}
|
||||
|
||||
if part.index < 0 || part.index >= len(slice) {
|
||||
return nil, fmt.Errorf("array index %d out of range for %s", part.index, part.key)
|
||||
}
|
||||
|
||||
current = slice[part.index]
|
||||
} else {
|
||||
// Handle map access
|
||||
m, ok := current.(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("path component %s is not a map", part.key)
|
||||
}
|
||||
|
||||
var exists bool
|
||||
current, exists = m[part.key]
|
||||
if !exists {
|
||||
return nil, nil // Key not found
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return current, nil
|
||||
}
|
||||
|
||||
// setValueAtPath sets a value at the specified path, creating nested structures as needed
|
||||
func (m *Manager) setValueAtPath(data interface{}, path string, value interface{}) error {
|
||||
if path == "" {
|
||||
return fmt.Errorf("empty path")
|
||||
}
|
||||
|
||||
parts := m.parsePath(path)
|
||||
current := data
|
||||
|
||||
// Navigate to the parent of the target
|
||||
for _, part := range parts[:len(parts)-1] {
|
||||
if part.isArray {
|
||||
slice, ok := current.([]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("path component %s is not an array", part.key)
|
||||
}
|
||||
|
||||
if part.index < 0 || part.index >= len(slice) {
|
||||
return fmt.Errorf("array index %d out of range for %s", part.index, part.key)
|
||||
}
|
||||
|
||||
current = slice[part.index]
|
||||
} else {
|
||||
m, ok := current.(map[string]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("path component %s is not a map", part.key)
|
||||
}
|
||||
|
||||
next, exists := m[part.key]
|
||||
if !exists {
|
||||
// Create new map for next level
|
||||
next = make(map[string]interface{})
|
||||
m[part.key] = next
|
||||
}
|
||||
current = next
|
||||
}
|
||||
}
|
||||
|
||||
// Set the final value
|
||||
finalPart := parts[len(parts)-1]
|
||||
if finalPart.isArray {
|
||||
return fmt.Errorf("cannot set array element directly")
|
||||
} else {
|
||||
m, ok := current.(map[string]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("cannot set value on non-map")
|
||||
}
|
||||
m[finalPart.key] = value
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// pathPart represents a single component in a dot-notation path
|
||||
type pathPart struct {
|
||||
key string
|
||||
isArray bool
|
||||
index int
|
||||
}
|
||||
|
||||
// parsePath parses a dot-notation path into components
|
||||
func (m *Manager) parsePath(path string) []pathPart {
|
||||
var parts []pathPart
|
||||
components := strings.Split(path, ".")
|
||||
|
||||
for _, component := range components {
|
||||
if strings.Contains(component, "[") && strings.Contains(component, "]") {
|
||||
// Handle array syntax like "items[0]"
|
||||
openBracket := strings.Index(component, "[")
|
||||
closeBracket := strings.Index(component, "]")
|
||||
|
||||
key := component[:openBracket]
|
||||
indexStr := component[openBracket+1 : closeBracket]
|
||||
|
||||
if key != "" {
|
||||
parts = append(parts, pathPart{
|
||||
key: key,
|
||||
isArray: false,
|
||||
})
|
||||
}
|
||||
|
||||
if index, err := strconv.Atoi(indexStr); err == nil {
|
||||
parts = append(parts, pathPart{
|
||||
key: key,
|
||||
isArray: true,
|
||||
index: index,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
parts = append(parts, pathPart{
|
||||
key: component,
|
||||
isArray: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return parts
|
||||
}
|
||||
|
||||
// LoadConfig loads the entire config file as a map
|
||||
func (m *Manager) LoadConfig() (map[string]interface{}, error) {
|
||||
data, err := m.getValue(m.configPath, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if data == nil {
|
||||
return make(map[string]interface{}), nil
|
||||
}
|
||||
|
||||
configMap, ok := data.(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("config file is not a valid YAML map")
|
||||
}
|
||||
|
||||
return configMap, nil
|
||||
}
|
||||
|
||||
// LoadSecrets loads the entire secrets file as a map
|
||||
func (m *Manager) LoadSecrets() (map[string]interface{}, error) {
|
||||
data, err := m.getValue(m.secretsPath, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if data == nil {
|
||||
return make(map[string]interface{}), nil
|
||||
}
|
||||
|
||||
secretsMap, ok := data.(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("secrets file is not a valid YAML map")
|
||||
}
|
||||
|
||||
return secretsMap, nil
|
||||
}
|
Reference in New Issue
Block a user