Skip to content

Commit

Permalink
Allow slice config values (#374)
Browse files Browse the repository at this point in the history
  • Loading branch information
mgdigital authored Jan 26, 2025
1 parent d5d3535 commit 9bd4ad2
Show file tree
Hide file tree
Showing 5 changed files with 73 additions and 16 deletions.
25 changes: 18 additions & 7 deletions internal/boilerplate/config/configfx/configfx_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ func TestConfig(t *testing.T) {
type Nested struct {
NestedKey string `validate:"uppercase"`
NestedKeyFromConfig string
NestedKeyFromEnv int `validate:"min=1,max=10"`
IntWithValidation int `validate:"min=1,max=10"`
IntSlice []int
DB int
}
Expand All @@ -23,6 +23,7 @@ func TestConfig(t *testing.T) {
Bar int
Nested Nested
StringSlice []string
StructSlice []Nested
Duration time.Duration
Duration2 time.Duration
}
Expand All @@ -42,7 +43,7 @@ func TestConfig(t *testing.T) {
Target: func() (configresolver.Resolver, error) {
return configresolver.NewEnv(map[string]string{
"TEST_DURATION": "2s",
"TEST_NESTED_NESTED_KEY_FROM_ENV": "2",
"TEST_NESTED_INT_WITH_VALIDATION": "2",
"TEST_NESTED_DB": "3",
}, configresolver.WithPriority(-10)), nil
},
Expand All @@ -51,16 +52,16 @@ func TestConfig(t *testing.T) {
fx.Provide(
fx.Annotated{
Group: "config_resolvers",
Target: func() (configresolver.Resolver, error) {
return configresolver.NewFromYamlFile("./test_config.yaml", false)
Target: func(val *validator.Validate) (configresolver.Resolver, error) {
return configresolver.NewFromYamlFile("./test_config.yaml", false, val)
},
},
),
fx.Provide(
fx.Annotated{
Group: "config_resolvers",
Target: func() (configresolver.Resolver, error) {
return configresolver.NewFromYamlFile("./missing.yaml", true)
Target: func(val *validator.Validate) (configresolver.Resolver, error) {
return configresolver.NewFromYamlFile("./missing.yaml", true, val)
},
},
),
Expand All @@ -76,10 +77,20 @@ func TestConfig(t *testing.T) {
Nested: Nested{
NestedKey: "NESTED",
NestedKeyFromConfig: "from_config",
NestedKeyFromEnv: 2,
IntWithValidation: 2,
IntSlice: []int{1, 2, 3},
DB: 3,
},
StructSlice: []Nested{
{
NestedKey: "FOO",
IntWithValidation: 1,
},
{
NestedKey: "BAR",
IntWithValidation: 2,
},
},
}, cfg)
shutdownErr := shutdowner.Shutdown()
assert.NoError(t, shutdownErr)
Expand Down
10 changes: 7 additions & 3 deletions internal/boilerplate/config/configfx/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"github.com/adrg/xdg"
"github.com/bitmagnet-io/bitmagnet/internal/boilerplate/config"
"github.com/bitmagnet-io/bitmagnet/internal/boilerplate/config/configresolver"
"github.com/go-playground/validator/v10"
"go.uber.org/fx"
"os"
"strings"
Expand All @@ -23,10 +24,11 @@ func New() fx.Option {
fx.Provide(
fx.Annotated{
Group: "config_resolvers",
Target: func() (configresolver.Resolver, error) {
Target: func(val *validator.Validate) (configresolver.Resolver, error) {
return configresolver.NewFromYamlFile(
file,
false,
val,
configresolver.WithPriority(-i),
)
},
Expand All @@ -48,10 +50,11 @@ func New() fx.Option {
fx.Provide(
fx.Annotated{
Group: "config_resolvers",
Target: func() (configresolver.Resolver, error) {
Target: func(val *validator.Validate) (configresolver.Resolver, error) {
return configresolver.NewFromYamlFile(
"./config.yml",
true,
val,
configresolver.WithPriority(10),
)
},
Expand All @@ -63,10 +66,11 @@ func New() fx.Option {
fx.Provide(
fx.Annotated{
Group: "config_resolvers",
Target: func() (configresolver.Resolver, error) {
Target: func(val *validator.Validate) (configresolver.Resolver, error) {
return configresolver.NewFromYamlFile(
configFilePath,
true,
val,
configresolver.WithPriority(20),
)
},
Expand Down
5 changes: 5 additions & 0 deletions internal/boilerplate/config/configfx/test_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,8 @@ test:
- 1
- 2
- 3
struct_slice:
- nested_key: FOO
int_with_validation: 1
- nested_key: BAR
int_with_validation: 2
44 changes: 40 additions & 4 deletions internal/boilerplate/config/configresolver/mapresolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,25 @@ package configresolver

import (
"fmt"
"github.com/go-playground/validator/v10"
"github.com/iancoleman/strcase"
"github.com/mitchellh/mapstructure"
"reflect"
)

type mapResolver struct {
baseResolver
m map[string]interface{}
validator *validator.Validate
m map[string]interface{}
}

func NewMap(m map[string]interface{}, options ...Option) Resolver {
r := &mapResolver{m: m}
func NewMap(m map[string]interface{}, val *validator.Validate, options ...Option) Resolver {
r := &mapResolver{m: m, validator: val}
r.applyOptions(append([]Option{WithKey("map")}, options...)...)
return r
}

func (r mapResolver) Resolve(path []string, valueType reflect.Type) (interface{}, bool, error) {
func (r mapResolver) Resolve(path []string, valueType reflect.Type) (any, bool, error) {
if len(path) == 0 {
return r.m, true, nil
}
Expand All @@ -41,9 +45,41 @@ func (r mapResolver) Resolve(path []string, valueType reflect.Type) (interface{}
return nil, true, fmt.Errorf("error coercing config map path '%s' with value '%s' to type %v: %w", currentPath, strV, valueType, coerceErr)
}
return coerced, true, nil
} else if sliceV, sliceOk := rawV.([]interface{}); sliceOk {
resolvedSlice, err := r.resolveSlice(currentPath, sliceV, valueType)
return resolvedSlice, true, err
}
return rawV, true, nil
}
}
return nil, false, nil
}

func (r mapResolver) resolveSlice(currentPath []string, sliceV []any, valueType reflect.Type) (any, error) {
if valueType.Kind() != reflect.Slice {
return nil, fmt.Errorf("received slice at path '%s', expected %s", currentPath, valueType.String())
}
var resolvedSlice []any
for _, sliceItem := range sliceV {
resolvedValue := reflect.New(valueType.Elem())
decoder, decoderErr := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
Result: resolvedValue.Interface(),
MatchName: func(mapKey, fieldName string) bool {
return mapKey == strcase.ToSnake(fieldName)
},
})
if decoderErr != nil {
return nil, decoderErr
}
if decodeErr := decoder.Decode(sliceItem); decodeErr != nil {
return nil, decodeErr
}
if valueType.Elem().Kind() == reflect.Struct {
if validateErr := r.validator.Struct(resolvedValue.Interface()); validateErr != nil {
return nil, validateErr
}
}
resolvedSlice = append(resolvedSlice, reflect.Indirect(resolvedValue).Interface())
}
return resolvedSlice, nil
}
5 changes: 3 additions & 2 deletions internal/boilerplate/config/configresolver/yaml.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
package configresolver

import (
"github.com/go-playground/validator/v10"
"gopkg.in/yaml.v3"
"os"
)

func NewFromYamlFile(path string, ignoreMissing bool, options ...Option) (Resolver, error) {
func NewFromYamlFile(path string, ignoreMissing bool, val *validator.Validate, options ...Option) (Resolver, error) {
m := make(map[string]interface{})
data, readErr := os.ReadFile(path)
if readErr != nil {
Expand All @@ -18,5 +19,5 @@ func NewFromYamlFile(path string, ignoreMissing bool, options ...Option) (Resolv
return nil, parseErr
}
}
return NewMap(m, append([]Option{WithKey(path)}, options...)...), nil
return NewMap(m, val, append([]Option{WithKey(path)}, options...)...), nil
}

0 comments on commit 9bd4ad2

Please sign in to comment.