Skip to content

Commit d79c469

Browse files
committed
[confmap] use string representation on stringy types
1 parent 69b2850 commit d79c469

File tree

5 files changed

+141
-5
lines changed

5 files changed

+141
-5
lines changed

confmap/confmap.go

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -111,21 +111,36 @@ func (l *Conf) unsanitizedGet(key string) any {
111111
return l.k.Get(key)
112112
}
113113

114+
// sanitize recursively removes expandedValue references from the given data.
115+
// It uses the expandedValue.Value field to replace the expandedValue references.
114116
func sanitize(a any) any {
117+
return sanitizeExpanded(a, false)
118+
}
119+
120+
// sanitizeToStringMap recursively removes expandedValue references from the given data.
121+
// It uses the expandedValue.Original field to replace the expandedValue references.
122+
func sanitizeToStr(a any) any {
123+
return sanitizeExpanded(a, true)
124+
}
125+
126+
func sanitizeExpanded(a any, useOriginal bool) any {
115127
switch m := a.(type) {
116128
case map[string]any:
117129
c := maps.Copy(m)
118130
for k, v := range m {
119-
c[k] = sanitize(v)
131+
c[k] = sanitizeExpanded(v, useOriginal)
120132
}
121133
return c
122134
case []any:
123135
var newSlice []any
124136
for _, e := range m {
125-
newSlice = append(newSlice, sanitize(e))
137+
newSlice = append(newSlice, sanitizeExpanded(e, useOriginal))
126138
}
127139
return newSlice
128140
case expandedValue:
141+
if useOriginal {
142+
return m.Original
143+
}
129144
return m.Value
130145
}
131146
return a
@@ -134,7 +149,7 @@ func sanitize(a any) any {
134149
// Get can retrieve any value given the key to use.
135150
func (l *Conf) Get(key string) any {
136151
val := l.unsanitizedGet(key)
137-
return sanitize(val)
152+
return sanitizeExpanded(val, false)
138153
}
139154

140155
// IsSet checks to see if the key has been set in any of the data locations.
@@ -244,6 +259,21 @@ func castTo(exp expandedValue, useOriginal bool) (any, error) {
244259
return exp.Value, nil
245260
}
246261

262+
// Check if a reflect.Type is of the form T:
263+
// T = string | map[string]T | []T | [n]T
264+
func isStringyStructure(t reflect.Type) bool {
265+
if t.Kind() == reflect.String {
266+
return true
267+
}
268+
if t.Kind() == reflect.Map {
269+
return t.Key().Kind() == reflect.String && isStringyStructure(t.Elem())
270+
}
271+
if t.Kind() == reflect.Slice || t.Kind() == reflect.Array {
272+
return isStringyStructure(t.Elem())
273+
}
274+
return false
275+
}
276+
247277
// When a value has been loaded from an external source via a provider, we keep both the
248278
// parsed value and the original string value. This allows us to expand the value to its
249279
// original string representation when decoding into a string field, and use the original otherwise.
@@ -256,10 +286,13 @@ func useExpandValue() mapstructure.DecodeHookFuncType {
256286
return castTo(exp, to.Kind() == reflect.String)
257287
}
258288

259-
// If the target field is a map or slice, sanitize input to remove expandedValue references.
260289
switch to.Kind() {
261290
case reflect.Array, reflect.Slice, reflect.Map:
262-
// This does not handle map[string]string and []string explicitly.
291+
if isStringyStructure(to) {
292+
// If the target field is a stringy structure, sanitize to use the original string value everywhere.
293+
return sanitizeToStr(data), nil
294+
}
295+
// Otherwise, sanitize to use the parsed value everywhere.
263296
return sanitize(data), nil
264297
}
265298
return data, nil

confmap/confmap_test.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"fmt"
99
"os"
1010
"path/filepath"
11+
"reflect"
1112
"strings"
1213
"testing"
1314

@@ -874,3 +875,57 @@ func TestExpandedValue(t *testing.T) {
874875
cfgBool := ConfigBool{}
875876
assert.Error(t, cm.Unmarshal(&cfgBool))
876877
}
878+
879+
func TestStringyTypes(t *testing.T) {
880+
tests := []struct {
881+
valueOfType any
882+
isStringy bool
883+
}{
884+
{
885+
valueOfType: "string",
886+
isStringy: true,
887+
},
888+
{
889+
valueOfType: 1,
890+
isStringy: false,
891+
},
892+
{
893+
valueOfType: map[string]any{},
894+
isStringy: false,
895+
},
896+
{
897+
valueOfType: []any{},
898+
isStringy: false,
899+
},
900+
{
901+
valueOfType: map[string]string{},
902+
isStringy: true,
903+
},
904+
{
905+
valueOfType: []string{},
906+
isStringy: true,
907+
},
908+
{
909+
valueOfType: map[string][]string{},
910+
isStringy: true,
911+
},
912+
{
913+
valueOfType: map[string]map[string]string{},
914+
isStringy: true,
915+
},
916+
{
917+
valueOfType: []map[string]any{},
918+
isStringy: false,
919+
},
920+
{
921+
valueOfType: []map[string]string{},
922+
isStringy: true,
923+
},
924+
}
925+
926+
for _, tt := range tests {
927+
// Create a reflect.Type from the value
928+
to := reflect.TypeOf(tt.valueOfType)
929+
assert.Equal(t, tt.isStringy, isStringyStructure(to))
930+
}
931+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
field: [key: ["${env:VALUE}"]]
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
field: ["${env:VALUE}"]

confmap/internal/e2e/types_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -599,3 +599,49 @@ func TestIssue10937_MapType(t *testing.T) {
599599
require.NoError(t, err)
600600
require.Equal(t, map[string]configopaque.String{"key": "1234"}, cfg.Field)
601601
}
602+
603+
func TestIssue10937_ArrayType(t *testing.T) {
604+
t.Setenv("VALUE", "1234")
605+
606+
resolver := NewResolver(t, "types_slice.yaml")
607+
conf, err := resolver.Resolve(context.Background())
608+
require.NoError(t, err)
609+
610+
var cfgStrSlice TargetConfig[[]string]
611+
err = conf.Unmarshal(&cfgStrSlice)
612+
require.NoError(t, err)
613+
require.Equal(t, []string{"1234"}, cfgStrSlice.Field)
614+
615+
var cfgStrArray TargetConfig[[1]string]
616+
err = conf.Unmarshal(&cfgStrArray)
617+
require.NoError(t, err)
618+
require.Equal(t, [1]string{"1234"}, cfgStrArray.Field)
619+
620+
var cfgAnySlice TargetConfig[[]any]
621+
err = conf.Unmarshal(&cfgAnySlice)
622+
require.NoError(t, err)
623+
require.Equal(t, []any{1234}, cfgAnySlice.Field)
624+
625+
var cfgAnyArray TargetConfig[[1]any]
626+
err = conf.Unmarshal(&cfgAnyArray)
627+
require.NoError(t, err)
628+
require.Equal(t, [1]any{1234}, cfgAnyArray.Field)
629+
}
630+
631+
func TestIssue10937_ComplexType(t *testing.T) {
632+
t.Setenv("VALUE", "1234")
633+
634+
resolver := NewResolver(t, "types_complex.yaml")
635+
conf, err := resolver.Resolve(context.Background())
636+
require.NoError(t, err)
637+
638+
var cfgStringy TargetConfig[[]map[string][]string]
639+
err = conf.Unmarshal(&cfgStringy)
640+
require.NoError(t, err)
641+
require.Equal(t, []map[string][]string{{"key": {"1234"}}}, cfgStringy.Field)
642+
643+
var cfgNotStringy TargetConfig[[]map[string][]any]
644+
err = conf.Unmarshal(&cfgNotStringy)
645+
require.NoError(t, err)
646+
require.Equal(t, []map[string][]any{{"key": {1234}}}, cfgNotStringy.Field)
647+
}

0 commit comments

Comments
 (0)