diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 067e1a5..023cc3e 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -18,7 +18,7 @@ jobs: # Optional: golangci-lint command line arguments. # args: --issues-exit-code=0 - # args: --out-format checkstyle + args: --out-format checkstyle # Optional: show only new issues if it's a pull request. The default value is `false`. # only-new-issues: true diff --git a/unpack.go b/unpack.go index f2069e7..ee131bd 100644 --- a/unpack.go +++ b/unpack.go @@ -2,6 +2,7 @@ package reflectutils import ( "encoding" + "encoding/json" "flag" "reflect" "strconv" @@ -10,12 +11,15 @@ import ( "github.com/pkg/errors" ) -var textUnmarshallerType = reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem() -var flagValueType = reflect.TypeOf((*flag.Value)(nil)).Elem() +var ( + textUnmarshallerType = reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem() + flagValueType = reflect.TypeOf((*flag.Value)(nil)).Elem() +) type stringSetterOpts struct { split string sliceAppend bool + forceJSON bool } type StringSetterArg func(*stringSetterOpts) @@ -41,6 +45,15 @@ func SliceAppend(b bool) StringSetterArg { } } +// ForceJSON controls if types will be decoded with JSON +// unmarshal. This overrides normal decoding patterns. The default +// is false. +func ForceJSON(b bool) StringSetterArg { + return func(o *stringSetterOpts) { + o.forceJSON = b + } +} + // MakeStringSetter handles setting a reflect.Value from a string. // Based on type, it returns a function to do the work. It is assumed that the // reflect.Type matches the reflect.Value. If not, panic is likely. @@ -64,6 +77,17 @@ func MakeStringSetter(t reflect.Type, optArgs ...StringSetterArg) (func(target r for _, f := range optArgs { f(&opts) } + if opts.forceJSON { + return func(target reflect.Value, value string) error { + p := reflect.New(t.Elem()) + target.Set(p) + err := json.Unmarshal([]byte(value), target.Interface()) + if err != nil { + return errors.WithStack(err) + } + return nil + }, nil + } if setter, ok := settersByType[t]; ok { return func(target reflect.Value, value string) error { out := setter.Call([]reflect.Value{reflect.ValueOf(value)}) diff --git a/unpack_test.go b/unpack_test.go index 12f1188..845ac21 100644 --- a/unpack_test.go +++ b/unpack_test.go @@ -30,6 +30,7 @@ func (bp *Bar) Set(s string) error { *bp = Bar(s + "/e") return nil } + func (bp Bar) String() string { return "b/" + string(bp) } @@ -37,6 +38,10 @@ func (bp Bar) String() string { var _ flag.Value = func() *Bar { var x Bar; return &x }() func TestStringSetter(t *testing.T) { + type J struct { + A int + B string + } type tsType struct { Int int `value:"38"` Int8 int8 `value:"-9"` @@ -75,7 +80,7 @@ func TestStringSetter(t *testing.T) { FooP *Foo `value:"foo" want:"~foo~"` Dur time.Duration `value:"30m" want:"30m0s"` DurP *time.Duration `value:"15m" want:"15m0s"` - DurArray []time.Duration `value:"15m,45m" want:"[15m0s 45m0s]"` + DurArray []time.Duration `value:"15m,45m" want:"[15m0s 45m0s]"` Bar Bar `value:"bar" want:"b/bar/e"` BarArray [2]Bar `value:"a,b,c" want:"[b/a/e b/b,c/e]"` BarP *Bar `value:"bar" want:"b/bar/e"` @@ -89,6 +94,7 @@ func TestStringSetter(t *testing.T) { SS5 []string `value:"foo" want:"[foo bar]" value2:"bar"` SS6 []string `value:"foo" want:"[bar]" value2:"bar" sa:"f"` RG01 *[]int `value:"823:29" want:"[823 29]" split:":"` + S *J `value:"{\"A\":10,\"B\":\"bar\"}" want:"{A:10 B:bar}" fj:"t"` } var ts tsType vp := reflect.ValueOf(&ts) @@ -116,6 +122,12 @@ func TestStringSetter(t *testing.T) { t.Log(" slice append", b) opts = append(opts, reflectutils.SliceAppend(b)) } + if fj, ok := f.Tag.Lookup("fj"); ok { + b, err := strconv.ParseBool(fj) + require.NoError(t, err, "parse fj") + t.Log(" force JSON", b) + opts = append(opts, reflectutils.ForceJSON(b)) + } fn, err := reflectutils.MakeStringSetter(f.Type, opts...) if !assert.NoErrorf(t, err, "make string setter for %s", f.Name) {