From 80a0ef81b7bc4ea402af87088bff657c47275998 Mon Sep 17 00:00:00 2001 From: Alexander Groshev Date: Tue, 6 May 2025 10:08:18 +0300 Subject: [PATCH 1/4] fix of parsing slice of strings with commas --- aconfig_test.go | 21 +++++++++++++++++++++ reflection.go | 2 +- testdata/slice-strings.json | 3 +++ utils.go | 7 ++++++- 4 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 testdata/slice-strings.json diff --git a/aconfig_test.go b/aconfig_test.go index bf50fae..a07fec2 100644 --- a/aconfig_test.go +++ b/aconfig_test.go @@ -1618,6 +1618,27 @@ func TestSliceOfDeepStructs(t *testing.T) { mustEqual(t, cfg, want) } +func TestSliceOfStrings(t *testing.T) { + type TestConfig struct { + Strings []string + } + var cfg TestConfig + loader := LoaderFor(&cfg, Config{ + SkipDefaults: true, + SkipEnv: true, + SkipFlags: true, + Files: []string{"testdata/slice-strings.json"}, + }) + + failIfErr(t, loader.Load()) + + want := TestConfig{ + Strings: []string{"hello", "world", "comma1, comma2, comma3,"}, + } + + mustEqual(t, cfg, want) +} + func failIfOk(tb testing.TB, err error) { tb.Helper() if err == nil { diff --git a/reflection.go b/reflection.go index 36991d8..4026125 100644 --- a/reflection.go +++ b/reflection.go @@ -314,7 +314,7 @@ func (l *Loader) setSlice(field *fieldData, value string) error { return nil } - vals := strings.Split(value, ",") + vals := strings.Split(value, sliceSeparator) slice := reflect.MakeSlice(field.field.Type, len(vals), len(vals)) for i, val := range vals { val = strings.TrimSpace(val) diff --git a/testdata/slice-strings.json b/testdata/slice-strings.json new file mode 100644 index 0000000..30491e5 --- /dev/null +++ b/testdata/slice-strings.json @@ -0,0 +1,3 @@ +{ + "strings": [ "hello", "world", "comma1, comma2, comma3," ] +} diff --git a/utils.go b/utils.go index 3152a89..633ca24 100644 --- a/utils.go +++ b/utils.go @@ -11,6 +11,11 @@ import ( "unicode" ) +// sliceSeparator is a separator for slice elements in string. +// Used unicocde control character 001F - record separator(RS) +// https://www.unicode.org/charts/nameslist/n_0000.html +const sliceSeparator = "\u001E" + func assertStruct(x interface{}) { if x == nil { panic("aconfig: destination cannot be nil") @@ -185,7 +190,7 @@ func sliceToString(curr interface{}) string { b := &strings.Builder{} for i, v := range curr { if i > 0 { - b.WriteByte(',') + b.WriteString(sliceSeparator) } fmt.Fprint(b, v) } From 929657e667afd699c3ce94deb064d590a164e66b Mon Sep 17 00:00:00 2001 From: Alexander Groshev Date: Tue, 6 May 2025 10:16:12 +0300 Subject: [PATCH 2/4] fixed mistyping in comment --- utils.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils.go b/utils.go index 633ca24..04fbf1d 100644 --- a/utils.go +++ b/utils.go @@ -12,7 +12,7 @@ import ( ) // sliceSeparator is a separator for slice elements in string. -// Used unicocde control character 001F - record separator(RS) +// Used unicocde control character 001E - record separator(RS) // https://www.unicode.org/charts/nameslist/n_0000.html const sliceSeparator = "\u001E" From be7ed270d4cc76185179b90dfb70934de5347969 Mon Sep 17 00:00:00 2001 From: Alexander Groshev Date: Tue, 13 May 2025 13:36:48 +0300 Subject: [PATCH 3/4] make the slice separator customizable --- aconfig.go | 7 +++++++ aconfig_test.go | 9 +++++---- reflection.go | 4 ++-- utils.go | 9 ++------- 4 files changed, 16 insertions(+), 13 deletions(-) diff --git a/aconfig.go b/aconfig.go index 8c345eb..281a088 100644 --- a/aconfig.go +++ b/aconfig.go @@ -98,6 +98,9 @@ type Config struct { // ".env": aconfigdotenv.New(), // } FileDecoders map[string]FileDecoder + + // SliceSeparator hold the separator for slice values. Default is ",". + SliceSeparator string } // FileDecoder is used to read config from files. See aconfig submodules. @@ -205,6 +208,10 @@ func (l *Loader) init() { // TODO: should be prefixed ? l.flagSet.String(l.config.FileFlag, "", "config file param") } + + if l.config.SliceSeparator == "" { + l.config.SliceSeparator = "," + } } // Flags returngs flag.FlagSet to create your own flags. diff --git a/aconfig_test.go b/aconfig_test.go index a07fec2..dcb7789 100644 --- a/aconfig_test.go +++ b/aconfig_test.go @@ -1624,10 +1624,11 @@ func TestSliceOfStrings(t *testing.T) { } var cfg TestConfig loader := LoaderFor(&cfg, Config{ - SkipDefaults: true, - SkipEnv: true, - SkipFlags: true, - Files: []string{"testdata/slice-strings.json"}, + SkipDefaults: true, + SkipEnv: true, + SkipFlags: true, + Files: []string{"testdata/slice-strings.json"}, + SliceSeparator: "\u001E", }) failIfErr(t, loader.Load()) diff --git a/reflection.go b/reflection.go index 4026125..c4892e9 100644 --- a/reflection.go +++ b/reflection.go @@ -198,7 +198,7 @@ func (l *Loader) setFieldData(field *fieldData, value interface{}) error { case reflect.Slice: if isPrimitive(field.field.Type.Elem()) { - return l.setSlice(field, sliceToString(value)) + return l.setSlice(field, l.sliceToString(value)) } in := reflect.ValueOf(value) @@ -314,7 +314,7 @@ func (l *Loader) setSlice(field *fieldData, value string) error { return nil } - vals := strings.Split(value, sliceSeparator) + vals := strings.Split(value, l.config.SliceSeparator) slice := reflect.MakeSlice(field.field.Type, len(vals), len(vals)) for i, val := range vals { val = strings.TrimSpace(val) diff --git a/utils.go b/utils.go index 04fbf1d..ef94e1d 100644 --- a/utils.go +++ b/utils.go @@ -11,11 +11,6 @@ import ( "unicode" ) -// sliceSeparator is a separator for slice elements in string. -// Used unicocde control character 001E - record separator(RS) -// https://www.unicode.org/charts/nameslist/n_0000.html -const sliceSeparator = "\u001E" - func assertStruct(x interface{}) { if x == nil { panic("aconfig: destination cannot be nil") @@ -184,13 +179,13 @@ func (d *jsonDecoder) DecodeFile(filename string) (map[string]interface{}, error return raw, nil } -func sliceToString(curr interface{}) string { +func (l *Loader) sliceToString(curr interface{}) string { switch curr := curr.(type) { case []interface{}: b := &strings.Builder{} for i, v := range curr { if i > 0 { - b.WriteString(sliceSeparator) + b.WriteString(l.config.SliceSeparator) } fmt.Fprint(b, v) } From 169ca3614844efe8dd863f2b4d61b771521e8c89 Mon Sep 17 00:00:00 2001 From: Alexander Groshev Date: Mon, 18 Aug 2025 16:14:28 +0300 Subject: [PATCH 4/4] respect tag name for structs in a slice --- aconfig_test.go | 18 +++++---- reflection.go | 46 ++++++++++++++++------ testdata/slice-struct-primitive-slice.json | 3 +- 3 files changed, 45 insertions(+), 22 deletions(-) diff --git a/aconfig_test.go b/aconfig_test.go index dcb7789..2311efd 100644 --- a/aconfig_test.go +++ b/aconfig_test.go @@ -1536,10 +1536,11 @@ func TestFileConfigFlagDelim(t *testing.T) { func TestSliceOfStructsWithSliceOfPrimitives(t *testing.T) { type TestService struct { - Name string - Strings []string - Integers []int - Booleans []bool + Name string + Strings []string + Integers []int + Booleans []bool + AnotherStrings []string `json:"another_strings"` } type TestConfig struct { @@ -1558,10 +1559,11 @@ func TestSliceOfStructsWithSliceOfPrimitives(t *testing.T) { want := TestConfig{ Services: []TestService{ { - Name: "service1", - Strings: []string{"string1", "string2"}, - Integers: []int{1, 2}, - Booleans: []bool{true, false}, + Name: "service1", + Strings: []string{"string1", "string2"}, + Integers: []int{1, 2}, + Booleans: []bool{true, false}, + AnotherStrings: []string{"another1", "another2"}, }, }, } diff --git a/reflection.go b/reflection.go index c4892e9..3d5f2b5 100644 --- a/reflection.go +++ b/reflection.go @@ -358,22 +358,42 @@ func (l *Loader) setMap(field *fieldData, value string) error { } func (l *Loader) m2s(m map[string]interface{}, structValue reflect.Value) error { - for name, value := range m { - name = strings.Title(name) - structFieldValue := structValue.FieldByName(name) - if !structFieldValue.IsValid() { - return fmt.Errorf("no such field %q in struct", name) - } + for _, dec := range l.config.FileDecoders { + for name, value := range m { + structFieldValue := structValue.FieldByName(name) + for i := 0; i < structValue.NumField(); i++ { + // first try to find field by name + tagName := structValue.Type().Field(i).Tag.Get(dec.Format()) + // if tag is set - use it + if strings.EqualFold(tagName, name) { + name = structValue.Type().Field(i).Name + structFieldValue = structValue.FieldByName(name) + break + } + // if tag is not set - try to find field by name + if tagName == "" { + if strings.EqualFold(structValue.Type().Field(i).Name, name) { + name = structValue.Type().Field(i).Name + structFieldValue = structValue.FieldByName(name) + break + } + } + } - if !structFieldValue.CanSet() { - return fmt.Errorf("cannot set %q field value", name) - } + if !structFieldValue.IsValid() { + return fmt.Errorf("no such field %q in struct", name) + } - field, _ := structValue.Type().FieldByName(name) + if !structFieldValue.CanSet() { + return fmt.Errorf("cannot set %q field value", name) + } - fd := l.newFieldData(field, structFieldValue, nil) - if err := l.setFieldData(fd, value); err != nil { - return err + field, _ := structValue.Type().FieldByName(name) + + fd := l.newFieldData(field, structFieldValue, nil) + if err := l.setFieldData(fd, value); err != nil { + return err + } } } return nil diff --git a/testdata/slice-struct-primitive-slice.json b/testdata/slice-struct-primitive-slice.json index 0010a0e..d7676e0 100644 --- a/testdata/slice-struct-primitive-slice.json +++ b/testdata/slice-struct-primitive-slice.json @@ -4,7 +4,8 @@ "name": "service1", "strings": ["string1", "string2"], "integers": [1, 2], - "booleans": [true, false] + "booleans": [true, false], + "another_strings": ["another1", "another2"] } ] }