Skip to content

Commit

Permalink
improve callback expansion
Browse files Browse the repository at this point in the history
  • Loading branch information
choffmeister committed Dec 15, 2021
1 parent 6b50b83 commit feb9a32
Show file tree
Hide file tree
Showing 2 changed files with 124 additions and 56 deletions.
34 changes: 19 additions & 15 deletions expandenv.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,25 @@ import (
"strings"
)

type VariableLookup = func(key string) (string, error)
type VariableLookup = func(key string) (*string, error)

func ExpandEnv(input interface{}) (interface{}, error) {
return Expand(input, func(key string) (string, error) {
return Expand(input, func(key string) (*string, error) {
value, ok := os.LookupEnv(key)
if !ok {
return "", fmt.Errorf("environment variable %s is missing", key)
return nil, fmt.Errorf("environment variable %s is missing", key)
}
return value, nil
return &value, nil
})
}

func ExpandMap(input interface{}, values map[string]string) (interface{}, error) {
return Expand(input, func(key string) (string, error) {
return Expand(input, func(key string) (*string, error) {
value, ok := values[key]
if !ok {
return "", fmt.Errorf("variable %s is missing", key)
return nil, fmt.Errorf("variable %s is missing", key)
}
return value, nil
return &value, nil
})
}

Expand Down Expand Up @@ -112,27 +112,31 @@ func expandValue(str string, values VariableLookup) (interface{}, error) {
if !hasFallback {
return nil, err
} else {
value = fallback
value = &fallback
}
}

if value == nil {
return str, nil
}

switch format {
case "":
return value, nil
return *value, nil
case "string":
return value, nil
return *value, nil
case "number":
formatted, err := strconv.Atoi(value)
formatted, err := strconv.Atoi(*value)
if err != nil {
formatted, err := strconv.ParseFloat(value, 64)
formatted, err := strconv.ParseFloat(*value, 64)
if err != nil {
return nil, fmt.Errorf("%s is not a valid number", value)
return nil, fmt.Errorf("%s is not a valid number", *value)
}
return formatted, nil
}
return formatted, nil
case "boolean":
switch value {
switch *value {
case "0":
return false, nil
case "1":
Expand All @@ -146,7 +150,7 @@ func expandValue(str string, values VariableLookup) (interface{}, error) {
case "yes":
return true, nil
default:
return nil, fmt.Errorf("%s is not a valid boolean", value)
return nil, fmt.Errorf("%s is not a valid boolean", *value)
}
default:
return nil, fmt.Errorf("format %s is not supported", format)
Expand Down
146 changes: 105 additions & 41 deletions expandenv_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,32 @@ import (
"gopkg.in/yaml.v3"
)

func TestExpandMap(t *testing.T) {
values := map[string]string{
"ENV_A": "a",
"ENV_B": "b",
"ENV_42": "42",
"ENV_42_5": "42.5",
"ENV_YES": "yes",
"ENV_MULTI_LINE": "line1\nline2",
func TestExpand(t *testing.T) {
values := func(key string) (*string, error) {
switch key {
case "FN_A":
result := "a"
return &result, nil
case "FN_B":
result := "b"
return &result, nil
case "FN_42":
result := "42"
return &result, nil
case "FN_42_5":
result := "42.5"
return &result, nil
case "FN_YES":
result := "yes"
return &result, nil
case "FN_MULTI_LINE":
result := "line1\nline2"
return &result, nil
case "FN_IGNORE":
return nil, nil
default:
return nil, fmt.Errorf("unknown")
}
}

testCases := []struct {
Expand Down Expand Up @@ -62,77 +80,123 @@ func TestExpandMap(t *testing.T) {
label: "static-map",
},
{
input: "${ENV_A}",
input: "${FN_A}",
output: "a",
label: "variabled-string",
},
{
input: "prefix ${ENV_B} suffix",
input: "prefix ${FN_B} suffix",
output: "prefix b suffix",
label: "variabled-string-2",
},
{
input: "\\${ENV_A}",
output: "${ENV_A}",
input: "\\${FN_A}",
output: "${FN_A}",
label: "variabled-escacped-string",
},
{
input: "prefix \\${ENV_A} suffix",
output: "prefix ${ENV_A} suffix",
input: "prefix \\${FN_A} suffix",
output: "prefix ${FN_A} suffix",
label: "variabled-escacped-string-2",
},
{
input: map[string]interface{}{"single": "some ${ENV_A}", "multi": "some ${ENV_MULTI_LINE}"},
input: map[string]interface{}{"single": "some ${FN_A}", "multi": "some ${FN_MULTI_LINE}"},
output: map[string]interface{}{"single": "some a", "multi": "some line1\nline2"},
label: "variabled-nested",
},
{
input: "${ENV_42}",
input: "${FN_42}",
output: "42",
label: "variabled-format",
},
{
input: "${ENV_42:number}",
input: "${FN_42:number}",
output: 42,
label: "variabled-format-2",
},
{
input: "${ENV_42_5:number}",
input: "${FN_42_5:number}",
output: 42.5,
label: "variabled-format-3",
},
{
input: "${ENV_YES:boolean}",
input: "${FN_YES:boolean}",
output: true,
label: "variabled-format-4",
},
{
input: "${ENV_42:boolean}",
output: "${ENV_42:boolean}",
input: "${FN_42:boolean}",
output: "${FN_42:boolean}",
label: "variabled-format-2",
error: fmt.Errorf("42 is not a valid boolean"),
},
{
input: "foo: some ${ENV_A} ${ENV_UNKNOWN}",
output: "foo: some a ${ENV_UNKNOWN}",
input: "foo: some ${FN_A} ${FN_UNKNOWN}",
output: "foo: some a ${FN_UNKNOWN}",
label: "variabled-unknown",
error: fmt.Errorf("variable ENV_UNKNOWN is missing"),
error: fmt.Errorf("unknown"),
},
{
input: "foo: some ${ENV_A} |${ENV_B:-fallback}|",
input: "foo: some ${FN_A} |${FN_B:-fallback}|",
output: "foo: some a |b|",
label: "variabled-fallback",
},
{
input: "foo: some ${ENV_A} |${ENV_UNKNOWN:-fallback}|",
input: "foo: some ${FN_A} |${FN_UNKNOWN:-fallback}|",
output: "foo: some a |fallback|",
label: "variabled-fallback-1",
},
{
input: "foo: some ${ENV_A} |${ENV_UNKNOWN:-}|",
input: "foo: some ${FN_A} |${FN_UNKNOWN:-}|",
output: "foo: some a ||",
label: "variabled-fallback-2",
},
{
input: "foo: ${FN_IGNORE}",
output: "foo: ${FN_IGNORE}",
label: "variabled-ignored",
},
}

for _, testCase := range testCases {
output, err := Expand(testCase.input, values)
if testCase.error == nil {
assert.NoError(t, err, testCase.label)
} else {
assert.EqualError(t, err, testCase.error.Error(), testCase.label)
}
assert.Equal(t, testCase.output, output, testCase.label)
}
}

func TestExpandMap(t *testing.T) {
values := map[string]string{
"MAP_A": "a",
"MAP_B": "b",
}

testCases := []struct {
input interface{}
output interface{}
label string
error error
}{
{
input: "${MAP_A}",
output: "a",
label: "variabled-string",
},
{
input: "prefix ${MAP_B} suffix",
output: "prefix b suffix",
label: "variabled-string-2",
},
{
input: "foo: some ${MAP_A} ${MAP_UNKNOWN}",
output: "foo: some a ${MAP_UNKNOWN}",
label: "variabled-unknown",
error: fmt.Errorf("variable MAP_UNKNOWN is missing"),
},
}

for _, testCase := range testCases {
Expand Down Expand Up @@ -187,26 +251,26 @@ func TestExpandEnv(t *testing.T) {

func TestExpandMapWithYaml(t *testing.T) {
values := map[string]string{
"ENV_A": "a",
"ENV_MULTI_LINE": "line1\nline2",
"MAP_A": "a",
"MAP_MULTI_LINE": "line1\nline2",
}

yamlBytes := []byte(`
a: ${ENV_A}
b: prefix ${ENV_A} suffix
a: ${MAP_A}
b: prefix ${MAP_A} suffix
c:
- ${ENV_A}
- ${ENV_A}
d: ${ENV_MULTI_LINE}
e: ${ENV_UNKNOWN}
f: \${ENV_ESCAPED}
g: \\${ENV_ESCAPED}
- ${MAP_A}
- ${MAP_A}
d: ${MAP_MULTI_LINE}
e: ${MAP_UNKNOWN}
f: \${MAP_ESCAPED}
g: \\${MAP_ESCAPED}
`)
var yamlRaw interface{}
err := yaml.Unmarshal(yamlBytes, &yamlRaw)
assert.NoError(t, err)
yamlRaw, err = ExpandMap(yamlRaw, values)
assert.EqualError(t, err, "variable ENV_UNKNOWN is missing")
assert.EqualError(t, err, "variable MAP_UNKNOWN is missing")
yamlBytes, err = yaml.Marshal(yamlRaw)
assert.NoError(t, err)
assert.Equal(t, `a: a
Expand All @@ -217,8 +281,8 @@ c:
d: |-
line1
line2
e: ${ENV_UNKNOWN}
f: ${ENV_ESCAPED}
g: \${ENV_ESCAPED}
e: ${MAP_UNKNOWN}
f: ${MAP_ESCAPED}
g: \${MAP_ESCAPED}
`, string(yamlBytes))
}

0 comments on commit feb9a32

Please sign in to comment.