- 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.
114 lines
2.8 KiB
Go
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
|
|
}
|