From 2d028684f68be9417a3f84eb7275934e16263d7e Mon Sep 17 00:00:00 2001 From: Naveen Gogineni Date: Wed, 20 Nov 2024 12:59:20 -0500 Subject: [PATCH 1/6] Add map value source --- flag_bool_with_inverse.go | 2 +- godoc-current.txt | 13 ++++ testdata/godoc-v3.x.txt | 13 ++++ value_source.go | 103 ++++++++++++++++++++++++++++--- value_source_test.go | 127 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 248 insertions(+), 10 deletions(-) diff --git a/flag_bool_with_inverse.go b/flag_bool_with_inverse.go index cc401bb9fc..ecc090b478 100644 --- a/flag_bool_with_inverse.go +++ b/flag_bool_with_inverse.go @@ -106,7 +106,7 @@ func (parent *BoolWithInverseFlag) initialize() { sources := []ValueSource{} for _, envVar := range child.GetEnvVars() { - sources = append(sources, &envVarValueSource{Key: strings.ToUpper(parent.InversePrefix) + envVar}) + sources = append(sources, EnvVar(strings.ToUpper(parent.InversePrefix)+envVar)) } parent.negativeFlag.Sources = NewValueSourceChain(sources...) } diff --git a/godoc-current.txt b/godoc-current.txt index 079b09b459..66a89f0702 100644 --- a/godoc-current.txt +++ b/godoc-current.txt @@ -591,6 +591,11 @@ type DocGenerationMultiValueFlag interface { type DurationFlag = FlagBase[time.Duration, NoConfig, durationValue] +type EnvValueSource interface { + IsFromEnv() bool + Key() string +} + type ErrorFormatter interface { Format(s fmt.State, verb rune) } @@ -848,6 +853,12 @@ func (i MapBase[T, C, VC]) ToString(t map[string]T) string func (i *MapBase[T, C, VC]) Value() map[string]T Value returns the mapping of values set by this flag +type MapSource struct { + // Has unexported fields. +} + +func NewMapSource(name string, m map[any]any) *MapSource + type MultiError interface { error Errors() []error @@ -1000,6 +1011,8 @@ func EnvVar(key string) ValueSource func File(path string) ValueSource +func NewMapValueSource(key string, ms *MapSource) ValueSource + type ValueSourceChain struct { Chain []ValueSource } diff --git a/testdata/godoc-v3.x.txt b/testdata/godoc-v3.x.txt index 079b09b459..66a89f0702 100644 --- a/testdata/godoc-v3.x.txt +++ b/testdata/godoc-v3.x.txt @@ -591,6 +591,11 @@ type DocGenerationMultiValueFlag interface { type DurationFlag = FlagBase[time.Duration, NoConfig, durationValue] +type EnvValueSource interface { + IsFromEnv() bool + Key() string +} + type ErrorFormatter interface { Format(s fmt.State, verb rune) } @@ -848,6 +853,12 @@ func (i MapBase[T, C, VC]) ToString(t map[string]T) string func (i *MapBase[T, C, VC]) Value() map[string]T Value returns the mapping of values set by this flag +type MapSource struct { + // Has unexported fields. +} + +func NewMapSource(name string, m map[any]any) *MapSource + type MultiError interface { error Errors() []error @@ -1000,6 +1011,8 @@ func EnvVar(key string) ValueSource func File(path string) ValueSource +func NewMapValueSource(key string, ms *MapSource) ValueSource + type ValueSourceChain struct { Chain []ValueSource } diff --git a/value_source.go b/value_source.go index 7d3f7ee45c..bdb37950e8 100644 --- a/value_source.go +++ b/value_source.go @@ -17,6 +17,11 @@ type ValueSource interface { Lookup() (string, bool) } +type EnvValueSource interface { + IsFromEnv() bool + Key() string +} + // ValueSourceChain contains an ordered series of ValueSource that // allows for lookup where the first ValueSource to resolve is // returned @@ -38,8 +43,8 @@ func (vsc *ValueSourceChain) EnvKeys() []string { vals := []string{} for _, src := range vsc.Chain { - if v, ok := src.(*envVarValueSource); ok { - vals = append(vals, v.Key) + if v, ok := src.(EnvValueSource); ok { + vals = append(vals, v.Key()) } } @@ -83,21 +88,29 @@ func (vsc *ValueSourceChain) LookupWithSource() (string, ValueSource, bool) { // envVarValueSource encapsulates a ValueSource from an environment variable type envVarValueSource struct { - Key string + key string } func (e *envVarValueSource) Lookup() (string, bool) { - return os.LookupEnv(strings.TrimSpace(string(e.Key))) + return os.LookupEnv(strings.TrimSpace(string(e.key))) +} + +func (e *envVarValueSource) IsFromEnv() bool { + return true } -func (e *envVarValueSource) String() string { return fmt.Sprintf("environment variable %[1]q", e.Key) } +func (e *envVarValueSource) Key() string { + return e.key +} + +func (e *envVarValueSource) String() string { return fmt.Sprintf("environment variable %[1]q", e.key) } func (e *envVarValueSource) GoString() string { - return fmt.Sprintf("&envVarValueSource{Key:%[1]q}", e.Key) + return fmt.Sprintf("&envVarValueSource{Key:%[1]q}", e.key) } func EnvVar(key string) ValueSource { return &envVarValueSource{ - Key: key, + key: key, } } @@ -107,7 +120,7 @@ func EnvVars(keys ...string) ValueSourceChain { vsc := ValueSourceChain{Chain: []ValueSource{}} for _, key := range keys { - vsc.Chain = append(vsc.Chain, &envVarValueSource{Key: key}) + vsc.Chain = append(vsc.Chain, EnvVar(key)) } return vsc @@ -138,8 +151,80 @@ func Files(paths ...string) ValueSourceChain { vsc := ValueSourceChain{Chain: []ValueSource{}} for _, path := range paths { - vsc.Chain = append(vsc.Chain, &fileValueSource{Path: path}) + vsc.Chain = append(vsc.Chain, File(path)) } return vsc } + +type MapSource struct { + name string + m map[any]any +} + +func NewMapSource(name string, m map[any]any) *MapSource { + return &MapSource{ + name: name, + m: m, + } +} + +func (ms *MapSource) lookup(name string) (any, bool) { + // nestedVal checks if the name has '.' delimiters. + // If so, it tries to traverse the tree by the '.' delimited sections to find + // a nested value for the key. + if sections := strings.Split(name, "."); len(sections) > 1 { + node := ms.m + for _, section := range sections[:len(sections)-1] { + child, ok := node[section] + if !ok { + return nil, false + } + + switch child := child.(type) { + case map[string]any: + node = make(map[any]any, len(child)) + for k, v := range child { + node[k] = v + } + case map[any]any: + node = child + default: + return nil, false + } + } + if val, ok := node[sections[len(sections)-1]]; ok { + return val, true + } + } + + return nil, false +} + +type mapValueSource struct { + key string + ms *MapSource +} + +func NewMapValueSource(key string, ms *MapSource) ValueSource { + return &mapValueSource{ + key: key, + ms: ms, + } +} + +func (mvs *mapValueSource) String() string { + return fmt.Sprintf("map source key %[1]q from %[2]q", mvs.key, mvs.ms.name) +} + +func (mvs *mapValueSource) GoString() string { + return fmt.Sprintf("&mapValueSource{key:%[1]q, src:%[2]q}", mvs.key, mvs.ms.m) +} + +func (mvs *mapValueSource) Lookup() (string, bool) { + if v, ok := mvs.ms.lookup(mvs.key); !ok { + return "", false + } else { + return fmt.Sprintf("%+v", v), true + } +} diff --git a/value_source_test.go b/value_source_test.go index 57e9d49e28..b3a4c74597 100644 --- a/value_source_test.go +++ b/value_source_test.go @@ -189,3 +189,130 @@ func (svs *staticValueSource) GoString() string { } func (svs *staticValueSource) String() string { return svs.v } func (svs *staticValueSource) Lookup() (string, bool) { return svs.v, true } + +func TestMapValueSource(t *testing.T) { + tests := []struct { + name string + m map[any]any + key string + val string + found bool + }{ + { + name: "No map no key", + }, + { + name: "No map with key", + key: "foo", + }, + { + name: "Empty map no key", + m: map[any]any{}, + }, + { + name: "Empty map with key", + key: "foo", + m: map[any]any{}, + }, + { + name: "Level 1 no key", + key: ".foob", + m: map[any]any{ + "foo": 10, + }, + }, + { + name: "Level 2", + key: "foo.bar", + m: map[any]any{ + "foo": map[any]any{ + "bar": 10, + }, + }, + val: "10", + found: true, + }, + { + name: "Level 2 invalid key", + key: "foo.bar1", + m: map[any]any{ + "foo": map[any]any{ + "bar": "10", + }, + }, + }, + { + name: "Level 3 no entry", + key: "foo.bar.t", + m: map[any]any{ + "foo": map[any]any{ + "bar": "sss", + }, + }, + }, + { + name: "Level 3", + key: "foo.bar.t", + m: map[any]any{ + "foo": map[any]any{ + "bar": map[any]any{ + "t": "sss", + }, + }, + }, + val: "sss", + found: true, + }, + { + name: "Level 3 invalid key", + key: "foo.bar.t", + m: map[any]any{ + "foo": map[any]any{ + "bar": map[any]any{ + "t1": 10, + }, + }, + }, + }, + { + name: "Level 4 no entry", + key: "foo.bar.t.gh", + m: map[any]any{ + "foo": map[any]any{ + "bar": map[any]any{ + "t1": 10, + }, + }, + }, + }, + { + name: "Level 4 slice entry", + key: "foo.bar.t.gh", + m: map[any]any{ + "foo": map[any]any{ + "bar": map[any]any{ + "t": map[any]any{ + "gh": []int{10}, + }, + }, + }, + }, + val: "[10]", + found: true, + }, + } + + for _, test := range tests { + t.Run(test.key, func(t *testing.T) { + ms := NewMapSource("test", test.m) + m := NewMapValueSource(test.key, ms) + val, b := m.Lookup() + if !test.found { + assert.False(t, b) + } else { + assert.True(t, b) + assert.Equal(t, val, test.val) + } + }) + } +} From dcdab0028bb6759646afba9a5f5671054446d599 Mon Sep 17 00:00:00 2001 From: Naveen Gogineni Date: Thu, 21 Nov 2024 13:48:54 -0500 Subject: [PATCH 2/6] Add comment for env --- godoc-current.txt | 1 + testdata/godoc-v3.x.txt | 1 + value_source.go | 4 +++- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/godoc-current.txt b/godoc-current.txt index 66a89f0702..406ec7c50b 100644 --- a/godoc-current.txt +++ b/godoc-current.txt @@ -595,6 +595,7 @@ type EnvValueSource interface { IsFromEnv() bool Key() string } + EnvValueSource is to specifically detect env sources when printing help text type ErrorFormatter interface { Format(s fmt.State, verb rune) diff --git a/testdata/godoc-v3.x.txt b/testdata/godoc-v3.x.txt index 66a89f0702..406ec7c50b 100644 --- a/testdata/godoc-v3.x.txt +++ b/testdata/godoc-v3.x.txt @@ -595,6 +595,7 @@ type EnvValueSource interface { IsFromEnv() bool Key() string } + EnvValueSource is to specifically detect env sources when printing help text type ErrorFormatter interface { Format(s fmt.State, verb rune) diff --git a/value_source.go b/value_source.go index bdb37950e8..e01701d61f 100644 --- a/value_source.go +++ b/value_source.go @@ -17,6 +17,8 @@ type ValueSource interface { Lookup() (string, bool) } +// EnvValueSource is to specifically detect env sources when +// printing help text type EnvValueSource interface { IsFromEnv() bool Key() string @@ -43,7 +45,7 @@ func (vsc *ValueSourceChain) EnvKeys() []string { vals := []string{} for _, src := range vsc.Chain { - if v, ok := src.(EnvValueSource); ok { + if v, ok := src.(EnvValueSource); ok && v.IsFromEnv() { vals = append(vals, v.Key()) } } From f3b77e188f7bc4d15aa2f654afa9aee4df52dfd1 Mon Sep 17 00:00:00 2001 From: Naveen Gogineni Date: Thu, 21 Nov 2024 14:11:23 -0500 Subject: [PATCH 3/6] Add tests --- value_source_test.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/value_source_test.go b/value_source_test.go index b3a4c74597..9d8a06a056 100644 --- a/value_source_test.go +++ b/value_source_test.go @@ -316,3 +316,15 @@ func TestMapValueSource(t *testing.T) { }) } } + +func TestMapValueSourceStringer(t *testing.T) { + m := map[any]any{ + "foo": map[any]any{ + "bar": 10, + }, + } + mvs := NewMapValueSource("bar", NewMapSource("test", m)) + + assert.Equal(t, `&mapValueSource{key:"bar", src:map["foo":map["bar":'\n']]}`, mvs.GoString()) + assert.Equal(t, `map source key "bar" from "test"`, mvs.String()) +} From acc590b45dba53189c44ec9b700768eb0c63e81a Mon Sep 17 00:00:00 2001 From: Naveen Gogineni Date: Thu, 21 Nov 2024 14:14:39 -0500 Subject: [PATCH 4/6] Add tests --- value_source_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/value_source_test.go b/value_source_test.go index 9d8a06a056..a12359b3e2 100644 --- a/value_source_test.go +++ b/value_source_test.go @@ -290,7 +290,7 @@ func TestMapValueSource(t *testing.T) { key: "foo.bar.t.gh", m: map[any]any{ "foo": map[any]any{ - "bar": map[any]any{ + "bar": map[string]any{ "t": map[any]any{ "gh": []int{10}, }, From 596e7c5a1a6f8c994208d7dc9b647f2fde9aa2c6 Mon Sep 17 00:00:00 2001 From: Naveen Gogineni Date: Thu, 21 Nov 2024 18:25:37 -0500 Subject: [PATCH 5/6] Add a map source interface --- godoc-current.txt | 16 ++++++++++++---- value_source.go | 36 +++++++++++++++++++++++++++--------- value_source_test.go | 4 ++-- 3 files changed, 41 insertions(+), 15 deletions(-) diff --git a/godoc-current.txt b/godoc-current.txt index 406ec7c50b..3550ebe1f4 100644 --- a/godoc-current.txt +++ b/godoc-current.txt @@ -854,11 +854,19 @@ func (i MapBase[T, C, VC]) ToString(t map[string]T) string func (i *MapBase[T, C, VC]) Value() map[string]T Value returns the mapping of values set by this flag -type MapSource struct { - // Has unexported fields. +type MapSource interface { + fmt.Stringer + fmt.GoStringer + + // Lookup returns the value from the source based on key + // and if it was found + // or returns an empty string and false + Lookup(string) (any, bool) } + MapSource is a source which can be used to look up a value based on a key + typically for use with a cli.Flag -func NewMapSource(name string, m map[any]any) *MapSource +func NewMapSource(name string, m map[any]any) MapSource type MultiError interface { error @@ -1012,7 +1020,7 @@ func EnvVar(key string) ValueSource func File(path string) ValueSource -func NewMapValueSource(key string, ms *MapSource) ValueSource +func NewMapValueSource(key string, ms MapSource) ValueSource type ValueSourceChain struct { Chain []ValueSource diff --git a/value_source.go b/value_source.go index e01701d61f..edc75d2e4f 100644 --- a/value_source.go +++ b/value_source.go @@ -24,6 +24,19 @@ type EnvValueSource interface { Key() string } +// MapSource is a source which can be used to look up a value +// based on a key +// typically for use with a cli.Flag +type MapSource interface { + fmt.Stringer + fmt.GoStringer + + // Lookup returns the value from the source based on key + // and if it was found + // or returns an empty string and false + Lookup(string) (any, bool) +} + // ValueSourceChain contains an ordered series of ValueSource that // allows for lookup where the first ValueSource to resolve is // returned @@ -159,19 +172,24 @@ func Files(paths ...string) ValueSourceChain { return vsc } -type MapSource struct { +type mapSource struct { name string m map[any]any } -func NewMapSource(name string, m map[any]any) *MapSource { - return &MapSource{ +func NewMapSource(name string, m map[any]any) MapSource { + return &mapSource{ name: name, m: m, } } -func (ms *MapSource) lookup(name string) (any, bool) { +func (ms *mapSource) String() string { return fmt.Sprintf("map source %[1]q", ms.name) } +func (ms *mapSource) GoString() string { + return fmt.Sprintf("&mapSource{name:%[1]q}", ms.name) +} + +func (ms *mapSource) Lookup(name string) (any, bool) { // nestedVal checks if the name has '.' delimiters. // If so, it tries to traverse the tree by the '.' delimited sections to find // a nested value for the key. @@ -205,10 +223,10 @@ func (ms *MapSource) lookup(name string) (any, bool) { type mapValueSource struct { key string - ms *MapSource + ms MapSource } -func NewMapValueSource(key string, ms *MapSource) ValueSource { +func NewMapValueSource(key string, ms MapSource) ValueSource { return &mapValueSource{ key: key, ms: ms, @@ -216,15 +234,15 @@ func NewMapValueSource(key string, ms *MapSource) ValueSource { } func (mvs *mapValueSource) String() string { - return fmt.Sprintf("map source key %[1]q from %[2]q", mvs.key, mvs.ms.name) + return fmt.Sprintf("key %[1]q from %[2]s", mvs.key, mvs.ms.String()) } func (mvs *mapValueSource) GoString() string { - return fmt.Sprintf("&mapValueSource{key:%[1]q, src:%[2]q}", mvs.key, mvs.ms.m) + return fmt.Sprintf("&mapValueSource{key:%[1]q, src:%[2]s}", mvs.key, mvs.ms.GoString()) } func (mvs *mapValueSource) Lookup() (string, bool) { - if v, ok := mvs.ms.lookup(mvs.key); !ok { + if v, ok := mvs.ms.Lookup(mvs.key); !ok { return "", false } else { return fmt.Sprintf("%+v", v), true diff --git a/value_source_test.go b/value_source_test.go index a12359b3e2..fa02d1f54e 100644 --- a/value_source_test.go +++ b/value_source_test.go @@ -325,6 +325,6 @@ func TestMapValueSourceStringer(t *testing.T) { } mvs := NewMapValueSource("bar", NewMapSource("test", m)) - assert.Equal(t, `&mapValueSource{key:"bar", src:map["foo":map["bar":'\n']]}`, mvs.GoString()) - assert.Equal(t, `map source key "bar" from "test"`, mvs.String()) + assert.Equal(t, `&mapValueSource{key:"bar", src:&mapSource{name:"test"}}`, mvs.GoString()) + assert.Equal(t, `key "bar" from map source "test"`, mvs.String()) } From 6c78afd67d01c5e45204fd5bcadbbd740973074a Mon Sep 17 00:00:00 2001 From: Naveen Gogineni Date: Thu, 21 Nov 2024 18:27:06 -0500 Subject: [PATCH 6/6] Run make docs --- testdata/godoc-v3.x.txt | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/testdata/godoc-v3.x.txt b/testdata/godoc-v3.x.txt index 406ec7c50b..3550ebe1f4 100644 --- a/testdata/godoc-v3.x.txt +++ b/testdata/godoc-v3.x.txt @@ -854,11 +854,19 @@ func (i MapBase[T, C, VC]) ToString(t map[string]T) string func (i *MapBase[T, C, VC]) Value() map[string]T Value returns the mapping of values set by this flag -type MapSource struct { - // Has unexported fields. +type MapSource interface { + fmt.Stringer + fmt.GoStringer + + // Lookup returns the value from the source based on key + // and if it was found + // or returns an empty string and false + Lookup(string) (any, bool) } + MapSource is a source which can be used to look up a value based on a key + typically for use with a cli.Flag -func NewMapSource(name string, m map[any]any) *MapSource +func NewMapSource(name string, m map[any]any) MapSource type MultiError interface { error @@ -1012,7 +1020,7 @@ func EnvVar(key string) ValueSource func File(path string) ValueSource -func NewMapValueSource(key string, ms *MapSource) ValueSource +func NewMapValueSource(key string, ms MapSource) ValueSource type ValueSourceChain struct { Chain []ValueSource