package config import ( "fmt" "path/filepath" "github.com/spf13/afero" "gopkg.in/yaml.v3" ) // Loader handles loading configuration from YAML files. type Loader struct { fs afero.Fs } // NewLoader creates a new configuration loader. func NewLoader(fs afero.Fs) *Loader { return &Loader{ fs: fs, } } // FindConfigFile searches for a configuration file by looking up the directory tree. func (l *Loader) FindConfigFile(filename string) (string, error) { if filename == "" { filename = "config.yaml" } // First check if the file exists in the current directory (simple case) if _, err := l.fs.Stat(filename); err == nil { return filename, nil } // For more complex cases, try to walk up the directory tree // Start from current directory or root for in-memory filesystems currentDir := "." // Try to get the absolute path, but if it fails (e.g., in-memory fs), // just use the current directory if absPath, err := filepath.Abs("."); err == nil { currentDir = absPath } // Search up the directory tree for { configPath := filepath.Join(currentDir, filename) if _, err := l.fs.Stat(configPath); err == nil { return configPath, nil } // Move up one directory parentDir := filepath.Dir(currentDir) if parentDir == currentDir || currentDir == "." || currentDir == "/" { // Reached the root directory or can't go up further break } currentDir = parentDir } return "", fmt.Errorf("configuration file %s not found in directory tree", filename) } // LoadYAML loads a YAML file and returns the parsed configuration. func (l *Loader) LoadYAML(filePath string) (map[string]interface{}, error) { data, err := afero.ReadFile(l.fs, filePath) if err != nil { return nil, fmt.Errorf("failed to read file %s: %w", filePath, err) } var config map[string]interface{} if err := yaml.Unmarshal(data, &config); err != nil { return nil, fmt.Errorf("failed to parse YAML from %s: %w", filePath, err) } if config == nil { config = make(map[string]interface{}) } return config, nil } // MergeConfigs performs a deep merge of two configuration maps. // The override map values take precedence over the base map. func (l *Loader) MergeConfigs(base, override map[string]interface{}) map[string]interface{} { if base == nil { base = make(map[string]interface{}) } for key, value := range override { if baseValue, exists := base[key]; exists { // If both values are maps, merge them recursively if baseMap, baseOk := baseValue.(map[string]interface{}); baseOk { if overrideMap, overrideOk := value.(map[string]interface{}); overrideOk { base[key] = l.MergeConfigs(baseMap, overrideMap) continue } } } // Otherwise, override the value base[key] = value } return base }