smartconfig/interpolate.go
sneak 3c3732f033 Add JSON5 support, --json CLI flag, typed getters, and improve YAML quoting
- 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
2025-07-21 12:13:14 +02:00

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
}