Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

basicflag provider support skips already set items #230

Closed
wants to merge 5 commits into from
Closed
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
65 changes: 62 additions & 3 deletions providers/basicflag/basicflag.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,49 @@ package basicflag
import (
"errors"
"flag"
"fmt"

"github.com/knadh/koanf/maps"
)

// KoanfIntf is an interface that represents a small subset of methods
// used by this package from Koanf{}. When using this package, a live
// instance of Koanf{} should be passed.
type KoanfIntf interface {
Exists(string) bool
}

// CallBack is a callback function that allows the caller to modify the key and value
type CallBack func(key string, value flag.Value) (string, any)

// Pflag implements a pflag command line provider.
type Pflag struct {
ko KoanfIntf
delim string
flagset *flag.FlagSet
cb func(key string, value string) (string, interface{})
// cb is a callback function that allows the caller to modify the key and value as a string
cb func(key string, value string) (string, interface{})
// flagCB is a callback function that allows the caller to modify the key and flag.Value
flagCb CallBack
}

// Provider returns a commandline flags provider that returns
// a nested map[string]interface{} of environment variable where the
// nesting hierarchy of keys are defined by delim. For instance, the
// delim "." will convert the key `parent.child.key: 1`
// to `{parent: {child: {key: 1}}}`.
func Provider(f *flag.FlagSet, delim string) *Pflag {
return &Pflag{
// It takes two WithEnableMerge and WithCallBack options.
func Provider(f *flag.FlagSet, delim string, opts ...Option) *Pflag {
p := Pflag{
flagset: f,
delim: delim,
}

for _, opt := range opts {
opt(&p)
}

return &p
}

// ProviderWithValue works exactly the same as Provider except the callback
Expand All @@ -43,7 +65,13 @@ func ProviderWithValue(f *flag.FlagSet, delim string, cb func(key string, value
// Read reads the flag variables and returns a nested conf map.
func (p *Pflag) Read() (map[string]interface{}, error) {
mp := make(map[string]interface{})

p.flagset.VisitAll(func(f *flag.Flag) {
if p.ko != nil {
p.readWithMerge(mp, f)
return
}

if p.cb != nil {
key, value := p.cb(f.Name, f.Value.String())
// If the callback blanked the key, it should be omitted
Expand All @@ -55,9 +83,40 @@ func (p *Pflag) Read() (map[string]interface{}, error) {
mp[f.Name] = f.Value.String()
}
})

return maps.Unflatten(mp, p.delim), nil
}

func (p *Pflag) readWithMerge(mp map[string]interface{}, f *flag.Flag) {
var (
key string
value any
)

if p.flagCb != nil {
key, value = p.flagCb(f.Name, f.Value)
} else {
// All Value types provided by flag package satisfy the Getter interface
// if user defined types are used, they must satisfy the Getter interface
getter, ok := f.Value.(flag.Getter)
if !ok {
panic(fmt.Sprintf("flag %s does not implement flag.Getter", f.Name))
}
key, value = f.Name, getter.Get()
}

if key == "" {
return
}

// if the key is set, and the flag value is the default value, skip it
if p.ko.Exists(key) && f.Value.String() == f.DefValue {
return
}

mp[key] = value
}

// ReadBytes is not supported by the basicflag koanf.
func (p *Pflag) ReadBytes() ([]byte, error) {
return nil, errors.New("basicflag provider does not support this method")
Expand Down
22 changes: 22 additions & 0 deletions providers/basicflag/options.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package basicflag

// Option is a function that modifies the Pflag
type Option func(*Pflag)

// WithCallBack is a callback function that allows the caller to modify the key and value
func WithCallBack(cb CallBack) Option {
return func(p *Pflag) {
p.flagCb = cb
}
}

// WithEnableMerge It takes a Koanf instance to see if the the flags defined
// have been set from other providers, for instance, a config file.
// If they are not, then the default values of the flags are merged.
// If they do exist, the flag values are not merged but only
// the values that have been explicitly set in the command line are merged.
func WithEnableMerge(ko KoanfIntf) Option {
return func(p *Pflag) {
p.ko = ko
}
}
64 changes: 61 additions & 3 deletions tests/koanf_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ import (
"testing"
"time"

"github.com/spf13/pflag"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/knadh/koanf/parsers/dotenv"
"github.com/knadh/koanf/parsers/hcl"
"github.com/knadh/koanf/parsers/hjson"
Expand All @@ -28,9 +32,6 @@ import (
"github.com/knadh/koanf/providers/posflag"
"github.com/knadh/koanf/providers/rawbytes"
"github.com/knadh/koanf/v2"
"github.com/spf13/pflag"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

const (
Expand Down Expand Up @@ -647,6 +648,63 @@ func TestFlags(t *testing.T) {

}

func Test_basicFlag_Load(t *testing.T) {
assertFunc := func(t *testing.T, k *koanf.Koanf) {
assert.Equal(t, "val1", k.String("key.one-example"))
assert.Equal(t, "val2", k.String("key.two_example"))
assert.Equal(t, 123, k.Int("key.int"))
assert.Equal(t, 123.123, k.Float64("key.float"))
}

fs := &flag.FlagSet{}
fs.String("key.one-example", "val1", "")
fs.String("key.two_example", "val2", "")
fs.Int("key.int", 123, "")
fs.Float64("key.float", 123.123, "")

k := koanf.New(".")
assert.Nil(t, k.Load(basicflag.Provider(fs, ".", basicflag.WithEnableMerge(k)), nil))
assertFunc(t, k)

// Test load with a custom key, val callback.
k = koanf.New(".")
p := basicflag.Provider(fs, ".", basicflag.WithEnableMerge(k),
basicflag.WithCallBack(func(key string, value flag.Value) (string, any) {
if key == "key.float" {
return "", ""
}
return key, value.(flag.Getter).Get()
}))
assert.Nil(t, k.Load(p, nil), nil)
assert.Equal(t, "val1", k.String("key.one-example"))
assert.Equal(t, "val2", k.String("key.two_example"))
assert.Equal(t, 123, k.Int("key.int"))
assert.Equal(t, "", k.String("key.float"))
}

func TestLoad_basicFlag_Overridden(t *testing.T) {
assertFunc := func(t *testing.T, k *koanf.Koanf) {
// type was not set by the cli flag, but the json file provided it.
// so it was not overridden.
assert.Equal(t, "json", k.String("type"))
// parent1.name was set by the cli flag, so overrides the json file value.
assert.Equal(t, "parent1_cli_value", k.String("parent1.name"))
}

fs := &flag.FlagSet{}
fs.String("type", "cli", "")
fs.String("parent1.name", "parent1_default_value", "")

_ = fs.Set("parent1.name", "parent1_cli_value")

k := koanf.New(".")
// Load JSON config.
assert.Nil(t, k.Load(file.Provider("../mock/mock.json"), json.Parser()), nil)
assert.Nil(t, k.Load(basicflag.Provider(fs, ".", basicflag.WithEnableMerge(k)), nil))

assertFunc(t, k)
}

func TestConfMapValues(t *testing.T) {
var (
assert = assert.New(t)
Expand Down