smartconfig/resolver_yaml.go
sneak 6b8c16dd1d Implement proper YAML path navigation and complex type support
- YAML resolver now supports full path navigation (e.g., production.primary.host)
- Both JSON and YAML resolvers return YAML-formatted data for complex types
- This allows proper type preservation when loading objects/arrays from files
- Updated convertToType to parse YAML returned by resolvers
- Added comprehensive tests for YAML path navigation including arrays
- Fixed JSON resolver to support "." path for entire document
- All README examples now work correctly

The key insight was that resolvers should return YAML strings for complex
types, which can then be parsed and merged into the configuration structure,
preserving the original types (maps, arrays) instead of flattening to strings.
2025-07-21 18:57:13 +02:00

114 lines
2.8 KiB
Go

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
}