From 5d2c10766fe93f76cd47da48127986b7a1d1db60 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 Sep 2024 04:53:42 +0000 Subject: [PATCH 01/36] build(deps): bump actions/checkout from 4.1.7 to 4.2.0 Bumps [actions/checkout](https://github.com/actions/checkout) from 4.1.7 to 4.2.0. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/692973e3d937129bcbf40652eb9f2f61becf3332...d632683dd7b4114ad314bca15554477dd762a938) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d31cfb71..68b9a5b1 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -16,7 +16,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Set up Go uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 @@ -32,7 +32,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Set up Go uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 From d3897c317f70237686015b0537d2d7129c9572ae Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 3 Oct 2024 04:54:31 +0000 Subject: [PATCH 02/36] build(deps): bump golangci/golangci-lint-action from 6.1.0 to 6.1.1 Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 6.1.0 to 6.1.1. - [Release notes](https://github.com/golangci/golangci-lint-action/releases) - [Commits](https://github.com/golangci/golangci-lint-action/compare/aaa42aa0628b4ae2578232a66b541047968fac86...971e284b6050e8a5849b72094c50ab08da042db8) --- updated-dependencies: - dependency-name: golangci/golangci-lint-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d31cfb71..870e0cfb 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -40,6 +40,6 @@ jobs: go-version: "1.22" - name: Lint - uses: golangci/golangci-lint-action@aaa42aa0628b4ae2578232a66b541047968fac86 # v6.1.0 + uses: golangci/golangci-lint-action@971e284b6050e8a5849b72094c50ab08da042db8 # v6.1.1 with: version: v1.59.0 From c05903f9de97118bbd2d3d25bf75a7dec3233a39 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 24 Oct 2024 05:12:01 +0000 Subject: [PATCH 03/36] build(deps): bump actions/checkout from 4.2.0 to 4.2.2 Bumps [actions/checkout](https://github.com/actions/checkout) from 4.2.0 to 4.2.2. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/d632683dd7b4114ad314bca15554477dd762a938...11bd71901bbe5b1630ceea73d27597364c9af683) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 9d1f8561..2700d33b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -16,7 +16,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up Go uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 @@ -32,7 +32,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up Go uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 From 480842d50b79540b7c120bcf5cf8c6cf74280365 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 25 Oct 2024 04:38:44 +0000 Subject: [PATCH 04/36] build(deps): bump actions/setup-go from 5.0.2 to 5.1.0 Bumps [actions/setup-go](https://github.com/actions/setup-go) from 5.0.2 to 5.1.0. - [Release notes](https://github.com/actions/setup-go/releases) - [Commits](https://github.com/actions/setup-go/compare/0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32...41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed) --- updated-dependencies: - dependency-name: actions/setup-go dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 9d1f8561..9b77bbbd 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -19,7 +19,7 @@ jobs: uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Set up Go - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 + uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0 with: go-version: ${{ matrix.go }} @@ -35,7 +35,7 @@ jobs: uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Set up Go - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 + uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0 with: go-version: "1.22" From 42691d328d862093357eef6218b844f79ea629ed Mon Sep 17 00:00:00 2001 From: Mahad Zaryab Date: Sat, 16 Nov 2024 15:50:25 -0500 Subject: [PATCH 05/36] Add Check For Data Being a reflect.Value Type Signed-off-by: Mahad Zaryab --- decode_hooks.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/decode_hooks.go b/decode_hooks.go index 1f3c69d4..7af1aa9a 100644 --- a/decode_hooks.go +++ b/decode_hooks.go @@ -100,7 +100,12 @@ func ComposeDecodeHookFunc(fs ...DecodeHookFunc) DecodeHookFunc { if err != nil { return nil, err } - newFrom = reflect.ValueOf(data) + if v, ok := data.(reflect.Value); ok { + newFrom = v + data = v.Interface() + } else { + newFrom = reflect.ValueOf(data) + } } return data, nil From 8ded786a1df79cb1b9f7badd83dedef6a1f997b9 Mon Sep 17 00:00:00 2001 From: Mahad Zaryab Date: Sat, 16 Nov 2024 15:50:50 -0500 Subject: [PATCH 06/36] Add Unit Test Signed-off-by: Mahad Zaryab --- decode_hooks_test.go | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/decode_hooks_test.go b/decode_hooks_test.go index 6549ae09..a4ec2a00 100644 --- a/decode_hooks_test.go +++ b/decode_hooks_test.go @@ -219,6 +219,36 @@ func TestComposeDecodeHookFunc_safe_nofuncs(t *testing.T) { } } +func TestComposeDecodeHookFunc_ReflectValueHook(t *testing.T) { + reflectValueHook := func( + f reflect.Kind, + t reflect.Kind, + data interface{}, + ) (interface{}, error) { + new := data.(string) + "foo" + return reflect.ValueOf(new), nil + } + + stringHook := func( + f reflect.Kind, + t reflect.Kind, + data interface{}, + ) (interface{}, error) { + return data.(string) + "bar", nil + } + + f := ComposeDecodeHookFunc(reflectValueHook, stringHook) + + result, err := DecodeHookExec( + f, reflect.ValueOf(""), reflect.ValueOf([]byte(""))) + if err != nil { + t.Fatalf("bad: %s", err) + } + if result.(string) != "foobar" { + t.Fatalf("bad: %#v", result) + } +} + func TestStringToSliceHookFunc(t *testing.T) { f := StringToSliceHookFunc(",") From 5d61bd5a6d75d556ad7ffa27f551f0614aed63d8 Mon Sep 17 00:00:00 2001 From: Mahad Zaryab Date: Sat, 16 Nov 2024 16:06:54 -0500 Subject: [PATCH 07/36] Remove Interface Call Signed-off-by: Mahad Zaryab --- decode_hooks.go | 1 - 1 file changed, 1 deletion(-) diff --git a/decode_hooks.go b/decode_hooks.go index 7af1aa9a..76f76bc1 100644 --- a/decode_hooks.go +++ b/decode_hooks.go @@ -102,7 +102,6 @@ func ComposeDecodeHookFunc(fs ...DecodeHookFunc) DecodeHookFunc { } if v, ok := data.(reflect.Value); ok { newFrom = v - data = v.Interface() } else { newFrom = reflect.ValueOf(data) } From 788aa96a175cc8a2a2c1771f4221d0e227856959 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 Dec 2024 05:01:11 +0000 Subject: [PATCH 08/36] build(deps): bump actions/setup-go from 5.1.0 to 5.2.0 Bumps [actions/setup-go](https://github.com/actions/setup-go) from 5.1.0 to 5.2.0. - [Release notes](https://github.com/actions/setup-go/releases) - [Commits](https://github.com/actions/setup-go/compare/41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed...3041bf56c941b39c61721a86cd11f3bb1338122a) --- updated-dependencies: - dependency-name: actions/setup-go dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index fb222e9d..97475aed 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -19,7 +19,7 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up Go - uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0 + uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0 with: go-version: ${{ matrix.go }} @@ -35,7 +35,7 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up Go - uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0 + uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0 with: go-version: "1.22" From d157c2beb4163811aad16992070ed3acc00e4fef Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 Jan 2025 04:24:00 +0000 Subject: [PATCH 09/36] build(deps): bump actions/setup-go from 5.2.0 to 5.3.0 Bumps [actions/setup-go](https://github.com/actions/setup-go) from 5.2.0 to 5.3.0. - [Release notes](https://github.com/actions/setup-go/releases) - [Commits](https://github.com/actions/setup-go/compare/3041bf56c941b39c61721a86cd11f3bb1338122a...f111f3307d8850f501ac008e886eec1fd1932a34) --- updated-dependencies: - dependency-name: actions/setup-go dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 97475aed..ec248ae7 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -19,7 +19,7 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up Go - uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0 + uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 with: go-version: ${{ matrix.go }} @@ -35,7 +35,7 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up Go - uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0 + uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 with: go-version: "1.22" From 10b325bac3745da8060d03e819b51be22c00715c Mon Sep 17 00:00:00 2001 From: Mark Sagi-Kazar Date: Tue, 18 Feb 2025 15:49:06 +0100 Subject: [PATCH 10/36] ci: add Go 1.24 to the test matrix Signed-off-by: Mark Sagi-Kazar --- .github/workflows/ci.yaml | 17 ++++++++++++++--- .golangci.yaml | 34 ++++++++++++++++------------------ 2 files changed, 30 insertions(+), 21 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index ec248ae7..291a6cc5 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -12,7 +12,18 @@ jobs: strategy: matrix: - go: ["1.18", "1.19", "1.20", "1.21", "1.22", "stable", "oldstable"] + go: + [ + "1.18", + "1.19", + "1.20", + "1.21", + "1.22", + "1.23", + "1.24", + "stable", + "oldstable", + ] steps: - name: Checkout repository @@ -37,9 +48,9 @@ jobs: - name: Set up Go uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 with: - go-version: "1.22" + go-version: "1.24" - name: Lint uses: golangci/golangci-lint-action@971e284b6050e8a5849b72094c50ab08da042db8 # v6.1.1 with: - version: v1.59.0 + version: v1.64.5 diff --git a/.golangci.yaml b/.golangci.yaml index 763143aa..40532c17 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -1,23 +1,21 @@ run: - timeout: 5m + timeout: 5m linters-settings: - gci: - sections: - - standard - - default - - prefix(github.com/go-viper/mapstructure) - golint: - min-confidence: 0 - goimports: - local-prefixes: github.com/go-viper/maptstructure + gci: + sections: + - standard + - default + - prefix(github.com/go-viper/mapstructure) + goimports: + local-prefixes: github.com/go-viper/maptstructure linters: - disable-all: true - enable: - - gci - - gofmt - - gofumpt - - goimports - - staticcheck - # - stylecheck + disable-all: true + enable: + - gci + - gofmt + - gofumpt + - goimports + - staticcheck + # - stylecheck From 97e523c2ddda689b5570cd7587d702f11c76a023 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 18 Feb 2025 14:54:24 +0000 Subject: [PATCH 11/36] build(deps): bump golangci/golangci-lint-action from 6.1.1 to 6.5.0 Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 6.1.1 to 6.5.0. - [Release notes](https://github.com/golangci/golangci-lint-action/releases) - [Commits](https://github.com/golangci/golangci-lint-action/compare/971e284b6050e8a5849b72094c50ab08da042db8...2226d7cb06a077cd73e56eedd38eecad18e5d837) --- updated-dependencies: - dependency-name: golangci/golangci-lint-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 291a6cc5..953ef77e 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -51,6 +51,6 @@ jobs: go-version: "1.24" - name: Lint - uses: golangci/golangci-lint-action@971e284b6050e8a5849b72094c50ab08da042db8 # v6.1.1 + uses: golangci/golangci-lint-action@2226d7cb06a077cd73e56eedd38eecad18e5d837 # v6.5.0 with: version: v1.64.5 From 39bca9fa6eee3b86ad7539094318434e91d99547 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 13 Mar 2025 04:17:05 +0000 Subject: [PATCH 12/36] build(deps): bump golangci/golangci-lint-action from 6.5.0 to 6.5.1 Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 6.5.0 to 6.5.1. - [Release notes](https://github.com/golangci/golangci-lint-action/releases) - [Commits](https://github.com/golangci/golangci-lint-action/compare/2226d7cb06a077cd73e56eedd38eecad18e5d837...4696ba8babb6127d732c3c6dde519db15edab9ea) --- updated-dependencies: - dependency-name: golangci/golangci-lint-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 953ef77e..211bbb97 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -51,6 +51,6 @@ jobs: go-version: "1.24" - name: Lint - uses: golangci/golangci-lint-action@2226d7cb06a077cd73e56eedd38eecad18e5d837 # v6.5.0 + uses: golangci/golangci-lint-action@4696ba8babb6127d732c3c6dde519db15edab9ea # v6.5.1 with: version: v1.64.5 From 49f08b138c52639c4ad867798ed35ce248b1f75d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 19 Mar 2025 04:55:34 +0000 Subject: [PATCH 13/36] build(deps): bump actions/setup-go from 5.3.0 to 5.4.0 Bumps [actions/setup-go](https://github.com/actions/setup-go) from 5.3.0 to 5.4.0. - [Release notes](https://github.com/actions/setup-go/releases) - [Commits](https://github.com/actions/setup-go/compare/f111f3307d8850f501ac008e886eec1fd1932a34...0aaccfd150d50ccaeb58ebd88d36e91967a5f35b) --- updated-dependencies: - dependency-name: actions/setup-go dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 211bbb97..168899d4 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -30,7 +30,7 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up Go - uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 + uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0 with: go-version: ${{ matrix.go }} @@ -46,7 +46,7 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up Go - uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 + uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0 with: go-version: "1.24" From 8724a8bd2c14d8ab2bbe0cfdcf87ea83ca2d9bee Mon Sep 17 00:00:00 2001 From: Rostislav Date: Mon, 24 Mar 2025 22:51:41 +0200 Subject: [PATCH 14/36] feat(decoder-configs) add AllowUnsetPointer option to skip unset errors for pointer fields --- mapstructure.go | 12 +++++++++--- mapstructure_test.go | 26 ++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/mapstructure.go b/mapstructure.go index e77e63ba..b2fd2568 100644 --- a/mapstructure.go +++ b/mapstructure.go @@ -222,6 +222,12 @@ type DecoderConfig struct { // will affect all nested structs as well. ErrorUnset bool + // AllowUnsetPointer, if set to true, will prevent fields with pointer types + // from being reported as unset, even if ErrorUnset is true and the field was + // not present in the input data. This allows pointer fields to be optional + // without triggering an error when they are missing. + AllowUnsetPointer bool + // ZeroFields, if set to true, will zero fields before writing them. // For example, a map will be emptied before decoded values are put in // it. If this is false, a map will be merged. @@ -1459,9 +1465,9 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e } if !rawMapVal.IsValid() { - // There was no matching key in the map for the value in - // the struct. Remember it for potential errors and metadata. - targetValKeysUnused[fieldName] = struct{}{} + if !(d.config.AllowUnsetPointer && fieldValue.Kind() == reflect.Ptr) { + targetValKeysUnused[fieldName] = struct{}{} + } continue } } diff --git a/mapstructure_test.go b/mapstructure_test.go index 519e7229..ceff4925 100644 --- a/mapstructure_test.go +++ b/mapstructure_test.go @@ -1675,6 +1675,32 @@ func TestDecoder_ErrorUnset(t *testing.T) { } } +func TestDecoder_ErrorUnset_AllowUnsetPointer(t *testing.T) { + t.Parallel() + + input := map[string]interface{}{ + "vstring": "hello", + "foo": "bar", + } + + var result BasicPointer + config := &DecoderConfig{ + ErrorUnset: true, + AllowUnsetPointer: true, + Result: &result, + } + + decoder, err := NewDecoder(config) + if err != nil { + t.Fatalf("err: %s", err) + } + + err = decoder.Decode(input) + if err != nil { + t.Fatal("error not expected") + } +} + func TestMap(t *testing.T) { t.Parallel() From 9ea3f2a92c4a874f066d20a45b4217030e5bc824 Mon Sep 17 00:00:00 2001 From: Rostislav Date: Mon, 24 Mar 2025 22:58:32 +0200 Subject: [PATCH 15/36] fix: restore comment removed by mistake in previous commit --- mapstructure.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mapstructure.go b/mapstructure.go index b2fd2568..6c587aea 100644 --- a/mapstructure.go +++ b/mapstructure.go @@ -1465,6 +1465,8 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e } if !rawMapVal.IsValid() { + // There was no matching key in the map for the value in + // the struct. Remember it for potential errors and metadata. if !(d.config.AllowUnsetPointer && fieldValue.Kind() == reflect.Ptr) { targetValKeysUnused[fieldName] = struct{}{} } From 2e0142542529a9f16169b9958ebf8d9a4c11f6c0 Mon Sep 17 00:00:00 2001 From: Tobias Klauser Date: Tue, 15 Apr 2025 00:04:34 +0200 Subject: [PATCH 16/36] feat: add decode hook for netip.Prefix Signed-off-by: Tobias Klauser --- decode_hooks.go | 20 ++++++++++++++++++++ decode_hooks_test.go | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/decode_hooks.go b/decode_hooks.go index 76f76bc1..57c6de69 100644 --- a/decode_hooks.go +++ b/decode_hooks.go @@ -390,6 +390,26 @@ func StringToNetIPAddrPortHookFunc() DecodeHookFunc { } } +// StringToNetIPPrefixHookFunc returns a DecodeHookFunc that converts +// strings to netip.Prefix. +func StringToNetIPPrefixHookFunc() DecodeHookFunc { + return func( + f reflect.Type, + t reflect.Type, + data interface{}, + ) (interface{}, error) { + if f.Kind() != reflect.String { + return data, nil + } + if t != reflect.TypeOf(netip.Prefix{}) { + return data, nil + } + + // Convert it by parsing + return netip.ParsePrefix(data.(string)) + } +} + // StringToBasicTypeHookFunc returns a DecodeHookFunc that converts // strings to basic types. // int8, uint8, int16, uint16, int32, uint32, int64, uint64, int, uint, float32, float64, bool, byte, rune, complex64, complex128 diff --git a/decode_hooks_test.go b/decode_hooks_test.go index a4ec2a00..52681e33 100644 --- a/decode_hooks_test.go +++ b/decode_hooks_test.go @@ -709,6 +709,42 @@ func TestStringToNetIPAddrPortHookFunc(t *testing.T) { } } +func TestStringToNetIPPrefixHookFunc(t *testing.T) { + strValue := reflect.ValueOf("5") + prefixValue := reflect.ValueOf(netip.Prefix{}) + cases := []struct { + f, t reflect.Value + result interface{} + err bool + }{ + { + reflect.ValueOf("192.0.2.1/24"), prefixValue, + netip.PrefixFrom(netip.AddrFrom4([4]byte{0xc0, 0x00, 0x02, 0x01}), 24), + false, + }, + { + reflect.ValueOf("fd7a:115c::626b:430b/118"), prefixValue, + netip.PrefixFrom(netip.AddrFrom16([16]byte{0xfd, 0x7a, 0x11, 0x5c, 12: 0x62, 0x6b, 0x43, 0x0b}), 118), + false, + }, + {strValue, prefixValue, netip.Prefix{}, true}, + {strValue, strValue, "5", false}, + } + + for i, tc := range cases { + f := StringToNetIPPrefixHookFunc() + actual, err := DecodeHookExec(f, tc.f, tc.t) + if tc.err != (err != nil) { + t.Fatalf("case %d: expected err %#v", i, tc.err) + } + if !reflect.DeepEqual(actual, tc.result) { + t.Fatalf( + "case %d:\nexpected %#v,\ngot %#v", + i, tc.result, actual) + } + } +} + func TestStringToBasicTypeHookFunc(t *testing.T) { strValue := reflect.ValueOf("42") From fda5b2294cb137f35f56e0139a26f2252d979c0d Mon Sep 17 00:00:00 2001 From: Mark Sagi-Kazar Date: Thu, 17 Apr 2025 14:21:43 +0200 Subject: [PATCH 17/36] docs: update badges Signed-off-by: Mark Sagi-Kazar --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index dd5ec69d..844357e5 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,9 @@ # mapstructure -[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/go-viper/mapstructure/ci.yaml?branch=main&style=flat-square)](https://github.com/go-viper/mapstructure/actions?query=workflow%3ACI) -[![go.dev reference](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat-square)](https://pkg.go.dev/mod/github.com/go-viper/mapstructure/v2) -![Go Version](https://img.shields.io/badge/go%20version-%3E=1.18-61CFDD.svg?style=flat-square) +[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/go-viper/mapstructure/ci.yaml?style=flat-square)](https://github.com/go-viper/mapstructure/actions/workflows/ci.yaml) +[![go.dev reference](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat-square)](https://pkg.go.dev/mod/github.com/go-viper/mapstructure) +![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/go-viper/mapstructure?style=flat-square&color=61CFDD) +[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/go-viper/mapstructure/badge?style=flat-square)](https://deps.dev/go/github.com%252Fgo-viper%252Fmapstructure) mapstructure is a Go library for decoding generic map values to structures and vice versa, while providing helpful error handling. From 95ff52671e4bcecc6fe3761f256a2d038ffca424 Mon Sep 17 00:00:00 2001 From: Mark Sagi-Kazar Date: Thu, 17 Apr 2025 14:26:45 +0200 Subject: [PATCH 18/36] chore: update golangci-lint Signed-off-by: Mark Sagi-Kazar --- .editorconfig | 3 + .github/workflows/ci.yaml | 4 +- .golangci.yaml | 54 ++++-- flake.lock | 390 +++++++++++--------------------------- flake.nix | 45 +++-- 5 files changed, 173 insertions(+), 323 deletions(-) diff --git a/.editorconfig b/.editorconfig index 1f664d13..faef0c91 100644 --- a/.editorconfig +++ b/.editorconfig @@ -16,3 +16,6 @@ indent_style = tab [*.nix] indent_size = 2 + +[.golangci.yaml] +indent_size = 2 diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 168899d4..21796166 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -51,6 +51,6 @@ jobs: go-version: "1.24" - name: Lint - uses: golangci/golangci-lint-action@4696ba8babb6127d732c3c6dde519db15edab9ea # v6.5.1 + uses: golangci/golangci-lint-action@1481404843c368bc19ca9406f87d6e0fc97bdcfd # v7.0.0 with: - version: v1.64.5 + version: v2.0.2 diff --git a/.golangci.yaml b/.golangci.yaml index 40532c17..ec1680b3 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -1,21 +1,39 @@ -run: - timeout: 5m +version: "2" -linters-settings: - gci: - sections: - - standard - - default - - prefix(github.com/go-viper/mapstructure) - goimports: - local-prefixes: github.com/go-viper/maptstructure +run: + timeout: 10m linters: - disable-all: true - enable: - - gci - - gofmt - - gofumpt - - goimports - - staticcheck - # - stylecheck + enable: + - govet + - ineffassign + # - misspell + - nolintlint + # - revive + + disable: + - errcheck + - staticcheck + - unused + + settings: + misspell: + locale: US + nolintlint: + allow-unused: false # report any unused nolint directives + require-specific: false # don't require nolint directives to be specific about which linter is being skipped + +formatters: + enable: + - gci + - gofmt + - gofumpt + - goimports + # - golines + + settings: + gci: + sections: + - standard + - default + - localmodule diff --git a/flake.lock b/flake.lock index 4bea8154..5e67bdd6 100644 --- a/flake.lock +++ b/flake.lock @@ -2,30 +2,28 @@ "nodes": { "cachix": { "inputs": { - "devenv": "devenv_2", + "devenv": [ + "devenv" + ], "flake-compat": [ - "devenv", - "flake-compat" + "devenv" ], - "nixpkgs": [ - "devenv", - "nixpkgs" + "git-hooks": [ + "devenv" ], - "pre-commit-hooks": [ - "devenv", - "pre-commit-hooks" - ] + "nixpkgs": "nixpkgs" }, "locked": { - "lastModified": 1712055811, - "narHash": "sha256-7FcfMm5A/f02yyzuavJe06zLa9hcMHsagE28ADcmQvk=", + "lastModified": 1742042642, + "narHash": "sha256-D0gP8srrX0qj+wNYNPdtVJsQuFzIng3q43thnHXQ/es=", "owner": "cachix", "repo": "cachix", - "rev": "02e38da89851ec7fec3356a5c04bc8349cae0e30", + "rev": "a624d3eaf4b1d225f918de8543ed739f2f574203", "type": "github" }, "original": { "owner": "cachix", + "ref": "latest", "repo": "cachix", "type": "github" } @@ -33,52 +31,21 @@ "devenv": { "inputs": { "cachix": "cachix", - "flake-compat": "flake-compat_2", - "nix": "nix_2", - "nixpkgs": "nixpkgs_2", - "pre-commit-hooks": "pre-commit-hooks" - }, - "locked": { - "lastModified": 1717245169, - "narHash": "sha256-+mW3rTBjGU8p1THJN0lX/Dd/8FbnF+3dB+mJuSaxewE=", - "owner": "cachix", - "repo": "devenv", - "rev": "c3f9f053c077c6f88a3de5276d9178c62baa3fc3", - "type": "github" - }, - "original": { - "owner": "cachix", - "repo": "devenv", - "type": "github" - } - }, - "devenv_2": { - "inputs": { - "flake-compat": [ - "devenv", - "cachix", - "flake-compat" - ], + "flake-compat": "flake-compat", + "git-hooks": "git-hooks", "nix": "nix", - "nixpkgs": "nixpkgs", - "poetry2nix": "poetry2nix", - "pre-commit-hooks": [ - "devenv", - "cachix", - "pre-commit-hooks" - ] + "nixpkgs": "nixpkgs_3" }, "locked": { - "lastModified": 1708704632, - "narHash": "sha256-w+dOIW60FKMaHI1q5714CSibk99JfYxm0CzTinYWr+Q=", + "lastModified": 1744876578, + "narHash": "sha256-8MTBj2REB8t29sIBLpxbR0+AEGJ7f+RkzZPAGsFd40c=", "owner": "cachix", "repo": "devenv", - "rev": "2ee4450b0f4b95a1b90f2eb5ffea98b90e48c196", + "rev": "7ff7c351bba20d0615be25ecdcbcf79b57b85fe1", "type": "github" }, "original": { "owner": "cachix", - "ref": "python-rewrite", "repo": "devenv", "type": "github" } @@ -86,27 +53,11 @@ "flake-compat": { "flake": false, "locked": { - "lastModified": 1673956053, - "narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=", - "owner": "edolstra", - "repo": "flake-compat", - "rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9", - "type": "github" - }, - "original": { - "owner": "edolstra", - "repo": "flake-compat", - "type": "github" - } - }, - "flake-compat_2": { - "flake": false, - "locked": { - "lastModified": 1696426674, - "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", + "lastModified": 1733328505, + "narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=", "owner": "edolstra", "repo": "flake-compat", - "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", + "rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec", "type": "github" }, "original": { @@ -117,14 +68,18 @@ }, "flake-parts": { "inputs": { - "nixpkgs-lib": "nixpkgs-lib" + "nixpkgs-lib": [ + "devenv", + "nix", + "nixpkgs" + ] }, "locked": { - "lastModified": 1717285511, - "narHash": "sha256-iKzJcpdXih14qYVcZ9QC9XuZYnPc6T8YImb6dX166kw=", + "lastModified": 1712014858, + "narHash": "sha256-sB4SWl2lX95bExY2gMFG5HIzvva5AVMJd4Igm+GpZNw=", "owner": "hercules-ci", "repo": "flake-parts", - "rev": "2a55567fcf15b1b1c7ed712a2c6fadaec7412ea8", + "rev": "9126214d0a59633752a136528f5f3b9aa8565b7d", "type": "github" }, "original": { @@ -133,39 +88,46 @@ "type": "github" } }, - "flake-utils": { + "flake-parts_2": { "inputs": { - "systems": "systems" + "nixpkgs-lib": "nixpkgs-lib" }, "locked": { - "lastModified": 1689068808, - "narHash": "sha256-6ixXo3wt24N/melDWjq70UuHQLxGV8jZvooRanIHXw0=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "919d646de7be200f3bf08cb76ae1f09402b6f9b4", + "lastModified": 1743550720, + "narHash": "sha256-hIshGgKZCgWh6AYJpJmRgFdR3WUbkY04o82X05xqQiY=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "c621e8422220273271f52058f618c94e405bb0f5", "type": "github" }, "original": { - "owner": "numtide", - "repo": "flake-utils", + "owner": "hercules-ci", + "repo": "flake-parts", "type": "github" } }, - "flake-utils_2": { + "git-hooks": { "inputs": { - "systems": "systems_2" + "flake-compat": [ + "devenv" + ], + "gitignore": "gitignore", + "nixpkgs": [ + "devenv", + "nixpkgs" + ] }, "locked": { - "lastModified": 1710146030, - "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", + "lastModified": 1742649964, + "narHash": "sha256-DwOTp7nvfi8mRfuL1escHDXabVXFGT1VlPD1JHrtrco=", + "owner": "cachix", + "repo": "git-hooks.nix", + "rev": "dcf5072734cb576d2b0c59b2ac44f5050b5eac82", "type": "github" }, "original": { - "owner": "numtide", - "repo": "flake-utils", + "owner": "cachix", + "repo": "git-hooks.nix", "type": "github" } }, @@ -173,7 +135,7 @@ "inputs": { "nixpkgs": [ "devenv", - "pre-commit-hooks", + "git-hooks", "nixpkgs" ] }, @@ -191,166 +153,109 @@ "type": "github" } }, - "nix": { - "inputs": { - "flake-compat": "flake-compat", - "nixpkgs": [ - "devenv", - "cachix", - "devenv", - "nixpkgs" - ], - "nixpkgs-regression": "nixpkgs-regression" - }, - "locked": { - "lastModified": 1712911606, - "narHash": "sha256-BGvBhepCufsjcUkXnEEXhEVjwdJAwPglCC2+bInc794=", - "owner": "domenkozar", - "repo": "nix", - "rev": "b24a9318ea3f3600c1e24b4a00691ee912d4de12", - "type": "github" - }, - "original": { - "owner": "domenkozar", - "ref": "devenv-2.21", - "repo": "nix", - "type": "github" - } - }, - "nix-github-actions": { - "inputs": { - "nixpkgs": [ - "devenv", - "cachix", - "devenv", - "poetry2nix", - "nixpkgs" - ] - }, + "libgit2": { + "flake": false, "locked": { - "lastModified": 1688870561, - "narHash": "sha256-4UYkifnPEw1nAzqqPOTL2MvWtm3sNGw1UTYTalkTcGY=", - "owner": "nix-community", - "repo": "nix-github-actions", - "rev": "165b1650b753316aa7f1787f3005a8d2da0f5301", + "lastModified": 1697646580, + "narHash": "sha256-oX4Z3S9WtJlwvj0uH9HlYcWv+x1hqp8mhXl7HsLu2f0=", + "owner": "libgit2", + "repo": "libgit2", + "rev": "45fd9ed7ae1a9b74b957ef4f337bc3c8b3df01b5", "type": "github" }, "original": { - "owner": "nix-community", - "repo": "nix-github-actions", + "owner": "libgit2", + "repo": "libgit2", "type": "github" } }, - "nix_2": { + "nix": { "inputs": { "flake-compat": [ - "devenv", - "flake-compat" + "devenv" ], - "nixpkgs": [ - "devenv", - "nixpkgs" + "flake-parts": "flake-parts", + "libgit2": "libgit2", + "nixpkgs": "nixpkgs_2", + "nixpkgs-23-11": [ + "devenv" + ], + "nixpkgs-regression": [ + "devenv" ], - "nixpkgs-regression": "nixpkgs-regression_2" + "pre-commit-hooks": [ + "devenv" + ] }, "locked": { - "lastModified": 1712911606, - "narHash": "sha256-BGvBhepCufsjcUkXnEEXhEVjwdJAwPglCC2+bInc794=", + "lastModified": 1741798497, + "narHash": "sha256-E3j+3MoY8Y96mG1dUIiLFm2tZmNbRvSiyN7CrSKuAVg=", "owner": "domenkozar", "repo": "nix", - "rev": "b24a9318ea3f3600c1e24b4a00691ee912d4de12", + "rev": "f3f44b2baaf6c4c6e179de8cbb1cc6db031083cd", "type": "github" }, "original": { "owner": "domenkozar", - "ref": "devenv-2.21", + "ref": "devenv-2.24", "repo": "nix", "type": "github" } }, "nixpkgs": { "locked": { - "lastModified": 1692808169, - "narHash": "sha256-x9Opq06rIiwdwGeK2Ykj69dNc2IvUH1fY55Wm7atwrE=", + "lastModified": 1733212471, + "narHash": "sha256-M1+uCoV5igihRfcUKrr1riygbe73/dzNnzPsmaLCmpo=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "9201b5ff357e781bf014d0330d18555695df7ba8", + "rev": "55d15ad12a74eb7d4646254e13638ad0c4128776", "type": "github" }, "original": { "owner": "NixOS", - "ref": "nixpkgs-unstable", + "ref": "nixos-unstable", "repo": "nixpkgs", "type": "github" } }, "nixpkgs-lib": { "locked": { - "lastModified": 1717284937, - "narHash": "sha256-lIbdfCsf8LMFloheeE6N31+BMIeixqyQWbSr2vk79EQ=", - "type": "tarball", - "url": "https://github.com/NixOS/nixpkgs/archive/eb9ceca17df2ea50a250b6b27f7bf6ab0186f198.tar.gz" - }, - "original": { - "type": "tarball", - "url": "https://github.com/NixOS/nixpkgs/archive/eb9ceca17df2ea50a250b6b27f7bf6ab0186f198.tar.gz" - } - }, - "nixpkgs-regression": { - "locked": { - "lastModified": 1643052045, - "narHash": "sha256-uGJ0VXIhWKGXxkeNnq4TvV3CIOkUJ3PAoLZ3HMzNVMw=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2", - "type": "github" - }, - "original": { - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2", - "type": "github" - } - }, - "nixpkgs-regression_2": { - "locked": { - "lastModified": 1643052045, - "narHash": "sha256-uGJ0VXIhWKGXxkeNnq4TvV3CIOkUJ3PAoLZ3HMzNVMw=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2", + "lastModified": 1743296961, + "narHash": "sha256-b1EdN3cULCqtorQ4QeWgLMrd5ZGOjLSLemfa00heasc=", + "owner": "nix-community", + "repo": "nixpkgs.lib", + "rev": "e4822aea2a6d1cdd36653c134cacfd64c97ff4fa", "type": "github" }, "original": { - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2", + "owner": "nix-community", + "repo": "nixpkgs.lib", "type": "github" } }, - "nixpkgs-stable": { + "nixpkgs_2": { "locked": { - "lastModified": 1710695816, - "narHash": "sha256-3Eh7fhEID17pv9ZxrPwCLfqXnYP006RKzSs0JptsN84=", + "lastModified": 1717432640, + "narHash": "sha256-+f9c4/ZX5MWDOuB1rKoWj+lBNm0z0rs4CK47HBLxy1o=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "614b4613980a522ba49f0d194531beddbb7220d3", + "rev": "88269ab3044128b7c2f4c7d68448b2fb50456870", "type": "github" }, "original": { "owner": "NixOS", - "ref": "nixos-23.11", + "ref": "release-24.05", "repo": "nixpkgs", "type": "github" } }, - "nixpkgs_2": { + "nixpkgs_3": { "locked": { - "lastModified": 1713361204, - "narHash": "sha256-TA6EDunWTkc5FvDCqU3W2T3SFn0gRZqh6D/hJnM02MM=", + "lastModified": 1733477122, + "narHash": "sha256-qamMCz5mNpQmgBwc8SB5tVMlD5sbwVIToVZtSxMph9s=", "owner": "cachix", "repo": "devenv-nixpkgs", - "rev": "285676e87ad9f0ca23d8714a6ab61e7e027020c6", + "rev": "7bd9e84d0452f6d2e63b6e6da29fe73fac951857", "type": "github" }, "original": { @@ -360,13 +265,13 @@ "type": "github" } }, - "nixpkgs_3": { + "nixpkgs_4": { "locked": { - "lastModified": 1717112898, - "narHash": "sha256-7R2ZvOnvd9h8fDd65p0JnB7wXfUvreox3xFdYWd1BnY=", + "lastModified": 1744536153, + "narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "6132b0f6e344ce2fe34fc051b72fb46e34f668e0", + "rev": "18dd725c29603f582cf1900e0d25f9f1063dbf11", "type": "github" }, "original": { @@ -376,94 +281,11 @@ "type": "github" } }, - "poetry2nix": { - "inputs": { - "flake-utils": "flake-utils", - "nix-github-actions": "nix-github-actions", - "nixpkgs": [ - "devenv", - "cachix", - "devenv", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1692876271, - "narHash": "sha256-IXfZEkI0Mal5y1jr6IRWMqK8GW2/f28xJenZIPQqkY0=", - "owner": "nix-community", - "repo": "poetry2nix", - "rev": "d5006be9c2c2417dafb2e2e5034d83fabd207ee3", - "type": "github" - }, - "original": { - "owner": "nix-community", - "repo": "poetry2nix", - "type": "github" - } - }, - "pre-commit-hooks": { - "inputs": { - "flake-compat": [ - "devenv", - "flake-compat" - ], - "flake-utils": "flake-utils_2", - "gitignore": "gitignore", - "nixpkgs": [ - "devenv", - "nixpkgs" - ], - "nixpkgs-stable": "nixpkgs-stable" - }, - "locked": { - "lastModified": 1713775815, - "narHash": "sha256-Wu9cdYTnGQQwtT20QQMg7jzkANKQjwBD9iccfGKkfls=", - "owner": "cachix", - "repo": "pre-commit-hooks.nix", - "rev": "2ac4dcbf55ed43f3be0bae15e181f08a57af24a4", - "type": "github" - }, - "original": { - "owner": "cachix", - "repo": "pre-commit-hooks.nix", - "type": "github" - } - }, "root": { "inputs": { "devenv": "devenv", - "flake-parts": "flake-parts", - "nixpkgs": "nixpkgs_3" - } - }, - "systems": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - }, - "systems_2": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" + "flake-parts": "flake-parts_2", + "nixpkgs": "nixpkgs_4" } } }, diff --git a/flake.nix b/flake.nix index 4ed0f533..3b116f42 100644 --- a/flake.nix +++ b/flake.nix @@ -5,35 +5,42 @@ devenv.url = "github:cachix/devenv"; }; - outputs = inputs@{ flake-parts, ... }: + outputs = + inputs@{ flake-parts, ... }: flake-parts.lib.mkFlake { inherit inputs; } { imports = [ inputs.devenv.flakeModule ]; - systems = [ "x86_64-linux" "x86_64-darwin" "aarch64-darwin" ]; + systems = [ + "x86_64-linux" + "x86_64-darwin" + "aarch64-darwin" + ]; - perSystem = { config, self', inputs', pkgs, system, ... }: rec { - devenv.shells = { - default = { - languages = { - go.enable = true; - }; + perSystem = + { pkgs, ... }: + rec { + devenv.shells = { + default = { + languages = { + go.enable = true; + }; - pre-commit.hooks = { - nixpkgs-fmt.enable = true; - }; + pre-commit.hooks = { + nixpkgs-fmt.enable = true; + }; - packages = with pkgs; [ - golangci-lint - ]; + packages = with pkgs; [ + golangci-lint + ]; - # https://github.com/cachix/devenv/issues/528#issuecomment-1556108767 - containers = pkgs.lib.mkForce { }; - }; + # https://github.com/cachix/devenv/issues/528#issuecomment-1556108767 + containers = pkgs.lib.mkForce { }; + }; - ci = devenv.shells.default; + ci = devenv.shells.default; + }; }; - }; }; } From 1f37f263a56e7d7cef35a3383c19173d309cb253 Mon Sep 17 00:00:00 2001 From: Mark Sagi-Kazar Date: Thu, 17 Apr 2025 14:27:57 +0200 Subject: [PATCH 19/36] ci: add openssf scorecard Signed-off-by: Mark Sagi-Kazar --- .github/workflows/analysis-scorecard.yaml | 47 +++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 .github/workflows/analysis-scorecard.yaml diff --git a/.github/workflows/analysis-scorecard.yaml b/.github/workflows/analysis-scorecard.yaml new file mode 100644 index 00000000..5fce75fb --- /dev/null +++ b/.github/workflows/analysis-scorecard.yaml @@ -0,0 +1,47 @@ +name: OpenSSF Scorecard + +on: + branch_protection_rule: + push: + branches: [main] + schedule: + - cron: "30 0 * * 5" + +permissions: + contents: read + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + + permissions: + actions: read + contents: read + id-token: write + security-events: write + + steps: + - name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + + - name: Run analysis + uses: ossf/scorecard-action@f49aabe0b5af0936a0987cfb85d86b75731b0186 # v2.4.1 + with: + results_file: results.sarif + results_format: sarif + publish_results: true + + - name: Upload results as artifact + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: OpenSSF Scorecard results + path: results.sarif + retention-days: 5 + + - name: Upload results to GitHub Security tab + uses: github/codeql-action/upload-sarif@cdcdbb579706841c47f7063dda365e292e5cad7a # v2.13.4 + with: + sarif_file: results.sarif From ef196921ef24d9c33fb5458f192c322c9d8b0f1a Mon Sep 17 00:00:00 2001 From: Mark Sagi-Kazar Date: Thu, 17 Apr 2025 14:30:55 +0200 Subject: [PATCH 20/36] docs: fix osi link Signed-off-by: Mark Sagi-Kazar --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 844357e5..b61ee2fa 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/go-viper/mapstructure/ci.yaml?style=flat-square)](https://github.com/go-viper/mapstructure/actions/workflows/ci.yaml) [![go.dev reference](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat-square)](https://pkg.go.dev/mod/github.com/go-viper/mapstructure) ![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/go-viper/mapstructure?style=flat-square&color=61CFDD) -[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/go-viper/mapstructure/badge?style=flat-square)](https://deps.dev/go/github.com%252Fgo-viper%252Fmapstructure) +[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/go-viper/mapstructure/badge?style=flat-square)](https://deps.dev/go/github.com%252Fgo-viper%252Fmapstructure%252Fv2) mapstructure is a Go library for decoding generic map values to structures and vice versa, while providing helpful error handling. From 3f55435a6d0df69b7507754aeaa770d87bed0c76 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 18 Apr 2025 04:57:12 +0000 Subject: [PATCH 21/36] build(deps): bump github/codeql-action from 2.13.4 to 3.28.15 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 2.13.4 to 3.28.15. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/cdcdbb579706841c47f7063dda365e292e5cad7a...45775bd8235c68ba998cffa5171334d58593da47) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 3.28.15 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/analysis-scorecard.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/analysis-scorecard.yaml b/.github/workflows/analysis-scorecard.yaml index 5fce75fb..00300685 100644 --- a/.github/workflows/analysis-scorecard.yaml +++ b/.github/workflows/analysis-scorecard.yaml @@ -42,6 +42,6 @@ jobs: retention-days: 5 - name: Upload results to GitHub Security tab - uses: github/codeql-action/upload-sarif@cdcdbb579706841c47f7063dda365e292e5cad7a # v2.13.4 + uses: github/codeql-action/upload-sarif@45775bd8235c68ba998cffa5171334d58593da47 # v3.28.15 with: sarif_file: results.sarif From 4bb5ef51126ec3c4bfcc9178a512c653bba9be9c Mon Sep 17 00:00:00 2001 From: Tiago Peczenyj Date: Thu, 1 May 2025 14:42:15 +0200 Subject: [PATCH 22/36] Update README.md dont need to escape the / inside sed search and replace command, you can use any placeholder for s/FOO/BAR/g this makes the command more clear IMHO --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b61ee2fa..bc4be08e 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ The API is the same, so you don't need to change anything else. Here is a script that can help you with the migration: ```shell -sed -i 's/github.com\/mitchellh\/mapstructure/github.com\/go-viper\/mapstructure\/v2/g' $(find . -type f -name '*.go') +sed -i 's|github.com/mitchellh/mapstructure|github.com/go-viper/mapstructure/v2|g' $(find . -type f -name '*.go') ``` If you need more time to migrate your code, that is absolutely fine. From fa8a45f0146ca88af88bc802d62b34639da36b11 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 May 2025 05:24:58 +0000 Subject: [PATCH 23/36] build(deps): bump github/codeql-action from 3.28.15 to 3.28.17 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.28.15 to 3.28.17. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/45775bd8235c68ba998cffa5171334d58593da47...60168efe1c415ce0f5521ea06d5c2062adbeed1b) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 3.28.17 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/analysis-scorecard.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/analysis-scorecard.yaml b/.github/workflows/analysis-scorecard.yaml index 00300685..856a6dda 100644 --- a/.github/workflows/analysis-scorecard.yaml +++ b/.github/workflows/analysis-scorecard.yaml @@ -42,6 +42,6 @@ jobs: retention-days: 5 - name: Upload results to GitHub Security tab - uses: github/codeql-action/upload-sarif@45775bd8235c68ba998cffa5171334d58593da47 # v3.28.15 + uses: github/codeql-action/upload-sarif@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17 with: sarif_file: results.sarif From d1c5bb817fd6d2e47f413de3461843579814f72e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 8 May 2025 04:36:56 +0000 Subject: [PATCH 24/36] build(deps): bump actions/setup-go from 5.4.0 to 5.5.0 Bumps [actions/setup-go](https://github.com/actions/setup-go) from 5.4.0 to 5.5.0. - [Release notes](https://github.com/actions/setup-go/releases) - [Commits](https://github.com/actions/setup-go/compare/0aaccfd150d50ccaeb58ebd88d36e91967a5f35b...d35c59abb061a4a6fb18e82ac0862c26744d6ab5) --- updated-dependencies: - dependency-name: actions/setup-go dependency-version: 5.5.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 21796166..985178e9 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -30,7 +30,7 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up Go - uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0 + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 with: go-version: ${{ matrix.go }} @@ -46,7 +46,7 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up Go - uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0 + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 with: go-version: "1.24" From c87739f566861e8ffc7503f1a423c23238bdfd19 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Jun 2025 05:12:14 +0000 Subject: [PATCH 25/36] build(deps): bump ossf/scorecard-action from 2.4.1 to 2.4.2 Bumps [ossf/scorecard-action](https://github.com/ossf/scorecard-action) from 2.4.1 to 2.4.2. - [Release notes](https://github.com/ossf/scorecard-action/releases) - [Changelog](https://github.com/ossf/scorecard-action/blob/main/RELEASE.md) - [Commits](https://github.com/ossf/scorecard-action/compare/f49aabe0b5af0936a0987cfb85d86b75731b0186...05b42c624433fc40578a4040d5cf5e36ddca8cde) --- updated-dependencies: - dependency-name: ossf/scorecard-action dependency-version: 2.4.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/analysis-scorecard.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/analysis-scorecard.yaml b/.github/workflows/analysis-scorecard.yaml index 856a6dda..42122f0a 100644 --- a/.github/workflows/analysis-scorecard.yaml +++ b/.github/workflows/analysis-scorecard.yaml @@ -28,7 +28,7 @@ jobs: persist-credentials: false - name: Run analysis - uses: ossf/scorecard-action@f49aabe0b5af0936a0987cfb85d86b75731b0186 # v2.4.1 + uses: ossf/scorecard-action@05b42c624433fc40578a4040d5cf5e36ddca8cde # v2.4.2 with: results_file: results.sarif results_format: sarif From 5bf706bc6a2d81864d4b861515498488aed8d1c3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 4 Jun 2025 04:38:53 +0000 Subject: [PATCH 26/36] build(deps): bump github/codeql-action from 3.28.17 to 3.28.19 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.28.17 to 3.28.19. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/60168efe1c415ce0f5521ea06d5c2062adbeed1b...fca7ace96b7d713c7035871441bd52efbe39e27e) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 3.28.19 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/analysis-scorecard.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/analysis-scorecard.yaml b/.github/workflows/analysis-scorecard.yaml index 856a6dda..7b0092aa 100644 --- a/.github/workflows/analysis-scorecard.yaml +++ b/.github/workflows/analysis-scorecard.yaml @@ -42,6 +42,6 @@ jobs: retention-days: 5 - name: Upload results to GitHub Security tab - uses: github/codeql-action/upload-sarif@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17 + uses: github/codeql-action/upload-sarif@fca7ace96b7d713c7035871441bd52efbe39e27e # v3.28.19 with: sarif_file: results.sarif From 9c2eaaea7a5f2d4b21cee53846b044fa5a2caa5c Mon Sep 17 00:00:00 2001 From: Michael Kuc Date: Wed, 4 Jun 2025 13:38:08 +0000 Subject: [PATCH 27/36] docs: improve omitempty documentation. Improves the documentation of the existing `omitempty` struct tag, as it relates to nil and empty slices. --- mapstructure.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/mapstructure.go b/mapstructure.go index e77e63ba..47e786c1 100644 --- a/mapstructure.go +++ b/mapstructure.go @@ -115,15 +115,17 @@ // // When decoding from a struct to any other value, you may use the // ",omitempty" suffix on your tag to omit that value if it equates to -// the zero value. The zero value of all types is specified in the Go -// specification. +// the zero value, or a zero-length element. The zero value of all types is +// specified in the Go specification. // // For example, the zero type of a numeric type is zero ("0"). If the struct // field value is zero and a numeric type, the field is empty, and it won't -// be encoded into the destination type. +// be encoded into the destination type. And likewise for the URLs field, if the +// slice is nil or empty, it won't be encoded into the destination type. // // type Source struct { -// Age int `mapstructure:",omitempty"` +// Age int `mapstructure:",omitempty"` +// URLs []string `mapstructure:",omitempty"` // } // // # Unexported fields From 21da84398aec0a6d7d7f0dc828466321ff78b4e6 Mon Sep 17 00:00:00 2001 From: Michael Kuc Date: Wed, 4 Jun 2025 13:38:13 +0000 Subject: [PATCH 28/36] feat: add omitzero struct tag. Adds a tag for omitting zero-valued elements, such as nil slices and maps. This enables a bit more fine-grained control over whether a slice will be copied across (by explicitly choosing to set as nil or zero-length). --- mapstructure.go | 24 +++++++++++ mapstructure_test.go | 97 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 121 insertions(+) diff --git a/mapstructure.go b/mapstructure.go index 47e786c1..60311d6b 100644 --- a/mapstructure.go +++ b/mapstructure.go @@ -128,6 +128,25 @@ // URLs []string `mapstructure:",omitempty"` // } // +// # Omit Zero Values +// +// When decoding from a struct to any other value, you may use the +// ",omitzero" suffix on your tag to omit that value if it equates to the zero +// value. The zero value of all types is specified in the Go specification. +// +// For example, the zero type of a numeric type is zero ("0"). If the struct +// field value is zero and a numeric type, the field is empty, and it won't +// be encoded into the destination type. And likewise for the URLs field, if the +// slice is nil, it won't be encoded into the destination type. +// +// Note that if the field is a slice, and it is empty but not nil, it will +// still be encoded into the destination type. +// +// type Source struct { +// Age int `mapstructure:",omitzero"` +// URLs []string `mapstructure:",omitzero"` +// } +// // # Unexported fields // // Since unexported (private) struct fields cannot be set outside the package @@ -1013,6 +1032,11 @@ func (d *Decoder) decodeMapFromStruct(name string, dataVal reflect.Value, val re continue } + // If "omitzero" is specified in the tag, it ignores zero values. + if strings.Index(tagValue[index+1:], "omitzero") != -1 && v.IsZero() { + continue + } + // If "squash" is specified in the tag, we squash the field down. squash = squash || strings.Contains(tagValue[index+1:], d.config.SquashTagOption) if squash { diff --git a/mapstructure_test.go b/mapstructure_test.go index 519e7229..248afaa8 100644 --- a/mapstructure_test.go +++ b/mapstructure_test.go @@ -255,6 +255,21 @@ type StructWithOmitEmpty struct { OmitNestedField *Nested `mapstructure:"omittable-nested,omitempty"` } +type StructWithOmitZero struct { + VisibleStringField string `mapstructure:"visible-string"` + OmitStringField string `mapstructure:"omittable-string,omitzero"` + VisibleIntField int `mapstructure:"visible-int"` + OmitIntField int `mapstructure:"omittable-int,omitzero"` + VisibleFloatField float64 `mapstructure:"visible-float"` + OmitFloatField float64 `mapstructure:"omittable-float,omitzero"` + VisibleSliceField []interface{} `mapstructure:"visible-slice"` + OmitSliceField []interface{} `mapstructure:"omittable-slice,omitzero"` + VisibleMapField map[string]interface{} `mapstructure:"visible-map"` + OmitMapField map[string]interface{} `mapstructure:"omittable-map,omitzero"` + NestedField *Nested `mapstructure:"visible-nested"` + OmitNestedField *Nested `mapstructure:"omittable-nested,omitzero"` +} + type TypeConversionResult struct { IntToFloat float32 IntToUint uint @@ -2932,6 +2947,88 @@ func TestDecode_StructTaggedWithOmitempty_KeepNonEmptyValues(t *testing.T) { } } +func TestDecode_StructTaggedWithOmitzero_KeepNonZeroValues(t *testing.T) { + t.Parallel() + + input := &StructWithOmitZero{ + VisibleStringField: "", + OmitStringField: "string", + VisibleIntField: 0, + OmitIntField: 1, + VisibleFloatField: 0.0, + OmitFloatField: 1.0, + VisibleSliceField: nil, + OmitSliceField: []interface{}{}, + VisibleMapField: nil, + OmitMapField: map[string]interface{}{}, + NestedField: nil, + OmitNestedField: &Nested{}, + } + + var emptySlice []interface{} + var emptyMap map[string]interface{} + var emptyNested *Nested + expected := &map[string]interface{}{ + "visible-string": "", + "omittable-string": "string", + "visible-int": 0, + "omittable-int": 1, + "visible-float": 0.0, + "omittable-float": 1.0, + "visible-slice": emptySlice, + "omittable-slice": []interface{}{}, + "visible-map": emptyMap, + "omittable-map": map[string]interface{}{}, + "visible-nested": emptyNested, + "omittable-nested": &Nested{}, + } + + actual := &map[string]interface{}{} + Decode(input, actual) + + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("Decode() expected: %#v, got: %#v", expected, actual) + } +} + +func TestDecode_StructTaggedWithOmitzero_DropZeroValues(t *testing.T) { + t.Parallel() + + input := &StructWithOmitZero{ + VisibleStringField: "", + OmitStringField: "", + VisibleIntField: 0, + OmitIntField: 0, + VisibleFloatField: 0.0, + OmitFloatField: 0.0, + VisibleSliceField: nil, + OmitSliceField: nil, + VisibleMapField: nil, + OmitMapField: nil, + NestedField: nil, + OmitNestedField: nil, + } + + var emptySlice []interface{} + var emptyMap map[string]interface{} + var emptyNested *Nested + expected := &map[string]interface{}{ + "visible-string": "", + "visible-int": 0, + "visible-float": 0.0, + "visible-slice": emptySlice, + "visible-map": emptyMap, + "visible-nested": emptyNested, + } + + actual := &map[string]interface{}{} + Decode(input, actual) + + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("Decode() expected: %#v, got: %#v", expected, actual) + } +} + func TestDecode_mapToStruct(t *testing.T) { type Target struct { String string From ef60c974643d9e157392b2db8321be3c156786c2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 12 Jun 2025 05:05:01 +0000 Subject: [PATCH 29/36] build(deps): bump github/codeql-action from 3.28.19 to 3.29.0 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.28.19 to 3.29.0. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/fca7ace96b7d713c7035871441bd52efbe39e27e...ce28f5bb42b7a9f2c824e633a3f6ee835bab6858) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 3.29.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/analysis-scorecard.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/analysis-scorecard.yaml b/.github/workflows/analysis-scorecard.yaml index 59116e85..5292812e 100644 --- a/.github/workflows/analysis-scorecard.yaml +++ b/.github/workflows/analysis-scorecard.yaml @@ -42,6 +42,6 @@ jobs: retention-days: 5 - name: Upload results to GitHub Security tab - uses: github/codeql-action/upload-sarif@fca7ace96b7d713c7035871441bd52efbe39e27e # v3.28.19 + uses: github/codeql-action/upload-sarif@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0 with: sarif_file: results.sarif From d0d489ccca2f3220f7dc3e7c7922e1aed5a297f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0ediv=C3=BD?= Date: Sat, 14 Jun 2025 15:07:46 +0200 Subject: [PATCH 30/36] add error structs. --- errors.go | 44 +++++++++ mapstructure.go | 228 ++++++++++++++++++++++++++++++++----------- mapstructure_test.go | 14 ++- 3 files changed, 224 insertions(+), 62 deletions(-) create mode 100644 errors.go diff --git a/errors.go b/errors.go new file mode 100644 index 00000000..46e39c06 --- /dev/null +++ b/errors.go @@ -0,0 +1,44 @@ +package mapstructure + +import ( + "fmt" + "reflect" +) + +// ErrCannotDecode is a generic error type that holds information about +// a decoding error together with the name of the field that caused the error. +type ErrCannotDecode struct { + Name string + Err error +} + +func (e *ErrCannotDecode) Error() string { + return fmt.Sprintf("'%s': %s", e.Name, e.Err) +} + +// ErrCannotParse extends ErrCannotDecode to include additional information +// about the expected type and the actual value that could not be parsed. +type ErrCannotParse struct { + ErrCannotDecode + Expected reflect.Value + Value interface{} +} + +func (e *ErrCannotParse) Error() string { + return fmt.Sprintf("'%s' cannot parse '%s' as '%s': %s", + e.Name, e.Value, e.Expected.Type(), e.Err) +} + +// ErrUnconvertibleType is an error type that indicates a value could not be +// converted to the expected type. It includes the name of the field, the +// expected type, and the actual value that was attempted to be converted. +type ErrUnconvertibleType struct { + Name string + Expected reflect.Value + Value interface{} +} + +func (e *ErrUnconvertibleType) Error() string { + return fmt.Sprintf("'%s' expected type '%s', got unconvertible type '%s', value: '%v'", + e.Name, e.Expected.Type(), reflect.TypeOf(e.Value), e.Value) +} diff --git a/mapstructure.go b/mapstructure.go index 60311d6b..29e8b2d2 100644 --- a/mapstructure.go +++ b/mapstructure.go @@ -525,7 +525,10 @@ func (d *Decoder) decode(name string, input interface{}, outVal reflect.Value) e var err error input, err = d.cachedDecodeHook(inputVal, outVal) if err != nil { - return fmt.Errorf("error decoding '%s': %w", name, err) + return &ErrCannotDecode{ + Name: name, + Err: err, + } } } if isNil(input) { @@ -563,7 +566,10 @@ func (d *Decoder) decode(name string, input interface{}, outVal reflect.Value) e err = d.decodeFunc(name, input, outVal) default: // If we reached this point then we weren't able to decode it - return fmt.Errorf("%s: unsupported type: %s", name, outputKind) + return &ErrCannotDecode{ + Name: name, + Err: fmt.Errorf("unsupported type: %s", outputKind), + } } // If we reached here, then we successfully decoded SOMETHING, so @@ -624,9 +630,11 @@ func (d *Decoder) decodeBasic(name string, data interface{}, val reflect.Value) dataValType := dataVal.Type() if !dataValType.AssignableTo(val.Type()) { - return fmt.Errorf( - "'%s' expected type '%s', got '%s'", - name, val.Type(), dataValType) + return &ErrUnconvertibleType{ + Name: name, + Expected: val, + Value: data, + } } val.Set(dataVal) @@ -677,9 +685,11 @@ func (d *Decoder) decodeString(name string, data interface{}, val reflect.Value) } if !converted { - return fmt.Errorf( - "'%s' expected type '%s', got unconvertible type '%s', value: '%v'", - name, val.Type(), dataVal.Type(), data) + return &ErrUnconvertibleType{ + Name: name, + Expected: val, + Value: data, + } } return nil @@ -713,20 +723,35 @@ func (d *Decoder) decodeInt(name string, data interface{}, val reflect.Value) er if err == nil { val.SetInt(i) } else { - return fmt.Errorf("cannot parse '%s' as int: %s", name, err) + return &ErrCannotParse{ + ErrCannotDecode: ErrCannotDecode{ + Name: name, + Err: err, + }, + Expected: val, + Value: data, + } } case dataType.PkgPath() == "encoding/json" && dataType.Name() == "Number": jn := data.(json.Number) i, err := jn.Int64() if err != nil { - return fmt.Errorf( - "error decoding json.Number into %s: %s", name, err) + return &ErrCannotParse{ + ErrCannotDecode: ErrCannotDecode{ + Name: name, + Err: err, + }, + Expected: val, + Value: data, + } } val.SetInt(i) default: - return fmt.Errorf( - "'%s' expected type '%s', got unconvertible type '%s', value: '%v'", - name, val.Type(), dataVal.Type(), data) + return &ErrUnconvertibleType{ + Name: name, + Expected: val, + Value: data, + } } return nil @@ -741,8 +766,14 @@ func (d *Decoder) decodeUint(name string, data interface{}, val reflect.Value) e case dataKind == reflect.Int: i := dataVal.Int() if i < 0 && !d.config.WeaklyTypedInput { - return fmt.Errorf("cannot parse '%s', %d overflows uint", - name, i) + return &ErrCannotParse{ + ErrCannotDecode: ErrCannotDecode{ + Name: name, + Err: fmt.Errorf("%d overflows uint", i), + }, + Expected: val, + Value: data, + } } val.SetUint(uint64(i)) case dataKind == reflect.Uint: @@ -750,8 +781,14 @@ func (d *Decoder) decodeUint(name string, data interface{}, val reflect.Value) e case dataKind == reflect.Float32: f := dataVal.Float() if f < 0 && !d.config.WeaklyTypedInput { - return fmt.Errorf("cannot parse '%s', %f overflows uint", - name, f) + return &ErrCannotParse{ + ErrCannotDecode: ErrCannotDecode{ + Name: name, + Err: fmt.Errorf("%f overflows uint", f), + }, + Expected: val, + Value: data, + } } val.SetUint(uint64(f)) case dataKind == reflect.Bool && d.config.WeaklyTypedInput: @@ -770,20 +807,35 @@ func (d *Decoder) decodeUint(name string, data interface{}, val reflect.Value) e if err == nil { val.SetUint(i) } else { - return fmt.Errorf("cannot parse '%s' as uint: %s", name, err) + return &ErrCannotParse{ + ErrCannotDecode: ErrCannotDecode{ + Name: name, + Err: err, + }, + Expected: val, + Value: data, + } } case dataType.PkgPath() == "encoding/json" && dataType.Name() == "Number": jn := data.(json.Number) i, err := strconv.ParseUint(string(jn), 0, 64) if err != nil { - return fmt.Errorf( - "error decoding json.Number into %s: %s", name, err) + return &ErrCannotParse{ + ErrCannotDecode: ErrCannotDecode{ + Name: name, + Err: err, + }, + Expected: val, + Value: data, + } } val.SetUint(i) default: - return fmt.Errorf( - "'%s' expected type '%s', got unconvertible type '%s', value: '%v'", - name, val.Type(), dataVal.Type(), data) + return &ErrUnconvertibleType{ + Name: name, + Expected: val, + Value: data, + } } return nil @@ -809,12 +861,21 @@ func (d *Decoder) decodeBool(name string, data interface{}, val reflect.Value) e } else if dataVal.String() == "" { val.SetBool(false) } else { - return fmt.Errorf("cannot parse '%s' as bool: %s", name, err) + return &ErrCannotParse{ + ErrCannotDecode: ErrCannotDecode{ + Name: name, + Err: err, + }, + Expected: val, + Value: data, + } } default: - return fmt.Errorf( - "'%s' expected type '%s', got unconvertible type '%#v', value: '%#v'", - name, val, dataVal, data) + return &ErrUnconvertibleType{ + Name: name, + Expected: val, + Value: data, + } } return nil @@ -848,20 +909,35 @@ func (d *Decoder) decodeFloat(name string, data interface{}, val reflect.Value) if err == nil { val.SetFloat(f) } else { - return fmt.Errorf("cannot parse '%s' as float: %s", name, err) + return &ErrCannotParse{ + ErrCannotDecode: ErrCannotDecode{ + Name: name, + Err: err, + }, + Expected: val, + Value: data, + } } case dataType.PkgPath() == "encoding/json" && dataType.Name() == "Number": jn := data.(json.Number) i, err := jn.Float64() if err != nil { - return fmt.Errorf( - "error decoding json.Number into %s: %s", name, err) + return &ErrCannotParse{ + ErrCannotDecode: ErrCannotDecode{ + Name: name, + Err: err, + }, + Expected: val, + Value: data, + } } val.SetFloat(i) default: - return fmt.Errorf( - "'%s' expected type '%s', got unconvertible type '%s', value: '%v'", - name, val.Type(), dataVal.Type(), data) + return &ErrUnconvertibleType{ + Name: name, + Expected: val, + Value: data, + } } return nil @@ -875,9 +951,11 @@ func (d *Decoder) decodeComplex(name string, data interface{}, val reflect.Value case dataKind == reflect.Complex64: val.SetComplex(dataVal.Complex()) default: - return fmt.Errorf( - "'%s' expected type '%s', got unconvertible type '%s', value: '%v'", - name, val.Type(), dataVal.Type(), data) + return &ErrUnconvertibleType{ + Name: name, + Expected: val, + Value: data, + } } return nil @@ -921,7 +999,11 @@ func (d *Decoder) decodeMap(name string, data interface{}, val reflect.Value) er fallthrough default: - return fmt.Errorf("'%s' expected a map, got '%s'", name, dataVal.Kind()) + return &ErrUnconvertibleType{ + Name: name, + Expected: val, + Value: data, + } } } @@ -1007,7 +1089,10 @@ func (d *Decoder) decodeMapFromStruct(name string, dataVal reflect.Value, val re // to the map value. v := dataVal.Field(i) if !v.Type().AssignableTo(valMap.Type().Elem()) { - return fmt.Errorf("cannot assign type '%s' to map value field of type '%s'", v.Type(), valMap.Type().Elem()) + return &ErrCannotDecode{ + Name: name + "." + f.Name, + Err: fmt.Errorf("cannot assign type %q to map value field of type %q", v.Type(), valMap.Type().Elem()), + } } tagValue := f.Tag.Get(d.config.TagName) @@ -1047,12 +1132,18 @@ func (d *Decoder) decodeMapFromStruct(name string, dataVal reflect.Value, val re // The final type must be a struct if v.Kind() != reflect.Struct { - return fmt.Errorf("cannot squash non-struct type '%s'", v.Type()) + return &ErrCannotDecode{ + Name: name + "." + f.Name, + Err: fmt.Errorf("cannot squash non-struct type %q", v.Type()), + } } } else { if strings.Index(tagValue[index+1:], "remain") != -1 { if v.Kind() != reflect.Map { - return fmt.Errorf("error remain-tag field with invalid type: '%s'", v.Type()) + return &ErrCannotDecode{ + Name: name + "." + f.Name, + Err: fmt.Errorf("error remain-tag field with invalid type: %q", v.Type()), + } } ptr := v.MapRange() @@ -1172,9 +1263,11 @@ func (d *Decoder) decodeFunc(name string, data interface{}, val reflect.Value) e // into that. Then set the value of the pointer to this type. dataVal := reflect.Indirect(reflect.ValueOf(data)) if val.Type() != dataVal.Type() { - return fmt.Errorf( - "'%s' expected type '%s', got unconvertible type '%s', value: '%v'", - name, val.Type(), dataVal.Type(), data) + return &ErrUnconvertibleType{ + Name: name, + Expected: val, + Value: data, + } } val.Set(dataVal) return nil @@ -1215,8 +1308,10 @@ func (d *Decoder) decodeSlice(name string, data interface{}, val reflect.Value) } } - return fmt.Errorf( - "'%s': source data must be an array or slice, got %s", name, dataValKind) + return &ErrCannotDecode{ + Name: name, + Err: fmt.Errorf("source data must be an array or slice, got %s", dataValKind), + } } // If the input value is nil, then don't allocate since empty != nil @@ -1283,13 +1378,17 @@ func (d *Decoder) decodeArray(name string, data interface{}, val reflect.Value) } } - return fmt.Errorf( - "'%s': source data must be an array or slice, got %s", name, dataValKind) + return &ErrCannotDecode{ + Name: name, + Err: fmt.Errorf("source data must be an array or slice, got %s", dataValKind), + } } if dataVal.Len() > arrayType.Len() { - return fmt.Errorf( - "'%s': expected source data to have length less or equal to %d, got %d", name, arrayType.Len(), dataVal.Len()) + return &ErrCannotDecode{ + Name: name, + Err: fmt.Errorf("expected source data to have length less or equal to %d, got %d", arrayType.Len(), dataVal.Len()), + } } // Make a new array to hold our result, same size as the original data. @@ -1354,16 +1453,20 @@ func (d *Decoder) decodeStruct(name string, data interface{}, val reflect.Value) return result default: - return fmt.Errorf("'%s' expected a map, got '%s'", name, dataVal.Kind()) + return &ErrCannotDecode{ + Name: name, + Err: fmt.Errorf("expected a map or struct, got %q", dataValKind), + } } } func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) error { dataValType := dataVal.Type() if kind := dataValType.Key().Kind(); kind != reflect.String && kind != reflect.Interface { - return fmt.Errorf( - "'%s' needs a map with string keys, has '%s' keys", - name, dataValType.Key().Kind()) + return &ErrCannotDecode{ + Name: name, + Err: fmt.Errorf("needs a map with string keys, has %q keys", kind), + } } dataValKeys := make(map[reflect.Value]struct{}) @@ -1436,7 +1539,10 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e structs = append(structs, fieldVal.Elem().Elem()) } default: - errs = append(errs, fmt.Errorf("%s: unsupported type for squash: %s", fieldType.Name, fieldVal.Kind())) + errs = append(errs, &ErrCannotDecode{ + Name: name + "." + fieldType.Name, + Err: fmt.Errorf("unsupported type for squash: %s", fieldVal.Kind()), + }) } continue } @@ -1543,8 +1649,10 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e } sort.Strings(keys) - err := fmt.Errorf("'%s' has invalid keys: %s", name, strings.Join(keys, ", ")) - errs = append(errs, err) + errs = append(errs, &ErrCannotDecode{ + Name: name, + Err: fmt.Errorf("has invalid keys: %s", strings.Join(keys, ", ")), + }) } if d.config.ErrorUnset && len(targetValKeysUnused) > 0 { @@ -1554,8 +1662,10 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e } sort.Strings(keys) - err := fmt.Errorf("'%s' has unset fields: %s", name, strings.Join(keys, ", ")) - errs = append(errs, err) + errs = append(errs, &ErrCannotDecode{ + Name: name, + Err: fmt.Errorf("has unset fields: %s", strings.Join(keys, ", ")), + }) } if err := errors.Join(errs...); err != nil { diff --git a/mapstructure_test.go b/mapstructure_test.go index 248afaa8..ae0a67e6 100644 --- a/mapstructure_test.go +++ b/mapstructure_test.go @@ -2604,8 +2604,11 @@ func TestInvalidType(t *testing.T) { errs := derr.Unwrap() - if errs[0].Error() != "'Vstring' expected type 'string', got unconvertible type 'int', value: '42'" { + var unconvertibleErr *ErrUnconvertibleType + if !errors.As(errs[0], &unconvertibleErr) { t.Errorf("got unexpected error: %s", err) + } else if unconvertibleErr.Expected.Type() != reflect.TypeOf("") { + t.Errorf("expected type should be string, got: %s", unconvertibleErr.Expected) } inputNegIntUint := map[string]interface{}{ @@ -2623,8 +2626,11 @@ func TestInvalidType(t *testing.T) { errs = derr.Unwrap() - if errs[0].Error() != "cannot parse 'Vuint', -42 overflows uint" { + var parseErr *ErrCannotParse + if !errors.As(errs[0], &parseErr) { t.Errorf("got unexpected error: %s", err) + } else if parseErr.Expected.Type() != reflect.TypeOf(uint(0)) { + t.Errorf("expected type should be uint, got: %s", parseErr.Expected) } inputNegFloatUint := map[string]interface{}{ @@ -2642,8 +2648,10 @@ func TestInvalidType(t *testing.T) { errs = derr.Unwrap() - if errs[0].Error() != "cannot parse 'Vuint', -42.000000 overflows uint" { + if !errors.As(errs[0], &parseErr) { t.Errorf("got unexpected error: %s", err) + } else if parseErr.Expected.Type() != reflect.TypeOf(uint(0)) { + t.Errorf("expected type should be uint, got: %s", parseErr.Expected) } } From b82cfe5c8e588a6eb9e52694753bc1f043a86487 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0ediv=C3=BD?= Date: Sat, 14 Jun 2025 15:43:00 +0200 Subject: [PATCH 31/36] use DecodeError as internal interface for all errors. --- errors.go | 55 ++++++----- mapstructure.go | 214 ++++++++++++++++--------------------------- mapstructure_test.go | 19 ++-- 3 files changed, 123 insertions(+), 165 deletions(-) diff --git a/errors.go b/errors.go index 46e39c06..40b78d22 100644 --- a/errors.go +++ b/errors.go @@ -5,40 +5,53 @@ import ( "reflect" ) -// ErrCannotDecode is a generic error type that holds information about +// DecodeError is a generic error type that holds information about // a decoding error together with the name of the field that caused the error. -type ErrCannotDecode struct { - Name string - Err error +type DecodeError struct { + name string + err error } -func (e *ErrCannotDecode) Error() string { - return fmt.Sprintf("'%s': %s", e.Name, e.Err) +func newDecodeError(name string, err error) *DecodeError { + return &DecodeError{ + name: name, + err: err, + } } -// ErrCannotParse extends ErrCannotDecode to include additional information -// about the expected type and the actual value that could not be parsed. -type ErrCannotParse struct { - ErrCannotDecode +func (e *DecodeError) Name() string { + return e.name +} + +func (e *DecodeError) Unwrap() error { + return e.err +} + +func (e *DecodeError) Error() string { + return fmt.Sprintf("'%s' %s", e.name, e.err) +} + +// ParseError is an error type that indicates a value could not be parsed +// into the expected type. +type ParseError struct { Expected reflect.Value Value interface{} + Err error } -func (e *ErrCannotParse) Error() string { - return fmt.Sprintf("'%s' cannot parse '%s' as '%s': %s", - e.Name, e.Value, e.Expected.Type(), e.Err) +func (e *ParseError) Error() string { + return fmt.Sprintf("cannot parse '%s' as '%s': %s", + e.Value, e.Expected.Type(), e.Err) } -// ErrUnconvertibleType is an error type that indicates a value could not be -// converted to the expected type. It includes the name of the field, the -// expected type, and the actual value that was attempted to be converted. -type ErrUnconvertibleType struct { - Name string +// UnconvertibleTypeError is an error type that indicates a value could not be +// converted to the expected type. +type UnconvertibleTypeError struct { Expected reflect.Value Value interface{} } -func (e *ErrUnconvertibleType) Error() string { - return fmt.Sprintf("'%s' expected type '%s', got unconvertible type '%s', value: '%v'", - e.Name, e.Expected.Type(), reflect.TypeOf(e.Value), e.Value) +func (e *UnconvertibleTypeError) Error() string { + return fmt.Sprintf("expected type '%s', got unconvertible type '%s', value: '%v'", + e.Expected.Type(), reflect.TypeOf(e.Value), e.Value) } diff --git a/mapstructure.go b/mapstructure.go index 29e8b2d2..57e14bf1 100644 --- a/mapstructure.go +++ b/mapstructure.go @@ -525,10 +525,7 @@ func (d *Decoder) decode(name string, input interface{}, outVal reflect.Value) e var err error input, err = d.cachedDecodeHook(inputVal, outVal) if err != nil { - return &ErrCannotDecode{ - Name: name, - Err: err, - } + return newDecodeError(name, err) } } if isNil(input) { @@ -566,10 +563,7 @@ func (d *Decoder) decode(name string, input interface{}, outVal reflect.Value) e err = d.decodeFunc(name, input, outVal) default: // If we reached this point then we weren't able to decode it - return &ErrCannotDecode{ - Name: name, - Err: fmt.Errorf("unsupported type: %s", outputKind), - } + return newDecodeError(name, fmt.Errorf("unsupported type: %s", outputKind)) } // If we reached here, then we successfully decoded SOMETHING, so @@ -630,11 +624,10 @@ func (d *Decoder) decodeBasic(name string, data interface{}, val reflect.Value) dataValType := dataVal.Type() if !dataValType.AssignableTo(val.Type()) { - return &ErrUnconvertibleType{ - Name: name, + return newDecodeError(name, &UnconvertibleTypeError{ Expected: val, Value: data, - } + }) } val.Set(dataVal) @@ -685,11 +678,10 @@ func (d *Decoder) decodeString(name string, data interface{}, val reflect.Value) } if !converted { - return &ErrUnconvertibleType{ - Name: name, + return newDecodeError(name, &UnconvertibleTypeError{ Expected: val, Value: data, - } + }) } return nil @@ -723,35 +715,28 @@ func (d *Decoder) decodeInt(name string, data interface{}, val reflect.Value) er if err == nil { val.SetInt(i) } else { - return &ErrCannotParse{ - ErrCannotDecode: ErrCannotDecode{ - Name: name, - Err: err, - }, + return newDecodeError(name, &ParseError{ Expected: val, Value: data, - } + Err: err, + }) } case dataType.PkgPath() == "encoding/json" && dataType.Name() == "Number": jn := data.(json.Number) i, err := jn.Int64() if err != nil { - return &ErrCannotParse{ - ErrCannotDecode: ErrCannotDecode{ - Name: name, - Err: err, - }, + return newDecodeError(name, &ParseError{ Expected: val, Value: data, - } + Err: err, + }) } val.SetInt(i) default: - return &ErrUnconvertibleType{ - Name: name, + return newDecodeError(name, &UnconvertibleTypeError{ Expected: val, Value: data, - } + }) } return nil @@ -766,14 +751,11 @@ func (d *Decoder) decodeUint(name string, data interface{}, val reflect.Value) e case dataKind == reflect.Int: i := dataVal.Int() if i < 0 && !d.config.WeaklyTypedInput { - return &ErrCannotParse{ - ErrCannotDecode: ErrCannotDecode{ - Name: name, - Err: fmt.Errorf("%d overflows uint", i), - }, + return newDecodeError(name, &ParseError{ Expected: val, Value: data, - } + Err: fmt.Errorf("%d overflows uint", i), + }) } val.SetUint(uint64(i)) case dataKind == reflect.Uint: @@ -781,14 +763,11 @@ func (d *Decoder) decodeUint(name string, data interface{}, val reflect.Value) e case dataKind == reflect.Float32: f := dataVal.Float() if f < 0 && !d.config.WeaklyTypedInput { - return &ErrCannotParse{ - ErrCannotDecode: ErrCannotDecode{ - Name: name, - Err: fmt.Errorf("%f overflows uint", f), - }, + return newDecodeError(name, &ParseError{ Expected: val, Value: data, - } + Err: fmt.Errorf("%f overflows uint", f), + }) } val.SetUint(uint64(f)) case dataKind == reflect.Bool && d.config.WeaklyTypedInput: @@ -807,35 +786,28 @@ func (d *Decoder) decodeUint(name string, data interface{}, val reflect.Value) e if err == nil { val.SetUint(i) } else { - return &ErrCannotParse{ - ErrCannotDecode: ErrCannotDecode{ - Name: name, - Err: err, - }, + return newDecodeError(name, &ParseError{ Expected: val, Value: data, - } + Err: err, + }) } case dataType.PkgPath() == "encoding/json" && dataType.Name() == "Number": jn := data.(json.Number) i, err := strconv.ParseUint(string(jn), 0, 64) if err != nil { - return &ErrCannotParse{ - ErrCannotDecode: ErrCannotDecode{ - Name: name, - Err: err, - }, + return newDecodeError(name, &ParseError{ Expected: val, Value: data, - } + Err: err, + }) } val.SetUint(i) default: - return &ErrUnconvertibleType{ - Name: name, + return newDecodeError(name, &UnconvertibleTypeError{ Expected: val, Value: data, - } + }) } return nil @@ -861,21 +833,17 @@ func (d *Decoder) decodeBool(name string, data interface{}, val reflect.Value) e } else if dataVal.String() == "" { val.SetBool(false) } else { - return &ErrCannotParse{ - ErrCannotDecode: ErrCannotDecode{ - Name: name, - Err: err, - }, + return newDecodeError(name, &ParseError{ Expected: val, Value: data, - } + Err: err, + }) } default: - return &ErrUnconvertibleType{ - Name: name, + return newDecodeError(name, &UnconvertibleTypeError{ Expected: val, Value: data, - } + }) } return nil @@ -909,35 +877,28 @@ func (d *Decoder) decodeFloat(name string, data interface{}, val reflect.Value) if err == nil { val.SetFloat(f) } else { - return &ErrCannotParse{ - ErrCannotDecode: ErrCannotDecode{ - Name: name, - Err: err, - }, + return newDecodeError(name, &ParseError{ Expected: val, Value: data, - } + Err: err, + }) } case dataType.PkgPath() == "encoding/json" && dataType.Name() == "Number": jn := data.(json.Number) i, err := jn.Float64() if err != nil { - return &ErrCannotParse{ - ErrCannotDecode: ErrCannotDecode{ - Name: name, - Err: err, - }, + return newDecodeError(name, &ParseError{ Expected: val, Value: data, - } + Err: err, + }) } val.SetFloat(i) default: - return &ErrUnconvertibleType{ - Name: name, + return newDecodeError(name, &UnconvertibleTypeError{ Expected: val, Value: data, - } + }) } return nil @@ -951,11 +912,10 @@ func (d *Decoder) decodeComplex(name string, data interface{}, val reflect.Value case dataKind == reflect.Complex64: val.SetComplex(dataVal.Complex()) default: - return &ErrUnconvertibleType{ - Name: name, + return newDecodeError(name, &UnconvertibleTypeError{ Expected: val, Value: data, - } + }) } return nil @@ -999,11 +959,10 @@ func (d *Decoder) decodeMap(name string, data interface{}, val reflect.Value) er fallthrough default: - return &ErrUnconvertibleType{ - Name: name, + return newDecodeError(name, &UnconvertibleTypeError{ Expected: val, Value: data, - } + }) } } @@ -1089,10 +1048,10 @@ func (d *Decoder) decodeMapFromStruct(name string, dataVal reflect.Value, val re // to the map value. v := dataVal.Field(i) if !v.Type().AssignableTo(valMap.Type().Elem()) { - return &ErrCannotDecode{ - Name: name + "." + f.Name, - Err: fmt.Errorf("cannot assign type %q to map value field of type %q", v.Type(), valMap.Type().Elem()), - } + return newDecodeError( + name+"."+f.Name, + fmt.Errorf("cannot assign type %q to map value field of type %q", v.Type(), valMap.Type().Elem()), + ) } tagValue := f.Tag.Get(d.config.TagName) @@ -1132,18 +1091,18 @@ func (d *Decoder) decodeMapFromStruct(name string, dataVal reflect.Value, val re // The final type must be a struct if v.Kind() != reflect.Struct { - return &ErrCannotDecode{ - Name: name + "." + f.Name, - Err: fmt.Errorf("cannot squash non-struct type %q", v.Type()), - } + return newDecodeError( + name+"."+f.Name, + fmt.Errorf("cannot squash non-struct type %q", v.Type()), + ) } } else { if strings.Index(tagValue[index+1:], "remain") != -1 { if v.Kind() != reflect.Map { - return &ErrCannotDecode{ - Name: name + "." + f.Name, - Err: fmt.Errorf("error remain-tag field with invalid type: %q", v.Type()), - } + return newDecodeError( + name+"."+f.Name, + fmt.Errorf("error remain-tag field with invalid type: %q", v.Type()), + ) } ptr := v.MapRange() @@ -1263,11 +1222,10 @@ func (d *Decoder) decodeFunc(name string, data interface{}, val reflect.Value) e // into that. Then set the value of the pointer to this type. dataVal := reflect.Indirect(reflect.ValueOf(data)) if val.Type() != dataVal.Type() { - return &ErrUnconvertibleType{ - Name: name, + return newDecodeError(name, &UnconvertibleTypeError{ Expected: val, Value: data, - } + }) } val.Set(dataVal) return nil @@ -1308,10 +1266,8 @@ func (d *Decoder) decodeSlice(name string, data interface{}, val reflect.Value) } } - return &ErrCannotDecode{ - Name: name, - Err: fmt.Errorf("source data must be an array or slice, got %s", dataValKind), - } + return newDecodeError(name, + fmt.Errorf("source data must be an array or slice, got %s", dataValKind)) } // If the input value is nil, then don't allocate since empty != nil @@ -1378,17 +1334,13 @@ func (d *Decoder) decodeArray(name string, data interface{}, val reflect.Value) } } - return &ErrCannotDecode{ - Name: name, - Err: fmt.Errorf("source data must be an array or slice, got %s", dataValKind), - } + return newDecodeError(name, + fmt.Errorf("source data must be an array or slice, got %s", dataValKind)) } if dataVal.Len() > arrayType.Len() { - return &ErrCannotDecode{ - Name: name, - Err: fmt.Errorf("expected source data to have length less or equal to %d, got %d", arrayType.Len(), dataVal.Len()), - } + return newDecodeError(name, + fmt.Errorf("expected source data to have length less or equal to %d, got %d", arrayType.Len(), dataVal.Len())) } // Make a new array to hold our result, same size as the original data. @@ -1453,20 +1405,16 @@ func (d *Decoder) decodeStruct(name string, data interface{}, val reflect.Value) return result default: - return &ErrCannotDecode{ - Name: name, - Err: fmt.Errorf("expected a map or struct, got %q", dataValKind), - } + return newDecodeError(name, + fmt.Errorf("expected a map or struct, got %q", dataValKind)) } } func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) error { dataValType := dataVal.Type() if kind := dataValType.Key().Kind(); kind != reflect.String && kind != reflect.Interface { - return &ErrCannotDecode{ - Name: name, - Err: fmt.Errorf("needs a map with string keys, has %q keys", kind), - } + return newDecodeError(name, + fmt.Errorf("needs a map with string keys, has %q keys", kind)) } dataValKeys := make(map[reflect.Value]struct{}) @@ -1539,10 +1487,10 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e structs = append(structs, fieldVal.Elem().Elem()) } default: - errs = append(errs, &ErrCannotDecode{ - Name: name + "." + fieldType.Name, - Err: fmt.Errorf("unsupported type for squash: %s", fieldVal.Kind()), - }) + errs = append(errs, newDecodeError( + name+"."+fieldType.Name, + fmt.Errorf("unsupported type for squash: %s", fieldVal.Kind()), + )) } continue } @@ -1649,10 +1597,10 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e } sort.Strings(keys) - errs = append(errs, &ErrCannotDecode{ - Name: name, - Err: fmt.Errorf("has invalid keys: %s", strings.Join(keys, ", ")), - }) + errs = append(errs, newDecodeError( + name, + fmt.Errorf("has invalid keys: %s", strings.Join(keys, ", ")), + )) } if d.config.ErrorUnset && len(targetValKeysUnused) > 0 { @@ -1662,10 +1610,10 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e } sort.Strings(keys) - errs = append(errs, &ErrCannotDecode{ - Name: name, - Err: fmt.Errorf("has unset fields: %s", strings.Join(keys, ", ")), - }) + errs = append(errs, newDecodeError( + name, + fmt.Errorf("has unset fields: %s", strings.Join(keys, ", ")), + )) } if err := errors.Join(errs...); err != nil { diff --git a/mapstructure_test.go b/mapstructure_test.go index ae0a67e6..8239202d 100644 --- a/mapstructure_test.go +++ b/mapstructure_test.go @@ -2604,11 +2604,11 @@ func TestInvalidType(t *testing.T) { errs := derr.Unwrap() - var unconvertibleErr *ErrUnconvertibleType - if !errors.As(errs[0], &unconvertibleErr) { + var decoderErr *DecodeError + if !errors.As(errs[0], &decoderErr) { t.Errorf("got unexpected error: %s", err) - } else if unconvertibleErr.Expected.Type() != reflect.TypeOf("") { - t.Errorf("expected type should be string, got: %s", unconvertibleErr.Expected) + } else if errors.Is(decoderErr.Unwrap(), &UnconvertibleTypeError{}) { + t.Errorf("error should be UnconvertibleTypeError, got: %s", decoderErr.Unwrap()) } inputNegIntUint := map[string]interface{}{ @@ -2626,11 +2626,10 @@ func TestInvalidType(t *testing.T) { errs = derr.Unwrap() - var parseErr *ErrCannotParse - if !errors.As(errs[0], &parseErr) { + if !errors.As(errs[0], &decoderErr) { t.Errorf("got unexpected error: %s", err) - } else if parseErr.Expected.Type() != reflect.TypeOf(uint(0)) { - t.Errorf("expected type should be uint, got: %s", parseErr.Expected) + } else if errors.Is(decoderErr.Unwrap(), &ParseError{}) { + t.Errorf("error should be ParseError, got: %s", decoderErr.Unwrap()) } inputNegFloatUint := map[string]interface{}{ @@ -2648,10 +2647,8 @@ func TestInvalidType(t *testing.T) { errs = derr.Unwrap() - if !errors.As(errs[0], &parseErr) { + if !errors.As(errs[0], &decoderErr) { t.Errorf("got unexpected error: %s", err) - } else if parseErr.Expected.Type() != reflect.TypeOf(uint(0)) { - t.Errorf("expected type should be uint, got: %s", parseErr.Expected) } } From 9661f6d07c319da00ae0508d99df5f3f0c3953bd Mon Sep 17 00:00:00 2001 From: Mark Sagi-Kazar Date: Mon, 16 Jun 2025 15:23:02 +0200 Subject: [PATCH 32/36] feat: add common error interface Signed-off-by: Mark Sagi-Kazar --- errors.go | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/errors.go b/errors.go index 40b78d22..379fb8e8 100644 --- a/errors.go +++ b/errors.go @@ -5,6 +5,17 @@ import ( "reflect" ) +// Error interface is implemented by all errors emitted by mapstructure. +// +// Errors from underlying libraries MAY not be wrapped. +// +// Use [errors.As] to check if an error implements this interface. +type Error interface { + error + + mapstructure() +} + // DecodeError is a generic error type that holds information about // a decoding error together with the name of the field that caused the error. type DecodeError struct { @@ -28,30 +39,39 @@ func (e *DecodeError) Unwrap() error { } func (e *DecodeError) Error() string { - return fmt.Sprintf("'%s' %s", e.name, e.err) + return fmt.Sprintf("decoding '%s': %s", e.name, e.err) } +func (*DecodeError) mapstructure() {} + // ParseError is an error type that indicates a value could not be parsed // into the expected type. type ParseError struct { Expected reflect.Value - Value interface{} + Value any Err error } func (e *ParseError) Error() string { - return fmt.Sprintf("cannot parse '%s' as '%s': %s", - e.Value, e.Expected.Type(), e.Err) + return fmt.Sprintf("cannot parse '%s' as '%s': %s", e.Value, e.Expected.Type(), e.Err) } +func (*ParseError) mapstructure() {} + // UnconvertibleTypeError is an error type that indicates a value could not be // converted to the expected type. type UnconvertibleTypeError struct { Expected reflect.Value - Value interface{} + Value any } func (e *UnconvertibleTypeError) Error() string { - return fmt.Sprintf("expected type '%s', got unconvertible type '%s', value: '%v'", - e.Expected.Type(), reflect.TypeOf(e.Value), e.Value) + return fmt.Sprintf( + "expected type '%s', got unconvertible type '%s', value: '%v'", + e.Expected.Type(), + reflect.TypeOf(e.Value), + e.Value, + ) } + +func (*UnconvertibleTypeError) mapstructure() {} From a3f8b227dcdae324c070d389152837f0aa635f4b Mon Sep 17 00:00:00 2001 From: Mark Sagi-Kazar Date: Mon, 16 Jun 2025 15:31:17 +0200 Subject: [PATCH 33/36] revert: error message change Signed-off-by: Mark Sagi-Kazar --- errors.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/errors.go b/errors.go index 379fb8e8..beda2542 100644 --- a/errors.go +++ b/errors.go @@ -39,7 +39,7 @@ func (e *DecodeError) Unwrap() error { } func (e *DecodeError) Error() string { - return fmt.Sprintf("decoding '%s': %s", e.name, e.err) + return fmt.Sprintf("'%s' %s", e.name, e.err) } func (*DecodeError) mapstructure() {} From ed3f92181528ff776a0324107b8b55026e93766a Mon Sep 17 00:00:00 2001 From: Mark Sagi-Kazar Date: Mon, 16 Jun 2025 15:33:44 +0200 Subject: [PATCH 34/36] feat: remove value from error messages Signed-off-by: Mark Sagi-Kazar --- errors.go | 5 ++--- mapstructure_examples_test.go | 10 +++++----- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/errors.go b/errors.go index beda2542..7570d8ad 100644 --- a/errors.go +++ b/errors.go @@ -53,7 +53,7 @@ type ParseError struct { } func (e *ParseError) Error() string { - return fmt.Sprintf("cannot parse '%s' as '%s': %s", e.Value, e.Expected.Type(), e.Err) + return fmt.Sprintf("cannot parse value as '%s': %s", e.Expected.Type(), e.Err) } func (*ParseError) mapstructure() {} @@ -67,10 +67,9 @@ type UnconvertibleTypeError struct { func (e *UnconvertibleTypeError) Error() string { return fmt.Sprintf( - "expected type '%s', got unconvertible type '%s', value: '%v'", + "expected type '%s', got unconvertible type '%s'", e.Expected.Type(), reflect.TypeOf(e.Value), - e.Value, ) } diff --git a/mapstructure_examples_test.go b/mapstructure_examples_test.go index a8735d4d..f6bb04dd 100644 --- a/mapstructure_examples_test.go +++ b/mapstructure_examples_test.go @@ -65,11 +65,11 @@ func ExampleDecode_errors() { // Output: // decoding failed due to the following error(s): // - // 'Name' expected type 'string', got unconvertible type 'int', value: '123' - // 'Age' expected type 'int', got unconvertible type 'string', value: 'bad value' - // 'Emails[0]' expected type 'string', got unconvertible type 'int', value: '1' - // 'Emails[1]' expected type 'string', got unconvertible type 'int', value: '2' - // 'Emails[2]' expected type 'string', got unconvertible type 'int', value: '3' + // 'Name' expected type 'string', got unconvertible type 'int' + // 'Age' expected type 'int', got unconvertible type 'string' + // 'Emails[0]' expected type 'string', got unconvertible type 'int' + // 'Emails[1]' expected type 'string', got unconvertible type 'int' + // 'Emails[2]' expected type 'string', got unconvertible type 'int' } func ExampleDecode_metadata() { From 6a283a390ee7bc0f9331f58199db234902e0739f Mon Sep 17 00:00:00 2001 From: Mark Sagi-Kazar Date: Mon, 16 Jun 2025 15:43:07 +0200 Subject: [PATCH 35/36] chore: update error type doc Signed-off-by: Mark Sagi-Kazar --- errors.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/errors.go b/errors.go index 7570d8ad..31a3edfb 100644 --- a/errors.go +++ b/errors.go @@ -7,8 +7,6 @@ import ( // Error interface is implemented by all errors emitted by mapstructure. // -// Errors from underlying libraries MAY not be wrapped. -// // Use [errors.As] to check if an error implements this interface. type Error interface { error From 5f34b05aa12639380ef7c2af69eb6f8fd629dbd0 Mon Sep 17 00:00:00 2001 From: Mark Sagi-Kazar Date: Mon, 16 Jun 2025 15:47:27 +0200 Subject: [PATCH 36/36] update linter Signed-off-by: Mark Sagi-Kazar --- .github/workflows/ci.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 985178e9..69e660ee 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -51,6 +51,6 @@ jobs: go-version: "1.24" - name: Lint - uses: golangci/golangci-lint-action@1481404843c368bc19ca9406f87d6e0fc97bdcfd # v7.0.0 + uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8.0.0 with: - version: v2.0.2 + version: v2.1.6