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.
87 lines
2.7 KiB
Go
87 lines
2.7 KiB
Go
// This test file verifies that YAML syntax allows unquoted interpolation markers.
|
|
//
|
|
// Key findings:
|
|
// - YAML treats unquoted ${...} expressions as string values
|
|
// - Users can write interpolations without quotes (e.g., port: ${ENV:PORT})
|
|
// - Even complex shell commands with special characters work unquoted
|
|
// - This confirms our design approach: parse YAML first, then walk string values
|
|
// looking for interpolation patterns
|
|
//
|
|
// This means users have explicit control over types:
|
|
// - port: ${ENV:PORT} # We can return a number if ENV:PORT="8080"
|
|
// - port: "${ENV:PORT}" # Always returns a string
|
|
// - enabled: ${ENV:ENABLED} # We can return a boolean if ENV:ENABLED="true"
|
|
|
|
package smartconfig
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
func TestYAMLInterpolationSyntax(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
yaml string
|
|
shouldParse bool
|
|
desc string
|
|
}{
|
|
{
|
|
name: "unquoted interpolation for number",
|
|
yaml: `port: ${ENV:PORT}`,
|
|
shouldParse: true,
|
|
desc: "Users should be able to use interpolation without quotes to get numbers",
|
|
},
|
|
{
|
|
name: "unquoted interpolation for boolean",
|
|
yaml: `enabled: ${ENV:ENABLED}`,
|
|
shouldParse: true,
|
|
desc: "Users should be able to use interpolation without quotes to get booleans",
|
|
},
|
|
{
|
|
name: "interpolation with special shell chars",
|
|
yaml: `command: ${EXEC:cat /etc/hosts | grep localhost}`,
|
|
shouldParse: true,
|
|
desc: "Special shell characters should work unquoted",
|
|
},
|
|
{
|
|
name: "interpolation with redirect",
|
|
yaml: `output: ${EXEC:echo hello > /tmp/test}`,
|
|
shouldParse: true,
|
|
desc: "Shell redirects should work unquoted",
|
|
},
|
|
{
|
|
name: "complex exec with special chars",
|
|
yaml: `data: ${EXEC:awk '{print $1}' /etc/passwd}`,
|
|
shouldParse: true, // YAML treats the whole expression as a string
|
|
desc: "Complex commands with quotes work unquoted",
|
|
},
|
|
{
|
|
name: "nested interpolation unquoted",
|
|
yaml: `value: ${ENV:PREFIX_${ENV:SUFFIX}}`,
|
|
shouldParse: true,
|
|
desc: "Nested interpolations should work unquoted",
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
var data map[string]interface{}
|
|
err := yaml.Unmarshal([]byte(tc.yaml), &data)
|
|
|
|
if tc.shouldParse && err != nil {
|
|
t.Errorf("%s - expected to parse but got error: %v", tc.desc, err)
|
|
} else if !tc.shouldParse && err == nil {
|
|
t.Errorf("%s - expected parse error but succeeded", tc.desc)
|
|
}
|
|
|
|
if err == nil {
|
|
for key, value := range data {
|
|
t.Logf("%s: %v (type: %T)", key, value, value)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|