smartconfig/yaml_syntax_test.go
sneak cf13b275b7 Squashed commit of the following:
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.
2025-07-21 15:19:28 +02:00

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)
}
}
})
}
}