Skip to content

Commit

Permalink
Better Generic Support
Browse files Browse the repository at this point in the history
- Allow generics to be set as their Set() string in the config
- Allow converting internal ISC types to Generics when:
  - The data used to define the Generic can be expressed in JSON
  - The target Generic implements `FromJson([]byte) error`
  - The source ISC implements `Json(name string) ([]byte, error)`
- Update existing ISC implementations to also implement
  `Json(name string) ([]byte, error)`

And of course there are tests for the new features.
  • Loading branch information
danhunsaker committed Mar 18, 2023
1 parent 560c87b commit 098c58a
Show file tree
Hide file tree
Showing 9 changed files with 395 additions and 1 deletion.
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
}
97 changes: 97 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 @@ -1004,4 +1072,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
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

0 comments on commit 098c58a

Please sign in to comment.