- 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.
70 lines
1.9 KiB
Go
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
|
|
}
|