Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ package confita

import (
"context"
"encoding/json"
"errors"
"fmt"
"reflect"
"regexp"
"strconv"
"strings"
"time"
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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())
}
Expand Down
64 changes: 64 additions & 0 deletions config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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{}{},
Expand Down