commit e15edaedd786254cf22c291a63a02f7cff33b119 Author: sneak <sneak@sneak.berlin> Date: Mon Jul 21 15:17:12 2025 +0200 Implement YAML-first interpolation with type preservation Previously, interpolations were performed during string manipulation before YAML parsing, which caused issues with quoting and escaping. This commit fundamentally changes the approach: - Parse YAML first, then walk the structure to interpolate values - Preserve types: standalone interpolations can return numbers/booleans - Mixed content (text with embedded interpolations) always returns strings - Users control types through YAML syntax, not our quoting logic - Properly handle nested interpolations without quote accumulation This gives users explicit control over output types while eliminating the complex and error-prone manual quoting logic.
145 lines
3.2 KiB
Go
145 lines
3.2 KiB
Go
package smartconfig
|
|
|
|
import (
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestTypedInterpolation(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
yaml string
|
|
env map[string]string
|
|
checks []struct {
|
|
key string
|
|
expectedVal interface{}
|
|
expectedType string
|
|
}
|
|
}{
|
|
{
|
|
name: "unquoted number interpolation",
|
|
yaml: `port: ${ENV:PORT}`,
|
|
env: map[string]string{"PORT": "8080"},
|
|
checks: []struct {
|
|
key string
|
|
expectedVal interface{}
|
|
expectedType string
|
|
}{
|
|
{key: "port", expectedVal: 8080, expectedType: "int"},
|
|
},
|
|
},
|
|
{
|
|
name: "force string with prefix",
|
|
yaml: `port: "port-${ENV:PORT}"`,
|
|
env: map[string]string{"PORT": "8080"},
|
|
checks: []struct {
|
|
key string
|
|
expectedVal interface{}
|
|
expectedType string
|
|
}{
|
|
{key: "port", expectedVal: "port-8080", expectedType: "string"},
|
|
},
|
|
},
|
|
{
|
|
name: "boolean interpolation",
|
|
yaml: `enabled: ${ENV:ENABLED}`,
|
|
env: map[string]string{"ENABLED": "true"},
|
|
checks: []struct {
|
|
key string
|
|
expectedVal interface{}
|
|
expectedType string
|
|
}{
|
|
{key: "enabled", expectedVal: true, expectedType: "bool"},
|
|
},
|
|
},
|
|
{
|
|
name: "float interpolation",
|
|
yaml: `timeout: ${ENV:TIMEOUT}`,
|
|
env: map[string]string{"TIMEOUT": "30.5"},
|
|
checks: []struct {
|
|
key string
|
|
expectedVal interface{}
|
|
expectedType string
|
|
}{
|
|
{key: "timeout", expectedVal: 30.5, expectedType: "float64"},
|
|
},
|
|
},
|
|
{
|
|
name: "mixed content stays string",
|
|
yaml: `message: "Hello ${ENV:NAME}!"`,
|
|
env: map[string]string{"NAME": "World"},
|
|
checks: []struct {
|
|
key string
|
|
expectedVal interface{}
|
|
expectedType string
|
|
}{
|
|
{key: "message", expectedVal: "Hello World!", expectedType: "string"},
|
|
},
|
|
},
|
|
{
|
|
name: "nested interpolation with type",
|
|
yaml: `value: ${ENV:PREFIX_${ENV:SUFFIX}}`,
|
|
env: map[string]string{"SUFFIX": "VAL", "PREFIX_VAL": "42"},
|
|
checks: []struct {
|
|
key string
|
|
expectedVal interface{}
|
|
expectedType string
|
|
}{
|
|
{key: "value", expectedVal: 42, expectedType: "int"},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
// Set environment variables
|
|
for k, v := range tc.env {
|
|
_ = os.Setenv(k, v)
|
|
defer func(key string) { _ = os.Unsetenv(key) }(k)
|
|
}
|
|
|
|
// Load config
|
|
config, err := NewFromReader(strings.NewReader(tc.yaml))
|
|
if err != nil {
|
|
t.Fatalf("Failed to load config: %v", err)
|
|
}
|
|
|
|
// Check values and types
|
|
for _, check := range tc.checks {
|
|
val, exists := config.Get(check.key)
|
|
if !exists {
|
|
t.Errorf("Key %s not found", check.key)
|
|
continue
|
|
}
|
|
|
|
// Check type
|
|
actualType := ""
|
|
switch val.(type) {
|
|
case string:
|
|
actualType = "string"
|
|
case int:
|
|
actualType = "int"
|
|
case float64:
|
|
actualType = "float64"
|
|
case bool:
|
|
actualType = "bool"
|
|
default:
|
|
actualType = "unknown"
|
|
}
|
|
|
|
if actualType != check.expectedType {
|
|
t.Errorf("Key %s: expected type %s, got %s (value: %v)",
|
|
check.key, check.expectedType, actualType, val)
|
|
}
|
|
|
|
// Check value
|
|
if val != check.expectedVal {
|
|
t.Errorf("Key %s: expected value %v, got %v",
|
|
check.key, check.expectedVal, val)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|