Skip to content
Merged
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
129 changes: 129 additions & 0 deletions configurator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,11 @@

type testCase struct {
caseName string
input interface{}

Check failure on line 47 in configurator_test.go

View workflow job for this annotation

GitHub Actions / runner / golangci-lint

any: interface{} can be replaced by any (modernize)
provider provider.Provider
options []Option

dataAssertion func(*testing.T, interface{})

Check failure on line 51 in configurator_test.go

View workflow job for this annotation

GitHub Actions / runner / golangci-lint

any: interface{} can be replaced by any (modernize)
errAssertion func(*testing.T, error)
}

Expand All @@ -56,7 +56,7 @@

func hasStaticError(out error) func(*testing.T, error) {
return func(t *testing.T, in error) {
if out != in {

Check failure on line 59 in configurator_test.go

View workflow job for this annotation

GitHub Actions / runner / golangci-lint

comparing with != will fail on wrapped errors. Use errors.Is to check for a specific error (errorlint)
t.Errorf("Error returned: %v [ expected %v ]", in, out)
}
}
Expand Down Expand Up @@ -226,8 +226,8 @@
}
}

func deepEqual(x interface{}) func(*testing.T, interface{}) {

Check failure on line 229 in configurator_test.go

View workflow job for this annotation

GitHub Actions / runner / golangci-lint

any: interface{} can be replaced by any (modernize)
return func(t *testing.T, y interface{}) {

Check failure on line 230 in configurator_test.go

View workflow job for this annotation

GitHub Actions / runner / golangci-lint

any: interface{} can be replaced by any (modernize)
t.Helper()

assert.Equalf(t, x, y, "Expected equality with %v but %v", x, y)
Expand All @@ -240,7 +240,7 @@
)

for _, tCase := range []testCase{
testCase{

Check failure on line 243 in configurator_test.go

View workflow job for this annotation

GitHub Actions / runner / golangci-lint

File is not properly formatted (gofmt)
caseName: "basic-error",
input: &basicStruct1{},
provider: &mockProvider{err: errTest},
Expand Down Expand Up @@ -742,6 +742,135 @@
}
}

type prefixedConfig struct {
prefix []string
value any
}

func (p *prefixedConfig) WalkPrefix() []string { return p.prefix }
func (p *prefixedConfig) WalkValue() any { return p.value }

type outerPrefixedConfig struct {
Direct string `mock:"direct"`
Nested *prefixedConfig
}

func TestPrefixedPopulate(t *testing.T) {
for _, tc := range []struct {
name string
have any
provider provider.Provider
want any
}{
{
name: "single segment prefix",
have: &prefixedConfig{
prefix: []string{"ns"},
value: &basicStruct1{},
},
provider: &mockProvider{st: map[string]string{"ns.Fiz": "bar"}},
want: &basicStruct1{Fiz: "bar"},
},
{
name: "multi segment prefix",
have: &prefixedConfig{
prefix: []string{"foo", "bar"},
value: &basicStruct1{},
},
provider: &mockProvider{st: map[string]string{"foo.bar.Fiz": "baz"}},
want: &basicStruct1{Fiz: "baz"},
},
{
name: "prefix with tagged field",
have: &prefixedConfig{
prefix: []string{"ns"},
value: &basicStructBool{},
},
provider: &mockProvider{st: map[string]string{"ns.fzz": "true"}},
want: &basicStructBool{Bool: true},
},
{
name: "prefix with nested struct",
have: &prefixedConfig{
prefix: []string{"pfx"},
value: &nestedStruct{},
},
provider: &mockProvider{st: map[string]string{"pfx.nested.inner": "42"}},
want: func() *nestedStruct {
var v int64 = 42
ns := &nestedStruct{}
Comment thread
AlexisMontagne marked this conversation as resolved.

ns.Nested.Inner = &v

return ns
}(),
},
{
name: "empty prefix behaves like normal populate",
have: &prefixedConfig{
prefix: nil,
value: &basicStruct1{},
},
provider: &mockProvider{st: map[string]string{"Fiz": "val"}},
want: &basicStruct1{Fiz: "val"},
},
} {
t.Run(tc.name, func(t *testing.T) {
c := NewConfiguratorWithOptions(WithProviders(tc.provider))

err := c.Populate(context.Background(), tc.have)

require.NoError(t, err)
assert.Equal(t, tc.want, tc.have.(*prefixedConfig).value)
})
}
}

func TestNestedPrefixedPopulate(t *testing.T) {
for _, tc := range []struct {
name string
have *outerPrefixedConfig
provider provider.Provider
wantOuter string
wantInner *basicStruct1
}{
{
name: "nested prefixed field",
have: &outerPrefixedConfig{
Nested: &prefixedConfig{
prefix: []string{"ns"},
value: &basicStruct1{},
},
},
provider: &mockProvider{st: map[string]string{"direct": "top", "Nested.ns.Fiz": "deep"}},
wantOuter: "top",
wantInner: &basicStruct1{Fiz: "deep"},
},
{
name: "nested prefixed with multi-segment prefix",
have: &outerPrefixedConfig{
Nested: &prefixedConfig{
prefix: []string{"a", "b"},
value: &basicStruct1{},
},
},
provider: &mockProvider{st: map[string]string{"Nested.a.b.Fiz": "val"}},
wantOuter: "",
wantInner: &basicStruct1{Fiz: "val"},
},
} {
t.Run(tc.name, func(t *testing.T) {
c := NewConfiguratorWithOptions(WithProviders(tc.provider))

err := c.Populate(context.Background(), tc.have)

require.NoError(t, err)
assert.Equal(t, tc.wantOuter, tc.have.Direct)
assert.Equal(t, tc.wantInner, tc.have.Nested.value)
})
}
}

func ExampleNewDefaultConfigurator() {
os.Setenv("FOO", "bar")
cfg := struct {
Expand Down
33 changes: 30 additions & 3 deletions internal/help/writer.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"fmt"
"io"
"reflect"
"strings"

"github.com/upfluence/cfg/internal/reflectutil"
Expand All @@ -26,8 +27,34 @@ var (
flags.NewDefaultProvider(),
},
}

helperType = reflect.TypeFor[helper]()
)

type helper interface {
Help() string
}

func fieldHelp(f *walker.Field) string {
fv := reflectutil.IndirectedValue(f.Value).FieldByName(f.Field.Name)

if fv.CanAddr() && fv.Addr().Type().Implements(helperType) {
if h := fv.Addr().Interface().(helper).Help(); h != "" {
return h
}
} else if fv.Type().Implements(helperType) && fv.CanInterface() {
if h := fv.Interface().(helper).Help(); h != "" {
return h
}
}

if h, ok := f.Field.Tag.Lookup("help"); ok {
return h
}

return ""
}

type Writer struct {
Providers []provider.Provider
Factory setter.Factory
Expand Down Expand Up @@ -69,12 +96,12 @@ func (w *Writer) writeConfig(out io.Writer, in interface{}) (int, error) {
b.WriteString(": ")
b.WriteString(s.String())

if h, ok := f.Field.Tag.Lookup("help"); ok {
if h := fieldHelp(f); h != "" {
b.WriteString(" ")
b.WriteString(h)
}

defaultValue := w.fieldDefault(f)
defaultValue := fieldDefault(f)
providedKeys, tagDefault := w.providerKeys(f)

if tagDefault != "" {
Expand Down Expand Up @@ -106,7 +133,7 @@ func (w *Writer) writeConfig(out io.Writer, in interface{}) (int, error) {
)
}

func (w *Writer) fieldDefault(f *walker.Field) string {
func fieldDefault(f *walker.Field) string {
fv := reflectutil.IndirectedValue(f.Value).FieldByName(f.Field.Name)

if reflectutil.IsZero(fv) {
Expand Down
34 changes: 34 additions & 0 deletions internal/help/writer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,22 @@ type dbConfig struct {
Port int `default:"5432" env:"PORT" flag:"port"`
}

type helpString string

func (h helpString) Help() string { return string(h) }

type helperFieldConfig struct {
Dynamic helpString `env:"-" flag:"dyn"`
}

type helperOverridesTagConfig struct {
Dynamic helpString `env:"-" flag:"dyn" help:"from tag"`
}

type helperEmptyFallsBackConfig struct {
Dynamic helpString `env:"-" flag:"dyn" help:"from tag"`
}

func TestPrintDefaults(t *testing.T) {
for _, tt := range []struct {
name string
Expand Down Expand Up @@ -85,6 +101,24 @@ func TestPrintDefaults(t *testing.T) {
"\t- DB.Host: string (default: localhost) (env: DB_HOST, flag: --db.host)\n" +
"\t- DB.Port: integer (default: 5432) (env: DB_PORT, flag: --db.port)\n",
},
{
name: "Help() method provides help text",
in: &helperFieldConfig{Dynamic: "dynamic help"},
out: "Arguments:\n" +
"\t- Dynamic: string dynamic help (default: dynamic help) (flag: --dyn)\n",
},
{
name: "Help() method overrides struct tag",
in: &helperOverridesTagConfig{Dynamic: "from method"},
out: "Arguments:\n" +
"\t- Dynamic: string from method (default: from method) (flag: --dyn)\n",
},
{
name: "empty Help() falls back to struct tag",
in: &helperEmptyFallsBackConfig{},
out: "Arguments:\n" +
"\t- Dynamic: string from tag (flag: --dyn)\n",
},
} {
t.Run(tt.name, func(t *testing.T) {
var b bytes.Buffer
Expand Down
6 changes: 3 additions & 3 deletions internal/setter/setter.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ import (
var (
durationType = reflect.TypeOf(time.Duration(0))
timeType = reflect.TypeOf(time.Time{})
valueType = reflect.TypeOf((*Value)(nil)).Elem()
textUnmarshalerType = reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem()
jsonUnmarshalerType = reflect.TypeOf((*json.Unmarshaler)(nil)).Elem()
valueType = reflect.TypeFor[Value]()
textUnmarshalerType = reflect.TypeFor[encoding.TextUnmarshaler]()
jsonUnmarshalerType = reflect.TypeFor[json.Unmarshaler]()

durationParser = &staticParser{t: "duration", fn: parseDuration}
boolParser = &staticParser{t: "bool", fn: parseBool}
Expand Down
48 changes: 45 additions & 3 deletions internal/walker/walker.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,39 @@

type WalkFunc func(*Field) error

func Walk(in interface{}, fn WalkFunc) error {
// Prefixed is an optional interface that a value passed to Walk can
// implement to inject dynamic key prefix segments. When Walk receives a
// Prefixed value it builds a synthetic ancestor chain from the prefix
// segments and walks the inner value returned by WalkValue.
Comment on lines +24 to +27
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll be 100% straight here: I have no idea what this means.
How about adding a little example to the comment? 🤔

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

checkout ./configurator_test.go it explicit the behavour quite extensively

type Prefixed interface {
WalkPrefix() []string
WalkValue() any
}

func Walk(in any, fn WalkFunc) error {
return walkValue(in, fn, nil)
}

func walkValue(in any, fn WalkFunc, ancestor *Field) error {
if p, ok := in.(Prefixed); ok {
return walkPrefixed(p, fn, ancestor)
}

return walkStruct(in, fn, ancestor)
}

func walkPrefixed(p Prefixed, fn WalkFunc, ancestor *Field) error {
for _, seg := range p.WalkPrefix() {
ancestor = &Field{
Field: reflect.StructField{Name: seg},
Ancestor: ancestor,
}
}

return walkValue(p.WalkValue(), fn, ancestor)
}

func walkStruct(in any, fn WalkFunc, ancestor *Field) error {
if in == nil {
return ErrShouldBeAStructPtr
}
Expand All @@ -40,7 +72,7 @@
return ErrShouldBeAStructPtr
}

return walk(inv, fn, nil)
return walk(inv, fn, ancestor)
}

func indirectedType(t reflect.Type) reflect.Type {
Expand All @@ -67,6 +99,16 @@
return v.Addr()
}

func walkField(nv reflect.Value, fn WalkFunc, f *Field) error {
if nv.CanInterface() {
if p, ok := nv.Interface().(Prefixed); ok {
return walkPrefixed(p, fn, f)
}
}

return walk(nv, fn, f)
}

func walk(v reflect.Value, fn WalkFunc, a *Field) error {
vit := indirectedType(v.Type())

Expand All @@ -88,7 +130,7 @@

if unicode.IsUpper(rune(sf.Name[0])) {
switch err := fn(&f); err {
case SkipStruct:

Check failure on line 133 in internal/walker/walker.go

View workflow job for this annotation

GitHub Actions / runner / golangci-lint

switch on an error will fail on wrapped errors. Use errors.Is to check for specific errors (errorlint)
continue
case nil:
default:
Expand All @@ -106,7 +148,7 @@
nv.Set(reflect.New(sf.Type.Elem()))
}

if err := walk(nv, fn, &f); err != nil {
if err := walkField(nv, fn, &f); err != nil {
return err
}

Expand Down
Loading
Loading