smartconfig/resolver_json.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

70 lines
1.9 KiB
Go

package smartconfig
import (
"encoding/json"
"fmt"
"os"
"strings"
"github.com/tidwall/gjson"
"gopkg.in/yaml.v3"
)
// JSONResolver reads values from JSON files.
// Usage: ${JSON:/path/to/file.json:json.path}
type JSONResolver struct{}
// Resolve reads a JSON 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 *JSONResolver) Resolve(value string) (string, error) {
parts := strings.SplitN(value, ":", 2)
if len(parts) != 2 {
return "", fmt.Errorf("invalid JSON resolver format, expected FILE:PATH")
}
filePath := parts[0]
jsonPath := parts[1]
data, err := os.ReadFile(filePath)
if err != nil {
return "", fmt.Errorf("failed to read JSON file %s: %w", filePath, err)
}
// gjson supports JSON5 syntax including comments, trailing commas, etc.
// Special case: if path is ".", return the entire JSON
if jsonPath == "." {
// Parse and convert to YAML for consistency
var jsonData interface{}
if err := json.Unmarshal(data, &jsonData); err != nil {
return "", fmt.Errorf("failed to parse JSON: %w", err)
}
yamlBytes, err := yaml.Marshal(jsonData)
if err != nil {
return "", fmt.Errorf("failed to marshal to YAML: %w", err)
}
return strings.TrimSpace(string(yamlBytes)), nil
}
result := gjson.GetBytes(data, jsonPath)
if !result.Exists() {
return "", fmt.Errorf("path %s not found in JSON file", jsonPath)
}
// For complex types (objects/arrays), we need to parse and convert to YAML
if result.IsObject() || result.IsArray() {
// Use result.Value() to get the underlying interface{}
jsonData := result.Value()
// Convert to YAML
yamlBytes, err := yaml.Marshal(jsonData)
if err != nil {
return "", fmt.Errorf("failed to marshal to YAML: %w", err)
}
return strings.TrimSpace(string(yamlBytes)), nil
}
// For simple types, just return the string representation
return result.String(), nil
}