From 802665b7de8e6354f779acd87d4a9b0dc2a18c59 Mon Sep 17 00:00:00 2001 From: David Sharnoff Date: Thu, 14 Mar 2024 14:34:56 -0700 Subject: [PATCH] add ForceJSON option for string decoding (#13) * add ForceJSON option for string decoding * update lint job * more go versions --- .github/workflows/go.yml | 2 +- .github/workflows/golangci-lint.yml | 40 ++++++++++++++++++++--------- unpack.go | 28 ++++++++++++++++++-- unpack_test.go | 14 +++++++++- 4 files changed, 68 insertions(+), 16 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 474e174..1f0b6b6 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -4,7 +4,7 @@ jobs: test: strategy: matrix: - go-version: [1.16.x, 1.17.x, 1.18.x] + go-version: [1.16.x, 1.17.x, 1.18.x, 1.19.x, 1.20.x, 1.21.x, 1.22.x] os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} steps: diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 067e1a5..ae728e7 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -1,33 +1,49 @@ name: golangci-lint -on: [push ] +on: [push] +permissions: + contents: read + # Optional: allow read access to pull request. Use with `only-new-issues` option. + # pull-requests: read + jobs: golangci: name: lint runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: '1.21' + cache: false - name: golangci-lint - uses: golangci/golangci-lint-action@v2 + uses: golangci/golangci-lint-action@v4 with: - # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version - version: latest + # Require: The version of golangci-lint to use. + # When `install-mode` is `binary` (default) the value can be v1.2 or v1.2.3 or `latest` to use the latest version. + # When `install-mode` is `goinstall` the value can be v1.2.3, `latest`, or the hash of a commit. + version: v1.54 # Optional: working directory, useful for monorepos # working-directory: somedir - output: checkstyle # Optional: golangci-lint command line arguments. - # args: --issues-exit-code=0 - # args: --out-format checkstyle + # + # Note: By default, the `.golangci.yml` file should be at the root of the repository. + # The location of the configuration file can be changed by using `--config=` + # args: --timeout=30m --config=/my/path/.golangci.yml --issues-exit-code=0 # Optional: show only new issues if it's a pull request. The default value is `false`. # only-new-issues: true - # Optional: if set to true then the action will use pre-installed Go. - # skip-go-installation: true + # Optional: if set to true, then all caching functionality will be completely disabled, + # takes precedence over all other caching options. + # skip-cache: true - # Optional: if set to true then the action don't cache or restore ~/go/pkg. + # Optional: if set to true, then the action won't cache or restore ~/go/pkg. # skip-pkg-cache: true - # Optional: if set to true then the action don't cache or restore ~/.cache/go-build. + # Optional: if set to true, then the action won't cache or restore ~/.cache/go-build. # skip-build-cache: true + + # Optional: The mode to install golangci-lint. It can be 'binary' or 'goinstall'. + # install-mode: "goinstall" 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) {