- Updated JSON resolver to use gjson library for JSON5 support (comments, trailing commas) - Added --json flag to CLI tool for JSON output format - Added typed getter methods: GetInt(), GetUint(), GetFloat(), GetBool(), GetBytes() - GetBytes() supports human-readable formats like "10G", "20KiB" via go-humanize - Fixed YAML interpolation to always quote output values for safety - Updated README to clarify YAML-only input and automatic quoting behavior - Added .gitignore to exclude compiled binary - Fixed test config files to use unquoted interpolation syntax - Updated tests to expect quoted interpolation output
117 lines
2.7 KiB
Go
117 lines
2.7 KiB
Go
package smartconfig
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// findInterpolations finds all ${...} patterns in the string, handling nested cases
|
|
func findInterpolations(s string) []struct{ start, end int } {
|
|
var results []struct{ start, end int }
|
|
|
|
for i := 0; i < len(s); i++ {
|
|
if i+2 < len(s) && s[i:i+2] == "${" {
|
|
// Found start of interpolation
|
|
start := i
|
|
braceCount := 1
|
|
j := i + 2
|
|
|
|
// Find matching closing brace
|
|
for j < len(s) && braceCount > 0 {
|
|
if j+2 <= len(s) && s[j:j+2] == "${" {
|
|
braceCount++
|
|
j += 2
|
|
} else if s[j] == '}' {
|
|
braceCount--
|
|
j++
|
|
} else {
|
|
j++
|
|
}
|
|
}
|
|
|
|
if braceCount == 0 {
|
|
results = append(results, struct{ start, end int }{start, j})
|
|
i = j - 1 // Skip past this interpolation
|
|
}
|
|
}
|
|
}
|
|
|
|
return results
|
|
}
|
|
|
|
// interpolate handles nested interpolations properly
|
|
func (c *Config) interpolate(content string, depth int) (string, error) {
|
|
return c.interpolateWithQuoting(content, depth, true)
|
|
}
|
|
|
|
// interpolateWithQuoting handles interpolation with control over quoting
|
|
func (c *Config) interpolateWithQuoting(content string, depth int, shouldQuote bool) (string, error) {
|
|
if depth >= maxRecursionDepth {
|
|
return content, nil
|
|
}
|
|
|
|
result := content
|
|
changed := true
|
|
|
|
for changed && depth < maxRecursionDepth {
|
|
changed = false
|
|
positions := findInterpolations(result)
|
|
|
|
// Process from end to beginning to maintain string positions
|
|
for i := len(positions) - 1; i >= 0; i-- {
|
|
pos := positions[i]
|
|
fullMatch := result[pos.start:pos.end]
|
|
|
|
// Extract the content inside ${}
|
|
inner := fullMatch[2 : len(fullMatch)-1]
|
|
|
|
// Find the resolver and value
|
|
colonIdx := strings.Index(inner, ":")
|
|
if colonIdx == -1 {
|
|
continue
|
|
}
|
|
|
|
resolverName := inner[:colonIdx]
|
|
value := inner[colonIdx+1:]
|
|
|
|
// Check if value contains nested interpolations
|
|
if strings.Contains(value, "${") {
|
|
// Recursively interpolate the value first, without quoting
|
|
interpolatedValue, err := c.interpolateWithQuoting(value, depth+1, false)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
value = interpolatedValue
|
|
}
|
|
|
|
// Resolve the value
|
|
resolver, ok := c.resolvers[resolverName]
|
|
if !ok {
|
|
return "", fmt.Errorf("unknown resolver: %s", resolverName)
|
|
}
|
|
|
|
resolved, err := resolver.Resolve(value)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to resolve %s:%s: %w", resolverName, value, err)
|
|
}
|
|
|
|
// Only quote if requested (top-level interpolations)
|
|
finalValue := resolved
|
|
if shouldQuote && depth == 0 {
|
|
finalValue = strconv.Quote(resolved)
|
|
}
|
|
|
|
// Replace the match
|
|
result = result[:pos.start] + finalValue + result[pos.end:]
|
|
changed = true
|
|
}
|
|
|
|
if changed {
|
|
depth++
|
|
}
|
|
}
|
|
|
|
return result, nil
|
|
}
|