diff --git a/config.go b/config.go index 04e7410..c4b8011 100644 --- a/config.go +++ b/config.go @@ -2,9 +2,11 @@ package confita import ( "context" + "encoding/json" "errors" "fmt" "reflect" + "regexp" "strconv" "strings" "time" @@ -269,6 +271,12 @@ func (f *FieldConfig) Set(data string) error { var durationType = reflect.TypeOf(time.Duration(0)) var timeType = reflect.TypeOf(time.Time{}) +var unquotedNumericJSONKeyRE = regexp.MustCompile(`([{,]\s*)(-?\d+)\s*:`) + +func normalizeNumericJSONKeys(data string) string { + return unquotedNumericJSONKeyRE.ReplaceAllString(data, `$1"$2":`) +} + func convert(data string, value reflect.Value) error { t := value.Type() if t == durationType { @@ -346,6 +354,34 @@ func convert(data string, value reflect.Value) error { return err } value.SetFloat(f) + case reflect.Map: + keyKind := t.Key().Kind() + switch keyKind { + case reflect.String, + reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + default: + return fmt.Errorf("map key type '%s' not supported", keyKind) + } + + nv := reflect.New(t) + if err := json.Unmarshal([]byte(data), nv.Interface()); err != nil { + if keyKind != reflect.String { + normalized := normalizeNumericJSONKeys(data) + if normalized != data { + nvRetry := reflect.New(t) + if errRetry := json.Unmarshal([]byte(normalized), nvRetry.Interface()); errRetry == nil { + value.Set(nvRetry.Elem()) + return nil + } else { + return errRetry + } + } + } + return err + } + + value.Set(nv.Elem()) default: return fmt.Errorf("field type '%s' not supported", t.Kind()) } diff --git a/config_test.go b/config_test.go index faf7702..4443e60 100644 --- a/config_test.go +++ b/config_test.go @@ -521,6 +521,61 @@ func TestSliceField(t *testing.T) { }) } +func TestMapField(t *testing.T) { + t.Run("map of string to []int (json)", func(t *testing.T) { + s := struct { + Settings map[string][]int `config:"settings"` + }{} + + st := store{ + "settings": `{"key1":[1,2,3],"key2":[4,5,6]}`, + } + + err := confita.NewLoader(st).Load(context.Background(), &s) + require.NoError(t, err) + require.Equal(t, map[string][]int{ + "key1": {1, 2, 3}, + "key2": {4, 5, 6}, + }, s.Settings) + }) + + t.Run("map of int64 to []int64 (json numeric keys)", func(t *testing.T) { + s := struct { + AllowedMethods map[int64][]int64 `config:"allowed_methods"` + }{} + + st := store{ + "allowed_methods": `{"7":[1101],"37":[1101],"671":[1101]}`, + } + + err := confita.NewLoader(st).Load(context.Background(), &s) + require.NoError(t, err) + require.Equal(t, map[int64][]int64{ + 7: {1101}, + 37: {1101}, + 671: {1101}, + }, s.AllowedMethods) + }) + + t.Run("map of int64 to []int64 (non-json numeric keys)", func(t *testing.T) { + s := struct { + AllowedMethods map[int64][]int64 `config:"allowed_methods"` + }{} + + st := store{ + "allowed_methods": `{7:[1101], 37:[1101], 671:[1101]}`, + } + + err := confita.NewLoader(st).Load(context.Background(), &s) + require.NoError(t, err) + require.Equal(t, map[int64][]int64{ + 7: {1101}, + 37: {1101}, + 671: {1101}, + }, s.AllowedMethods) + }) +} + var errorTests = []struct { testName string store store @@ -598,6 +653,15 @@ var errorTests = []struct { X uintptr `config:"X"` }), expectError: `field type 'uintptr' not supported`, +}, { + testName: "unsupported-map-key-type", + store: store{ + "X": `{"a":"b"}`, + }, + into: new(struct { + X map[struct{}]string `config:"X"` + }), + expectError: `map key type 'struct' not supported`, }, { testName: "not-struct-pointer", into: struct{}{},