package smartconfig import ( "fmt" "os" "strconv" "strings" "gopkg.in/yaml.v3" ) // YAMLResolver reads values from YAML files. // Usage: ${YAML:/path/to/file.yaml:yaml.path} type YAMLResolver struct{} // Resolve reads a YAML file and extracts the value at the specified path. // Returns the value as a YAML string that can be parsed back into the config. func (r *YAMLResolver) Resolve(value string) (string, error) { parts := strings.SplitN(value, ":", 2) if len(parts) != 2 { return "", fmt.Errorf("invalid YAML resolver format, expected FILE:PATH") } filePath := parts[0] yamlPath := parts[1] // Check for empty path if yamlPath == "" { return "", fmt.Errorf("empty YAML path") } data, err := os.ReadFile(filePath) if err != nil { return "", fmt.Errorf("failed to read YAML file %s: %w", filePath, err) } var yamlData interface{} if err := yaml.Unmarshal(data, &yamlData); err != nil { return "", fmt.Errorf("failed to parse YAML: %w", err) } // Navigate the path result, err := navigateYAMLPath(yamlData, yamlPath) if err != nil { return "", err } // Convert the result back to YAML yamlBytes, err := yaml.Marshal(result) if err != nil { return "", fmt.Errorf("failed to marshal result to YAML: %w", err) } // Return as YAML string (trim trailing newline for cleaner output) return strings.TrimSpace(string(yamlBytes)), nil } // navigateYAMLPath traverses the YAML structure following the dot-separated path func navigateYAMLPath(data interface{}, path string) (interface{}, error) { // Handle root document request if path == "." { return data, nil } // Split path by dots parts := strings.Split(path, ".") current := data for i, part := range parts { switch v := current.(type) { case map[string]interface{}: // Navigate map next, exists := v[part] if !exists { return nil, fmt.Errorf("path not found: %s (failed at '%s')", path, strings.Join(parts[:i+1], ".")) } current = next case map[interface{}]interface{}: // YAML can have non-string keys, handle them found := false for key, value := range v { if fmt.Sprintf("%v", key) == part { current = value found = true break } } if !found { return nil, fmt.Errorf("path not found: %s (failed at '%s')", path, strings.Join(parts[:i+1], ".")) } case []interface{}: // Handle array access index, err := strconv.Atoi(part) if err != nil { return nil, fmt.Errorf("invalid array index '%s' in path %s", part, path) } if index < 0 || index >= len(v) { return nil, fmt.Errorf("array index out of bounds: %d (array length: %d)", index, len(v)) } current = v[index] default: // Can't navigate further if i < len(parts)-1 { return nil, fmt.Errorf("cannot navigate path %s: '%s' is not a map or array", path, strings.Join(parts[:i], ".")) } } } return current, nil }