Skip to content

Commit ab27f1b

Browse files
committed
fix(fill): Map keys are case-insensitive, handle maps recursively
1 parent facd640 commit ab27f1b

File tree

3 files changed

+144
-17
lines changed

3 files changed

+144
-17
lines changed

base/fill/path_value.go

Lines changed: 57 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ func SetPathValue(path string, val interface{}, output interface{}) error {
3737
target, field, err := findTargetAtPath(steps, target)
3838
if err != nil {
3939
if err == ErrNotFound {
40-
return fmt.Errorf("at \"%s\": path not found", path)
40+
return fmt.Errorf("at %q: %w", path, ErrNotFound)
4141
}
4242
collector.Add(err)
4343
return collector.AsSingleError()
@@ -65,22 +65,22 @@ func GetPathValue(path string, input interface{}) (interface{}, error) {
6565
// Find target place to assign to, field is a non-empty string only if target is a map.
6666
target, field, err := findTargetAtPath(steps, target)
6767
if err != nil {
68-
if err == ErrNotFound {
69-
return nil, fmt.Errorf("at \"%s\": path not found", path)
70-
}
71-
return nil, fmt.Errorf("at \"%s\": %s", path, err)
68+
return nil, fmt.Errorf("at %q: %w", path, err)
7269
}
70+
// If all steps completed, resolve the target to a real value.
7371
if field == "" {
7472
return target.Interface(), nil
7573
}
76-
77-
lookup := target.MapIndex(reflect.ValueOf(field))
78-
if lookup.IsValid() {
74+
// Otherwise, one step is left, look it up using case-insensitive comparison.
75+
if lookup := mapLookupCaseInsensitive(target, field); lookup != nil {
7976
return lookup.Interface(), nil
8077
}
81-
return nil, fmt.Errorf("at \"%s\": invalid path", path)
78+
return nil, fmt.Errorf("at %q: %w", path, ErrNotFound)
8279
}
8380

81+
// Recursively look up the first step in place, until there are no steps left. If place is ever
82+
// a map with one step left, return that map and final step - this is done in order to enable
83+
// assignment to that map.
8484
func findTargetAtPath(steps []string, place reflect.Value) (reflect.Value, string, error) {
8585
if len(steps) == 0 {
8686
return place, "", nil
@@ -94,6 +94,16 @@ func findTargetAtPath(steps []string, place reflect.Value) (reflect.Value, strin
9494
return place, "", ErrNotFound
9595
}
9696
return findTargetAtPath(rest, field)
97+
} else if place.Kind() == reflect.Interface {
98+
var inner reflect.Value
99+
if place.IsNil() {
100+
alloc := reflect.New(place.Type().Elem())
101+
place.Set(alloc)
102+
inner = alloc.Elem()
103+
} else {
104+
inner = place.Elem()
105+
}
106+
return findTargetAtPath(steps, inner)
97107
} else if place.Kind() == reflect.Ptr {
98108
var inner reflect.Value
99109
if place.IsNil() {
@@ -108,9 +118,14 @@ func findTargetAtPath(steps []string, place reflect.Value) (reflect.Value, strin
108118
if place.IsNil() {
109119
place.Set(reflect.MakeMap(place.Type()))
110120
}
111-
// TODO: Handle case where `rest` has more steps and `val` is a struct: more
112-
// recursive is needed.
113-
return place, s, nil
121+
if len(steps) == 1 {
122+
return place, s, nil
123+
}
124+
found := mapLookupCaseInsensitive(place, s)
125+
if found == nil {
126+
return place, "", ErrNotFound
127+
}
128+
return findTargetAtPath(rest, *found)
114129
} else if place.Kind() == reflect.Slice {
115130
num, err := coerceToInt(s)
116131
if err != nil {
@@ -126,6 +141,36 @@ func findTargetAtPath(steps []string, place reflect.Value) (reflect.Value, strin
126141
}
127142
}
128143

144+
// Look up value in the map, using case-insensitive string comparison. Return nil if not found
145+
func mapLookupCaseInsensitive(mapValue reflect.Value, k string) *reflect.Value {
146+
// Try looking up the value without changing the string case
147+
key := reflect.ValueOf(k)
148+
result := mapValue.MapIndex(key)
149+
if result.IsValid() {
150+
return &result
151+
}
152+
// Try lower-casing the key and looking that up
153+
klower := strings.ToLower(k)
154+
key = reflect.ValueOf(klower)
155+
result = mapValue.MapIndex(key)
156+
if result.IsValid() {
157+
return &result
158+
}
159+
// Iterate over the map keys, compare each, using case-insensitive matching
160+
mapKeys := mapValue.MapKeys()
161+
for _, mk := range mapKeys {
162+
mstr := mk.String()
163+
if strings.ToLower(mstr) == klower {
164+
result = mapValue.MapIndex(mk)
165+
if result.IsValid() {
166+
return &result
167+
}
168+
}
169+
}
170+
// Not found, return nil
171+
return nil
172+
}
173+
129174
func coerceToTargetType(val interface{}, place reflect.Value) (interface{}, error) {
130175
switch place.Kind() {
131176
case reflect.Bool:

base/fill/path_value_test.go

Lines changed: 86 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,8 @@ func TestFillPathValue(t *testing.T) {
106106
}
107107

108108
c = Collection{}
109-
err = SetPathValue("not_found", "missing", &c)
110-
expect = `at "not_found": path not found`
109+
err = SetPathValue("non_existent", "missing", &c)
110+
expect = `at "non_existent": not found`
111111
if err == nil {
112112
t.Fatalf("expected: error \"%s\", got no error", expect)
113113
}
@@ -208,6 +208,10 @@ func TestGetPathValue(t *testing.T) {
208208
"extra": "misc",
209209
},
210210
List: []string{"cat", "dog", "eel"},
211+
Sub: SubElement {
212+
Num: 7,
213+
Text: "sandwich",
214+
},
211215
}
212216

213217
val, err := GetPathValue("name", &c)
@@ -226,8 +230,24 @@ func TestGetPathValue(t *testing.T) {
226230
t.Errorf("expected: val should be \"misc\"")
227231
}
228232

229-
val, err = GetPathValue("not_found", &c)
230-
expect := `at "not_found": path not found`
233+
val, err = GetPathValue("sub.num", &c)
234+
if err != nil {
235+
panic(err)
236+
}
237+
if val != 7 {
238+
t.Errorf("expected: val should be 7, got %v", val)
239+
}
240+
241+
val, err = GetPathValue("sub.text", &c)
242+
if err != nil {
243+
panic(err)
244+
}
245+
if val != "sandwich" {
246+
t.Errorf("expected: val should be \"sandwich\", got %v", val)
247+
}
248+
249+
val, err = GetPathValue("non_existent", &c)
250+
expect := `at "non_existent": not found`
231251
if err == nil {
232252
t.Fatalf("expected: error \"%s\", got no error", expect)
233253
}
@@ -236,7 +256,7 @@ func TestGetPathValue(t *testing.T) {
236256
}
237257

238258
val, err = GetPathValue("dict.missing_key", &c)
239-
expect = `at "dict.missing_key": invalid path`
259+
expect = `at "dict.missing_key": not found`
240260
if err == nil {
241261
t.Fatalf("expected: error \"%s\", got no error", expect)
242262
}
@@ -262,3 +282,64 @@ func TestGetPathValue(t *testing.T) {
262282
}
263283

264284
}
285+
286+
func TestDictKeysCaseInsenstive(t *testing.T) {
287+
obj := map[string]interface{}{
288+
"Parent": map[string]interface{}{
289+
"Child": "ok",
290+
},
291+
"First": map[string]interface{}{
292+
"Second": map[string]interface{}{
293+
"Third": "alright",
294+
},
295+
},
296+
}
297+
298+
val, err := GetPathValue("parent.child", obj)
299+
if err != nil {
300+
panic(err)
301+
}
302+
if val != "ok" {
303+
t.Errorf("expected: val should be \"ok\"")
304+
}
305+
306+
val, err = GetPathValue("parent.Child", obj)
307+
if err != nil {
308+
panic(err)
309+
}
310+
if val != "ok" {
311+
t.Errorf("expected: val should be \"ok\"")
312+
}
313+
314+
val, err = GetPathValue("parent.CHILD", obj)
315+
if err != nil {
316+
panic(err)
317+
}
318+
if val != "ok" {
319+
t.Errorf("expected: val should be \"ok\"")
320+
}
321+
322+
val, err = GetPathValue("Parent.Child", obj)
323+
if err != nil {
324+
panic(err)
325+
}
326+
if val != "ok" {
327+
t.Errorf("expected: val should be \"ok\"")
328+
}
329+
330+
val, err = GetPathValue("first.second.third", obj)
331+
if err != nil {
332+
panic(err)
333+
}
334+
if val != "alright" {
335+
t.Errorf("expected: val should be \"alright\"")
336+
}
337+
338+
val, err = GetPathValue("FIRST.SECOND.THIRD", obj)
339+
if err != nil {
340+
panic(err)
341+
}
342+
if val != "alright" {
343+
t.Errorf("expected: val should be \"alright\"")
344+
}
345+
}

base/fill/struct_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,7 @@ type SubElement struct {
258258
Num int
259259
Things *map[string]string
260260
Any interface{}
261+
Text string
261262
}
262263

263264
func (c *Collection) SetArbitrary(key string, val interface{}) error {

0 commit comments

Comments
 (0)