From 0c3102278a483ded20aea55092d9bd000832221c Mon Sep 17 00:00:00 2001 From: chrisprobinson Date: Sun, 6 Dec 2015 15:43:18 -0800 Subject: [PATCH 1/2] Update to add yaml support --- inputfilesupport/flag.go | 356 +++++++++++++++++++++++ inputfilesupport/flag_test.go | 336 +++++++++++++++++++++ inputfilesupport/helpers_test.go | 18 ++ inputfilesupport/input_source_context.go | 21 ++ inputfilesupport/map_input_source.go | 132 +++++++++ inputfilesupport/yaml_command_test.go | 167 +++++++++++ inputfilesupport/yaml_file_loader.go | 99 +++++++ 7 files changed, 1129 insertions(+) create mode 100644 inputfilesupport/flag.go create mode 100644 inputfilesupport/flag_test.go create mode 100644 inputfilesupport/helpers_test.go create mode 100644 inputfilesupport/input_source_context.go create mode 100644 inputfilesupport/map_input_source.go create mode 100644 inputfilesupport/yaml_command_test.go create mode 100644 inputfilesupport/yaml_file_loader.go diff --git a/inputfilesupport/flag.go b/inputfilesupport/flag.go new file mode 100644 index 0000000..c168bd4 --- /dev/null +++ b/inputfilesupport/flag.go @@ -0,0 +1,356 @@ +package inputfilesupport + +import ( + "flag" + "fmt" + "os" + "strconv" + "strings" + + "github.com/codegangsta/cli" +) + +// FlagInputSourceExtension is an extension interface of cli.Flag that +// allows a value to be set on the existing parsed flags. +type FlagInputSourceExtension interface { + cli.Flag + ApplyInputSourceValue(context *cli.Context, isc InputSourceContext) +} + +// GenericFlag is the flag type that wraps cli.GenericFlag to allow +// for other values to be specified +type GenericFlag struct { + cli.GenericFlag + set *flag.FlagSet +} + +// NewGenericFlag creates a new GenericFlag +func NewGenericFlag(flag cli.GenericFlag) *GenericFlag { + return &GenericFlag{GenericFlag: flag, set: nil} +} + +// ApplyInputSourceValue applies a generic value to the flagSet if required +func (f *GenericFlag) ApplyInputSourceValue(context *cli.Context, isc InputSourceContext) { + if f.set != nil { + if !context.IsSet(f.Name) && !isEnvVarSet(f.EnvVar) { + value := isc.Generic(f.GenericFlag.Name) + if value != nil { + eachName(f.Name, func(name string) { + f.set.Set(f.Name, value.String()) + }) + } + } + } +} + +// Apply saves the flagSet for later usage then calls +// the wrapped GenericFlag.Apply +func (f *GenericFlag) Apply(set *flag.FlagSet) { + f.set = set + f.GenericFlag.Apply(set) +} + +// StringSliceFlag is the flag type that wraps cli.StringSliceFlag to allow +// for other values to be specified +type StringSliceFlag struct { + cli.StringSliceFlag + set *flag.FlagSet +} + +// NewStringSliceFlag creates a new StringSliceFlag +func NewStringSliceFlag(flag cli.StringSliceFlag) *StringSliceFlag { + return &StringSliceFlag{StringSliceFlag: flag, set: nil} +} + +// ApplyInputSourceValue applies a StringSlice value to the flagSet if required +func (f *StringSliceFlag) ApplyInputSourceValue(context *cli.Context, isc InputSourceContext) { + if f.set != nil { + if !context.IsSet(f.Name) && !isEnvVarSet(f.EnvVar) { + value := isc.StringSlice(f.StringSliceFlag.Name) + if value != nil { + var sliceValue cli.StringSlice = value + eachName(f.Name, func(name string) { + underlyingFlag := f.set.Lookup(f.Name) + if underlyingFlag != nil { + underlyingFlag.Value = &sliceValue + } + }) + } + } + } +} + +// Apply saves the flagSet for later usage then calls +// the wrapped StringSliceFlag.Apply +func (f *StringSliceFlag) Apply(set *flag.FlagSet) { + f.set = set + f.StringSliceFlag.Apply(set) +} + +// IntSliceFlag is the flag type that wraps cli.IntSliceFlag to allow +// for other values to be specified +type IntSliceFlag struct { + cli.IntSliceFlag + set *flag.FlagSet +} + +// NewIntSliceFlag creates a new IntSliceFlag +func NewIntSliceFlag(flag cli.IntSliceFlag) *IntSliceFlag { + return &IntSliceFlag{IntSliceFlag: flag, set: nil} +} + +// ApplyInputSourceValue applies a IntSlice value if required +func (f *IntSliceFlag) ApplyInputSourceValue(context *cli.Context, isc InputSourceContext) { + if f.set != nil { + if !context.IsSet(f.Name) && !isEnvVarSet(f.EnvVar) { + value := isc.IntSlice(f.IntSliceFlag.Name) + if value != nil { + var sliceValue cli.IntSlice = value + eachName(f.Name, func(name string) { + underlyingFlag := f.set.Lookup(f.Name) + if underlyingFlag != nil { + underlyingFlag.Value = &sliceValue + } + }) + } + } + } +} + +// Apply saves the flagSet for later usage then calls +// the wrapped IntSliceFlag.Apply +func (f *IntSliceFlag) Apply(set *flag.FlagSet) { + f.set = set + f.IntSliceFlag.Apply(set) +} + +// BoolFlag is the flag type that wraps cli.BoolFlag to allow +// for other values to be specified +type BoolFlag struct { + cli.BoolFlag + set *flag.FlagSet +} + +// NewBoolFlag creates a new BoolFlag +func NewBoolFlag(flag cli.BoolFlag) *BoolFlag { + return &BoolFlag{BoolFlag: flag, set: nil} +} + +// ApplyInputSourceValue applies a Bool value to the flagSet if required +func (f *BoolFlag) ApplyInputSourceValue(context *cli.Context, isc InputSourceContext) { + if f.set != nil { + if !context.IsSet(f.Name) && !isEnvVarSet(f.EnvVar) { + value := isc.Bool(f.BoolFlag.Name) + if value { + eachName(f.Name, func(name string) { + f.set.Set(f.Name, strconv.FormatBool(value)) + }) + } + } + } +} + +// Apply saves the flagSet for later usage then calls +// the wrapped BoolFlag.Apply +func (f *BoolFlag) Apply(set *flag.FlagSet) { + f.set = set + f.BoolFlag.Apply(set) +} + +// BoolTFlag is the flag type that wraps cli.BoolTFlag to allow +// for other values to be specified +type BoolTFlag struct { + cli.BoolTFlag + set *flag.FlagSet +} + +// NewBoolTFlag creates a new BoolTFlag +func NewBoolTFlag(flag cli.BoolTFlag) *BoolTFlag { + return &BoolTFlag{BoolTFlag: flag, set: nil} +} + +// ApplyInputSourceValue applies a BoolT value to the flagSet if required +func (f *BoolTFlag) ApplyInputSourceValue(context *cli.Context, isc InputSourceContext) { + if f.set != nil { + if !context.IsSet(f.Name) && !isEnvVarSet(f.EnvVar) { + value := isc.BoolT(f.BoolTFlag.Name) + if !value { + eachName(f.Name, func(name string) { + f.set.Set(f.Name, strconv.FormatBool(value)) + }) + } + } + } +} + +// Apply saves the flagSet for later usage then calls +// the wrapped BoolTFlag.Apply +func (f *BoolTFlag) Apply(set *flag.FlagSet) { + f.set = set + + f.BoolTFlag.Apply(set) +} + +// StringFlag is the flag type that wraps cli.StringFlag to allow +// for other values to be specified +type StringFlag struct { + cli.StringFlag + set *flag.FlagSet +} + +// NewStringFlag creates a new StringFlag +func NewStringFlag(flag cli.StringFlag) *StringFlag { + return &StringFlag{StringFlag: flag, set: nil} +} + +// ApplyInputSourceValue applies a String value to the flagSet if required +func (f *StringFlag) ApplyInputSourceValue(context *cli.Context, isc InputSourceContext) { + if f.set != nil { + if !(context.IsSet(f.Name) || isEnvVarSet(f.EnvVar)) { + value := isc.String(f.StringFlag.Name) + if value != "" { + eachName(f.Name, func(name string) { + f.set.Set(f.Name, value) + }) + } + } + } +} + +// Apply saves the flagSet for later usage then calls +// the wrapped StringFlag.Apply +func (f *StringFlag) Apply(set *flag.FlagSet) { + f.set = set + + f.StringFlag.Apply(set) +} + +// IntFlag is the flag type that wraps cli.IntFlag to allow +// for other values to be specified +type IntFlag struct { + cli.IntFlag + set *flag.FlagSet +} + +// NewIntFlag creates a new IntFlag +func NewIntFlag(flag cli.IntFlag) *IntFlag { + return &IntFlag{IntFlag: flag, set: nil} +} + +// ApplyInputSourceValue applies a int value to the flagSet if required +func (f *IntFlag) ApplyInputSourceValue(context *cli.Context, isc InputSourceContext) { + if f.set != nil { + if !(context.IsSet(f.Name) || isEnvVarSet(f.EnvVar)) { + value := isc.Int(f.IntFlag.Name) + if value > 0 { + eachName(f.Name, func(name string) { + f.set.Set(f.Name, strconv.FormatInt(int64(value), 10)) + }) + } + } + } +} + +// Apply saves the flagSet for later usage then calls +// the wrapped IntFlag.Apply +func (f *IntFlag) Apply(set *flag.FlagSet) { + f.set = set + f.IntFlag.Apply(set) +} + +// DurationFlag is the flag type that wraps cli.DurationFlag to allow +// for other values to be specified +type DurationFlag struct { + cli.DurationFlag + set *flag.FlagSet +} + +// NewDurationFlag creates a new DurationFlag +func NewDurationFlag(flag cli.DurationFlag) *DurationFlag { + return &DurationFlag{DurationFlag: flag, set: nil} +} + +// ApplyInputSourceValue applies a Duration value to the flagSet if required +func (f *DurationFlag) ApplyInputSourceValue(context *cli.Context, isc InputSourceContext) { + if f.set != nil { + if !(context.IsSet(f.Name) || isEnvVarSet(f.EnvVar)) { + value := isc.Duration(f.DurationFlag.Name) + if value > 0 { + eachName(f.Name, func(name string) { + f.set.Set(f.Name, value.String()) + }) + } + } + } +} + +// Apply saves the flagSet for later usage then calls +// the wrapped DurationFlag.Apply +func (f *DurationFlag) Apply(set *flag.FlagSet) { + f.set = set + + f.DurationFlag.Apply(set) +} + +// Float64Flag is the flag type that wraps cli.Float64Flag to allow +// for other values to be specified +type Float64Flag struct { + cli.Float64Flag + set *flag.FlagSet +} + +// NewFloat64Flag creates a new Float64Flag +func NewFloat64Flag(flag cli.Float64Flag) *Float64Flag { + return &Float64Flag{Float64Flag: flag, set: nil} +} + +// ApplyInputSourceValue applies a Float64 value to the flagSet if required +func (f *Float64Flag) ApplyInputSourceValue(context *cli.Context, isc InputSourceContext) { + if f.set != nil { + if !(context.IsSet(f.Name) || isEnvVarSet(f.EnvVar)) { + value := isc.Float64(f.Float64Flag.Name) + if value > 0 { + floatStr := float64ToString(value) + eachName(f.Name, func(name string) { + f.set.Set(f.Name, floatStr) + }) + } + } + } +} + +// Apply saves the flagSet for later usage then calls +// the wrapped Float64Flag.Apply +func (f *Float64Flag) Apply(set *flag.FlagSet) { + f.set = set + + f.Float64Flag.Apply(set) +} + +func isEnvVarSet(envVars string) bool { + for _, envVar := range strings.Split(envVars, ",") { + envVar = strings.TrimSpace(envVar) + if envVal := os.Getenv(envVar); envVal != "" { + // TODO: Can't use this for bools as + // set means that it was true or false based on + // Bool flag type, should work for other types + if len(envVal) > 0 { + return true + } + } + } + + return false +} + +func float64ToString(f float64) string { + return fmt.Sprintf("%v", f) +} + +func eachName(longName string, fn func(string)) { + parts := strings.Split(longName, ",") + for _, name := range parts { + name = strings.Trim(name, " ") + fn(name) + } +} diff --git a/inputfilesupport/flag_test.go b/inputfilesupport/flag_test.go new file mode 100644 index 0000000..3ef7ce0 --- /dev/null +++ b/inputfilesupport/flag_test.go @@ -0,0 +1,336 @@ +package inputfilesupport + +import ( + "flag" + "fmt" + "os" + "strings" + "testing" + "time" + + "github.com/codegangsta/cli" +) + +type testApplyInputSource struct { + Flag FlagInputSourceExtension + FlagName string + FlagSetName string + Expected string + ContextValueString string + ContextValue flag.Value + EnvVarValue string + EnvVarName string + MapValue interface{} +} + +func TestGenericApplyInputSourceValue(t *testing.T) { + v := &Parser{"abc", "def"} + c := runTest(t, testApplyInputSource{ + Flag: NewGenericFlag(cli.GenericFlag{Name: "test", Value: &Parser{}}), + FlagName: "test", + MapValue: v, + }) + expect(t, v, c.Generic("test")) +} + +func TestGenericApplyInputSourceMethodContextSet(t *testing.T) { + p := &Parser{"abc", "def"} + c := runTest(t, testApplyInputSource{ + Flag: NewGenericFlag(cli.GenericFlag{Name: "test", Value: &Parser{}}), + FlagName: "test", + MapValue: &Parser{"efg", "hig"}, + ContextValueString: p.String(), + }) + expect(t, p, c.Generic("test")) +} + +func TestGenericApplyInputSourceMethodEnvVarSet(t *testing.T) { + c := runTest(t, testApplyInputSource{ + Flag: NewGenericFlag(cli.GenericFlag{Name: "test", Value: &Parser{}, EnvVar: "TEST"}), + FlagName: "test", + MapValue: &Parser{"efg", "hij"}, + EnvVarName: "TEST", + EnvVarValue: "abc,def", + }) + expect(t, &Parser{"abc", "def"}, c.Generic("test")) +} + +func TestStringSliceApplyInputSourceValue(t *testing.T) { + c := runTest(t, testApplyInputSource{ + Flag: NewStringSliceFlag(cli.StringSliceFlag{Name: "test"}), + FlagName: "test", + MapValue: []string{"hello", "world"}, + }) + expect(t, c.StringSlice("test"), []string{"hello", "world"}) +} + +func TestStringSliceApplyInputSourceMethodContextSet(t *testing.T) { + c := runTest(t, testApplyInputSource{ + Flag: NewStringSliceFlag(cli.StringSliceFlag{Name: "test"}), + FlagName: "test", + MapValue: []string{"hello", "world"}, + ContextValueString: "ohno", + }) + expect(t, c.StringSlice("test"), []string{"ohno"}) +} + +func TestStringSliceApplyInputSourceMethodEnvVarSet(t *testing.T) { + c := runTest(t, testApplyInputSource{ + Flag: NewStringSliceFlag(cli.StringSliceFlag{Name: "test", EnvVar: "TEST"}), + FlagName: "test", + MapValue: []string{"hello", "world"}, + EnvVarName: "TEST", + EnvVarValue: "oh,no", + }) + expect(t, c.StringSlice("test"), []string{"oh", "no"}) +} + +func TestIntSliceApplyInputSourceValue(t *testing.T) { + c := runTest(t, testApplyInputSource{ + Flag: NewIntSliceFlag(cli.IntSliceFlag{Name: "test"}), + FlagName: "test", + MapValue: []int{1, 2}, + }) + expect(t, c.IntSlice("test"), []int{1, 2}) +} + +func TestIntSliceApplyInputSourceMethodContextSet(t *testing.T) { + c := runTest(t, testApplyInputSource{ + Flag: NewIntSliceFlag(cli.IntSliceFlag{Name: "test"}), + FlagName: "test", + MapValue: []int{1, 2}, + ContextValueString: "3", + }) + expect(t, c.IntSlice("test"), []int{3}) +} + +func TestIntSliceApplyInputSourceMethodEnvVarSet(t *testing.T) { + c := runTest(t, testApplyInputSource{ + Flag: NewIntSliceFlag(cli.IntSliceFlag{Name: "test", EnvVar: "TEST"}), + FlagName: "test", + MapValue: []int{1, 2}, + EnvVarName: "TEST", + EnvVarValue: "3,4", + }) + expect(t, c.IntSlice("test"), []int{3, 4}) +} + +func TestBoolApplyInputSourceMethodSet(t *testing.T) { + c := runTest(t, testApplyInputSource{ + Flag: NewBoolFlag(cli.BoolFlag{Name: "test"}), + FlagName: "test", + MapValue: true, + }) + expect(t, true, c.Bool("test")) +} + +func TestBoolApplyInputSourceMethodContextSet(t *testing.T) { + c := runTest(t, testApplyInputSource{ + Flag: NewBoolFlag(cli.BoolFlag{Name: "test"}), + FlagName: "test", + MapValue: false, + ContextValueString: "true", + }) + expect(t, true, c.Bool("test")) +} + +func TestBoolApplyInputSourceMethodEnvVarSet(t *testing.T) { + c := runTest(t, testApplyInputSource{ + Flag: NewBoolFlag(cli.BoolFlag{Name: "test", EnvVar: "TEST"}), + FlagName: "test", + MapValue: false, + EnvVarName: "TEST", + EnvVarValue: "true", + }) + expect(t, true, c.Bool("test")) +} + +func TestBoolTApplyInputSourceMethodSet(t *testing.T) { + c := runTest(t, testApplyInputSource{ + Flag: NewBoolTFlag(cli.BoolTFlag{Name: "test"}), + FlagName: "test", + MapValue: false, + }) + expect(t, false, c.BoolT("test")) +} + +func TestBoolTApplyInputSourceMethodContextSet(t *testing.T) { + c := runTest(t, testApplyInputSource{ + Flag: NewBoolTFlag(cli.BoolTFlag{Name: "test"}), + FlagName: "test", + MapValue: true, + ContextValueString: "false", + }) + expect(t, false, c.BoolT("test")) +} + +func TestBoolTApplyInputSourceMethodEnvVarSet(t *testing.T) { + c := runTest(t, testApplyInputSource{ + Flag: NewBoolTFlag(cli.BoolTFlag{Name: "test", EnvVar: "TEST"}), + FlagName: "test", + MapValue: true, + EnvVarName: "TEST", + EnvVarValue: "false", + }) + expect(t, false, c.BoolT("test")) +} + +func TestStringApplyInputSourceMethodSet(t *testing.T) { + c := runTest(t, testApplyInputSource{ + Flag: NewStringFlag(cli.StringFlag{Name: "test"}), + FlagName: "test", + MapValue: "hello", + }) + expect(t, "hello", c.String("test")) +} + +func TestStringApplyInputSourceMethodContextSet(t *testing.T) { + c := runTest(t, testApplyInputSource{ + Flag: NewStringFlag(cli.StringFlag{Name: "test"}), + FlagName: "test", + MapValue: "hello", + ContextValueString: "goodbye", + }) + expect(t, "goodbye", c.String("test")) +} + +func TestStringApplyInputSourceMethodEnvVarSet(t *testing.T) { + c := runTest(t, testApplyInputSource{ + Flag: NewStringFlag(cli.StringFlag{Name: "test", EnvVar: "TEST"}), + FlagName: "test", + MapValue: "hello", + EnvVarName: "TEST", + EnvVarValue: "goodbye", + }) + expect(t, "goodbye", c.String("test")) +} + +func TestIntApplyInputSourceMethodSet(t *testing.T) { + c := runTest(t, testApplyInputSource{ + Flag: NewIntFlag(cli.IntFlag{Name: "test"}), + FlagName: "test", + MapValue: 15, + }) + expect(t, 15, c.Int("test")) +} + +func TestIntApplyInputSourceMethodContextSet(t *testing.T) { + c := runTest(t, testApplyInputSource{ + Flag: NewIntFlag(cli.IntFlag{Name: "test"}), + FlagName: "test", + MapValue: 15, + ContextValueString: "7", + }) + expect(t, 7, c.Int("test")) +} + +func TestIntApplyInputSourceMethodEnvVarSet(t *testing.T) { + c := runTest(t, testApplyInputSource{ + Flag: NewIntFlag(cli.IntFlag{Name: "test", EnvVar: "TEST"}), + FlagName: "test", + MapValue: 15, + EnvVarName: "TEST", + EnvVarValue: "12", + }) + expect(t, 12, c.Int("test")) +} + +func TestDurationApplyInputSourceMethodSet(t *testing.T) { + c := runTest(t, testApplyInputSource{ + Flag: NewDurationFlag(cli.DurationFlag{Name: "test"}), + FlagName: "test", + MapValue: time.Duration(30 * time.Second), + }) + expect(t, time.Duration(30*time.Second), c.Duration("test")) +} + +func TestDurationApplyInputSourceMethodContextSet(t *testing.T) { + c := runTest(t, testApplyInputSource{ + Flag: NewDurationFlag(cli.DurationFlag{Name: "test"}), + FlagName: "test", + MapValue: time.Duration(30 * time.Second), + ContextValueString: time.Duration(15 * time.Second).String(), + }) + expect(t, time.Duration(15*time.Second), c.Duration("test")) +} + +func TestDurationApplyInputSourceMethodEnvVarSet(t *testing.T) { + c := runTest(t, testApplyInputSource{ + Flag: NewDurationFlag(cli.DurationFlag{Name: "test", EnvVar: "TEST"}), + FlagName: "test", + MapValue: time.Duration(30 * time.Second), + EnvVarName: "TEST", + EnvVarValue: time.Duration(15 * time.Second).String(), + }) + expect(t, time.Duration(15*time.Second), c.Duration("test")) +} + +func TestFloat64ApplyInputSourceMethodSet(t *testing.T) { + c := runTest(t, testApplyInputSource{ + Flag: NewFloat64Flag(cli.Float64Flag{Name: "test"}), + FlagName: "test", + MapValue: 1.3, + }) + expect(t, 1.3, c.Float64("test")) +} + +func TestFloat64ApplyInputSourceMethodContextSet(t *testing.T) { + c := runTest(t, testApplyInputSource{ + Flag: NewFloat64Flag(cli.Float64Flag{Name: "test"}), + FlagName: "test", + MapValue: 1.3, + ContextValueString: fmt.Sprintf("%v", 1.4), + }) + expect(t, 1.4, c.Float64("test")) +} + +func TestFloat64ApplyInputSourceMethodEnvVarSet(t *testing.T) { + c := runTest(t, testApplyInputSource{ + Flag: NewFloat64Flag(cli.Float64Flag{Name: "test", EnvVar: "TEST"}), + FlagName: "test", + MapValue: 1.3, + EnvVarName: "TEST", + EnvVarValue: fmt.Sprintf("%v", 1.4), + }) + expect(t, 1.4, c.Float64("test")) +} + +func runTest(t *testing.T, test testApplyInputSource) *cli.Context { + inputSource := &MapInputSource{valueMap: map[string]interface{}{test.FlagName: test.MapValue}} + set := flag.NewFlagSet(test.FlagSetName, flag.ContinueOnError) + c := cli.NewContext(nil, set, nil) + if test.EnvVarName != "" && test.EnvVarValue != "" { + os.Setenv(test.EnvVarName, test.EnvVarValue) + defer os.Setenv(test.EnvVarName, "") + } + + test.Flag.Apply(set) + if test.ContextValue != nil { + flag := set.Lookup(test.FlagName) + flag.Value = test.ContextValue + } + if test.ContextValueString != "" { + set.Set(test.FlagName, test.ContextValueString) + } + test.Flag.ApplyInputSourceValue(c, inputSource) + + return c +} + +type Parser [2]string + +func (p *Parser) Set(value string) error { + parts := strings.Split(value, ",") + if len(parts) != 2 { + return fmt.Errorf("invalid format") + } + + (*p)[0] = parts[0] + (*p)[1] = parts[1] + + return nil +} + +func (p *Parser) String() string { + return fmt.Sprintf("%s,%s", p[0], p[1]) +} diff --git a/inputfilesupport/helpers_test.go b/inputfilesupport/helpers_test.go new file mode 100644 index 0000000..47b0069 --- /dev/null +++ b/inputfilesupport/helpers_test.go @@ -0,0 +1,18 @@ +package inputfilesupport + +import ( + "reflect" + "testing" +) + +func expect(t *testing.T, a interface{}, b interface{}) { + if !reflect.DeepEqual(b, a) { + t.Errorf("Expected %#v (type %v) - Got %#v (type %v)", b, reflect.TypeOf(b), a, reflect.TypeOf(a)) + } +} + +func refute(t *testing.T, a interface{}, b interface{}) { + if a == b { + t.Errorf("Did not expect %v (type %v) - Got %v (type %v)", b, reflect.TypeOf(b), a, reflect.TypeOf(a)) + } +} diff --git a/inputfilesupport/input_source_context.go b/inputfilesupport/input_source_context.go new file mode 100644 index 0000000..e759781 --- /dev/null +++ b/inputfilesupport/input_source_context.go @@ -0,0 +1,21 @@ +package inputfilesupport + +import ( + "time" + + "github.com/codegangsta/cli" +) + +// InputSourceContext is an interface used to allow +// other input sources to be implemented as needed. +type InputSourceContext interface { + Int(name string) int + Duration(name string) time.Duration + Float64(name string) float64 + String(name string) string + StringSlice(name string) []string + IntSlice(name string) []int + Generic(name string) cli.Generic + Bool(name string) bool + BoolT(name string) bool +} diff --git a/inputfilesupport/map_input_source.go b/inputfilesupport/map_input_source.go new file mode 100644 index 0000000..1100fbf --- /dev/null +++ b/inputfilesupport/map_input_source.go @@ -0,0 +1,132 @@ +package inputfilesupport + +import ( + "time" + + "github.com/codegangsta/cli" +) + +// MapInputSource implements InputSourceContext to return +// data from the map that is loaded. +// TODO: Didn't implement a way to write out various errors +// need to figure this part out. +type MapInputSource struct { + valueMap map[string]interface{} +} + +// Int returns an int from the map if it exists otherwise returns 0 +func (fsm *MapInputSource) Int(name string) int { + otherGenericValue, exists := fsm.valueMap[name] + if exists { + otherValue, isType := otherGenericValue.(int) + if isType { + return otherValue + } + } + + return 0 +} + +// Duration returns a duration from the map if it exists otherwise returns 0 +func (fsm *MapInputSource) Duration(name string) time.Duration { + otherGenericValue, exists := fsm.valueMap[name] + if exists { + otherValue, isType := otherGenericValue.(time.Duration) + if isType { + return otherValue + } + } + + return 0 +} + +// Float64 returns an float64 from the map if it exists otherwise returns 0 +func (fsm *MapInputSource) Float64(name string) float64 { + otherGenericValue, exists := fsm.valueMap[name] + if exists { + otherValue, isType := otherGenericValue.(float64) + if isType { + return otherValue + } + } + + return 0 +} + +// String returns a string from the map if it exists otherwise returns an empty string +func (fsm *MapInputSource) String(name string) string { + otherGenericValue, exists := fsm.valueMap[name] + if exists { + otherValue, isType := otherGenericValue.(string) + if isType { + return otherValue + } + } + + return "" +} + +// StringSlice returns an []string from the map if it exists otherwise returns nil +func (fsm *MapInputSource) StringSlice(name string) []string { + otherGenericValue, exists := fsm.valueMap[name] + if exists { + otherValue, isType := otherGenericValue.([]string) + if isType { + return otherValue + } + } + + return nil +} + +// IntSlice returns an []int from the map if it exists otherwise returns nil +func (fsm *MapInputSource) IntSlice(name string) []int { + otherGenericValue, exists := fsm.valueMap[name] + if exists { + otherValue, isType := otherGenericValue.([]int) + if isType { + return otherValue + } + } + + return nil +} + +// Generic returns an cli.Generic from the map if it exists otherwise returns nil +func (fsm *MapInputSource) Generic(name string) cli.Generic { + otherGenericValue, exists := fsm.valueMap[name] + if exists { + otherValue, isType := otherGenericValue.(cli.Generic) + if isType { + return otherValue + } + } + + return nil +} + +// Bool returns an bool from the map otherwise returns false +func (fsm *MapInputSource) Bool(name string) bool { + otherGenericValue, exists := fsm.valueMap[name] + if exists { + otherValue, isType := otherGenericValue.(bool) + if isType { + return otherValue + } + } + + return false +} + +// BoolT returns an bool from the map otherwise returns true +func (fsm *MapInputSource) BoolT(name string) bool { + otherGenericValue, exists := fsm.valueMap[name] + if exists { + otherValue, isType := otherGenericValue.(bool) + if isType { + return otherValue + } + } + + return true +} diff --git a/inputfilesupport/yaml_command_test.go b/inputfilesupport/yaml_command_test.go new file mode 100644 index 0000000..fcd8725 --- /dev/null +++ b/inputfilesupport/yaml_command_test.go @@ -0,0 +1,167 @@ +package inputfilesupport + +import ( + "flag" + "io/ioutil" + "os" + "testing" + + "github.com/codegangsta/cli" +) + +func TestCommandYamlFileTest(t *testing.T) { + app := cli.NewApp() + set := flag.NewFlagSet("test", 0) + ioutil.WriteFile("current.yaml", []byte("test: 15"), 0666) + defer os.Remove("current.yaml") + test := []string{"test-cmd", "--load", "current.yaml"} + set.Parse(test) + + c := cli.NewContext(app, set, nil) + + command := &cli.Command{ + Name: "test-cmd", + Aliases: []string{"tc"}, + Usage: "this is for testing", + Description: "testing", + Action: func(c *cli.Context) { + val := c.Int("test") + expect(t, val, 15) + }, + Flags: []cli.Flag{ + NewIntFlag(cli.IntFlag{Name: "test"}), + cli.StringFlag{Name: "load"}}, + } + command.Before = InitializeYaml("load", command.Flags) + err := command.Run(c) + + expect(t, err, nil) +} + +func TestCommandYamlFileTestGlobalEnvVarWins(t *testing.T) { + app := cli.NewApp() + set := flag.NewFlagSet("test", 0) + ioutil.WriteFile("current.yaml", []byte("test: 15"), 0666) + defer os.Remove("current.yaml") + + os.Setenv("THE_TEST", "10") + defer os.Setenv("THE_TEST", "") + test := []string{"test-cmd", "--load", "current.yaml"} + set.Parse(test) + + c := cli.NewContext(app, set, nil) + + command := &cli.Command{ + Name: "test-cmd", + Aliases: []string{"tc"}, + Usage: "this is for testing", + Description: "testing", + Action: func(c *cli.Context) { + val := c.Int("test") + expect(t, val, 10) + }, + Flags: []cli.Flag{ + NewIntFlag(cli.IntFlag{Name: "test", EnvVar: "THE_TEST"}), + cli.StringFlag{Name: "load"}}, + } + command.Before = InitializeYaml("load", command.Flags) + + err := command.Run(c) + + expect(t, err, nil) +} + +func TestCommandYamlFileTestSpecifiedFlagWins(t *testing.T) { + app := cli.NewApp() + set := flag.NewFlagSet("test", 0) + ioutil.WriteFile("current.yaml", []byte("test: 15"), 0666) + defer os.Remove("current.yaml") + + test := []string{"test-cmd", "--load", "current.yaml", "--test", "7"} + set.Parse(test) + + c := cli.NewContext(app, set, nil) + + command := &cli.Command{ + Name: "test-cmd", + Aliases: []string{"tc"}, + Usage: "this is for testing", + Description: "testing", + Action: func(c *cli.Context) { + val := c.Int("test") + expect(t, val, 7) + }, + Flags: []cli.Flag{ + NewIntFlag(cli.IntFlag{Name: "test"}), + cli.StringFlag{Name: "load"}}, + } + command.Before = InitializeYaml("load", command.Flags) + + err := command.Run(c) + + expect(t, err, nil) +} + +func TestCommandYamlFileTestDefaultValueFileWins(t *testing.T) { + app := cli.NewApp() + set := flag.NewFlagSet("test", 0) + ioutil.WriteFile("current.yaml", []byte("test: 15"), 0666) + defer os.Remove("current.yaml") + + test := []string{"test-cmd", "--load", "current.yaml"} + set.Parse(test) + + c := cli.NewContext(app, set, nil) + + command := &cli.Command{ + Name: "test-cmd", + Aliases: []string{"tc"}, + Usage: "this is for testing", + Description: "testing", + Action: func(c *cli.Context) { + val := c.Int("test") + expect(t, val, 15) + }, + Flags: []cli.Flag{ + NewIntFlag(cli.IntFlag{Name: "test", Value: 7}), + cli.StringFlag{Name: "load"}}, + } + command.Before = InitializeYaml("load", command.Flags) + + err := command.Run(c) + + expect(t, err, nil) +} + +func TestCommandYamlFileFlagHasDefaultGlobalEnvYamlSetGlobalEnvWins(t *testing.T) { + app := cli.NewApp() + set := flag.NewFlagSet("test", 0) + ioutil.WriteFile("current.yaml", []byte("test: 15"), 0666) + defer os.Remove("current.yaml") + + os.Setenv("THE_TEST", "11") + defer os.Setenv("THE_TEST", "") + + test := []string{"test-cmd", "--load", "current.yaml"} + set.Parse(test) + + c := cli.NewContext(app, set, nil) + + command := &cli.Command{ + Name: "test-cmd", + Aliases: []string{"tc"}, + Usage: "this is for testing", + Description: "testing", + Action: func(c *cli.Context) { + val := c.Int("test") + expect(t, val, 11) + }, + Flags: []cli.Flag{ + NewIntFlag(cli.IntFlag{Name: "test", Value: 7, EnvVar: "THE_TEST"}), + cli.StringFlag{Name: "load"}}, + } + command.Before = InitializeYaml("load", command.Flags) + err := command.Run(c) + + expect(t, err, nil) +} diff --git a/inputfilesupport/yaml_file_loader.go b/inputfilesupport/yaml_file_loader.go new file mode 100644 index 0000000..ac0636b --- /dev/null +++ b/inputfilesupport/yaml_file_loader.go @@ -0,0 +1,99 @@ +package inputfilesupport + +import ( + "fmt" + "io/ioutil" + "net/http" + "net/url" + "os" + + "github.com/codegangsta/cli" + "gopkg.in/yaml.v2" +) + +// LoadFlag is a default load flag used to get a inputFilePath for a yaml file +var LoadFlag = cli.StringFlag{ + Name: "load", + Usage: "file path to a yaml file", +} + +// InitializeYaml is used to initialize Before funcs for commands. +func InitializeYaml(filePathFlagName string, flags []cli.Flag) func(context *cli.Context) error { + return func(context *cli.Context) error { + filePath := context.String(filePathFlagName) + ymlLoader := &YamlSourceLoader{FilePath: filePath} + yamlInputSource, err := ymlLoader.Load() + if err != nil { + return fmt.Errorf("Unable to load Yaml file '%s': inner error: \n'%v'", filePath, err.Error()) + } + + for _, f := range flags { + inputSourceExtendedFlag, isType := f.(FlagInputSourceExtension) + if isType { + inputSourceExtendedFlag.ApplyInputSourceValue(context, yamlInputSource) + } + } + + return nil + } +} + +// YamlSourceLoader can load yaml files and return a InputSourceContext +// to be used for a parameter value +type YamlSourceLoader struct { + FilePath string +} + +// Load returns an input source if successful or an error if there is a failure +// loading the yaml file +func (ysl *YamlSourceLoader) Load() (InputSourceContext, error) { + var results map[string]interface{} + err := readCommandYaml(ysl.FilePath, &results) + if err != nil { + return nil, err + } + + return &MapInputSource{valueMap: results}, nil +} + +func readCommandYaml(filePath string, container interface{}) (err error) { + b, err := loadDataFrom(filePath) + if err != nil { + return err + } + + err = yaml.Unmarshal(b, container) + if err != nil { + return err + } + + err = nil + return +} + +func loadDataFrom(filePath string) ([]byte, error) { + u, err := url.Parse(filePath) + if err != nil { + return nil, err + } + + if u.Host != "" { // i have a host, now do i support the scheme? + switch u.Scheme { + case "http", "https": + res, err := http.Get(filePath) + if err != nil { + return nil, err + } + return ioutil.ReadAll(res.Body) + default: + return nil, fmt.Errorf("scheme of %s is unsupported", filePath) + } + } else if u.Path != "" { // i dont have a host, but I have a path. I am a local file. + if _, notFoundFileErr := os.Stat(filePath); notFoundFileErr != nil { + return nil, fmt.Errorf("Cannot read from file: '%s' because it does not exist.", filePath) + } + return ioutil.ReadFile(filePath) + } else { + return nil, fmt.Errorf("unable to determine how to load from path %s", filePath) + } +} From d3b02e41b0fdf1c0496499524b4d2885992bacd0 Mon Sep 17 00:00:00 2001 From: chrisprobinson Date: Wed, 9 Dec 2015 09:15:46 -0800 Subject: [PATCH 2/2] Update to add build contraints to not compile in yaml support in the case the golang version is less than golang 1.2 --- README.md | 46 +++++- {inputfilesupport => altsrc}/flag.go | 123 +++++++++++--- {inputfilesupport => altsrc}/flag_test.go | 2 +- {inputfilesupport => altsrc}/helpers_test.go | 2 +- altsrc/input_source_context.go | 21 +++ altsrc/map_input_source.go | 152 ++++++++++++++++++ .../yaml_command_test.go | 17 +- .../yaml_file_loader.go | 55 +++---- inputfilesupport/input_source_context.go | 21 --- inputfilesupport/map_input_source.go | 132 --------------- 10 files changed, 353 insertions(+), 218 deletions(-) rename {inputfilesupport => altsrc}/flag.go (72%) rename {inputfilesupport => altsrc}/flag_test.go (99%) rename {inputfilesupport => altsrc}/helpers_test.go (94%) create mode 100644 altsrc/input_source_context.go create mode 100644 altsrc/map_input_source.go rename {inputfilesupport => altsrc}/yaml_command_test.go (85%) rename {inputfilesupport => altsrc}/yaml_file_loader.go (53%) delete mode 100644 inputfilesupport/input_source_context.go delete mode 100644 inputfilesupport/map_input_source.go diff --git a/README.md b/README.md index ae0a4ca..95ac616 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ export PATH=$PATH:$GOPATH/bin ## Getting Started -One of the philosophies behind `cli.go` is that an API should be playful and full of discovery. So a `cli.go` app can be as little as one line of code in `main()`. +One of the philosophies behind `cli.go` is that an API should be playful and full of discovery. So a `cli.go` app can be as little as one line of code in `main()`. ``` go package main @@ -60,7 +60,7 @@ func main() { app.Action = func(c *cli.Context) { println("boom! I say!") } - + app.Run(os.Args) } ``` @@ -238,6 +238,48 @@ app.Flags = []cli.Flag { } ``` +#### Values from alternate input sources (YAML and others) + +There is a separate package altsrc that adds support for getting flag values from other input sources like YAML. + +In order to get values for a flag from an alternate input source the following code would be added to wrap an existing cli.Flag like below: + +``` go + altsrc.NewIntFlag(cli.IntFlag{Name: "test"}) +``` + +Initialization must also occur for these flags. Below is an example initializing getting data from a yaml file below. + +``` go + command.Before = altsrc.InitInputSourceWithContext(command.Flags, NewYamlSourceFromFlagFunc("load")) +``` + +The code above will use the "load" string as a flag name to get the file name of a yaml file from the cli.Context. +It will then use that file name to initialize the yaml input source for any flags that are defined on that command. +As a note the "load" flag used would also have to be defined on the command flags in order for this code snipped to work. + +Currently only YAML files are supported but developers can add support for other input sources by implementing the +altsrc.InputSourceContext for their given sources. + +Here is a more complete sample of a command using YAML support: + +``` go + command := &cli.Command{ + Name: "test-cmd", + Aliases: []string{"tc"}, + Usage: "this is for testing", + Description: "testing", + Action: func(c *cli.Context) { + // Action to run + }, + Flags: []cli.Flag{ + NewIntFlag(cli.IntFlag{Name: "test"}), + cli.StringFlag{Name: "load"}}, + } + command.Before = InitInputSourceWithContext(command.Flags, NewYamlSourceFromFlagFunc("load")) + err := command.Run(c) +``` + ### Subcommands Subcommands can be defined for a more git-like command line app. diff --git a/inputfilesupport/flag.go b/altsrc/flag.go similarity index 72% rename from inputfilesupport/flag.go rename to altsrc/flag.go index c168bd4..f13ffb4 100644 --- a/inputfilesupport/flag.go +++ b/altsrc/flag.go @@ -1,4 +1,4 @@ -package inputfilesupport +package altsrc import ( "flag" @@ -14,7 +14,53 @@ import ( // allows a value to be set on the existing parsed flags. type FlagInputSourceExtension interface { cli.Flag - ApplyInputSourceValue(context *cli.Context, isc InputSourceContext) + ApplyInputSourceValue(context *cli.Context, isc InputSourceContext) error +} + +// ApplyInputSourceValues iterates over all provided flags and +// executes ApplyInputSourceValue on flags implementing the +// FlagInputSourceExtension interface to initialize these flags +// to an alternate input source. +func ApplyInputSourceValues(context *cli.Context, inputSourceContext InputSourceContext, flags []cli.Flag) error { + for _, f := range flags { + inputSourceExtendedFlag, isType := f.(FlagInputSourceExtension) + if isType { + err := inputSourceExtendedFlag.ApplyInputSourceValue(context, inputSourceContext) + if err != nil { + return err + } + } + } + + return nil +} + +// InitInputSource is used to to setup an InputSourceContext on a cli.Command Before method. It will create a new +// input source based on the func provided. If there is no error it will then apply the new input source to any flags +// that are supported by the input source +func InitInputSource(flags []cli.Flag, createInputSource func() (InputSourceContext, error)) func(context *cli.Context) error { + return func(context *cli.Context) error { + inputSource, err := createInputSource() + if err != nil { + return fmt.Errorf("Unable to create input source: inner error: \n'%v'", err.Error()) + } + + return ApplyInputSourceValues(context, inputSource, flags) + } +} + +// InitInputSourceWithContext is used to to setup an InputSourceContext on a cli.Command Before method. It will create a new +// input source based on the func provided with potentially using existing cli.Context values to initialize itself. If there is +// no error it will then apply the new input source to any flags that are supported by the input source +func InitInputSourceWithContext(flags []cli.Flag, createInputSource func(context *cli.Context) (InputSourceContext, error)) func(context *cli.Context) error { + return func(context *cli.Context) error { + inputSource, err := createInputSource(context) + if err != nil { + return fmt.Errorf("Unable to create input source with context: inner error: \n'%v'", err.Error()) + } + + return ApplyInputSourceValues(context, inputSource, flags) + } } // GenericFlag is the flag type that wraps cli.GenericFlag to allow @@ -30,10 +76,13 @@ func NewGenericFlag(flag cli.GenericFlag) *GenericFlag { } // ApplyInputSourceValue applies a generic value to the flagSet if required -func (f *GenericFlag) ApplyInputSourceValue(context *cli.Context, isc InputSourceContext) { +func (f *GenericFlag) ApplyInputSourceValue(context *cli.Context, isc InputSourceContext) error { if f.set != nil { if !context.IsSet(f.Name) && !isEnvVarSet(f.EnvVar) { - value := isc.Generic(f.GenericFlag.Name) + value, err := isc.Generic(f.GenericFlag.Name) + if err != nil { + return err + } if value != nil { eachName(f.Name, func(name string) { f.set.Set(f.Name, value.String()) @@ -41,6 +90,8 @@ func (f *GenericFlag) ApplyInputSourceValue(context *cli.Context, isc InputSourc } } } + + return nil } // Apply saves the flagSet for later usage then calls @@ -63,10 +114,13 @@ func NewStringSliceFlag(flag cli.StringSliceFlag) *StringSliceFlag { } // ApplyInputSourceValue applies a StringSlice value to the flagSet if required -func (f *StringSliceFlag) ApplyInputSourceValue(context *cli.Context, isc InputSourceContext) { +func (f *StringSliceFlag) ApplyInputSourceValue(context *cli.Context, isc InputSourceContext) error { if f.set != nil { if !context.IsSet(f.Name) && !isEnvVarSet(f.EnvVar) { - value := isc.StringSlice(f.StringSliceFlag.Name) + value, err := isc.StringSlice(f.StringSliceFlag.Name) + if err != nil { + return err + } if value != nil { var sliceValue cli.StringSlice = value eachName(f.Name, func(name string) { @@ -78,6 +132,7 @@ func (f *StringSliceFlag) ApplyInputSourceValue(context *cli.Context, isc InputS } } } + return nil } // Apply saves the flagSet for later usage then calls @@ -100,10 +155,13 @@ func NewIntSliceFlag(flag cli.IntSliceFlag) *IntSliceFlag { } // ApplyInputSourceValue applies a IntSlice value if required -func (f *IntSliceFlag) ApplyInputSourceValue(context *cli.Context, isc InputSourceContext) { +func (f *IntSliceFlag) ApplyInputSourceValue(context *cli.Context, isc InputSourceContext) error { if f.set != nil { if !context.IsSet(f.Name) && !isEnvVarSet(f.EnvVar) { - value := isc.IntSlice(f.IntSliceFlag.Name) + value, err := isc.IntSlice(f.IntSliceFlag.Name) + if err != nil { + return err + } if value != nil { var sliceValue cli.IntSlice = value eachName(f.Name, func(name string) { @@ -115,6 +173,7 @@ func (f *IntSliceFlag) ApplyInputSourceValue(context *cli.Context, isc InputSour } } } + return nil } // Apply saves the flagSet for later usage then calls @@ -137,10 +196,13 @@ func NewBoolFlag(flag cli.BoolFlag) *BoolFlag { } // ApplyInputSourceValue applies a Bool value to the flagSet if required -func (f *BoolFlag) ApplyInputSourceValue(context *cli.Context, isc InputSourceContext) { +func (f *BoolFlag) ApplyInputSourceValue(context *cli.Context, isc InputSourceContext) error { if f.set != nil { if !context.IsSet(f.Name) && !isEnvVarSet(f.EnvVar) { - value := isc.Bool(f.BoolFlag.Name) + value, err := isc.Bool(f.BoolFlag.Name) + if err != nil { + return err + } if value { eachName(f.Name, func(name string) { f.set.Set(f.Name, strconv.FormatBool(value)) @@ -148,6 +210,7 @@ func (f *BoolFlag) ApplyInputSourceValue(context *cli.Context, isc InputSourceCo } } } + return nil } // Apply saves the flagSet for later usage then calls @@ -170,10 +233,13 @@ func NewBoolTFlag(flag cli.BoolTFlag) *BoolTFlag { } // ApplyInputSourceValue applies a BoolT value to the flagSet if required -func (f *BoolTFlag) ApplyInputSourceValue(context *cli.Context, isc InputSourceContext) { +func (f *BoolTFlag) ApplyInputSourceValue(context *cli.Context, isc InputSourceContext) error { if f.set != nil { if !context.IsSet(f.Name) && !isEnvVarSet(f.EnvVar) { - value := isc.BoolT(f.BoolTFlag.Name) + value, err := isc.BoolT(f.BoolTFlag.Name) + if err != nil { + return err + } if !value { eachName(f.Name, func(name string) { f.set.Set(f.Name, strconv.FormatBool(value)) @@ -181,6 +247,7 @@ func (f *BoolTFlag) ApplyInputSourceValue(context *cli.Context, isc InputSourceC } } } + return nil } // Apply saves the flagSet for later usage then calls @@ -204,10 +271,13 @@ func NewStringFlag(flag cli.StringFlag) *StringFlag { } // ApplyInputSourceValue applies a String value to the flagSet if required -func (f *StringFlag) ApplyInputSourceValue(context *cli.Context, isc InputSourceContext) { +func (f *StringFlag) ApplyInputSourceValue(context *cli.Context, isc InputSourceContext) error { if f.set != nil { if !(context.IsSet(f.Name) || isEnvVarSet(f.EnvVar)) { - value := isc.String(f.StringFlag.Name) + value, err := isc.String(f.StringFlag.Name) + if err != nil { + return err + } if value != "" { eachName(f.Name, func(name string) { f.set.Set(f.Name, value) @@ -215,6 +285,7 @@ func (f *StringFlag) ApplyInputSourceValue(context *cli.Context, isc InputSource } } } + return nil } // Apply saves the flagSet for later usage then calls @@ -238,10 +309,13 @@ func NewIntFlag(flag cli.IntFlag) *IntFlag { } // ApplyInputSourceValue applies a int value to the flagSet if required -func (f *IntFlag) ApplyInputSourceValue(context *cli.Context, isc InputSourceContext) { +func (f *IntFlag) ApplyInputSourceValue(context *cli.Context, isc InputSourceContext) error { if f.set != nil { if !(context.IsSet(f.Name) || isEnvVarSet(f.EnvVar)) { - value := isc.Int(f.IntFlag.Name) + value, err := isc.Int(f.IntFlag.Name) + if err != nil { + return err + } if value > 0 { eachName(f.Name, func(name string) { f.set.Set(f.Name, strconv.FormatInt(int64(value), 10)) @@ -249,6 +323,7 @@ func (f *IntFlag) ApplyInputSourceValue(context *cli.Context, isc InputSourceCon } } } + return nil } // Apply saves the flagSet for later usage then calls @@ -271,10 +346,13 @@ func NewDurationFlag(flag cli.DurationFlag) *DurationFlag { } // ApplyInputSourceValue applies a Duration value to the flagSet if required -func (f *DurationFlag) ApplyInputSourceValue(context *cli.Context, isc InputSourceContext) { +func (f *DurationFlag) ApplyInputSourceValue(context *cli.Context, isc InputSourceContext) error { if f.set != nil { if !(context.IsSet(f.Name) || isEnvVarSet(f.EnvVar)) { - value := isc.Duration(f.DurationFlag.Name) + value, err := isc.Duration(f.DurationFlag.Name) + if err != nil { + return err + } if value > 0 { eachName(f.Name, func(name string) { f.set.Set(f.Name, value.String()) @@ -282,6 +360,7 @@ func (f *DurationFlag) ApplyInputSourceValue(context *cli.Context, isc InputSour } } } + return nil } // Apply saves the flagSet for later usage then calls @@ -305,10 +384,13 @@ func NewFloat64Flag(flag cli.Float64Flag) *Float64Flag { } // ApplyInputSourceValue applies a Float64 value to the flagSet if required -func (f *Float64Flag) ApplyInputSourceValue(context *cli.Context, isc InputSourceContext) { +func (f *Float64Flag) ApplyInputSourceValue(context *cli.Context, isc InputSourceContext) error { if f.set != nil { if !(context.IsSet(f.Name) || isEnvVarSet(f.EnvVar)) { - value := isc.Float64(f.Float64Flag.Name) + value, err := isc.Float64(f.Float64Flag.Name) + if err != nil { + return err + } if value > 0 { floatStr := float64ToString(value) eachName(f.Name, func(name string) { @@ -317,6 +399,7 @@ func (f *Float64Flag) ApplyInputSourceValue(context *cli.Context, isc InputSourc } } } + return nil } // Apply saves the flagSet for later usage then calls diff --git a/inputfilesupport/flag_test.go b/altsrc/flag_test.go similarity index 99% rename from inputfilesupport/flag_test.go rename to altsrc/flag_test.go index 3ef7ce0..ac4d1f5 100644 --- a/inputfilesupport/flag_test.go +++ b/altsrc/flag_test.go @@ -1,4 +1,4 @@ -package inputfilesupport +package altsrc import ( "flag" diff --git a/inputfilesupport/helpers_test.go b/altsrc/helpers_test.go similarity index 94% rename from inputfilesupport/helpers_test.go rename to altsrc/helpers_test.go index 47b0069..3b7f7e9 100644 --- a/inputfilesupport/helpers_test.go +++ b/altsrc/helpers_test.go @@ -1,4 +1,4 @@ -package inputfilesupport +package altsrc import ( "reflect" diff --git a/altsrc/input_source_context.go b/altsrc/input_source_context.go new file mode 100644 index 0000000..6d695ff --- /dev/null +++ b/altsrc/input_source_context.go @@ -0,0 +1,21 @@ +package altsrc + +import ( + "time" + + "github.com/codegangsta/cli" +) + +// InputSourceContext is an interface used to allow +// other input sources to be implemented as needed. +type InputSourceContext interface { + Int(name string) (int, error) + Duration(name string) (time.Duration, error) + Float64(name string) (float64, error) + String(name string) (string, error) + StringSlice(name string) ([]string, error) + IntSlice(name string) ([]int, error) + Generic(name string) (cli.Generic, error) + Bool(name string) (bool, error) + BoolT(name string) (bool, error) +} diff --git a/altsrc/map_input_source.go b/altsrc/map_input_source.go new file mode 100644 index 0000000..f1670fb --- /dev/null +++ b/altsrc/map_input_source.go @@ -0,0 +1,152 @@ +package altsrc + +import ( + "fmt" + "reflect" + "time" + + "github.com/codegangsta/cli" +) + +// MapInputSource implements InputSourceContext to return +// data from the map that is loaded. +type MapInputSource struct { + valueMap map[string]interface{} +} + +// Int returns an int from the map if it exists otherwise returns 0 +func (fsm *MapInputSource) Int(name string) (int, error) { + otherGenericValue, exists := fsm.valueMap[name] + if exists { + otherValue, isType := otherGenericValue.(int) + if !isType { + return 0, incorrectTypeForFlagError(name, "int", otherGenericValue) + } + + return otherValue, nil + } + + return 0, nil +} + +// Duration returns a duration from the map if it exists otherwise returns 0 +func (fsm *MapInputSource) Duration(name string) (time.Duration, error) { + otherGenericValue, exists := fsm.valueMap[name] + if exists { + otherValue, isType := otherGenericValue.(time.Duration) + if !isType { + return 0, incorrectTypeForFlagError(name, "duration", otherGenericValue) + } + return otherValue, nil + } + + return 0, nil +} + +// Float64 returns an float64 from the map if it exists otherwise returns 0 +func (fsm *MapInputSource) Float64(name string) (float64, error) { + otherGenericValue, exists := fsm.valueMap[name] + if exists { + otherValue, isType := otherGenericValue.(float64) + if !isType { + return 0, incorrectTypeForFlagError(name, "float64", otherGenericValue) + } + return otherValue, nil + } + + return 0, nil +} + +// String returns a string from the map if it exists otherwise returns an empty string +func (fsm *MapInputSource) String(name string) (string, error) { + otherGenericValue, exists := fsm.valueMap[name] + if exists { + otherValue, isType := otherGenericValue.(string) + if !isType { + return "", incorrectTypeForFlagError(name, "string", otherGenericValue) + } + return otherValue, nil + } + + return "", nil +} + +// StringSlice returns an []string from the map if it exists otherwise returns nil +func (fsm *MapInputSource) StringSlice(name string) ([]string, error) { + otherGenericValue, exists := fsm.valueMap[name] + if exists { + otherValue, isType := otherGenericValue.([]string) + if !isType { + return nil, incorrectTypeForFlagError(name, "[]string", otherGenericValue) + } + return otherValue, nil + } + + return nil, nil +} + +// IntSlice returns an []int from the map if it exists otherwise returns nil +func (fsm *MapInputSource) IntSlice(name string) ([]int, error) { + otherGenericValue, exists := fsm.valueMap[name] + if exists { + otherValue, isType := otherGenericValue.([]int) + if !isType { + return nil, incorrectTypeForFlagError(name, "[]int", otherGenericValue) + } + return otherValue, nil + } + + return nil, nil +} + +// Generic returns an cli.Generic from the map if it exists otherwise returns nil +func (fsm *MapInputSource) Generic(name string) (cli.Generic, error) { + otherGenericValue, exists := fsm.valueMap[name] + if exists { + otherValue, isType := otherGenericValue.(cli.Generic) + if !isType { + return nil, incorrectTypeForFlagError(name, "cli.Generic", otherGenericValue) + } + return otherValue, nil + } + + return nil, nil +} + +// Bool returns an bool from the map otherwise returns false +func (fsm *MapInputSource) Bool(name string) (bool, error) { + otherGenericValue, exists := fsm.valueMap[name] + if exists { + otherValue, isType := otherGenericValue.(bool) + if !isType { + return false, incorrectTypeForFlagError(name, "bool", otherGenericValue) + } + return otherValue, nil + } + + return false, nil +} + +// BoolT returns an bool from the map otherwise returns true +func (fsm *MapInputSource) BoolT(name string) (bool, error) { + otherGenericValue, exists := fsm.valueMap[name] + if exists { + otherValue, isType := otherGenericValue.(bool) + if !isType { + return true, incorrectTypeForFlagError(name, "bool", otherGenericValue) + } + return otherValue, nil + } + + return true, nil +} + +func incorrectTypeForFlagError(name, expectedTypeName string, value interface{}) error { + valueType := reflect.TypeOf(value) + valueTypeName := "" + if valueType != nil { + valueTypeName = valueType.Name() + } + + return fmt.Errorf("Mismatched type for flag '%s'. Expected '%s' but actual is '%s'", name, expectedTypeName, valueTypeName) +} diff --git a/inputfilesupport/yaml_command_test.go b/altsrc/yaml_command_test.go similarity index 85% rename from inputfilesupport/yaml_command_test.go rename to altsrc/yaml_command_test.go index fcd8725..c7ccbf7 100644 --- a/inputfilesupport/yaml_command_test.go +++ b/altsrc/yaml_command_test.go @@ -1,4 +1,9 @@ -package inputfilesupport +// Disabling building of yaml support in cases where golang is 1.0 or 1.1 +// as the encoding library is not implemented or supported. + +// +build !go1,!go1.1 + +package altsrc import ( "flag" @@ -32,7 +37,7 @@ func TestCommandYamlFileTest(t *testing.T) { NewIntFlag(cli.IntFlag{Name: "test"}), cli.StringFlag{Name: "load"}}, } - command.Before = InitializeYaml("load", command.Flags) + command.Before = InitInputSourceWithContext(command.Flags, NewYamlSourceFromFlagFunc("load")) err := command.Run(c) expect(t, err, nil) @@ -64,7 +69,7 @@ func TestCommandYamlFileTestGlobalEnvVarWins(t *testing.T) { NewIntFlag(cli.IntFlag{Name: "test", EnvVar: "THE_TEST"}), cli.StringFlag{Name: "load"}}, } - command.Before = InitializeYaml("load", command.Flags) + command.Before = InitInputSourceWithContext(command.Flags, NewYamlSourceFromFlagFunc("load")) err := command.Run(c) @@ -95,7 +100,7 @@ func TestCommandYamlFileTestSpecifiedFlagWins(t *testing.T) { NewIntFlag(cli.IntFlag{Name: "test"}), cli.StringFlag{Name: "load"}}, } - command.Before = InitializeYaml("load", command.Flags) + command.Before = InitInputSourceWithContext(command.Flags, NewYamlSourceFromFlagFunc("load")) err := command.Run(c) @@ -126,7 +131,7 @@ func TestCommandYamlFileTestDefaultValueFileWins(t *testing.T) { NewIntFlag(cli.IntFlag{Name: "test", Value: 7}), cli.StringFlag{Name: "load"}}, } - command.Before = InitializeYaml("load", command.Flags) + command.Before = InitInputSourceWithContext(command.Flags, NewYamlSourceFromFlagFunc("load")) err := command.Run(c) @@ -160,7 +165,7 @@ func TestCommandYamlFileFlagHasDefaultGlobalEnvYamlSetGlobalEnvWins(t *testing.T NewIntFlag(cli.IntFlag{Name: "test", Value: 7, EnvVar: "THE_TEST"}), cli.StringFlag{Name: "load"}}, } - command.Before = InitializeYaml("load", command.Flags) + command.Before = InitInputSourceWithContext(command.Flags, NewYamlSourceFromFlagFunc("load")) err := command.Run(c) expect(t, err, nil) diff --git a/inputfilesupport/yaml_file_loader.go b/altsrc/yaml_file_loader.go similarity index 53% rename from inputfilesupport/yaml_file_loader.go rename to altsrc/yaml_file_loader.go index ac0636b..1251aeb 100644 --- a/inputfilesupport/yaml_file_loader.go +++ b/altsrc/yaml_file_loader.go @@ -1,4 +1,9 @@ -package inputfilesupport +// Disabling building of yaml support in cases where golang is 1.0 or 1.1 +// as the encoding library is not implemented or supported. + +// +build !go1,!go1.1 + +package altsrc import ( "fmt" @@ -8,54 +13,34 @@ import ( "os" "github.com/codegangsta/cli" + "gopkg.in/yaml.v2" ) -// LoadFlag is a default load flag used to get a inputFilePath for a yaml file -var LoadFlag = cli.StringFlag{ - Name: "load", - Usage: "file path to a yaml file", -} - -// InitializeYaml is used to initialize Before funcs for commands. -func InitializeYaml(filePathFlagName string, flags []cli.Flag) func(context *cli.Context) error { - return func(context *cli.Context) error { - filePath := context.String(filePathFlagName) - ymlLoader := &YamlSourceLoader{FilePath: filePath} - yamlInputSource, err := ymlLoader.Load() - if err != nil { - return fmt.Errorf("Unable to load Yaml file '%s': inner error: \n'%v'", filePath, err.Error()) - } - - for _, f := range flags { - inputSourceExtendedFlag, isType := f.(FlagInputSourceExtension) - if isType { - inputSourceExtendedFlag.ApplyInputSourceValue(context, yamlInputSource) - } - } - - return nil - } -} - -// YamlSourceLoader can load yaml files and return a InputSourceContext -// to be used for a parameter value -type YamlSourceLoader struct { +type yamlSourceContext struct { FilePath string } -// Load returns an input source if successful or an error if there is a failure -// loading the yaml file -func (ysl *YamlSourceLoader) Load() (InputSourceContext, error) { +// NewYamlSourceFromFile creates a new Yaml InputSourceContext from a filepath. +func NewYamlSourceFromFile(file string) (InputSourceContext, error) { + ymlLoader := &yamlSourceLoader{FilePath: file} var results map[string]interface{} err := readCommandYaml(ysl.FilePath, &results) if err != nil { - return nil, err + return fmt.Errorf("Unable to load Yaml file '%s': inner error: \n'%v'", filePath, err.Error()) } return &MapInputSource{valueMap: results}, nil } +// NewYamlSourceFromFlagFunc creates a new Yaml InputSourceContext from a provided flag name and source context. +func NewYamlSourceFromFlagFunc(flagFileName string) func(InputSourceContext, error) { + return func(context cli.Context) { + filePath := context.String(flagFileName) + return NewYamlSourceFromFile(filePath) + } +} + func readCommandYaml(filePath string, container interface{}) (err error) { b, err := loadDataFrom(filePath) if err != nil { diff --git a/inputfilesupport/input_source_context.go b/inputfilesupport/input_source_context.go deleted file mode 100644 index e759781..0000000 --- a/inputfilesupport/input_source_context.go +++ /dev/null @@ -1,21 +0,0 @@ -package inputfilesupport - -import ( - "time" - - "github.com/codegangsta/cli" -) - -// InputSourceContext is an interface used to allow -// other input sources to be implemented as needed. -type InputSourceContext interface { - Int(name string) int - Duration(name string) time.Duration - Float64(name string) float64 - String(name string) string - StringSlice(name string) []string - IntSlice(name string) []int - Generic(name string) cli.Generic - Bool(name string) bool - BoolT(name string) bool -} diff --git a/inputfilesupport/map_input_source.go b/inputfilesupport/map_input_source.go deleted file mode 100644 index 1100fbf..0000000 --- a/inputfilesupport/map_input_source.go +++ /dev/null @@ -1,132 +0,0 @@ -package inputfilesupport - -import ( - "time" - - "github.com/codegangsta/cli" -) - -// MapInputSource implements InputSourceContext to return -// data from the map that is loaded. -// TODO: Didn't implement a way to write out various errors -// need to figure this part out. -type MapInputSource struct { - valueMap map[string]interface{} -} - -// Int returns an int from the map if it exists otherwise returns 0 -func (fsm *MapInputSource) Int(name string) int { - otherGenericValue, exists := fsm.valueMap[name] - if exists { - otherValue, isType := otherGenericValue.(int) - if isType { - return otherValue - } - } - - return 0 -} - -// Duration returns a duration from the map if it exists otherwise returns 0 -func (fsm *MapInputSource) Duration(name string) time.Duration { - otherGenericValue, exists := fsm.valueMap[name] - if exists { - otherValue, isType := otherGenericValue.(time.Duration) - if isType { - return otherValue - } - } - - return 0 -} - -// Float64 returns an float64 from the map if it exists otherwise returns 0 -func (fsm *MapInputSource) Float64(name string) float64 { - otherGenericValue, exists := fsm.valueMap[name] - if exists { - otherValue, isType := otherGenericValue.(float64) - if isType { - return otherValue - } - } - - return 0 -} - -// String returns a string from the map if it exists otherwise returns an empty string -func (fsm *MapInputSource) String(name string) string { - otherGenericValue, exists := fsm.valueMap[name] - if exists { - otherValue, isType := otherGenericValue.(string) - if isType { - return otherValue - } - } - - return "" -} - -// StringSlice returns an []string from the map if it exists otherwise returns nil -func (fsm *MapInputSource) StringSlice(name string) []string { - otherGenericValue, exists := fsm.valueMap[name] - if exists { - otherValue, isType := otherGenericValue.([]string) - if isType { - return otherValue - } - } - - return nil -} - -// IntSlice returns an []int from the map if it exists otherwise returns nil -func (fsm *MapInputSource) IntSlice(name string) []int { - otherGenericValue, exists := fsm.valueMap[name] - if exists { - otherValue, isType := otherGenericValue.([]int) - if isType { - return otherValue - } - } - - return nil -} - -// Generic returns an cli.Generic from the map if it exists otherwise returns nil -func (fsm *MapInputSource) Generic(name string) cli.Generic { - otherGenericValue, exists := fsm.valueMap[name] - if exists { - otherValue, isType := otherGenericValue.(cli.Generic) - if isType { - return otherValue - } - } - - return nil -} - -// Bool returns an bool from the map otherwise returns false -func (fsm *MapInputSource) Bool(name string) bool { - otherGenericValue, exists := fsm.valueMap[name] - if exists { - otherValue, isType := otherGenericValue.(bool) - if isType { - return otherValue - } - } - - return false -} - -// BoolT returns an bool from the map otherwise returns true -func (fsm *MapInputSource) BoolT(name string) bool { - otherGenericValue, exists := fsm.valueMap[name] - if exists { - otherValue, isType := otherGenericValue.(bool) - if isType { - return otherValue - } - } - - return true -}