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

Better Generic Support For ISCs #1706

Open
wants to merge 1 commit into
base: v2-maint
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
40 changes: 39 additions & 1 deletion altsrc/flag.go
Expand Up @@ -73,7 +73,32 @@ func (f *GenericFlag) ApplyInputSourceValue(cCtx *cli.Context, isc InputSourceCo
}
value, err := isc.Generic(name)
if err != nil {
return err
stringValue, stringErr := isc.String(name)
if stringErr != nil {
isce, isType := isc.(InputSourceContextWithExport)
if isType {
flagValue, isType := f.Value.(GenericWithImport)
if !isType {
return err
}
jsonValue, err := isce.Json(name)
if err != nil {
return err
}
if len(jsonValue) > 0 {
err := flagValue.FromJson(jsonValue)
if err != nil {
return err
}
value = flagValue
}
}
} else {
value = &stringGeneric{stringValue}
}
if value == nil {
return err
}
}
if value == nil {
continue
Expand Down Expand Up @@ -415,3 +440,16 @@ func isEnvVarSet(envVars []string) bool {
func float64ToString(f float64) string {
return fmt.Sprintf("%v", f)
}

type stringGeneric struct {
value string
}

func (s *stringGeneric) Set(value string) error {
s.value = value
return nil
}

func (s *stringGeneric) String() string {
return s.value
}
107 changes: 107 additions & 0 deletions altsrc/flag_test.go
@@ -1,11 +1,13 @@
package altsrc

import (
"encoding/json"
"flag"
"fmt"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
"testing"
"time"
Expand Down Expand Up @@ -65,6 +67,72 @@ func TestGenericApplyInputSourceValue(t *testing.T) {
refute(t, v, c.Generic("test"))
}

func TestGenericApplyInputSourceValue_Import(t *testing.T) {
v := &HostPort{"localhost", 33}
tis := testApplyInputSource{
Flag: NewGenericFlag(&cli.GenericFlag{Name: "test", Value: &HostPort{}}),
FlagName: "test",
MapValue: map[interface{}]interface{}{
"host": "localhost",
"port": 33,
},
}
c := runTest(t, tis)
expect(t, v, c.Generic("test"))

c = runRacyTest(t, tis)
refute(t, v, c.Generic("test"))
}

func TestGenericApplyInputSourceValue_ImportString(t *testing.T) {
v := &HostPort{"localhost", 33}
tis := testApplyInputSource{
Flag: NewGenericFlag(&cli.GenericFlag{Name: "test", Value: &HostPort{}}),
FlagName: "test",
MapValue: "localhost:33",
}
c := runTest(t, tis)
expect(t, v, c.Generic("test"))

c = runRacyTest(t, tis)
refute(t, v, c.Generic("test"))
}

func TestGenericApplyInputSourceValue_ImportNested(t *testing.T) {
v := &HostPort{"localhost", 33}
tis := testApplyInputSource{
Flag: NewGenericFlag(&cli.GenericFlag{Name: "top.test", Value: &HostPort{}}),
FlagName: "top",
MapValue: map[interface{}]interface{}{
"test": map[interface{}]interface{}{
"host": "localhost",
"port": 33,
},
},
}
c := runTest(t, tis)
expect(t, v, c.Generic("top.test"))

c = runRacyTest(t, tis)
refute(t, v, c.Generic("top.test"))
}

func TestGenericApplyInputSourceValue_ImportStringNested(t *testing.T) {
v := &HostPort{"localhost", 33}
tis := testApplyInputSource{
Flag: NewGenericFlag(&cli.GenericFlag{Name: "top.test", Value: &HostPort{}}),
FlagName: "top",
MapValue: map[interface{}]interface{}{
"test": "localhost:33",
},
}
c := runTest(t, tis)
expect(t, v, c.Generic("top.test"))

c = runRacyTest(t, tis)
refute(t, v, c.Generic("top.test"))
}

func TestGenericApplyInputSourceMethodContextSet(t *testing.T) {
p := &Parser{"abc", "def"}
tis := testApplyInputSource{
Expand Down Expand Up @@ -948,6 +1016,16 @@ func TestFloat64ApplyInputSourceMethodEnvVarSet(t *testing.T) {
refute(t, 1.4, c.Float64("test"))
}

func TestStringGeneric_Set(t *testing.T) {
generic := &stringGeneric{}

expect(t, "", generic.String())

generic.Set("test")

expect(t, "test", generic.String())
}

func runTest(t *testing.T, test testApplyInputSource) *cli.Context {
inputSource := &MapInputSource{
file: test.SourcePath,
Expand Down Expand Up @@ -1004,4 +1082,33 @@ func (p *Parser) String() string {
return fmt.Sprintf("%s,%s", p[0], p[1])
}

type HostPort struct {
A string `json:"host"`
B int `json:"port"`
}

func (hp *HostPort) Set(value string) error {
parts := strings.Split(value, ":")
if len(parts) != 2 {
return fmt.Errorf("invalid format")
}
port, err := strconv.Atoi(parts[1])
if err != nil {
return err
}

(*hp).A = parts[0]
(*hp).B = port

return nil
}

func (hp *HostPort) String() string {
return fmt.Sprintf("%s:%d", hp.A, hp.B)
}

func (hp *HostPort) FromJson(rawJson []byte) error {
return json.Unmarshal(rawJson, hp)
}

type bogus [1]uint
9 changes: 9 additions & 0 deletions altsrc/generic_with_import.go
@@ -0,0 +1,9 @@
package altsrc

import "github.com/urfave/cli/v2"

type GenericWithImport interface {
cli.Generic

FromJson([]byte) error
}
6 changes: 6 additions & 0 deletions altsrc/input_source_context.go
Expand Up @@ -30,3 +30,9 @@ type InputSourceContext interface {

isSet(name string) bool
}

type InputSourceContextWithExport interface {
InputSourceContext

Json(name string) ([]byte, error)
}
8 changes: 8 additions & 0 deletions altsrc/json_source_context.go
Expand Up @@ -286,6 +286,14 @@ func (x *jsonSource) Bool(name string) (bool, error) {
return v, nil
}

func (x *jsonSource) Json(name string) ([]byte, error) {
i, err := x.getValue(name)
if err != nil {
return nil, err
}
return json.Marshal(i)
}

func (x *jsonSource) isSet(name string) bool {
_, err := x.getValue(name)
return err == nil
Expand Down
12 changes: 12 additions & 0 deletions altsrc/json_source_context_test.go
@@ -0,0 +1,12 @@
package altsrc

import "testing"

func TestJsonSourceContext_Json(t *testing.T) {
ctx, err := NewJSONSource([]byte(`{"test":{"key":"value"}}`))
expect(t, nil, err)

export, err := ctx.(*jsonSource).Json("test")
expect(t, []byte(`{"key":"value"}`), export)
expect(t, nil, err)
}
112 changes: 112 additions & 0 deletions altsrc/map_input_source.go
@@ -1,6 +1,7 @@
package altsrc

import (
"encoding/json"
"fmt"
"math"
"reflect"
Expand Down Expand Up @@ -461,6 +462,51 @@ func (fsm *MapInputSource) Bool(name string) (bool, error) {
return false, nil
}

func (fsm *MapInputSource) Json(name string) ([]byte, error) {
otherGenericValue, exists := fsm.valueMap[name]
if exists {
otherMapValue, isType := otherGenericValue.(map[interface{}]interface{})
if isType {
jsonable, err := mapToJsonable(otherMapValue)
if err != nil {
return nil, err
}
return json.Marshal(jsonable)
}
otherSliceValue, isType := otherGenericValue.([]interface{})
if isType {
jsonable, err := sliceToJsonable(otherSliceValue)
if err != nil {
return nil, err
}
return json.Marshal(jsonable)
}
return nil, incorrectTypeForFlagError(name, "map[interface{}]interface{} | []interface{}", otherGenericValue)
}
nestedGenericValue, exists := nestedVal(name, fsm.valueMap)
if exists {
otherMapValue, isType := nestedGenericValue.(map[interface{}]interface{})
if isType {
jsonable, err := mapToJsonable(otherMapValue)
if err != nil {
return nil, err
}
return json.Marshal(jsonable)
}
otherSliceValue, isType := nestedGenericValue.([]interface{})
if isType {
jsonable, err := sliceToJsonable(otherSliceValue)
if err != nil {
return nil, err
}
return json.Marshal(jsonable)
}
return nil, incorrectTypeForFlagError(name, "map[interface{}]interface{} | []interface{}", nestedGenericValue)
}

return nil, nil
}

func (fsm *MapInputSource) isSet(name string) bool {
if _, exists := fsm.valueMap[name]; exists {
return exists
Expand All @@ -470,6 +516,72 @@ func (fsm *MapInputSource) isSet(name string) bool {
return exists
}

func mapToJsonable(mapValue map[interface{}]interface{}) (map[string]interface{}, error) {
output := make(map[string]interface{}, 0)
for key, value := range mapValue {
otherMapValue, isType := value.(map[interface{}]interface{})
if isType {
jsonable, err := mapToJsonable(otherMapValue)
if err != nil {
return nil, err
}
value = jsonable
}

otherSliceValue, isType := value.([]interface{})
if isType {
jsonable, err := sliceToJsonable(otherSliceValue)
if err != nil {
return nil, err
}
value = jsonable
}

stringKey, isType := key.(string)
if isType {
output[stringKey] = value
continue
}

stringerKey, isType := key.(fmt.Stringer)
if isType {
output[stringerKey.String()] = value
continue
}

return nil, fmt.Errorf("Couldn't encode map to JSON. %v is neither a string nor Stringable", key)
}

return output, nil
}

func sliceToJsonable(value []interface{}) ([]interface{}, error) {
output := make([]interface{}, len(value))
for i, v := range value {
otherMapValue, isType := v.(map[interface{}]interface{})
if isType {
jsonable, err := mapToJsonable(otherMapValue)
if err != nil {
return nil, err
}
v = jsonable
}

otherSliceValue, isType := v.([]interface{})
if isType {
jsonable, err := sliceToJsonable(otherSliceValue)
if err != nil {
return nil, err
}
v = jsonable
}

output[i] = v
}

return output, nil
}

func incorrectTypeForFlagError(name, expectedTypeName string, value interface{}) error {
valueType := reflect.TypeOf(value)
valueTypeName := ""
Expand Down