pax_global_header00006660000000000000000000000064151037030550014511gustar00rootroot0000000000000052 comment=5f363c449e4473068438b846c284bb6b701cb46e golang-github-go-viper-mapstructure-2.4.0/000077500000000000000000000000001510370305500205255ustar00rootroot00000000000000golang-github-go-viper-mapstructure-2.4.0/.editorconfig000066400000000000000000000004171510370305500232040ustar00rootroot00000000000000root = true [*] charset = utf-8 end_of_line = lf indent_size = 4 indent_style = space insert_final_newline = true trim_trailing_whitespace = true [*.go] indent_style = tab [{Makefile,*.mk}] indent_style = tab [*.nix] indent_size = 2 [.golangci.yaml] indent_size = 2 golang-github-go-viper-mapstructure-2.4.0/.envrc000066400000000000000000000003471510370305500216470ustar00rootroot00000000000000if ! has nix_direnv_version || ! nix_direnv_version 3.0.4; then source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/3.0.4/direnvrc" "sha256-DzlYZ33mWF/Gs8DDeyjr8mnVmQGx7ASYqA5WlxwvBG4=" fi use flake . --impure golang-github-go-viper-mapstructure-2.4.0/.github/000077500000000000000000000000001510370305500220655ustar00rootroot00000000000000golang-github-go-viper-mapstructure-2.4.0/.github/.editorconfig000066400000000000000000000000411510370305500245350ustar00rootroot00000000000000[{*.yml,*.yaml}] indent_size = 2 golang-github-go-viper-mapstructure-2.4.0/.github/dependabot.yaml000066400000000000000000000003031510370305500250520ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: gomod directory: / schedule: interval: daily - package-ecosystem: github-actions directory: / schedule: interval: daily golang-github-go-viper-mapstructure-2.4.0/.github/workflows/000077500000000000000000000000001510370305500241225ustar00rootroot00000000000000golang-github-go-viper-mapstructure-2.4.0/.github/workflows/analysis-scorecard.yaml000066400000000000000000000022331510370305500305740ustar00rootroot00000000000000name: 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@05b42c624433fc40578a4040d5cf5e36ddca8cde # v2.4.2 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@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2 with: sarif_file: results.sarif golang-github-go-viper-mapstructure-2.4.0/.github/workflows/ci.yaml000066400000000000000000000022631510370305500254040ustar00rootroot00000000000000name: CI on: push: branches: [main] pull_request: jobs: test: name: Test runs-on: ubuntu-latest strategy: matrix: go: [ "1.18", "1.19", "1.20", "1.21", "1.22", "1.23", "1.24", "stable", "oldstable", ] steps: - name: Checkout repository uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up Go uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 with: go-version: ${{ matrix.go }} - name: Test run: go test -race -v -shuffle=on ./... lint: name: Lint runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up Go uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 with: go-version: "1.24" - name: Lint uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8.0.0 with: version: v2.1.6 golang-github-go-viper-mapstructure-2.4.0/.gitignore000066400000000000000000000001011510370305500225050ustar00rootroot00000000000000/.devenv/ /.direnv/ /.pre-commit-config.yaml /bin/ /build/ /var/ golang-github-go-viper-mapstructure-2.4.0/.golangci.yaml000066400000000000000000000014051510370305500232520ustar00rootroot00000000000000version: "2" run: timeout: 10m linters: 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 gofmt: simplify: true rewrite-rules: - pattern: interface{} replacement: any exclusions: paths: - internal/ golang-github-go-viper-mapstructure-2.4.0/CHANGELOG.md000066400000000000000000000062251510370305500223430ustar00rootroot00000000000000> [!WARNING] > As of v2 of this library, change log can be found in GitHub releases. ## 1.5.1 * Wrap errors so they're compatible with `errors.Is` and `errors.As` [GH-282] * Fix map of slices not decoding properly in certain cases. [GH-266] ## 1.5.0 * New option `IgnoreUntaggedFields` to ignore decoding to any fields without `mapstructure` (or the configured tag name) set [GH-277] * New option `ErrorUnset` which makes it an error if any fields in a target struct are not set by the decoding process. [GH-225] * New function `OrComposeDecodeHookFunc` to help compose decode hooks. [GH-240] * Decoding to slice from array no longer crashes [GH-265] * Decode nested struct pointers to map [GH-271] * Fix issue where `,squash` was ignored if `Squash` option was set. [GH-280] * Fix issue where fields with `,omitempty` would sometimes decode into a map with an empty string key [GH-281] ## 1.4.3 * Fix cases where `json.Number` didn't decode properly [GH-261] ## 1.4.2 * Custom name matchers to support any sort of casing, formatting, etc. for field names. [GH-250] * Fix possible panic in ComposeDecodeHookFunc [GH-251] ## 1.4.1 * Fix regression where `*time.Time` value would be set to empty and not be sent to decode hooks properly [GH-232] ## 1.4.0 * A new decode hook type `DecodeHookFuncValue` has been added that has access to the full values. [GH-183] * Squash is now supported with embedded fields that are struct pointers [GH-205] * Empty strings will convert to 0 for all numeric types when weakly decoding [GH-206] ## 1.3.3 * Decoding maps from maps creates a settable value for decode hooks [GH-203] ## 1.3.2 * Decode into interface type with a struct value is supported [GH-187] ## 1.3.1 * Squash should only squash embedded structs. [GH-194] ## 1.3.0 * Added `",omitempty"` support. This will ignore zero values in the source structure when encoding. [GH-145] ## 1.2.3 * Fix duplicate entries in Keys list with pointer values. [GH-185] ## 1.2.2 * Do not add unsettable (unexported) values to the unused metadata key or "remain" value. [GH-150] ## 1.2.1 * Go modules checksum mismatch fix ## 1.2.0 * Added support to capture unused values in a field using the `",remain"` value in the mapstructure tag. There is an example to showcase usage. * Added `DecoderConfig` option to always squash embedded structs * `json.Number` can decode into `uint` types * Empty slices are preserved and not replaced with nil slices * Fix panic that can occur in when decoding a map into a nil slice of structs * Improved package documentation for godoc ## 1.1.2 * Fix error when decode hook decodes interface implementation into interface type. [GH-140] ## 1.1.1 * Fix panic that can happen in `decodePtr` ## 1.1.0 * Added `StringToIPHookFunc` to convert `string` to `net.IP` and `net.IPNet` [GH-133] * Support struct to struct decoding [GH-137] * If source map value is nil, then destination map value is nil (instead of empty) * If source slice value is nil, then destination slice value is nil (instead of empty) * If source pointer is nil, then destination pointer is set to nil (instead of allocated zero value of type) ## 1.0.0 * Initial tagged stable release. golang-github-go-viper-mapstructure-2.4.0/LICENSE000066400000000000000000000020751510370305500215360ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2013 Mitchell Hashimoto Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. golang-github-go-viper-mapstructure-2.4.0/README.md000066400000000000000000000070771510370305500220170ustar00rootroot00000000000000# mapstructure [![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/v2) ![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%252Fv2) mapstructure is a Go library for decoding generic map values to structures and vice versa, while providing helpful error handling. This library is most useful when decoding values from some data stream (JSON, Gob, etc.) where you don't _quite_ know the structure of the underlying data until you read a part of it. You can therefore read a `map[string]interface{}` and use this library to decode it into the proper underlying native Go structure. ## Installation ```shell go get github.com/go-viper/mapstructure/v2 ``` ## Migrating from `github.com/mitchellh/mapstructure` [@mitchehllh](https://github.com/mitchellh) announced his intent to archive some of his unmaintained projects (see [here](https://gist.github.com/mitchellh/90029601268e59a29e64e55bab1c5bdc) and [here](https://github.com/mitchellh/mapstructure/issues/349)). This is a repository achieved the "blessed fork" status. You can migrate to this package by changing your import paths in your Go files to `github.com/go-viper/mapstructure/v2`. 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') ``` If you need more time to migrate your code, that is absolutely fine. Some of the latest fixes are backported to the v1 release branch of this package, so you can use the Go modules `replace` feature until you are ready to migrate: ```shell replace github.com/mitchellh/mapstructure => github.com/go-viper/mapstructure v1.6.0 ``` ## Usage & Example For usage and examples see the [documentation](https://pkg.go.dev/mod/github.com/go-viper/mapstructure/v2). The `Decode` function has examples associated with it there. ## But Why?! Go offers fantastic standard libraries for decoding formats such as JSON. The standard method is to have a struct pre-created, and populate that struct from the bytes of the encoded format. This is great, but the problem is if you have configuration or an encoding that changes slightly depending on specific fields. For example, consider this JSON: ```json { "type": "person", "name": "Mitchell" } ``` Perhaps we can't populate a specific structure without first reading the "type" field from the JSON. We could always do two passes over the decoding of the JSON (reading the "type" first, and the rest later). However, it is much simpler to just decode this into a `map[string]interface{}` structure, read the "type" key, then use something like this library to decode it into the proper structure. ## Credits Mapstructure was originally created by [@mitchellh](https://github.com/mitchellh). This is a maintained fork of the original library. Read more about the reasons for the fork [here](https://github.com/mitchellh/mapstructure/issues/349). ## License The project is licensed under the [MIT License](LICENSE). golang-github-go-viper-mapstructure-2.4.0/decode_hooks.go000066400000000000000000000444571510370305500235200ustar00rootroot00000000000000package mapstructure import ( "encoding" "errors" "fmt" "net" "net/netip" "net/url" "reflect" "strconv" "strings" "time" ) // typedDecodeHook takes a raw DecodeHookFunc (an any) and turns // it into the proper DecodeHookFunc type, such as DecodeHookFuncType. func typedDecodeHook(h DecodeHookFunc) DecodeHookFunc { // Create variables here so we can reference them with the reflect pkg var f1 DecodeHookFuncType var f2 DecodeHookFuncKind var f3 DecodeHookFuncValue // Fill in the variables into this interface and the rest is done // automatically using the reflect package. potential := []any{f1, f2, f3} v := reflect.ValueOf(h) vt := v.Type() for _, raw := range potential { pt := reflect.ValueOf(raw).Type() if vt.ConvertibleTo(pt) { return v.Convert(pt).Interface() } } return nil } // cachedDecodeHook takes a raw DecodeHookFunc (an any) and turns // it into a closure to be used directly // if the type fails to convert we return a closure always erroring to keep the previous behaviour func cachedDecodeHook(raw DecodeHookFunc) func(from reflect.Value, to reflect.Value) (any, error) { switch f := typedDecodeHook(raw).(type) { case DecodeHookFuncType: return func(from reflect.Value, to reflect.Value) (any, error) { return f(from.Type(), to.Type(), from.Interface()) } case DecodeHookFuncKind: return func(from reflect.Value, to reflect.Value) (any, error) { return f(from.Kind(), to.Kind(), from.Interface()) } case DecodeHookFuncValue: return func(from reflect.Value, to reflect.Value) (any, error) { return f(from, to) } default: return func(from reflect.Value, to reflect.Value) (any, error) { return nil, errors.New("invalid decode hook signature") } } } // DecodeHookExec executes the given decode hook. This should be used // since it'll naturally degrade to the older backwards compatible DecodeHookFunc // that took reflect.Kind instead of reflect.Type. func DecodeHookExec( raw DecodeHookFunc, from reflect.Value, to reflect.Value, ) (any, error) { switch f := typedDecodeHook(raw).(type) { case DecodeHookFuncType: return f(from.Type(), to.Type(), from.Interface()) case DecodeHookFuncKind: return f(from.Kind(), to.Kind(), from.Interface()) case DecodeHookFuncValue: return f(from, to) default: return nil, errors.New("invalid decode hook signature") } } // ComposeDecodeHookFunc creates a single DecodeHookFunc that // automatically composes multiple DecodeHookFuncs. // // The composed funcs are called in order, with the result of the // previous transformation. func ComposeDecodeHookFunc(fs ...DecodeHookFunc) DecodeHookFunc { cached := make([]func(from reflect.Value, to reflect.Value) (any, error), 0, len(fs)) for _, f := range fs { cached = append(cached, cachedDecodeHook(f)) } return func(f reflect.Value, t reflect.Value) (any, error) { var err error data := f.Interface() newFrom := f for _, c := range cached { data, err = c(newFrom, t) if err != nil { return nil, err } if v, ok := data.(reflect.Value); ok { newFrom = v } else { newFrom = reflect.ValueOf(data) } } return data, nil } } // OrComposeDecodeHookFunc executes all input hook functions until one of them returns no error. In that case its value is returned. // If all hooks return an error, OrComposeDecodeHookFunc returns an error concatenating all error messages. func OrComposeDecodeHookFunc(ff ...DecodeHookFunc) DecodeHookFunc { cached := make([]func(from reflect.Value, to reflect.Value) (any, error), 0, len(ff)) for _, f := range ff { cached = append(cached, cachedDecodeHook(f)) } return func(a, b reflect.Value) (any, error) { var allErrs string var out any var err error for _, c := range cached { out, err = c(a, b) if err != nil { allErrs += err.Error() + "\n" continue } return out, nil } return nil, errors.New(allErrs) } } // StringToSliceHookFunc returns a DecodeHookFunc that converts // string to []string by splitting on the given sep. func StringToSliceHookFunc(sep string) DecodeHookFunc { return func( f reflect.Type, t reflect.Type, data any, ) (any, error) { if f.Kind() != reflect.String { return data, nil } if t != reflect.SliceOf(f) { return data, nil } raw := data.(string) if raw == "" { return []string{}, nil } return strings.Split(raw, sep), nil } } // StringToWeakSliceHookFunc brings back the old (pre-v2) behavior of [StringToSliceHookFunc]. // // As of mapstructure v2.0.0 [StringToSliceHookFunc] checks if the return type is a string slice. // This function removes that check. func StringToWeakSliceHookFunc(sep string) DecodeHookFunc { return func( f reflect.Type, t reflect.Type, data any, ) (any, error) { if f.Kind() != reflect.String || t.Kind() != reflect.Slice { return data, nil } raw := data.(string) if raw == "" { return []string{}, nil } return strings.Split(raw, sep), nil } } // StringToTimeDurationHookFunc returns a DecodeHookFunc that converts // strings to time.Duration. func StringToTimeDurationHookFunc() DecodeHookFunc { return func( f reflect.Type, t reflect.Type, data any, ) (any, error) { if f.Kind() != reflect.String { return data, nil } if t != reflect.TypeOf(time.Duration(5)) { return data, nil } // Convert it by parsing d, err := time.ParseDuration(data.(string)) return d, wrapTimeParseDurationError(err) } } // StringToTimeLocationHookFunc returns a DecodeHookFunc that converts // strings to *time.Location. func StringToTimeLocationHookFunc() DecodeHookFunc { return func( f reflect.Type, t reflect.Type, data any, ) (any, error) { if f.Kind() != reflect.String { return data, nil } if t != reflect.TypeOf(time.Local) { return data, nil } d, err := time.LoadLocation(data.(string)) return d, wrapTimeParseLocationError(err) } } // StringToURLHookFunc returns a DecodeHookFunc that converts // strings to *url.URL. func StringToURLHookFunc() DecodeHookFunc { return func( f reflect.Type, t reflect.Type, data any, ) (any, error) { if f.Kind() != reflect.String { return data, nil } if t != reflect.TypeOf(&url.URL{}) { return data, nil } // Convert it by parsing u, err := url.Parse(data.(string)) return u, wrapUrlError(err) } } // StringToIPHookFunc returns a DecodeHookFunc that converts // strings to net.IP func StringToIPHookFunc() DecodeHookFunc { return func( f reflect.Type, t reflect.Type, data any, ) (any, error) { if f.Kind() != reflect.String { return data, nil } if t != reflect.TypeOf(net.IP{}) { return data, nil } // Convert it by parsing ip := net.ParseIP(data.(string)) if ip == nil { return net.IP{}, fmt.Errorf("failed parsing ip") } return ip, nil } } // StringToIPNetHookFunc returns a DecodeHookFunc that converts // strings to net.IPNet func StringToIPNetHookFunc() DecodeHookFunc { return func( f reflect.Type, t reflect.Type, data any, ) (any, error) { if f.Kind() != reflect.String { return data, nil } if t != reflect.TypeOf(net.IPNet{}) { return data, nil } // Convert it by parsing _, net, err := net.ParseCIDR(data.(string)) return net, wrapNetParseError(err) } } // StringToTimeHookFunc returns a DecodeHookFunc that converts // strings to time.Time. func StringToTimeHookFunc(layout string) DecodeHookFunc { return func( f reflect.Type, t reflect.Type, data any, ) (any, error) { if f.Kind() != reflect.String { return data, nil } if t != reflect.TypeOf(time.Time{}) { return data, nil } // Convert it by parsing ti, err := time.Parse(layout, data.(string)) return ti, wrapTimeParseError(err) } } // WeaklyTypedHook is a DecodeHookFunc which adds support for weak typing to // the decoder. // // Note that this is significantly different from the WeaklyTypedInput option // of the DecoderConfig. func WeaklyTypedHook( f reflect.Kind, t reflect.Kind, data any, ) (any, error) { dataVal := reflect.ValueOf(data) switch t { case reflect.String: switch f { case reflect.Bool: if dataVal.Bool() { return "1", nil } return "0", nil case reflect.Float32: return strconv.FormatFloat(dataVal.Float(), 'f', -1, 64), nil case reflect.Int: return strconv.FormatInt(dataVal.Int(), 10), nil case reflect.Slice: dataType := dataVal.Type() elemKind := dataType.Elem().Kind() if elemKind == reflect.Uint8 { return string(dataVal.Interface().([]uint8)), nil } case reflect.Uint: return strconv.FormatUint(dataVal.Uint(), 10), nil } } return data, nil } func RecursiveStructToMapHookFunc() DecodeHookFunc { return func(f reflect.Value, t reflect.Value) (any, error) { if f.Kind() != reflect.Struct { return f.Interface(), nil } var i any = struct{}{} if t.Type() != reflect.TypeOf(&i).Elem() { return f.Interface(), nil } m := make(map[string]any) t.Set(reflect.ValueOf(m)) return f.Interface(), nil } } // TextUnmarshallerHookFunc returns a DecodeHookFunc that applies // strings to the UnmarshalText function, when the target type // implements the encoding.TextUnmarshaler interface func TextUnmarshallerHookFunc() DecodeHookFuncType { return func( f reflect.Type, t reflect.Type, data any, ) (any, error) { if f.Kind() != reflect.String { return data, nil } result := reflect.New(t).Interface() unmarshaller, ok := result.(encoding.TextUnmarshaler) if !ok { return data, nil } str, ok := data.(string) if !ok { str = reflect.Indirect(reflect.ValueOf(&data)).Elem().String() } if err := unmarshaller.UnmarshalText([]byte(str)); err != nil { return nil, err } return result, nil } } // StringToNetIPAddrHookFunc returns a DecodeHookFunc that converts // strings to netip.Addr. func StringToNetIPAddrHookFunc() DecodeHookFunc { return func( f reflect.Type, t reflect.Type, data any, ) (any, error) { if f.Kind() != reflect.String { return data, nil } if t != reflect.TypeOf(netip.Addr{}) { return data, nil } // Convert it by parsing addr, err := netip.ParseAddr(data.(string)) return addr, wrapNetIPParseAddrError(err) } } // StringToNetIPAddrPortHookFunc returns a DecodeHookFunc that converts // strings to netip.AddrPort. func StringToNetIPAddrPortHookFunc() DecodeHookFunc { return func( f reflect.Type, t reflect.Type, data any, ) (any, error) { if f.Kind() != reflect.String { return data, nil } if t != reflect.TypeOf(netip.AddrPort{}) { return data, nil } // Convert it by parsing addrPort, err := netip.ParseAddrPort(data.(string)) return addrPort, wrapNetIPParseAddrPortError(err) } } // StringToNetIPPrefixHookFunc returns a DecodeHookFunc that converts // strings to netip.Prefix. func StringToNetIPPrefixHookFunc() DecodeHookFunc { return func( f reflect.Type, t reflect.Type, data any, ) (any, error) { if f.Kind() != reflect.String { return data, nil } if t != reflect.TypeOf(netip.Prefix{}) { return data, nil } // Convert it by parsing prefix, err := netip.ParsePrefix(data.(string)) return prefix, wrapNetIPParsePrefixError(err) } } // 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 func StringToBasicTypeHookFunc() DecodeHookFunc { return ComposeDecodeHookFunc( StringToInt8HookFunc(), StringToUint8HookFunc(), StringToInt16HookFunc(), StringToUint16HookFunc(), StringToInt32HookFunc(), StringToUint32HookFunc(), StringToInt64HookFunc(), StringToUint64HookFunc(), StringToIntHookFunc(), StringToUintHookFunc(), StringToFloat32HookFunc(), StringToFloat64HookFunc(), StringToBoolHookFunc(), // byte and rune are aliases for uint8 and int32 respectively // StringToByteHookFunc(), // StringToRuneHookFunc(), StringToComplex64HookFunc(), StringToComplex128HookFunc(), ) } // StringToInt8HookFunc returns a DecodeHookFunc that converts // strings to int8. func StringToInt8HookFunc() DecodeHookFunc { return func(f reflect.Type, t reflect.Type, data any) (any, error) { if f.Kind() != reflect.String || t.Kind() != reflect.Int8 { return data, nil } // Convert it by parsing i64, err := strconv.ParseInt(data.(string), 0, 8) return int8(i64), wrapStrconvNumError(err) } } // StringToUint8HookFunc returns a DecodeHookFunc that converts // strings to uint8. func StringToUint8HookFunc() DecodeHookFunc { return func(f reflect.Type, t reflect.Type, data any) (any, error) { if f.Kind() != reflect.String || t.Kind() != reflect.Uint8 { return data, nil } // Convert it by parsing u64, err := strconv.ParseUint(data.(string), 0, 8) return uint8(u64), wrapStrconvNumError(err) } } // StringToInt16HookFunc returns a DecodeHookFunc that converts // strings to int16. func StringToInt16HookFunc() DecodeHookFunc { return func(f reflect.Type, t reflect.Type, data any) (any, error) { if f.Kind() != reflect.String || t.Kind() != reflect.Int16 { return data, nil } // Convert it by parsing i64, err := strconv.ParseInt(data.(string), 0, 16) return int16(i64), wrapStrconvNumError(err) } } // StringToUint16HookFunc returns a DecodeHookFunc that converts // strings to uint16. func StringToUint16HookFunc() DecodeHookFunc { return func(f reflect.Type, t reflect.Type, data any) (any, error) { if f.Kind() != reflect.String || t.Kind() != reflect.Uint16 { return data, nil } // Convert it by parsing u64, err := strconv.ParseUint(data.(string), 0, 16) return uint16(u64), wrapStrconvNumError(err) } } // StringToInt32HookFunc returns a DecodeHookFunc that converts // strings to int32. func StringToInt32HookFunc() DecodeHookFunc { return func(f reflect.Type, t reflect.Type, data any) (any, error) { if f.Kind() != reflect.String || t.Kind() != reflect.Int32 { return data, nil } // Convert it by parsing i64, err := strconv.ParseInt(data.(string), 0, 32) return int32(i64), wrapStrconvNumError(err) } } // StringToUint32HookFunc returns a DecodeHookFunc that converts // strings to uint32. func StringToUint32HookFunc() DecodeHookFunc { return func(f reflect.Type, t reflect.Type, data any) (any, error) { if f.Kind() != reflect.String || t.Kind() != reflect.Uint32 { return data, nil } // Convert it by parsing u64, err := strconv.ParseUint(data.(string), 0, 32) return uint32(u64), wrapStrconvNumError(err) } } // StringToInt64HookFunc returns a DecodeHookFunc that converts // strings to int64. func StringToInt64HookFunc() DecodeHookFunc { return func(f reflect.Type, t reflect.Type, data any) (any, error) { if f.Kind() != reflect.String || t.Kind() != reflect.Int64 { return data, nil } // Convert it by parsing i64, err := strconv.ParseInt(data.(string), 0, 64) return int64(i64), wrapStrconvNumError(err) } } // StringToUint64HookFunc returns a DecodeHookFunc that converts // strings to uint64. func StringToUint64HookFunc() DecodeHookFunc { return func(f reflect.Type, t reflect.Type, data any) (any, error) { if f.Kind() != reflect.String || t.Kind() != reflect.Uint64 { return data, nil } // Convert it by parsing u64, err := strconv.ParseUint(data.(string), 0, 64) return uint64(u64), wrapStrconvNumError(err) } } // StringToIntHookFunc returns a DecodeHookFunc that converts // strings to int. func StringToIntHookFunc() DecodeHookFunc { return func(f reflect.Type, t reflect.Type, data any) (any, error) { if f.Kind() != reflect.String || t.Kind() != reflect.Int { return data, nil } // Convert it by parsing i64, err := strconv.ParseInt(data.(string), 0, 0) return int(i64), wrapStrconvNumError(err) } } // StringToUintHookFunc returns a DecodeHookFunc that converts // strings to uint. func StringToUintHookFunc() DecodeHookFunc { return func(f reflect.Type, t reflect.Type, data any) (any, error) { if f.Kind() != reflect.String || t.Kind() != reflect.Uint { return data, nil } // Convert it by parsing u64, err := strconv.ParseUint(data.(string), 0, 0) return uint(u64), wrapStrconvNumError(err) } } // StringToFloat32HookFunc returns a DecodeHookFunc that converts // strings to float32. func StringToFloat32HookFunc() DecodeHookFunc { return func(f reflect.Type, t reflect.Type, data any) (any, error) { if f.Kind() != reflect.String || t.Kind() != reflect.Float32 { return data, nil } // Convert it by parsing f64, err := strconv.ParseFloat(data.(string), 32) return float32(f64), wrapStrconvNumError(err) } } // StringToFloat64HookFunc returns a DecodeHookFunc that converts // strings to float64. func StringToFloat64HookFunc() DecodeHookFunc { return func(f reflect.Type, t reflect.Type, data any) (any, error) { if f.Kind() != reflect.String || t.Kind() != reflect.Float64 { return data, nil } // Convert it by parsing f64, err := strconv.ParseFloat(data.(string), 64) return f64, wrapStrconvNumError(err) } } // StringToBoolHookFunc returns a DecodeHookFunc that converts // strings to bool. func StringToBoolHookFunc() DecodeHookFunc { return func(f reflect.Type, t reflect.Type, data any) (any, error) { if f.Kind() != reflect.String || t.Kind() != reflect.Bool { return data, nil } // Convert it by parsing b, err := strconv.ParseBool(data.(string)) return b, wrapStrconvNumError(err) } } // StringToByteHookFunc returns a DecodeHookFunc that converts // strings to byte. func StringToByteHookFunc() DecodeHookFunc { return StringToUint8HookFunc() } // StringToRuneHookFunc returns a DecodeHookFunc that converts // strings to rune. func StringToRuneHookFunc() DecodeHookFunc { return StringToInt32HookFunc() } // StringToComplex64HookFunc returns a DecodeHookFunc that converts // strings to complex64. func StringToComplex64HookFunc() DecodeHookFunc { return func(f reflect.Type, t reflect.Type, data any) (any, error) { if f.Kind() != reflect.String || t.Kind() != reflect.Complex64 { return data, nil } // Convert it by parsing c128, err := strconv.ParseComplex(data.(string), 64) return complex64(c128), wrapStrconvNumError(err) } } // StringToComplex128HookFunc returns a DecodeHookFunc that converts // strings to complex128. func StringToComplex128HookFunc() DecodeHookFunc { return func(f reflect.Type, t reflect.Type, data any) (any, error) { if f.Kind() != reflect.String || t.Kind() != reflect.Complex128 { return data, nil } // Convert it by parsing c128, err := strconv.ParseComplex(data.(string), 128) return c128, wrapStrconvNumError(err) } } golang-github-go-viper-mapstructure-2.4.0/decode_hooks_test.go000066400000000000000000002251271510370305500245520ustar00rootroot00000000000000package mapstructure import ( "encoding/json" "errors" "fmt" "math" "math/big" "net" "net/netip" "net/url" "reflect" "strings" "testing" "time" ) type decodeHookTestSuite[F any, T any] struct { fn DecodeHookFunc ok []decodeHookTestCase[F, T] fail []decodeHookFailureTestCase[F, T] } func (ts decodeHookTestSuite[F, T]) Run(t *testing.T) { t.Run("OK", func(t *testing.T) { t.Parallel() for _, tc := range ts.ok { tc := tc t.Run("", func(t *testing.T) { t.Parallel() tc.Run(t, ts.fn) }) } }) t.Run("Fail", func(t *testing.T) { t.Parallel() for _, tc := range ts.fail { tc := tc t.Run("", func(t *testing.T) { t.Parallel() tc.Run(t, ts.fn) }) } }) t.Run("NoOp", func(t *testing.T) { t.Parallel() var zero F actual, err := DecodeHookExec(ts.fn, reflect.ValueOf(zero), reflect.ValueOf(zero)) if err != nil { t.Fatalf("unexpected error: %s", err) } if !reflect.DeepEqual(actual, zero) { t.Fatalf("expected %[1]T(%#[1]v), got %[2]T(%#[2]v)", zero, actual) } }) } type decodeHookTestCase[F any, T any] struct { from F expected T } func (tc decodeHookTestCase[F, T]) Run(t *testing.T, fn DecodeHookFunc) { var to T actual, err := DecodeHookExec(fn, reflect.ValueOf(tc.from), reflect.ValueOf(to)) if err != nil { t.Fatalf("unexpected error: %s", err) } if !reflect.DeepEqual(actual, tc.expected) { t.Fatalf("expected %[1]T(%#[1]v), got %[2]T(%#[2]v)", tc.expected, actual) } } type decodeHookFailureTestCase[F any, T any] struct { from F } func (tc decodeHookFailureTestCase[F, T]) Run(t *testing.T, fn DecodeHookFunc) { var to T _, err := DecodeHookExec(fn, reflect.ValueOf(tc.from), reflect.ValueOf(to)) if err == nil { t.Fatalf("expected error, got none") } } func TestComposeDecodeHookFunc(t *testing.T) { f1 := func( f reflect.Kind, t reflect.Kind, data any, ) (any, error) { return data.(string) + "foo", nil } f2 := func( f reflect.Kind, t reflect.Kind, data any, ) (any, error) { return data.(string) + "bar", nil } f := ComposeDecodeHookFunc(f1, f2) 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 TestComposeDecodeHookFunc_err(t *testing.T) { f1 := func(reflect.Kind, reflect.Kind, any) (any, error) { return nil, errors.New("foo") } f2 := func(reflect.Kind, reflect.Kind, any) (any, error) { panic("NOPE") } f := ComposeDecodeHookFunc(f1, f2) _, err := DecodeHookExec( f, reflect.ValueOf(""), reflect.ValueOf([]byte(""))) if err.Error() != "foo" { t.Fatalf("bad: %s", err) } } func TestComposeDecodeHookFunc_kinds(t *testing.T) { var f2From reflect.Kind f1 := func( f reflect.Kind, t reflect.Kind, data any, ) (any, error) { return int(42), nil } f2 := func( f reflect.Kind, t reflect.Kind, data any, ) (any, error) { f2From = f return data, nil } f := ComposeDecodeHookFunc(f1, f2) _, err := DecodeHookExec( f, reflect.ValueOf(""), reflect.ValueOf([]byte(""))) if err != nil { t.Fatalf("bad: %s", err) } if f2From != reflect.Int { t.Fatalf("bad: %#v", f2From) } } func TestOrComposeDecodeHookFunc(t *testing.T) { f1 := func( f reflect.Kind, t reflect.Kind, data any, ) (any, error) { return data.(string) + "foo", nil } f2 := func( f reflect.Kind, t reflect.Kind, data any, ) (any, error) { return data.(string) + "bar", nil } f := OrComposeDecodeHookFunc(f1, f2) result, err := DecodeHookExec( f, reflect.ValueOf(""), reflect.ValueOf([]byte(""))) if err != nil { t.Fatalf("bad: %s", err) } if result.(string) != "foo" { t.Fatalf("bad: %#v", result) } } func TestOrComposeDecodeHookFunc_correctValueIsLast(t *testing.T) { f1 := func( f reflect.Kind, t reflect.Kind, data any, ) (any, error) { return nil, errors.New("f1 error") } f2 := func( f reflect.Kind, t reflect.Kind, data any, ) (any, error) { return nil, errors.New("f2 error") } f3 := func( f reflect.Kind, t reflect.Kind, data any, ) (any, error) { return data.(string) + "bar", nil } f := OrComposeDecodeHookFunc(f1, f2, f3) result, err := DecodeHookExec( f, reflect.ValueOf(""), reflect.ValueOf([]byte(""))) if err != nil { t.Fatalf("bad: %s", err) } if result.(string) != "bar" { t.Fatalf("bad: %#v", result) } } func TestOrComposeDecodeHookFunc_err(t *testing.T) { f1 := func( f reflect.Kind, t reflect.Kind, data any, ) (any, error) { return nil, errors.New("f1 error") } f2 := func( f reflect.Kind, t reflect.Kind, data any, ) (any, error) { return nil, errors.New("f2 error") } f := OrComposeDecodeHookFunc(f1, f2) _, err := DecodeHookExec( f, reflect.ValueOf(""), reflect.ValueOf([]byte(""))) if err == nil { t.Fatalf("bad: should return an error") } if err.Error() != "f1 error\nf2 error\n" { t.Fatalf("bad: %s", err) } } func TestComposeDecodeHookFunc_safe_nofuncs(t *testing.T) { f := ComposeDecodeHookFunc() type myStruct2 struct { MyInt int } type myStruct1 struct { Blah map[string]myStruct2 } src := &myStruct1{Blah: map[string]myStruct2{ "test": { MyInt: 1, }, }} dst := &myStruct1{} dConf := &DecoderConfig{ Result: dst, ErrorUnused: true, DecodeHook: f, } d, err := NewDecoder(dConf) if err != nil { t.Fatal(err) } err = d.Decode(src) if err != nil { t.Fatal(err) } } func TestComposeDecodeHookFunc_ReflectValueHook(t *testing.T) { reflectValueHook := func( f reflect.Kind, t reflect.Kind, data any, ) (any, error) { new := data.(string) + "foo" return reflect.ValueOf(new), nil } stringHook := func( f reflect.Kind, t reflect.Kind, data any, ) (any, 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) { // Test comma separator commaSuite := decodeHookTestSuite[string, []string]{ fn: StringToSliceHookFunc(","), ok: []decodeHookTestCase[string, []string]{ {"foo,bar,baz", []string{"foo", "bar", "baz"}}, // Basic comma separation {"", []string{}}, // Empty string {"single", []string{"single"}}, // Single element {"one,two", []string{"one", "two"}}, // Two elements {"foo, bar, baz", []string{"foo", " bar", " baz"}}, // Preserves spaces {"foo,,bar", []string{"foo", "", "bar"}}, // Empty elements {",foo,bar,", []string{"", "foo", "bar", ""}}, // Leading/trailing separators {"foo,bar,baz,", []string{"foo", "bar", "baz", ""}}, // Trailing separator {",foo", []string{"", "foo"}}, // Leading separator only {"foo,", []string{"foo", ""}}, // Trailing separator only {"a,b,c,d,e,f,g,h,i,j", []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"}}, // Many elements }, fail: []decodeHookFailureTestCase[string, []string]{ // StringToSliceHookFunc doesn't have failure cases - it always succeeds }, } t.Run("CommaSeparator", commaSuite.Run) // Test semicolon separator semicolonSuite := decodeHookTestSuite[string, []string]{ fn: StringToSliceHookFunc(";"), ok: []decodeHookTestCase[string, []string]{ {"foo;bar;baz", []string{"foo", "bar", "baz"}}, // Basic semicolon separation {"", []string{}}, // Empty string {"single", []string{"single"}}, // Single element {"one;two", []string{"one", "two"}}, // Two elements {"foo; bar; baz", []string{"foo", " bar", " baz"}}, // Preserves spaces {"foo;;bar", []string{"foo", "", "bar"}}, // Empty elements {";foo;bar;", []string{"", "foo", "bar", ""}}, // Leading/trailing separators }, fail: []decodeHookFailureTestCase[string, []string]{}, } t.Run("SemicolonSeparator", semicolonSuite.Run) // Test pipe separator pipeSuite := decodeHookTestSuite[string, []string]{ fn: StringToSliceHookFunc("|"), ok: []decodeHookTestCase[string, []string]{ {"foo|bar|baz", []string{"foo", "bar", "baz"}}, // Basic pipe separation {"", []string{}}, // Empty string {"single", []string{"single"}}, // Single element {"foo||bar", []string{"foo", "", "bar"}}, // Empty elements }, fail: []decodeHookFailureTestCase[string, []string]{}, } t.Run("PipeSeparator", pipeSuite.Run) // Test space separator spaceSuite := decodeHookTestSuite[string, []string]{ fn: StringToSliceHookFunc(" "), ok: []decodeHookTestCase[string, []string]{ {"foo bar baz", []string{"foo", "bar", "baz"}}, // Basic space separation {"", []string{}}, // Empty string {"single", []string{"single"}}, // Single element {"foo bar", []string{"foo", "", "bar"}}, // Double space creates empty element }, fail: []decodeHookFailureTestCase[string, []string]{}, } t.Run("SpaceSeparator", spaceSuite.Run) // Test multi-character separator multiCharSuite := decodeHookTestSuite[string, []string]{ fn: StringToSliceHookFunc("::"), ok: []decodeHookTestCase[string, []string]{ {"foo::bar::baz", []string{"foo", "bar", "baz"}}, // Basic multi-char separation {"", []string{}}, // Empty string {"single", []string{"single"}}, // Single element {"foo::::bar", []string{"foo", "", "bar"}}, // Double separator creates empty element {"::foo::bar::", []string{"", "foo", "bar", ""}}, // Leading/trailing separators }, fail: []decodeHookFailureTestCase[string, []string]{}, } t.Run("MultiCharSeparator", multiCharSuite.Run) // Test edge cases with custom logic for type conversion t.Run("NonStringTypes", func(t *testing.T) { f := StringToSliceHookFunc(",") // Test that non-string types are passed through unchanged sliceValue := reflect.ValueOf([]string{"42"}) actual, err := DecodeHookExec(f, sliceValue, sliceValue) if err != nil { t.Fatalf("unexpected error: %s", err) } if !reflect.DeepEqual(actual, []string{"42"}) { t.Fatalf("expected %v, got %v", []string{"42"}, actual) } // Test byte slice passthrough byteValue := reflect.ValueOf([]byte("42")) actual, err = DecodeHookExec(f, byteValue, reflect.ValueOf([]byte{})) if err != nil { t.Fatalf("unexpected error: %s", err) } if !reflect.DeepEqual(actual, []byte("42")) { t.Fatalf("expected %v, got %v", []byte("42"), actual) } // Test string to string passthrough strValue := reflect.ValueOf("42") actual, err = DecodeHookExec(f, strValue, strValue) if err != nil { t.Fatalf("unexpected error: %s", err) } if !reflect.DeepEqual(actual, "42") { t.Fatalf("expected %v, got %v", "42", actual) } }) } func TestStringToWeakSliceHookFunc(t *testing.T) { f := StringToWeakSliceHookFunc(",") strValue := reflect.ValueOf("42") sliceValue := reflect.ValueOf([]string{"42"}) sliceValue2 := reflect.ValueOf([]byte("42")) cases := []struct { f, t reflect.Value result any err bool }{ {sliceValue, sliceValue, []string{"42"}, false}, {sliceValue2, sliceValue2, []byte("42"), false}, {reflect.ValueOf([]byte("42")), reflect.ValueOf([]byte{}), []byte("42"), false}, {strValue, strValue, "42", false}, { reflect.ValueOf("foo,bar,baz"), sliceValue, []string{"foo", "bar", "baz"}, false, }, { reflect.ValueOf("foo,bar,baz"), sliceValue2, []string{"foo", "bar", "baz"}, false, }, { reflect.ValueOf(""), sliceValue, []string{}, false, }, { reflect.ValueOf(""), sliceValue2, []string{}, false, }, } for i, tc := range cases { 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: expected %#v, got %#v", i, tc.result, actual) } } } func TestStringToTimeDurationHookFunc(t *testing.T) { suite := decodeHookTestSuite[string, time.Duration]{ fn: StringToTimeDurationHookFunc(), ok: []decodeHookTestCase[string, time.Duration]{ // Basic units {"5s", 5 * time.Second}, // Seconds {"10ms", 10 * time.Millisecond}, // Milliseconds {"100us", 100 * time.Microsecond}, // Microseconds {"1000ns", 1000 * time.Nanosecond}, // Nanoseconds {"2m", 2 * time.Minute}, // Minutes {"3h", 3 * time.Hour}, // Hours {"24h", 24 * time.Hour}, // Day in hours // Combinations {"1h30m", time.Hour + 30*time.Minute}, // Hour and minutes {"2h45m30s", 2*time.Hour + 45*time.Minute + 30*time.Second}, // Multiple units {"1m30s", time.Minute + 30*time.Second}, // Minutes and seconds {"500ms", 500 * time.Millisecond}, // Milliseconds only {"1.5s", time.Second + 500*time.Millisecond}, // Fractional seconds {"2.5h", 2*time.Hour + 30*time.Minute}, // Fractional hours // Zero values {"0s", 0}, // Zero seconds {"0ms", 0}, // Zero milliseconds {"0h", 0}, // Zero hours {"0", 0}, // Just zero // Negative durations {"-5s", -5 * time.Second}, // Negative seconds {"-1h30m", -(time.Hour + 30*time.Minute)}, // Negative combined {"-100ms", -100 * time.Millisecond}, // Negative milliseconds // Fractional values {"0.5s", 500 * time.Millisecond}, // Half second {"1.25m", time.Minute + 15*time.Second}, // Fractional minute {"0.1h", 6 * time.Minute}, // Fractional hour {"2.5ms", 2*time.Millisecond + 500*time.Microsecond}, // Fractional millisecond // Large values {"8760h", 8760 * time.Hour}, // 1 year in hours {"525600m", 525600 * time.Minute}, // 1 year in minutes {"1000000us", 1000000 * time.Microsecond}, // Large microseconds // Additional valid cases {".5s", 500 * time.Millisecond}, // Leading decimal is valid {"5µs", 5 * time.Microsecond}, // Unicode micro symbol is valid {"5.s", 5 * time.Second}, // Trailing decimal is valid {"5s5m5s", 10*time.Second + 5*time.Minute}, // Duplicate units are valid }, fail: []decodeHookFailureTestCase[string, time.Duration]{ {"5"}, // Missing unit {"abc"}, // Invalid format {""}, // Empty string {"5x"}, // Invalid unit {"5ss"}, // Double unit letters {"5..5s"}, // Multiple decimal points {"++5s"}, // Double plus sign {"--5s"}, // Double minus sign {" 5s "}, // Leading/trailing whitespace not handled {"\t10ms\n"}, // Tab/newline whitespace not handled {"\r1h\r"}, // Carriage return whitespace not handled {"5s "}, // Trailing space after unit {" 5 s"}, // Space before unit {"5 s 10 m"}, // Spaces in combined duration {"∞s"}, // Unicode infinity symbol {"1y"}, // Unsupported unit (years) {"1w"}, // Unsupported unit (weeks) {"1d"}, // Unsupported unit (days) }, } // Test non-string and non-duration type passthrough t.Run("Passthrough", func(t *testing.T) { f := StringToTimeDurationHookFunc() // Non-string type should pass through intValue := reflect.ValueOf(42) actual, err := DecodeHookExec(f, intValue, reflect.ValueOf(time.Duration(0))) if err != nil { t.Fatalf("unexpected error: %s", err) } if actual != 42 { t.Fatalf("expected 42, got %v", actual) } // Non-duration target type should pass through strValue := reflect.ValueOf("5s") actual, err = DecodeHookExec(f, strValue, strValue) if err != nil { t.Fatalf("unexpected error: %s", err) } if actual != "5s" { t.Fatalf("expected '5s', got %v", actual) } }) suite.Run(t) } func TestStringToTimeLocationHookFunc(t *testing.T) { newYork, _ := time.LoadLocation("America/New_York") london, _ := time.LoadLocation("Europe/London") tehran, _ := time.LoadLocation("Asia/Tehran") shanghai, _ := time.LoadLocation("Asia/Shanghai") suite := decodeHookTestSuite[string, *time.Location]{ fn: StringToTimeLocationHookFunc(), ok: []decodeHookTestCase[string, *time.Location]{ {"UTC", time.UTC}, {"Local", time.Local}, {"America/New_York", newYork}, {"Europe/London", london}, {"Asia/Tehran", tehran}, {"Asia/Shanghai", shanghai}, }, fail: []decodeHookFailureTestCase[string, *time.Location]{ {"UTC2"}, // Non-existent {"5s"}, // Duration-like, not a zone {"Europe\\London"}, // Invalid path separator {"../etc/passwd"}, // Unsafe path {"/etc/zoneinfo"}, // Absolute path (rejected by stdlib) {"Asia\\Tehran"}, // Invalid Windows-style path }, } suite.Run(t) } func TestStringToURLHookFunc(t *testing.T) { httpURL, _ := url.Parse("http://example.com") httpsURL, _ := url.Parse("https://example.com") ftpURL, _ := url.Parse("ftp://ftp.example.com") fileURL, _ := url.Parse("file:///path/to/file") complexURL, _ := url.Parse("https://user:pass@example.com:8080/path?query=value&foo=bar#fragment") ipURL, _ := url.Parse("http://192.168.1.1:8080") ipv6URL, _ := url.Parse("http://[::1]:8080") emptyURL, _ := url.Parse("") suite := decodeHookTestSuite[string, *url.URL]{ fn: StringToURLHookFunc(), ok: []decodeHookTestCase[string, *url.URL]{ {"http://example.com", httpURL}, // Basic HTTP URL {"https://example.com", httpsURL}, // HTTPS URL {"ftp://ftp.example.com", ftpURL}, // FTP URL {"file:///path/to/file", fileURL}, // File URL {"https://user:pass@example.com:8080/path?query=value&foo=bar#fragment", complexURL}, // Complex URL with all components {"http://192.168.1.1:8080", ipURL}, // IPv4 address with port {"http://[::1]:8080", ipv6URL}, // IPv6 address with port {"", emptyURL}, // Empty URL // Additional valid cases that url.Parse accepts {"http://", func() *url.URL { u, _ := url.Parse("http://"); return u }()}, // Scheme with empty host {"http://example.com:99999", func() *url.URL { u, _ := url.Parse("http://example.com:99999"); return u }()}, // High port number {"not a url at all", func() *url.URL { u, _ := url.Parse("not a url at all"); return u }()}, // Relative path (valid) }, fail: []decodeHookFailureTestCase[string, *url.URL]{ {"http ://example.com"}, // Space in scheme {"://invalid"}, // Missing scheme {"http://[invalid:ipv6"}, // Malformed IPv6 bracket }, } suite.Run(t) } func TestStringToTimeHookFunc(t *testing.T) { strValue := reflect.ValueOf("5") timeValue := reflect.ValueOf(time.Time{}) cases := []struct { f, t reflect.Value layout string result any err bool }{ { reflect.ValueOf("2006-01-02T15:04:05Z"), timeValue, time.RFC3339, time.Date(2006, 1, 2, 15, 4, 5, 0, time.UTC), false, }, {strValue, timeValue, time.RFC3339, time.Time{}, true}, {strValue, strValue, time.RFC3339, "5", false}, } for i, tc := range cases { f := StringToTimeHookFunc(tc.layout) 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: expected %#v, got %#v", i, tc.result, actual) } } } func TestStringToIPHookFunc(t *testing.T) { suite := decodeHookTestSuite[string, net.IP]{ fn: StringToIPHookFunc(), ok: []decodeHookTestCase[string, net.IP]{ // IPv4 addresses {"1.2.3.4", net.IPv4(0x01, 0x02, 0x03, 0x04)}, // Basic IPv4 {"192.168.1.1", net.IPv4(192, 168, 1, 1)}, // Private network address {"0.0.0.0", net.IPv4(0, 0, 0, 0)}, // Zero address {"255.255.255.255", net.IPv4(255, 255, 255, 255)}, // Broadcast address {"127.0.0.1", net.IPv4(127, 0, 0, 1)}, // Localhost {"10.0.0.1", net.IPv4(10, 0, 0, 1)}, // Private network // IPv6 addresses {"::1", net.ParseIP("::1")}, // IPv6 localhost {"2001:db8::1", net.ParseIP("2001:db8::1")}, // Documentation address {"fe80::1", net.ParseIP("fe80::1")}, // Link-local address {"2001:0db8:85a3:0000:0000:8a2e:0370:7334", net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334")}, // Full IPv6 address {"2001:db8:85a3::8a2e:370:7334", net.ParseIP("2001:db8:85a3::8a2e:370:7334")}, // Compressed IPv6 {"::", net.ParseIP("::")}, // IPv6 zero address {"::ffff:192.0.2.1", net.ParseIP("::ffff:192.0.2.1")}, // IPv4-mapped IPv6 }, fail: []decodeHookFailureTestCase[string, net.IP]{ {"5"}, // Single number {"256.1.1.1"}, // IPv4 octet too large {"1.2.3"}, // Too few IPv4 octets {"1.2.3.4.5"}, // Too many IPv4 octets {"not.an.ip.address"}, // Non-numeric text {""}, // Empty string {"192.168.1.256"}, // Last octet too large {"192.168.-1.1"}, // Negative octet {"gggg::1"}, // Invalid hex in IPv6 {"2001:db8::1::2"}, // Double :: in IPv6 {"[::1]"}, // IPv6 with brackets (not raw IP) {"192.168.1.1:8080"}, // IPv4 with port }, } suite.Run(t) } func TestStringToIPNetHookFunc(t *testing.T) { strValue := reflect.ValueOf("5") ipNetValue := reflect.ValueOf(net.IPNet{}) var nilNet *net.IPNet = nil cases := []struct { f, t reflect.Value result any err bool }{ { reflect.ValueOf("1.2.3.4/24"), ipNetValue, &net.IPNet{ IP: net.IP{0x01, 0x02, 0x03, 0x00}, Mask: net.IPv4Mask(0xff, 0xff, 0xff, 0x00), }, false, }, {strValue, ipNetValue, nilNet, true}, {strValue, strValue, "5", false}, } for i, tc := range cases { f := StringToIPNetHookFunc() 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: expected %#v, got %#v", i, tc.result, actual) } } } func TestWeaklyTypedHook(t *testing.T) { var f DecodeHookFunc = WeaklyTypedHook strValue := reflect.ValueOf("") cases := []struct { f, t reflect.Value result any err bool }{ // TO STRING { reflect.ValueOf(false), strValue, "0", // bool false converts to "0" false, }, { reflect.ValueOf(true), strValue, "1", // bool true converts to "1" false, }, { reflect.ValueOf(float32(7)), strValue, "7", // float32 converts to string false, }, { reflect.ValueOf(int(7)), strValue, "7", // int converts to string false, }, { reflect.ValueOf([]uint8("foo")), strValue, "foo", // byte slice converts to string false, }, { reflect.ValueOf(uint(7)), strValue, "7", // uint converts to string false, }, } for i, tc := range cases { 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: expected %#v, got %#v", i, tc.result, actual) } } } func TestStructToMapHookFuncTabled(t *testing.T) { var f DecodeHookFunc = RecursiveStructToMapHookFunc() type b struct { TestKey string } type a struct { Sub b } testStruct := a{ Sub: b{ TestKey: "testval", }, } testMap := map[string]any{ "Sub": map[string]any{ "TestKey": "testval", }, } cases := []struct { name string receiver any input any expected any err bool }{ { "map receiver", func() any { var res map[string]any return &res }(), testStruct, &testMap, false, }, { "interface receiver", func() any { var res any return &res }(), testStruct, func() any { var exp any = testMap return &exp }(), false, }, { "slice receiver errors", func() any { var res []string return &res }(), testStruct, new([]string), true, }, { "slice to slice - no change", func() any { var res []string return &res }(), []string{"a", "b"}, &[]string{"a", "b"}, false, }, { "string to string - no change", func() any { var res string return &res }(), "test", func() *string { s := "test" return &s }(), false, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { cfg := &DecoderConfig{ DecodeHook: f, Result: tc.receiver, } d, err := NewDecoder(cfg) if err != nil { t.Fatalf("unexpected err %#v", err) } err = d.Decode(tc.input) if tc.err != (err != nil) { t.Fatalf("expected err %#v", err) } if !reflect.DeepEqual(tc.expected, tc.receiver) { t.Fatalf("expected %#v, got %#v", tc.expected, tc.receiver) } }) } } func TestTextUnmarshallerHookFunc(t *testing.T) { type MyString string cases := []struct { f, t reflect.Value result any err bool }{ {reflect.ValueOf("42"), reflect.ValueOf(big.Int{}), big.NewInt(42), false}, {reflect.ValueOf("invalid"), reflect.ValueOf(big.Int{}), nil, true}, {reflect.ValueOf("5"), reflect.ValueOf("5"), "5", false}, {reflect.ValueOf(json.Number("42")), reflect.ValueOf(big.Int{}), big.NewInt(42), false}, {reflect.ValueOf(MyString("42")), reflect.ValueOf(big.Int{}), big.NewInt(42), false}, } for i, tc := range cases { f := TextUnmarshallerHookFunc() 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: expected %#v, got %#v", i, tc.result, actual) } } } func TestStringToNetIPAddrHookFunc(t *testing.T) { suite := decodeHookTestSuite[string, netip.Addr]{ fn: StringToNetIPAddrHookFunc(), ok: []decodeHookTestCase[string, netip.Addr]{ // IPv4 addresses {"192.0.2.1", netip.AddrFrom4([4]byte{0xc0, 0x00, 0x02, 0x01})}, // Documentation address {"127.0.0.1", netip.AddrFrom4([4]byte{127, 0, 0, 1})}, // Localhost {"0.0.0.0", netip.AddrFrom4([4]byte{0, 0, 0, 0})}, // Zero address {"255.255.255.255", netip.AddrFrom4([4]byte{255, 255, 255, 255})}, // Broadcast address {"10.0.0.1", netip.AddrFrom4([4]byte{10, 0, 0, 1})}, // Private network {"192.168.1.100", netip.AddrFrom4([4]byte{192, 168, 1, 100})}, // Private network // IPv6 addresses {"::1", netip.AddrFrom16([16]byte{15: 1})}, // IPv6 localhost {"2001:db8::1", netip.AddrFrom16([16]byte{0x20, 0x01, 0x0d, 0xb8, 12: 0, 0, 0, 1})}, // Documentation address {"fe80::1", netip.AddrFrom16([16]byte{0xfe, 0x80, 14: 0, 1})}, // Link-local address {"::", netip.AddrFrom16([16]byte{})}, // IPv6 zero address {"::ffff:192.0.2.1", netip.AddrFrom16([16]byte{10: 0xff, 0xff, 0xc0, 0x00, 0x02, 0x01})}, // IPv4-mapped IPv6 }, fail: []decodeHookFailureTestCase[string, netip.Addr]{ {"5"}, // Single number {"256.1.1.1"}, // IPv4 octet too large {"1.2.3"}, // Too few IPv4 octets {"1.2.3.4.5"}, // Too many IPv4 octets {"not.an.ip.address"}, // Non-numeric text {""}, // Empty string {"192.168.1.256"}, // Last octet too large {"192.168.-1.1"}, // Negative octet {"gggg::1"}, // Invalid hex in IPv6 {"2001:db8::1::2"}, // Double :: in IPv6 {"[::1]"}, // IPv6 with brackets {"192.168.1.1:8080"}, // IPv4 with port {"192.168.1.1/24"}, // IPv4 with CIDR }, } suite.Run(t) } func TestStringToNetIPAddrPortHookFunc(t *testing.T) { suite := decodeHookTestSuite[string, netip.AddrPort]{ fn: StringToNetIPAddrPortHookFunc(), ok: []decodeHookTestCase[string, netip.AddrPort]{ // IPv4 with ports {"192.0.2.1:80", netip.AddrPortFrom(netip.AddrFrom4([4]byte{0xc0, 0x00, 0x02, 0x01}), 80)}, // HTTP port {"127.0.0.1:8080", netip.AddrPortFrom(netip.AddrFrom4([4]byte{127, 0, 0, 1}), 8080)}, // Alternative HTTP port {"10.0.0.1:443", netip.AddrPortFrom(netip.AddrFrom4([4]byte{10, 0, 0, 1}), 443)}, // HTTPS port {"192.168.1.100:22", netip.AddrPortFrom(netip.AddrFrom4([4]byte{192, 168, 1, 100}), 22)}, // SSH port {"0.0.0.0:0", netip.AddrPortFrom(netip.AddrFrom4([4]byte{0, 0, 0, 0}), 0)}, // Zero address and port {"255.255.255.255:65535", netip.AddrPortFrom(netip.AddrFrom4([4]byte{255, 255, 255, 255}), 65535)}, // Max address and port // IPv6 with ports {"[::1]:80", netip.AddrPortFrom(netip.AddrFrom16([16]byte{15: 1}), 80)}, // IPv6 localhost with HTTP {"[2001:db8::1]:443", netip.AddrPortFrom(netip.AddrFrom16([16]byte{0x20, 0x01, 0x0d, 0xb8, 12: 0, 0, 0, 1}), 443)}, // Documentation address with HTTPS {"[fe80::1]:8080", netip.AddrPortFrom(netip.AddrFrom16([16]byte{0xfe, 0x80, 14: 0, 1}), 8080)}, // Link-local with alt HTTP {"[::]:22", netip.AddrPortFrom(netip.AddrFrom16([16]byte{}), 22)}, // IPv6 zero address with SSH {"[::ffff:192.0.2.1]:80", netip.AddrPortFrom(netip.AddrFrom16([16]byte{10: 0xff, 0xff, 0xc0, 0x00, 0x02, 0x01}), 80)}, // IPv4-mapped IPv6 with HTTP }, fail: []decodeHookFailureTestCase[string, netip.AddrPort]{ {"5"}, // Just a number {"192.168.1.1"}, // Missing port {"192.168.1.1:"}, // Empty port {"192.168.1.1:65536"}, // Port too large {"192.168.1.1:-1"}, // Negative port {"192.168.1.1:abc"}, // Non-numeric port {"256.1.1.1:80"}, // Invalid IP {"::1:80"}, // IPv6 without brackets {"[::1"}, // Missing closing bracket {"::1]:80"}, // Missing opening bracket {"[invalid::ip]:80"}, // Invalid IPv6 {""}, // Empty string {":80"}, // Missing IP {"192.168.1.1:80:443"}, // Multiple ports }, } suite.Run(t) } func TestStringToNetIPPrefixHookFunc(t *testing.T) { suite := decodeHookTestSuite[string, netip.Prefix]{ fn: StringToNetIPPrefixHookFunc(), ok: []decodeHookTestCase[string, netip.Prefix]{ // IPv4 CIDR notation {"192.0.2.1/24", netip.PrefixFrom(netip.AddrFrom4([4]byte{0xc0, 0x00, 0x02, 0x01}), 24)}, // Documentation network {"127.0.0.1/32", netip.PrefixFrom(netip.AddrFrom4([4]byte{127, 0, 0, 1}), 32)}, // Localhost single host {"10.0.0.0/8", netip.PrefixFrom(netip.AddrFrom4([4]byte{10, 0, 0, 0}), 8)}, // Class A private network {"192.168.1.0/24", netip.PrefixFrom(netip.AddrFrom4([4]byte{192, 168, 1, 0}), 24)}, // Class C private network {"172.16.0.0/12", netip.PrefixFrom(netip.AddrFrom4([4]byte{172, 16, 0, 0}), 12)}, // Class B private network {"0.0.0.0/0", netip.PrefixFrom(netip.AddrFrom4([4]byte{0, 0, 0, 0}), 0)}, // Default route {"255.255.255.255/32", netip.PrefixFrom(netip.AddrFrom4([4]byte{255, 255, 255, 255}), 32)}, // Broadcast single host {"192.168.1.1/30", netip.PrefixFrom(netip.AddrFrom4([4]byte{192, 168, 1, 1}), 30)}, // Point-to-point subnet // IPv6 CIDR notation {"fd7a:115c::626b:430b/118", netip.PrefixFrom(netip.AddrFrom16([16]byte{0xfd, 0x7a, 0x11, 0x5c, 12: 0x62, 0x6b, 0x43, 0x0b}), 118)}, // ULA with specific prefix {"2001:db8::/32", netip.PrefixFrom(netip.AddrFrom16([16]byte{0x20, 0x01, 0x0d, 0xb8}), 32)}, // Documentation network {"::1/128", netip.PrefixFrom(netip.AddrFrom16([16]byte{15: 1}), 128)}, // IPv6 localhost single host {"::/0", netip.PrefixFrom(netip.AddrFrom16([16]byte{}), 0)}, // IPv6 default route {"fe80::/10", netip.PrefixFrom(netip.AddrFrom16([16]byte{0xfe, 0x80}), 10)}, // Link-local network {"2001:db8::1/64", netip.PrefixFrom(netip.AddrFrom16([16]byte{0x20, 0x01, 0x0d, 0xb8, 12: 0, 0, 0, 1}), 64)}, // Standard IPv6 subnet {"::ffff:0:0/96", netip.PrefixFrom(netip.AddrFrom16([16]byte{10: 0xff, 0xff}), 96)}, // IPv4-mapped IPv6 prefix }, fail: []decodeHookFailureTestCase[string, netip.Prefix]{ {"5"}, // Just a number {"192.168.1.1"}, // Missing prefix length {"192.168.1.1/"}, // Empty prefix length {"192.168.1.1/33"}, // IPv4 prefix too large {"192.168.1.1/-1"}, // Negative prefix {"192.168.1.1/abc"}, // Non-numeric prefix {"256.1.1.1/24"}, // Invalid IP {"::1/129"}, // IPv6 prefix too large {"invalid::ip/64"}, // Invalid IPv6 {""}, // Empty string {"/24"}, // Missing IP {"192.168.1.1/24/8"}, // Multiple prefixes }, } suite.Run(t) } func TestStringToBasicTypeHookFunc(t *testing.T) { strValue := reflect.ValueOf("42") cases := []struct { f, t reflect.Value result any err bool }{ {strValue, strValue, "42", false}, {strValue, reflect.ValueOf(int8(0)), int8(42), false}, {strValue, reflect.ValueOf(uint8(0)), uint8(42), false}, {strValue, reflect.ValueOf(int16(0)), int16(42), false}, {strValue, reflect.ValueOf(uint16(0)), uint16(42), false}, {strValue, reflect.ValueOf(int32(0)), int32(42), false}, {strValue, reflect.ValueOf(uint32(0)), uint32(42), false}, {strValue, reflect.ValueOf(int64(0)), int64(42), false}, {strValue, reflect.ValueOf(uint64(0)), uint64(42), false}, {strValue, reflect.ValueOf(int(0)), int(42), false}, {strValue, reflect.ValueOf(uint(0)), uint(42), false}, {strValue, reflect.ValueOf(float32(0)), float32(42), false}, {strValue, reflect.ValueOf(float64(0)), float64(42), false}, {reflect.ValueOf("true"), reflect.ValueOf(bool(false)), true, false}, {strValue, reflect.ValueOf(byte(0)), byte(42), false}, {strValue, reflect.ValueOf(rune(0)), rune(42), false}, {strValue, reflect.ValueOf(complex64(0)), complex64(42), false}, {strValue, reflect.ValueOf(complex128(0)), complex128(42), false}, } for i, tc := range cases { f := StringToBasicTypeHookFunc() actual, err := DecodeHookExec(f, tc.f, tc.t) if tc.err != (err != nil) { t.Fatalf("case %d: expected err %#v", i, tc.err) } if !tc.err && !reflect.DeepEqual(actual, tc.result) { t.Fatalf( "case %d: expected %#v, got %#v", i, tc.result, actual) } } } func TestStringToInt8HookFunc(t *testing.T) { suite := decodeHookTestSuite[string, int8]{ fn: StringToInt8HookFunc(), ok: []decodeHookTestCase[string, int8]{ {"42", 42}, // Basic positive decimal {"-42", int8(-42)}, // Basic negative decimal {"0b101010", int8(42)}, // Binary notation {"052", int8(42)}, // Octal notation (legacy) {"0o52", int8(42)}, // Octal notation (modern) {"0x2a", int8(42)}, // Hex notation (lowercase) {"0X2A", int8(42)}, // Hex notation (uppercase) {"0", int8(0)}, // Zero {"+42", int8(42)}, // Explicit positive sign // Boundary values {"127", int8(127)}, // Max value {"-128", int8(-128)}, // Min value {"0x7F", int8(127)}, // Max value in hex {"-0x80", int8(-128)}, // Min value in hex {"0177", int8(127)}, // Max value in octal {"-0200", int8(-128)}, // Min value in octal {"0b01111111", int8(127)}, // Max value in binary {"-0b10000000", int8(-128)}, // Min value in binary // Zero variants {"+0", int8(0)}, // Explicit positive zero {"-0", int8(0)}, // Explicit negative zero {"00", int8(0)}, // Leading zero {"0x0", int8(0)}, // Hex zero {"0b0", int8(0)}, // Binary zero {"0o0", int8(0)}, // Octal zero }, fail: []decodeHookFailureTestCase[string, int8]{ {strings.Repeat("42", 42)}, // Very long number string {"128"}, // Overflow {"-129"}, // Underflow {"256"}, // Way over max {"-256"}, // Way under min {"42.5"}, // Float {"abc"}, // Non-numeric {""}, // Empty string {" 42 "}, // Whitespace not handled by strconv {"\t42\n"}, // Whitespace not handled by strconv {"\r42\r"}, // Whitespace not handled by strconv {"0x"}, // Invalid hex {"0b"}, // Invalid binary {"0o"}, // Invalid octal {"++42"}, // Double plus {"--42"}, // Double minus {"42abc"}, // Trailing non-numeric {"abc42"}, // Leading non-numeric {"42 43"}, // Multiple numbers {"0x10000"}, // Hex overflow {"-0x10001"}, // Hex underflow {"0777"}, // Octal overflow {"-01000"}, // Octal underflow {"0b100000000"}, // Binary overflow {"-0b100000001"}, // Binary underflow }, } suite.Run(t) } func TestStringToUint8HookFunc(t *testing.T) { suite := decodeHookTestSuite[string, uint8]{ fn: StringToUint8HookFunc(), ok: []decodeHookTestCase[string, uint8]{ {"42", 42}, // Basic decimal {"0b101010", uint8(42)}, // Binary notation {"052", uint8(42)}, // Octal notation (legacy) {"0o52", uint8(42)}, // Octal notation (modern) {"0x2a", uint8(42)}, // Hex notation (lowercase) {"0X2A", uint8(42)}, // Hex notation (uppercase) {"0", uint8(0)}, // Zero // Boundary values {"255", uint8(255)}, // Max value {"0xFF", uint8(255)}, // Max value in hex {"0377", uint8(255)}, // Max value in octal {"0b11111111", uint8(255)}, // Max value in binary {"1", uint8(1)}, // Min positive value // Zero variants {"00", uint8(0)}, // Leading zero {"0x0", uint8(0)}, // Hex zero {"0b0", uint8(0)}, // Binary zero {"0o0", uint8(0)}, // Octal zero }, fail: []decodeHookFailureTestCase[string, uint8]{ {strings.Repeat("42", 42)}, {"256"}, // Overflow {"512"}, // Way over max {"42.5"}, // Float {"abc"}, // Non-numeric {""}, // Empty string {"-1"}, // Negative number {"-42"}, // Negative number {"0x"}, // Invalid hex {"0b"}, // Invalid binary {"0o"}, // Invalid octal {"++42"}, // Double plus {" 42 "}, // Whitespace not handled by strconv {"\t42\n"}, // Whitespace not handled by strconv {"\r42\r"}, // Whitespace not handled by strconv {"42abc"}, // Trailing non-numeric {"abc42"}, // Leading non-numeric {"42 43"}, // Multiple numbers {"0x100"}, // Hex overflow {"0400"}, // Octal overflow {"0b100000000"}, // Binary overflow }, } suite.Run(t) } func TestStringToInt16HookFunc(t *testing.T) { suite := decodeHookTestSuite[string, int16]{ fn: StringToInt16HookFunc(), ok: []decodeHookTestCase[string, int16]{ {"42", 42}, {"-42", int16(-42)}, {"0b101010", int16(42)}, {"052", int16(42)}, {"0o52", int16(42)}, {"0x2a", int16(42)}, {"0X2A", int16(42)}, {"0", int16(0)}, }, fail: []decodeHookFailureTestCase[string, int16]{ {strings.Repeat("42", 42)}, {"42.42"}, {"0.0"}, }, } suite.Run(t) } func TestStringToUint16HookFunc(t *testing.T) { suite := decodeHookTestSuite[string, uint16]{ fn: StringToUint16HookFunc(), ok: []decodeHookTestCase[string, uint16]{ {"42", 42}, {"0b101010", uint16(42)}, {"052", uint16(42)}, {"0o52", uint16(42)}, {"0x2a", uint16(42)}, {"0X2A", uint16(42)}, {"0", uint16(0)}, }, fail: []decodeHookFailureTestCase[string, uint16]{ {strings.Repeat("42", 42)}, {"42.42"}, {"-42"}, {"0.0"}, }, } suite.Run(t) } func TestStringToInt32HookFunc(t *testing.T) { suite := decodeHookTestSuite[string, int32]{ fn: StringToInt32HookFunc(), ok: []decodeHookTestCase[string, int32]{ {"42", 42}, {"-42", int32(-42)}, {"0b101010", int32(42)}, {"052", int32(42)}, {"0o52", int32(42)}, {"0x2a", int32(42)}, {"0X2A", int32(42)}, {"0", int32(0)}, }, fail: []decodeHookFailureTestCase[string, int32]{ {strings.Repeat("42", 42)}, {"42.42"}, {"0.0"}, }, } suite.Run(t) } func TestStringToUint32HookFunc(t *testing.T) { suite := decodeHookTestSuite[string, uint32]{ fn: StringToUint32HookFunc(), ok: []decodeHookTestCase[string, uint32]{ {"42", 42}, {"0b101010", uint32(42)}, {"052", uint32(42)}, {"0o52", uint32(42)}, {"0x2a", uint32(42)}, {"0X2A", uint32(42)}, {"0", uint32(0)}, }, fail: []decodeHookFailureTestCase[string, uint32]{ {strings.Repeat("42", 42)}, {"42.42"}, {"-42"}, {"0.0"}, }, } suite.Run(t) } func TestStringToInt64HookFunc(t *testing.T) { suite := decodeHookTestSuite[string, int64]{ fn: StringToInt64HookFunc(), ok: []decodeHookTestCase[string, int64]{ {"42", 42}, {"-42", int64(-42)}, {"0b101010", int64(42)}, {"052", int64(42)}, {"0o52", int64(42)}, {"0x2a", int64(42)}, {"0X2A", int64(42)}, {"0", int64(0)}, }, fail: []decodeHookFailureTestCase[string, int64]{ {strings.Repeat("42", 42)}, {"42.42"}, {"0.0"}, }, } suite.Run(t) } func TestStringToUint64HookFunc(t *testing.T) { suite := decodeHookTestSuite[string, uint64]{ fn: StringToUint64HookFunc(), ok: []decodeHookTestCase[string, uint64]{ {"42", 42}, {"0b101010", uint64(42)}, {"052", uint64(42)}, {"0o52", uint64(42)}, {"0x2a", uint64(42)}, {"0X2A", uint64(42)}, {"0", uint64(0)}, }, fail: []decodeHookFailureTestCase[string, uint64]{ {strings.Repeat("42", 42)}, {"42.42"}, {"-42"}, {"0.0"}, }, } suite.Run(t) } func TestStringToIntHookFunc(t *testing.T) { suite := decodeHookTestSuite[string, int]{ fn: StringToIntHookFunc(), ok: []decodeHookTestCase[string, int]{ {"42", 42}, {"-42", int(-42)}, {"0b101010", int(42)}, {"052", int(42)}, {"0o52", int(42)}, {"0x2a", int(42)}, {"0X2A", int(42)}, {"0", int(0)}, }, fail: []decodeHookFailureTestCase[string, int]{ {strings.Repeat("42", 42)}, {"42.42"}, {"0.0"}, }, } suite.Run(t) } func TestStringToUintHookFunc(t *testing.T) { suite := decodeHookTestSuite[string, uint]{ fn: StringToUintHookFunc(), ok: []decodeHookTestCase[string, uint]{ {"42", 42}, {"0b101010", uint(42)}, {"052", uint(42)}, {"0o52", uint(42)}, {"0x2a", uint(42)}, {"0X2A", uint(42)}, {"0", uint(0)}, }, fail: []decodeHookFailureTestCase[string, uint]{ {strings.Repeat("42", 42)}, {"42.42"}, {"-42"}, {"0.0"}, }, } suite.Run(t) } func TestStringToFloat32HookFunc(t *testing.T) { suite := decodeHookTestSuite[string, float32]{ fn: StringToFloat32HookFunc(), ok: []decodeHookTestCase[string, float32]{ {"42.42", float32(42.42)}, // Basic decimal {"-42.42", float32(-42.42)}, // Negative decimal {"0", float32(0)}, // Zero as integer {"1e3", float32(1000)}, // Scientific notation {"1e-3", float32(0.001)}, // Small scientific notation // Integer values {"42", float32(42)}, // Positive integer {"-42", float32(-42)}, // Negative integer {"+42", float32(42)}, // Explicit positive integer // Zero variants {"0.0", float32(0.0)}, // Zero with decimal {"+0", float32(0)}, // Explicit positive zero {"-0", float32(0)}, // Explicit negative zero {"00.00", float32(0)}, // Zero with leading zeros // Scientific notation {"1E3", float32(1000)}, // Scientific notation (uppercase E) {"1.5e2", float32(150)}, // Fractional base with exponent {"1.5E2", float32(150)}, // Fractional base with uppercase E {"-1.5e2", float32(-150)}, // Negative fractional with exponent {"1e+3", float32(1000)}, // Explicit positive exponent {"1e-10", float32(1e-10)}, // Very small exponent {"3.14159", float32(3.14159)}, // Pi approximation // Special values - infinity {"inf", float32(math.Inf(1))}, // Infinity (lowercase) {"+inf", float32(math.Inf(1))}, // Positive infinity {"-inf", float32(math.Inf(-1))}, // Negative infinity {"Inf", float32(math.Inf(1))}, // Infinity (capitalized) {"+Inf", float32(math.Inf(1))}, // Positive infinity (capitalized) {"-Inf", float32(math.Inf(-1))}, // Negative infinity (capitalized) {"infinity", float32(math.Inf(1))}, // Infinity (full word) {"+infinity", float32(math.Inf(1))}, // Positive infinity (full word) {"-infinity", float32(math.Inf(-1))}, // Negative infinity (full word) {"Infinity", float32(math.Inf(1))}, // Infinity (full word capitalized) {"+Infinity", float32(math.Inf(1))}, // Positive infinity (full word capitalized) {"-Infinity", float32(math.Inf(-1))}, // Negative infinity (full word capitalized) // Decimal variations {".5", float32(0.5)}, // Leading decimal point {"-.5", float32(-0.5)}, // Negative leading decimal {"+.5", float32(0.5)}, // Positive leading decimal {"5.", float32(5.0)}, // Trailing decimal point {"-5.", float32(-5.0)}, // Negative trailing decimal {"+5.", float32(5.0)}, // Positive trailing decimal // Very small and large numbers {"1.1754943508222875e-38", float32(1.1754943508222875e-38)}, // Near min positive {"3.4028234663852886e+38", float32(3.4028234663852886e+38)}, // Near max }, fail: []decodeHookFailureTestCase[string, float32]{ {strings.Repeat("42", 420)}, {"42.42.42"}, {"abc"}, // Non-numeric {""}, // Empty string {"42abc"}, // Trailing non-numeric {"abc42"}, // Leading non-numeric {"42 43"}, // Multiple numbers {"++42"}, // Double plus {"--42"}, // Double minus {"1e"}, // Incomplete scientific notation {"1e+"}, // Incomplete scientific notation {"1e-"}, // Incomplete scientific notation {"1.2.3"}, // Multiple dots {"1..2"}, // Double dots {"."}, // Just a dot {" 42.5 "}, // Whitespace not handled by strconv {"\t42.5\n"}, // Whitespace not handled by strconv {"\r42.5\r"}, // Whitespace not handled by strconv {" 42.5 "}, // Whitespace not handled by strconv {"\t42.5\n"}, // Whitespace not handled by strconv {"\r42.5\r"}, // Whitespace not handled by strconv {"1e1e1"}, // Multiple exponents {"∞"}, // Unicode infinity {"NaΝ"}, // Unicode NaN lookalike }, } // Custom test for NaN since NaN != NaN t.Run("NaN", func(t *testing.T) { f := StringToFloat32HookFunc() actual, err := DecodeHookExec(f, reflect.ValueOf("nan"), reflect.ValueOf(float32(0))) if err != nil { t.Fatalf("unexpected error: %s", err) } if !math.IsNaN(float64(actual.(float32))) { t.Fatalf("expected NaN, got %v", actual) } }) suite.Run(t) } func TestStringToFloat64HookFunc(t *testing.T) { suite := decodeHookTestSuite[string, float64]{ fn: StringToFloat64HookFunc(), ok: []decodeHookTestCase[string, float64]{ {"42.42", float64(42.42)}, // Basic decimal {"-42.42", float64(-42.42)}, // Negative decimal {"0", float64(0)}, // Zero as integer {"0.0", float64(0)}, // Zero with decimal {"1e3", float64(1000)}, // Scientific notation {"1e-3", float64(0.001)}, // Small scientific notation // Integer values {"42", float64(42)}, // Positive integer {"-42", float64(-42)}, // Negative integer {"+42", float64(42)}, // Explicit positive integer // Zero variants {"+0", float64(0)}, // Explicit positive zero {"-0", float64(0)}, // Explicit negative zero {"00.00", float64(0)}, // Zero with leading zeros // Scientific notation {"1E3", float64(1000)}, // Scientific notation (uppercase E) {"1.5e2", float64(150)}, // Fractional base with exponent {"1.5E2", float64(150)}, // Fractional base with uppercase E {"-1.5e2", float64(-150)}, // Negative fractional with exponent {"1e+3", float64(1000)}, // Explicit positive exponent {"1e-15", float64(1e-15)}, // Very small exponent {"3.141592653589793", float64(3.141592653589793)}, // Pi with high precision // Special values - infinity {"inf", math.Inf(1)}, // Infinity (lowercase) {"+inf", math.Inf(1)}, // Positive infinity {"-inf", math.Inf(-1)}, // Negative infinity {"Inf", math.Inf(1)}, // Infinity (capitalized) {"+Inf", math.Inf(1)}, // Positive infinity (capitalized) {"-Inf", math.Inf(-1)}, // Negative infinity (capitalized) {"infinity", math.Inf(1)}, // Infinity (full word) {"+infinity", math.Inf(1)}, // Positive infinity (full word) {"-infinity", math.Inf(-1)}, // Negative infinity (full word) {"Infinity", math.Inf(1)}, // Infinity (full word capitalized) {"+Infinity", math.Inf(1)}, // Positive infinity (full word capitalized) {"-Infinity", math.Inf(-1)}, // Negative infinity (full word capitalized) // Decimal variations {".5", float64(0.5)}, // Leading decimal point {"-.5", float64(-0.5)}, // Negative leading decimal {"+.5", float64(0.5)}, // Positive leading decimal {"5.", float64(5.0)}, // Trailing decimal point {"-5.", float64(-5.0)}, // Negative trailing decimal {"+5.", float64(5.0)}, // Positive trailing decimal // Very small and large numbers {"2.2250738585072014e-308", float64(2.2250738585072014e-308)}, // Near min positive {"1.7976931348623157e+308", float64(1.7976931348623157e+308)}, // Near max {"4.9406564584124654e-324", float64(4.9406564584124654e-324)}, // Min positive subnormal }, fail: []decodeHookFailureTestCase[string, float64]{ {strings.Repeat("42", 420)}, {"42.42.42"}, {"abc"}, // Non-numeric {""}, // Empty string {"42abc"}, // Trailing non-numeric {"abc42"}, // Leading non-numeric {"42 43"}, // Multiple numbers {"++42"}, // Double plus {"--42"}, // Double minus {"1e"}, // Incomplete scientific notation {"1e+"}, // Incomplete scientific notation {"1e-"}, // Incomplete scientific notation {"1.2.3"}, // Multiple dots {"1..2"}, // Double dots {"."}, // Just a dot {"1e1e1"}, // Multiple exponents {"∞"}, // Unicode infinity {"NaΝ"}, // Unicode NaN lookalike }, } // Custom test for NaN since NaN != NaN t.Run("NaN", func(t *testing.T) { f := StringToFloat64HookFunc() actual, err := DecodeHookExec(f, reflect.ValueOf("nan"), reflect.ValueOf(float64(0))) if err != nil { t.Fatalf("unexpected error: %s", err) } if !math.IsNaN(actual.(float64)) { t.Fatalf("expected NaN, got %v", actual) } }) suite.Run(t) } func TestStringToComplex64HookFunc(t *testing.T) { suite := decodeHookTestSuite[string, complex64]{ fn: StringToComplex64HookFunc(), ok: []decodeHookTestCase[string, complex64]{ // Standard complex numbers {"42.42+42.42i", complex(float32(42.42), float32(42.42))}, // Basic complex number {"1+2i", complex(float32(1), float32(2))}, // Simple complex number {"-1-2i", complex(float32(-1), float32(-2))}, // Negative real and imaginary {"1-2i", complex(float32(1), float32(-2))}, // Positive real, negative imaginary {"-1+2i", complex(float32(-1), float32(2))}, // Negative real, positive imaginary // Real numbers only {"-42.42", complex(float32(-42.42), 0)}, // Negative real number {"42", complex(float32(42), 0)}, // Positive integer {"+42", complex(float32(42), 0)}, // Explicit positive integer {"0", complex(float32(0), 0)}, // Zero {"0.0", complex(float32(0), 0)}, // Zero with decimal {"+0", complex(float32(0), 0)}, // Explicit positive zero {"-0", complex(float32(0), 0)}, // Explicit negative zero // Scientific notation {"1e3", complex(float32(1000), 0)}, // Scientific notation {"1e-3", complex(float32(0.001), 0)}, // Small scientific notation {"1E3", complex(float32(1000), 0)}, // Uppercase E {"1e+3", complex(float32(1000), 0)}, // Explicit positive exponent {"1.5e2", complex(float32(150), 0)}, // Fractional with exponent {"-1.5e2", complex(float32(-150), 0)}, // Negative fractional with exponent // Imaginary numbers only {"1e3i", complex(float32(0), 1000)}, // Scientific notation imaginary {"1e-3i", complex(float32(0), 0.001)}, // Small scientific notation imaginary {"42i", complex(float32(0), 42)}, // Basic imaginary {"-42i", complex(float32(0), -42)}, // Negative imaginary {"+42i", complex(float32(0), 42)}, // Explicit positive imaginary {"0i", complex(float32(0), 0)}, // Zero imaginary {"1i", complex(float32(0), 1)}, // Unit imaginary {"-1i", complex(float32(0), -1)}, // Negative unit imaginary {"1.5i", complex(float32(0), 1.5)}, // Fractional imaginary // Scientific notation imaginary {"1E3i", complex(float32(0), 1000)}, // Uppercase E imaginary {"1e+3i", complex(float32(0), 1000)}, // Explicit positive exponent imaginary {"1.5e2i", complex(float32(0), 150)}, // Fractional with exponent imaginary {"-1.5e2i", complex(float32(0), -150)}, // Negative fractional with exponent imaginary // Complex with scientific notation {"1e3+2e2i", complex(float32(1000), float32(200))}, // Both parts scientific {"1e-3+2e-2i", complex(float32(0.001), float32(0.02))}, // Both parts small scientific {"1.5e2-2.5e1i", complex(float32(150), float32(-25))}, // Mixed signs with scientific // Decimal variations {".5", complex(float32(0.5), 0)}, // Leading decimal point {"-.5", complex(float32(-0.5), 0)}, // Negative leading decimal {"+.5", complex(float32(0.5), 0)}, // Positive leading decimal {"5.", complex(float32(5.0), 0)}, // Trailing decimal point {"-5.", complex(float32(-5.0), 0)}, // Negative trailing decimal {"+5.", complex(float32(5.0), 0)}, // Positive trailing decimal {".5i", complex(float32(0), 0.5)}, // Leading decimal imaginary {"-.5i", complex(float32(0), -0.5)}, // Negative leading decimal imaginary {"+.5i", complex(float32(0), 0.5)}, // Positive leading decimal imaginary {"5.i", complex(float32(0), 5.0)}, // Trailing decimal imaginary {"-5.i", complex(float32(0), -5.0)}, // Negative trailing decimal imaginary {"+5.i", complex(float32(0), 5.0)}, // Positive trailing decimal imaginary // Complex decimal variations {".5+.5i", complex(float32(0.5), float32(0.5))}, // Both parts leading decimal {"5.+5.i", complex(float32(5.0), float32(5.0))}, // Both parts trailing decimal {".5-.5i", complex(float32(0.5), float32(-0.5))}, // Leading decimal with negative // Special values - infinity {"inf", complex(float32(math.Inf(1)), 0)}, // Real infinity {"+inf", complex(float32(math.Inf(1)), 0)}, // Positive real infinity {"-inf", complex(float32(math.Inf(-1)), 0)}, // Negative real infinity {"Inf", complex(float32(math.Inf(1)), 0)}, // Capitalized infinity {"infinity", complex(float32(math.Inf(1)), 0)}, // Full word infinity {"Infinity", complex(float32(math.Inf(1)), 0)}, // Capitalized full word infinity {"infi", complex(float32(0), float32(math.Inf(1)))}, // Imaginary infinity {"+infi", complex(float32(0), float32(math.Inf(1)))}, // Positive imaginary infinity {"-infi", complex(float32(0), float32(math.Inf(-1)))}, // Negative imaginary infinity {"Infi", complex(float32(0), float32(math.Inf(1)))}, // Capitalized imaginary infinity {"infinityi", complex(float32(0), float32(math.Inf(1)))}, // Full word imaginary infinity {"Infinityi", complex(float32(0), float32(math.Inf(1)))}, // Capitalized full word imaginary infinity // Complex with special values {"inf+1i", complex(float32(math.Inf(1)), float32(1))}, // Real infinity with imaginary {"1+infi", complex(float32(1), float32(math.Inf(1)))}, // Real with imaginary infinity {"inf+infi", complex(float32(math.Inf(1)), float32(math.Inf(1)))}, // Both infinities {"-inf-infi", complex(float32(math.Inf(-1)), float32(math.Inf(-1)))}, // Both negative infinities // Parentheses format {"(42+42i)", complex(float32(42), float32(42))}, // Complex in parentheses {"(42)", complex(float32(42), float32(0))}, // Real in parentheses {"(42i)", complex(float32(0), float32(42))}, // Imaginary in parentheses {"(-42-42i)", complex(float32(-42), float32(-42))}, // Negative complex in parentheses }, fail: []decodeHookFailureTestCase[string, complex64]{ {strings.Repeat("42", 420)}, {"42.42.42"}, {"abc"}, // Non-numeric {""}, // Empty string {"42abc"}, // Trailing non-numeric {"abc42"}, // Leading non-numeric {"42+abc"}, // Invalid imaginary part {"abc+42i"}, // Invalid real part {"42++42i"}, // Double plus {"42+-+42i"}, // Multiple signs {"42+42j"}, // Wrong imaginary unit {"42+42k"}, // Wrong imaginary unit {"42 + 42i"}, // Spaces around operator {"42+42i+1"}, // Extra components {"42+42i+1i"}, // Multiple imaginary parts {"42+42i+1+2i"}, // Too many components {"(42+42i"}, // Unclosed parenthesis {"42+42i)"}, // Extra closing parenthesis {"((42+42i))"}, // Double parentheses {"(42+42i)(1+1i)"}, // Multiple complex numbers {"42i+42"}, // Imaginary first (not standard) {"i"}, // Just 'i' {"42.42.42+1i"}, // Invalid real part {"42+42.42.42i"}, // Invalid imaginary part {"1e"}, // Incomplete scientific notation {"1e+"}, // Incomplete scientific notation {"1e-"}, // Incomplete scientific notation {"1e+i"}, // Incomplete scientific notation {"1.2.3+1i"}, // Multiple dots in real {"1+1.2.3i"}, // Multiple dots in imaginary {"1..2+1i"}, // Double dots in real {"1+1..2i"}, // Double dots in imaginary {".+.i"}, // Just dots {"1e1e1+1i"}, // Multiple exponents in real {" 42+42i "}, // Whitespace not handled by strconv {"\t42i\n"}, // Whitespace not handled by strconv {"\r42\r"}, // Whitespace not handled by strconv {" 42+42i "}, // Whitespace not handled by strconv {"\t42i\n"}, // Whitespace not handled by strconv {"\r42\r"}, // Whitespace not handled by strconv {"1+1e1e1i"}, // Multiple exponents in imaginary {"∞"}, // Unicode infinity {"∞+∞i"}, // Unicode infinity complex {"NaΝ"}, // Unicode NaN lookalike }, } // Custom test for NaN since NaN != NaN t.Run("NaN", func(t *testing.T) { f := StringToComplex64HookFunc() // Test real NaN actual, err := DecodeHookExec(f, reflect.ValueOf("nan"), reflect.ValueOf(complex64(0))) if err != nil { t.Fatalf("unexpected error: %s", err) } c := actual.(complex64) if !math.IsNaN(float64(real(c))) || imag(c) != 0 { t.Fatalf("expected NaN+0i, got %v", c) } // Test imaginary NaN actual, err = DecodeHookExec(f, reflect.ValueOf("nani"), reflect.ValueOf(complex64(0))) if err != nil { t.Fatalf("unexpected error: %s", err) } c = actual.(complex64) if real(c) != 0 || !math.IsNaN(float64(imag(c))) { t.Fatalf("expected 0+NaNi, got %v", c) } // Test complex NaN actual, err = DecodeHookExec(f, reflect.ValueOf("nan+nani"), reflect.ValueOf(complex64(0))) if err != nil { t.Fatalf("unexpected error: %s", err) } c = actual.(complex64) if !math.IsNaN(float64(real(c))) || !math.IsNaN(float64(imag(c))) { t.Fatalf("expected NaN+NaNi, got %v", c) } }) suite.Run(t) } func TestStringToBoolHookFunc(t *testing.T) { suite := decodeHookTestSuite[string, bool]{ fn: StringToBoolHookFunc(), ok: []decodeHookTestCase[string, bool]{ // True values (only those accepted by strconv.ParseBool) {"true", true}, // Boolean true (lowercase) {"True", true}, // Boolean true (capitalized) {"TRUE", true}, // Boolean true (uppercase) {"t", true}, // Single character true (lowercase) {"T", true}, // Single character true (uppercase) {"1", true}, // Numeric true // False values (only those accepted by strconv.ParseBool) {"false", false}, // Boolean false (lowercase) {"False", false}, // Boolean false (capitalized) {"FALSE", false}, // Boolean false (uppercase) {"f", false}, // Single character false (lowercase) {"F", false}, // Single character false (uppercase) {"0", false}, // Numeric false }, fail: []decodeHookFailureTestCase[string, bool]{ {""}, // Empty string {"maybe"}, // Invalid boolean word {"yes"}, // Not accepted by strconv.ParseBool {"no"}, // Not accepted by strconv.ParseBool {"on"}, // Not accepted by strconv.ParseBool {"off"}, // Not accepted by strconv.ParseBool {"y"}, // Not accepted by strconv.ParseBool {"n"}, // Not accepted by strconv.ParseBool {"yes please"}, // Invalid boolean phrase {"true false"}, // Multiple boolean values {"2"}, // Invalid number (only 0/1 accepted) {"-1"}, // Negative number {"10"}, // Number greater than 1 {"abc"}, // Non-boolean text {"True False"}, // Multiple boolean values (capitalized) {"1.0"}, // Float representation of 1 {"0.0"}, // Float representation of 0 {"++true"}, // Double positive prefix {"--false"}, // Double negative prefix {"truee"}, // Typo in true {"fasle"}, // Typo in false {"tru"}, // Incomplete true {"fals"}, // Incomplete false {" true "}, // Whitespace not handled by strconv.ParseBool {"\ttrue\n"}, // Tab and newline whitespace {"\rfalse\r"}, // Carriage return whitespace {" 1 "}, // Whitespace around numeric true {" 0 "}, // Whitespace around numeric false {"∞"}, // Unicode infinity symbol {"тrue"}, // Cyrillic lookalike characters }, } // Test non-string and non-bool type passthrough t.Run("Passthrough", func(t *testing.T) { f := StringToBoolHookFunc() // Non-string type should pass through intValue := reflect.ValueOf(42) actual, err := DecodeHookExec(f, intValue, reflect.ValueOf(false)) if err != nil { t.Fatalf("unexpected error: %s", err) } if actual != 42 { t.Fatalf("expected 42, got %v", actual) } // Non-bool target type should pass through strValue := reflect.ValueOf("true") actual, err = DecodeHookExec(f, strValue, strValue) if err != nil { t.Fatalf("unexpected error: %s", err) } if actual != "true" { t.Fatalf("expected 'true', got %v", actual) } }) suite.Run(t) } func TestStringToComplex128HookFunc(t *testing.T) { suite := decodeHookTestSuite[string, complex128]{ fn: StringToComplex128HookFunc(), ok: []decodeHookTestCase[string, complex128]{ // Standard complex numbers {"42.42+42.42i", complex(42.42, 42.42)}, // Basic complex number {"1+2i", complex(1, 2)}, // Simple complex number {"-1-2i", complex(-1, -2)}, // Negative real and imaginary {"1-2i", complex(1, -2)}, // Positive real, negative imaginary {"-1+2i", complex(-1, 2)}, // Negative real, positive imaginary // Real numbers only {"-42.42", complex(-42.42, 0)}, // Negative real number {"42", complex(42, 0)}, // Positive integer {"+42", complex(42, 0)}, // Explicit positive integer {"0", complex(0, 0)}, // Zero {"0.0", complex(0, 0)}, // Zero with decimal {"+0", complex(0, 0)}, // Explicit positive zero {"-0", complex(0, 0)}, // Explicit negative zero // Scientific notation {"1e3", complex(1000, 0)}, // Scientific notation {"1e-3", complex(0.001, 0)}, // Small scientific notation {"1E3", complex(1000, 0)}, // Uppercase E {"1e+3", complex(1000, 0)}, // Explicit positive exponent {"1.5e2", complex(150, 0)}, // Fractional with exponent {"-1.5e2", complex(-150, 0)}, // Negative fractional with exponent {"1e-15", complex(1e-15, 0)}, // Very small scientific notation {"3.141592653589793", complex(3.141592653589793, 0)}, // Pi with high precision // Imaginary numbers only {"1e3i", complex(0, 1000)}, // Scientific notation imaginary {"1e-3i", complex(0, 0.001)}, // Small scientific notation imaginary {"42i", complex(0, 42)}, // Basic imaginary {"-42i", complex(0, -42)}, // Negative imaginary {"+42i", complex(0, 42)}, // Explicit positive imaginary {"0i", complex(0, 0)}, // Zero imaginary {"1i", complex(0, 1)}, // Unit imaginary {"-1i", complex(0, -1)}, // Negative unit imaginary {"1.5i", complex(0, 1.5)}, // Fractional imaginary // Scientific notation imaginary {"1E3i", complex(0, 1000)}, // Uppercase E imaginary {"1e+3i", complex(0, 1000)}, // Explicit positive exponent imaginary {"1.5e2i", complex(0, 150)}, // Fractional with exponent imaginary {"-1.5e2i", complex(0, -150)}, // Negative fractional with exponent imaginary {"1e-15i", complex(0, 1e-15)}, // Very small scientific notation imaginary // Complex with scientific notation {"1e3+2e2i", complex(1000, 200)}, // Both parts scientific {"1e-3+2e-2i", complex(0.001, 0.02)}, // Both parts small scientific {"1.5e2-2.5e1i", complex(150, -25)}, // Mixed signs with scientific {"1e-15+1e-15i", complex(1e-15, 1e-15)}, // Both parts very small scientific // Decimal variations {".5", complex(0.5, 0)}, // Leading decimal point {"-.5", complex(-0.5, 0)}, // Negative leading decimal {"+.5", complex(0.5, 0)}, // Positive leading decimal {"5.", complex(5.0, 0)}, // Trailing decimal point {"-5.", complex(-5.0, 0)}, // Negative trailing decimal {"+5.", complex(5.0, 0)}, // Positive trailing decimal {".5i", complex(0, 0.5)}, // Leading decimal imaginary {"-.5i", complex(0, -0.5)}, // Negative leading decimal imaginary {"+.5i", complex(0, 0.5)}, // Positive leading decimal imaginary {"5.i", complex(0, 5.0)}, // Trailing decimal imaginary {"-5.i", complex(0, -5.0)}, // Negative trailing decimal imaginary {"+5.i", complex(0, 5.0)}, // Positive trailing decimal imaginary // Complex decimal variations {".5+.5i", complex(0.5, 0.5)}, // Both parts leading decimal {"5.+5.i", complex(5.0, 5.0)}, // Both parts trailing decimal {".5-.5i", complex(0.5, -0.5)}, // Leading decimal with negative // Very small and large numbers {"2.2250738585072014e-308", complex(2.2250738585072014e-308, 0)}, // Near min positive real {"1.7976931348623157e+308", complex(1.7976931348623157e+308, 0)}, // Near max real {"4.9406564584124654e-324", complex(4.9406564584124654e-324, 0)}, // Min positive subnormal real {"2.2250738585072014e-308i", complex(0, 2.2250738585072014e-308)}, // Near min positive imaginary {"1.7976931348623157e+308i", complex(0, 1.7976931348623157e+308)}, // Near max imaginary {"4.9406564584124654e-324i", complex(0, 4.9406564584124654e-324)}, // Min positive subnormal imaginary // Special values - infinity {"inf", complex(math.Inf(1), 0)}, // Real infinity {"+inf", complex(math.Inf(1), 0)}, // Positive real infinity {"-inf", complex(math.Inf(-1), 0)}, // Negative real infinity {"Inf", complex(math.Inf(1), 0)}, // Capitalized infinity {"infinity", complex(math.Inf(1), 0)}, // Full word infinity {"Infinity", complex(math.Inf(1), 0)}, // Capitalized full word infinity {"infi", complex(0, math.Inf(1))}, // Imaginary infinity {"+infi", complex(0, math.Inf(1))}, // Positive imaginary infinity {"-infi", complex(0, math.Inf(-1))}, // Negative imaginary infinity {"Infi", complex(0, math.Inf(1))}, // Capitalized imaginary infinity {"infinityi", complex(0, math.Inf(1))}, // Full word imaginary infinity {"Infinityi", complex(0, math.Inf(1))}, // Capitalized full word imaginary infinity // Complex with special values {"inf+1i", complex(math.Inf(1), 1)}, // Real infinity with imaginary {"1+infi", complex(1, math.Inf(1))}, // Real with imaginary infinity {"inf+infi", complex(math.Inf(1), math.Inf(1))}, // Both infinities {"-inf-infi", complex(math.Inf(-1), math.Inf(-1))}, // Both negative infinities // Parentheses format {"(42+42i)", complex(42, 42)}, // Complex in parentheses {"(42)", complex(42, 0)}, // Real in parentheses {"(42i)", complex(0, 42)}, // Imaginary in parentheses {"(-42-42i)", complex(-42, -42)}, // Negative complex in parentheses }, fail: []decodeHookFailureTestCase[string, complex128]{ {strings.Repeat("42", 420)}, {"42.42.42"}, {"abc"}, // Non-numeric {""}, // Empty string {"42abc"}, // Trailing non-numeric {"abc42"}, // Leading non-numeric {"42+abc"}, // Invalid imaginary part {"abc+42i"}, // Invalid real part {"42++42i"}, // Double plus {"42+-+42i"}, // Multiple signs {"42+42j"}, // Wrong imaginary unit {"42+42k"}, // Wrong imaginary unit {"42 + 42i"}, // Spaces around operator {"42+42i+1"}, // Extra components {"42+42i+1i"}, // Multiple imaginary parts {"42+42i+1+2i"}, // Too many components {"(42+42i"}, // Unclosed parenthesis {"42+42i)"}, // Extra closing parenthesis {"((42+42i))"}, // Double parentheses {"(42+42i)(1+1i)"}, // Multiple complex numbers {"42i+42"}, // Imaginary first (not standard) {"i"}, // Just 'i' {"42.42.42+1i"}, // Invalid real part {"42+42.42.42i"}, // Invalid imaginary part {"1e"}, // Incomplete scientific notation {"1e+"}, // Incomplete scientific notation {"1e-"}, // Incomplete scientific notation {"1e+i"}, // Incomplete scientific notation {"1.2.3+1i"}, // Multiple dots in real {"1+1.2.3i"}, // Multiple dots in imaginary {"1..2+1i"}, // Double dots in real {"1+1..2i"}, // Double dots in imaginary {".+.i"}, // Just dots {"1e1e1+1i"}, // Multiple exponents in real {"1+1e1e1i"}, // Multiple exponents in imaginary {"∞"}, // Unicode infinity {"∞+∞i"}, // Unicode infinity complex {"NaΝ"}, // Unicode NaN lookalike }, } // Custom test for NaN since NaN != NaN t.Run("NaN", func(t *testing.T) { f := StringToComplex128HookFunc() // Test real NaN actual, err := DecodeHookExec(f, reflect.ValueOf("nan"), reflect.ValueOf(complex128(0))) if err != nil { t.Fatalf("unexpected error: %s", err) } c := actual.(complex128) if !math.IsNaN(real(c)) || imag(c) != 0 { t.Fatalf("expected NaN+0i, got %v", c) } // Test imaginary NaN actual, err = DecodeHookExec(f, reflect.ValueOf("nani"), reflect.ValueOf(complex128(0))) if err != nil { t.Fatalf("unexpected error: %s", err) } c = actual.(complex128) if real(c) != 0 || !math.IsNaN(imag(c)) { t.Fatalf("expected 0+NaNi, got %v", c) } // Test complex NaN actual, err = DecodeHookExec(f, reflect.ValueOf("nan+nani"), reflect.ValueOf(complex128(0))) if err != nil { t.Fatalf("unexpected error: %s", err) } c = actual.(complex128) if !math.IsNaN(real(c)) || !math.IsNaN(imag(c)) { t.Fatalf("expected NaN+NaNi, got %v", c) } }) suite.Run(t) } func TestErrorLeakageDecodeHook(t *testing.T) { cases := []struct { value any target any hook DecodeHookFunc allowNilError bool }{ // case 0 {1234, []string{}, StringToSliceHookFunc(","), true}, {"testing", time.Second, StringToTimeDurationHookFunc(), false}, {":testing", &url.URL{}, StringToURLHookFunc(), false}, {"testing", net.IP{}, StringToIPHookFunc(), false}, {"testing", net.IPNet{}, StringToIPNetHookFunc(), false}, // case 5 {"testing", time.Time{}, StringToTimeHookFunc(time.RFC3339), false}, {"testing", time.Time{}, StringToTimeHookFunc(time.RFC3339), false}, {true, true, WeaklyTypedHook, true}, {true, "string", WeaklyTypedHook, true}, {1.0, "string", WeaklyTypedHook, true}, // case 10 {1, "string", WeaklyTypedHook, true}, {[]uint8{0x00}, "string", WeaklyTypedHook, true}, {uint(0), "string", WeaklyTypedHook, true}, {struct{}{}, struct{}{}, RecursiveStructToMapHookFunc(), true}, {"testing", netip.Addr{}, StringToNetIPAddrHookFunc(), false}, // case 15 {"testing:testing", netip.AddrPort{}, StringToNetIPAddrPortHookFunc(), false}, {"testing", netip.Prefix{}, StringToNetIPPrefixHookFunc(), false}, {"testing", int8(0), StringToInt8HookFunc(), false}, {"testing", uint8(0), StringToUint8HookFunc(), false}, // case 20 {"testing", int16(0), StringToInt16HookFunc(), false}, {"testing", uint16(0), StringToUint16HookFunc(), false}, {"testing", int32(0), StringToInt32HookFunc(), false}, {"testing", uint32(0), StringToUint32HookFunc(), false}, {"testing", int64(0), StringToInt64HookFunc(), false}, // case 25 {"testing", uint64(0), StringToUint64HookFunc(), false}, {"testing", int(0), StringToIntHookFunc(), false}, {"testing", uint(0), StringToUintHookFunc(), false}, {"testing", float32(0), StringToFloat32HookFunc(), false}, {"testing", float64(0), StringToFloat64HookFunc(), false}, // case 30 {"testing", true, StringToBoolHookFunc(), false}, {"testing", byte(0), StringToByteHookFunc(), false}, {"testing", rune(0), StringToRuneHookFunc(), false}, {"testing", complex64(0), StringToComplex64HookFunc(), false}, {"testing", complex128(0), StringToComplex128HookFunc(), false}, } for i, tc := range cases { value := reflect.ValueOf(tc.value) target := reflect.ValueOf(tc.target) output, err := DecodeHookExec(tc.hook, value, target) if err == nil { if tc.allowNilError { continue } t.Fatalf("case %d: expected error from input %v:\n\toutput (%T): %#v\n\toutput (string): %v", i, tc.value, output, output, output) } strValue := fmt.Sprintf("%v", tc.value) if strings.Contains(err.Error(), strValue) { t.Errorf("case %d: error contains input value\n\terr: %v\n\tinput: %v", i, err, strValue) } else { t.Logf("case %d: got safe error: %v", i, err) } } } golang-github-go-viper-mapstructure-2.4.0/errors.go000066400000000000000000000111601510370305500223670ustar00rootroot00000000000000package mapstructure import ( "errors" "fmt" "net" "net/url" "reflect" "strconv" "strings" "time" ) // Error interface is implemented by all errors emitted by mapstructure. // // 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 { name string err error } func newDecodeError(name string, err error) *DecodeError { return &DecodeError{ name: name, err: err, } } 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) } 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 any Err error } func (e *ParseError) Error() string { return fmt.Sprintf("cannot parse value as '%s': %s", 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 any } func (e *UnconvertibleTypeError) Error() string { return fmt.Sprintf( "expected type '%s', got unconvertible type '%s'", e.Expected.Type(), reflect.TypeOf(e.Value), ) } func (*UnconvertibleTypeError) mapstructure() {} func wrapStrconvNumError(err error) error { if err == nil { return nil } if err, ok := err.(*strconv.NumError); ok { return &strconvNumError{Err: err} } return err } type strconvNumError struct { Err *strconv.NumError } func (e *strconvNumError) Error() string { return "strconv." + e.Err.Func + ": " + e.Err.Err.Error() } func (e *strconvNumError) Unwrap() error { return e.Err } func wrapUrlError(err error) error { if err == nil { return nil } if err, ok := err.(*url.Error); ok { return &urlError{Err: err} } return err } type urlError struct { Err *url.Error } func (e *urlError) Error() string { return fmt.Sprintf("%s", e.Err.Err) } func (e *urlError) Unwrap() error { return e.Err } func wrapNetParseError(err error) error { if err == nil { return nil } if err, ok := err.(*net.ParseError); ok { return &netParseError{Err: err} } return err } type netParseError struct { Err *net.ParseError } func (e *netParseError) Error() string { return "invalid " + e.Err.Type } func (e *netParseError) Unwrap() error { return e.Err } func wrapTimeParseError(err error) error { if err == nil { return nil } if err, ok := err.(*time.ParseError); ok { return &timeParseError{Err: err} } return err } type timeParseError struct { Err *time.ParseError } func (e *timeParseError) Error() string { if e.Err.Message == "" { return fmt.Sprintf("parsing time as %q: cannot parse as %q", e.Err.Layout, e.Err.LayoutElem) } return "parsing time " + e.Err.Message } func (e *timeParseError) Unwrap() error { return e.Err } func wrapNetIPParseAddrError(err error) error { if err == nil { return nil } if errMsg := err.Error(); strings.HasPrefix(errMsg, "ParseAddr") { errPieces := strings.Split(errMsg, ": ") return fmt.Errorf("ParseAddr: %s", errPieces[len(errPieces)-1]) } return err } func wrapNetIPParseAddrPortError(err error) error { if err == nil { return nil } errMsg := err.Error() if strings.HasPrefix(errMsg, "invalid port ") { return errors.New("invalid port") } else if strings.HasPrefix(errMsg, "invalid ip:port ") { return errors.New("invalid ip:port") } return err } func wrapNetIPParsePrefixError(err error) error { if err == nil { return nil } if errMsg := err.Error(); strings.HasPrefix(errMsg, "netip.ParsePrefix") { errPieces := strings.Split(errMsg, ": ") return fmt.Errorf("netip.ParsePrefix: %s", errPieces[len(errPieces)-1]) } return err } func wrapTimeParseDurationError(err error) error { if err == nil { return nil } errMsg := err.Error() if strings.HasPrefix(errMsg, "time: unknown unit ") { return errors.New("time: unknown unit") } else if strings.HasPrefix(errMsg, "time: ") { idx := strings.LastIndex(errMsg, " ") return errors.New(errMsg[:idx]) } return err } func wrapTimeParseLocationError(err error) error { if err == nil { return nil } errMsg := err.Error() if strings.Contains(errMsg, "unknown time zone") || strings.HasPrefix(errMsg, "time: unknown format") { return fmt.Errorf("invalid time zone format: %w", err) } return err } golang-github-go-viper-mapstructure-2.4.0/flake.lock000066400000000000000000000165061510370305500224710ustar00rootroot00000000000000{ "nodes": { "cachix": { "inputs": { "devenv": [ "devenv" ], "flake-compat": [ "devenv" ], "git-hooks": [ "devenv" ], "nixpkgs": "nixpkgs" }, "locked": { "lastModified": 1742042642, "narHash": "sha256-D0gP8srrX0qj+wNYNPdtVJsQuFzIng3q43thnHXQ/es=", "owner": "cachix", "repo": "cachix", "rev": "a624d3eaf4b1d225f918de8543ed739f2f574203", "type": "github" }, "original": { "owner": "cachix", "ref": "latest", "repo": "cachix", "type": "github" } }, "devenv": { "inputs": { "cachix": "cachix", "flake-compat": "flake-compat", "git-hooks": "git-hooks", "nix": "nix", "nixpkgs": "nixpkgs_3" }, "locked": { "lastModified": 1744876578, "narHash": "sha256-8MTBj2REB8t29sIBLpxbR0+AEGJ7f+RkzZPAGsFd40c=", "owner": "cachix", "repo": "devenv", "rev": "7ff7c351bba20d0615be25ecdcbcf79b57b85fe1", "type": "github" }, "original": { "owner": "cachix", "repo": "devenv", "type": "github" } }, "flake-compat": { "flake": false, "locked": { "lastModified": 1733328505, "narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=", "owner": "edolstra", "repo": "flake-compat", "rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec", "type": "github" }, "original": { "owner": "edolstra", "repo": "flake-compat", "type": "github" } }, "flake-parts": { "inputs": { "nixpkgs-lib": [ "devenv", "nix", "nixpkgs" ] }, "locked": { "lastModified": 1712014858, "narHash": "sha256-sB4SWl2lX95bExY2gMFG5HIzvva5AVMJd4Igm+GpZNw=", "owner": "hercules-ci", "repo": "flake-parts", "rev": "9126214d0a59633752a136528f5f3b9aa8565b7d", "type": "github" }, "original": { "owner": "hercules-ci", "repo": "flake-parts", "type": "github" } }, "flake-parts_2": { "inputs": { "nixpkgs-lib": "nixpkgs-lib" }, "locked": { "lastModified": 1743550720, "narHash": "sha256-hIshGgKZCgWh6AYJpJmRgFdR3WUbkY04o82X05xqQiY=", "owner": "hercules-ci", "repo": "flake-parts", "rev": "c621e8422220273271f52058f618c94e405bb0f5", "type": "github" }, "original": { "owner": "hercules-ci", "repo": "flake-parts", "type": "github" } }, "git-hooks": { "inputs": { "flake-compat": [ "devenv" ], "gitignore": "gitignore", "nixpkgs": [ "devenv", "nixpkgs" ] }, "locked": { "lastModified": 1742649964, "narHash": "sha256-DwOTp7nvfi8mRfuL1escHDXabVXFGT1VlPD1JHrtrco=", "owner": "cachix", "repo": "git-hooks.nix", "rev": "dcf5072734cb576d2b0c59b2ac44f5050b5eac82", "type": "github" }, "original": { "owner": "cachix", "repo": "git-hooks.nix", "type": "github" } }, "gitignore": { "inputs": { "nixpkgs": [ "devenv", "git-hooks", "nixpkgs" ] }, "locked": { "lastModified": 1709087332, "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", "owner": "hercules-ci", "repo": "gitignore.nix", "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", "type": "github" }, "original": { "owner": "hercules-ci", "repo": "gitignore.nix", "type": "github" } }, "libgit2": { "flake": false, "locked": { "lastModified": 1697646580, "narHash": "sha256-oX4Z3S9WtJlwvj0uH9HlYcWv+x1hqp8mhXl7HsLu2f0=", "owner": "libgit2", "repo": "libgit2", "rev": "45fd9ed7ae1a9b74b957ef4f337bc3c8b3df01b5", "type": "github" }, "original": { "owner": "libgit2", "repo": "libgit2", "type": "github" } }, "nix": { "inputs": { "flake-compat": [ "devenv" ], "flake-parts": "flake-parts", "libgit2": "libgit2", "nixpkgs": "nixpkgs_2", "nixpkgs-23-11": [ "devenv" ], "nixpkgs-regression": [ "devenv" ], "pre-commit-hooks": [ "devenv" ] }, "locked": { "lastModified": 1741798497, "narHash": "sha256-E3j+3MoY8Y96mG1dUIiLFm2tZmNbRvSiyN7CrSKuAVg=", "owner": "domenkozar", "repo": "nix", "rev": "f3f44b2baaf6c4c6e179de8cbb1cc6db031083cd", "type": "github" }, "original": { "owner": "domenkozar", "ref": "devenv-2.24", "repo": "nix", "type": "github" } }, "nixpkgs": { "locked": { "lastModified": 1733212471, "narHash": "sha256-M1+uCoV5igihRfcUKrr1riygbe73/dzNnzPsmaLCmpo=", "owner": "NixOS", "repo": "nixpkgs", "rev": "55d15ad12a74eb7d4646254e13638ad0c4128776", "type": "github" }, "original": { "owner": "NixOS", "ref": "nixos-unstable", "repo": "nixpkgs", "type": "github" } }, "nixpkgs-lib": { "locked": { "lastModified": 1743296961, "narHash": "sha256-b1EdN3cULCqtorQ4QeWgLMrd5ZGOjLSLemfa00heasc=", "owner": "nix-community", "repo": "nixpkgs.lib", "rev": "e4822aea2a6d1cdd36653c134cacfd64c97ff4fa", "type": "github" }, "original": { "owner": "nix-community", "repo": "nixpkgs.lib", "type": "github" } }, "nixpkgs_2": { "locked": { "lastModified": 1717432640, "narHash": "sha256-+f9c4/ZX5MWDOuB1rKoWj+lBNm0z0rs4CK47HBLxy1o=", "owner": "NixOS", "repo": "nixpkgs", "rev": "88269ab3044128b7c2f4c7d68448b2fb50456870", "type": "github" }, "original": { "owner": "NixOS", "ref": "release-24.05", "repo": "nixpkgs", "type": "github" } }, "nixpkgs_3": { "locked": { "lastModified": 1733477122, "narHash": "sha256-qamMCz5mNpQmgBwc8SB5tVMlD5sbwVIToVZtSxMph9s=", "owner": "cachix", "repo": "devenv-nixpkgs", "rev": "7bd9e84d0452f6d2e63b6e6da29fe73fac951857", "type": "github" }, "original": { "owner": "cachix", "ref": "rolling", "repo": "devenv-nixpkgs", "type": "github" } }, "nixpkgs_4": { "locked": { "lastModified": 1744536153, "narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=", "owner": "NixOS", "repo": "nixpkgs", "rev": "18dd725c29603f582cf1900e0d25f9f1063dbf11", "type": "github" }, "original": { "owner": "NixOS", "ref": "nixpkgs-unstable", "repo": "nixpkgs", "type": "github" } }, "root": { "inputs": { "devenv": "devenv", "flake-parts": "flake-parts_2", "nixpkgs": "nixpkgs_4" } } }, "root": "root", "version": 7 } golang-github-go-viper-mapstructure-2.4.0/flake.nix000066400000000000000000000020011510370305500223200ustar00rootroot00000000000000{ inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; flake-parts.url = "github:hercules-ci/flake-parts"; devenv.url = "github:cachix/devenv"; }; outputs = inputs@{ flake-parts, ... }: flake-parts.lib.mkFlake { inherit inputs; } { imports = [ inputs.devenv.flakeModule ]; systems = [ "x86_64-linux" "x86_64-darwin" "aarch64-darwin" ]; perSystem = { pkgs, ... }: rec { devenv.shells = { default = { languages = { go.enable = true; }; pre-commit.hooks = { nixpkgs-fmt.enable = true; }; packages = with pkgs; [ golangci-lint ]; # https://github.com/cachix/devenv/issues/528#issuecomment-1556108767 containers = pkgs.lib.mkForce { }; }; ci = devenv.shells.default; }; }; }; } golang-github-go-viper-mapstructure-2.4.0/go.mod000066400000000000000000000000641510370305500216330ustar00rootroot00000000000000module github.com/go-viper/mapstructure/v2 go 1.18 golang-github-go-viper-mapstructure-2.4.0/internal/000077500000000000000000000000001510370305500223415ustar00rootroot00000000000000golang-github-go-viper-mapstructure-2.4.0/internal/errors/000077500000000000000000000000001510370305500236555ustar00rootroot00000000000000golang-github-go-viper-mapstructure-2.4.0/internal/errors/errors.go000066400000000000000000000002521510370305500255170ustar00rootroot00000000000000package errors import "errors" func New(text string) error { return errors.New(text) } func As(err error, target interface{}) bool { return errors.As(err, target) } golang-github-go-viper-mapstructure-2.4.0/internal/errors/join.go000066400000000000000000000001641510370305500251440ustar00rootroot00000000000000//go:build go1.20 package errors import "errors" func Join(errs ...error) error { return errors.Join(errs...) } golang-github-go-viper-mapstructure-2.4.0/internal/errors/join_go1_19.go000066400000000000000000000025251510370305500262260ustar00rootroot00000000000000//go:build !go1.20 // Copyright 2022 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package errors // Join returns an error that wraps the given errors. // Any nil error values are discarded. // Join returns nil if every value in errs is nil. // The error formats as the concatenation of the strings obtained // by calling the Error method of each element of errs, with a newline // between each string. // // A non-nil error returned by Join implements the Unwrap() []error method. func Join(errs ...error) error { n := 0 for _, err := range errs { if err != nil { n++ } } if n == 0 { return nil } e := &joinError{ errs: make([]error, 0, n), } for _, err := range errs { if err != nil { e.errs = append(e.errs, err) } } return e } type joinError struct { errs []error } func (e *joinError) Error() string { // Since Join returns nil if every value in errs is nil, // e.errs cannot be empty. if len(e.errs) == 1 { return e.errs[0].Error() } b := []byte(e.errs[0].Error()) for _, err := range e.errs[1:] { b = append(b, '\n') b = append(b, err.Error()...) } // At this point, b has at least one byte '\n'. // return unsafe.String(&b[0], len(b)) return string(b) } func (e *joinError) Unwrap() []error { return e.errs } golang-github-go-viper-mapstructure-2.4.0/mapstructure.go000066400000000000000000001375451510370305500236310ustar00rootroot00000000000000// Package mapstructure exposes functionality to convert one arbitrary // Go type into another, typically to convert a map[string]any // into a native Go structure. // // The Go structure can be arbitrarily complex, containing slices, // other structs, etc. and the decoder will properly decode nested // maps and so on into the proper structures in the native Go struct. // See the examples to see what the decoder is capable of. // // The simplest function to start with is Decode. // // # Field Tags // // When decoding to a struct, mapstructure will use the field name by // default to perform the mapping. For example, if a struct has a field // "Username" then mapstructure will look for a key in the source value // of "username" (case insensitive). // // type User struct { // Username string // } // // You can change the behavior of mapstructure by using struct tags. // The default struct tag that mapstructure looks for is "mapstructure" // but you can customize it using DecoderConfig. // // # Renaming Fields // // To rename the key that mapstructure looks for, use the "mapstructure" // tag and set a value directly. For example, to change the "username" example // above to "user": // // type User struct { // Username string `mapstructure:"user"` // } // // # Embedded Structs and Squashing // // Embedded structs are treated as if they're another field with that name. // By default, the two structs below are equivalent when decoding with // mapstructure: // // type Person struct { // Name string // } // // type Friend struct { // Person // } // // type Friend struct { // Person Person // } // // This would require an input that looks like below: // // map[string]any{ // "person": map[string]any{"name": "alice"}, // } // // If your "person" value is NOT nested, then you can append ",squash" to // your tag value and mapstructure will treat it as if the embedded struct // were part of the struct directly. Example: // // type Friend struct { // Person `mapstructure:",squash"` // } // // Now the following input would be accepted: // // map[string]any{ // "name": "alice", // } // // When decoding from a struct to a map, the squash tag squashes the struct // fields into a single map. Using the example structs from above: // // Friend{Person: Person{Name: "alice"}} // // Will be decoded into a map: // // map[string]any{ // "name": "alice", // } // // DecoderConfig has a field that changes the behavior of mapstructure // to always squash embedded structs. // // # Remainder Values // // If there are any unmapped keys in the source value, mapstructure by // default will silently ignore them. You can error by setting ErrorUnused // in DecoderConfig. If you're using Metadata you can also maintain a slice // of the unused keys. // // You can also use the ",remain" suffix on your tag to collect all unused // values in a map. The field with this tag MUST be a map type and should // probably be a "map[string]any" or "map[any]any". // See example below: // // type Friend struct { // Name string // Other map[string]any `mapstructure:",remain"` // } // // Given the input below, Other would be populated with the other // values that weren't used (everything but "name"): // // map[string]any{ // "name": "bob", // "address": "123 Maple St.", // } // // # Omit Empty Values // // 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, 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. 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"` // 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 // where they are defined, the decoder will simply skip them. // // For this output type definition: // // type Exported struct { // private string // this unexported field will be skipped // Public string // } // // Using this map as input: // // map[string]any{ // "private": "I will be ignored", // "Public": "I made it through!", // } // // The following struct will be decoded: // // type Exported struct { // private: "" // field is left with an empty string (zero value) // Public: "I made it through!" // } // // # Other Configuration // // mapstructure is highly configurable. See the DecoderConfig struct // for other features and options that are supported. package mapstructure import ( "encoding/json" "fmt" "reflect" "sort" "strconv" "strings" "github.com/go-viper/mapstructure/v2/internal/errors" ) // DecodeHookFunc is the callback function that can be used for // data transformations. See "DecodeHook" in the DecoderConfig // struct. // // The type must be one of DecodeHookFuncType, DecodeHookFuncKind, or // DecodeHookFuncValue. // Values are a superset of Types (Values can return types), and Types are a // superset of Kinds (Types can return Kinds) and are generally a richer thing // to use, but Kinds are simpler if you only need those. // // The reason DecodeHookFunc is multi-typed is for backwards compatibility: // we started with Kinds and then realized Types were the better solution, // but have a promise to not break backwards compat so we now support // both. type DecodeHookFunc any // DecodeHookFuncType is a DecodeHookFunc which has complete information about // the source and target types. type DecodeHookFuncType func(reflect.Type, reflect.Type, any) (any, error) // DecodeHookFuncKind is a DecodeHookFunc which knows only the Kinds of the // source and target types. type DecodeHookFuncKind func(reflect.Kind, reflect.Kind, any) (any, error) // DecodeHookFuncValue is a DecodeHookFunc which has complete access to both the source and target // values. type DecodeHookFuncValue func(from reflect.Value, to reflect.Value) (any, error) // DecoderConfig is the configuration that is used to create a new decoder // and allows customization of various aspects of decoding. type DecoderConfig struct { // DecodeHook, if set, will be called before any decoding and any // type conversion (if WeaklyTypedInput is on). This lets you modify // the values before they're set down onto the resulting struct. The // DecodeHook is called for every map and value in the input. This means // that if a struct has embedded fields with squash tags the decode hook // is called only once with all of the input data, not once for each // embedded struct. // // If an error is returned, the entire decode will fail with that error. DecodeHook DecodeHookFunc // If ErrorUnused is true, then it is an error for there to exist // keys in the original map that were unused in the decoding process // (extra keys). ErrorUnused bool // If ErrorUnset is true, then it is an error for there to exist // fields in the result that were not set in the decoding process // (extra fields). This only applies to decoding to a struct. This // 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. ZeroFields bool // If WeaklyTypedInput is true, the decoder will make the following // "weak" conversions: // // - bools to string (true = "1", false = "0") // - numbers to string (base 10) // - bools to int/uint (true = 1, false = 0) // - strings to int/uint (base implied by prefix) // - int to bool (true if value != 0) // - string to bool (accepts: 1, t, T, TRUE, true, True, 0, f, F, // FALSE, false, False. Anything else is an error) // - empty array = empty map and vice versa // - negative numbers to overflowed uint values (base 10) // - slice of maps to a merged map // - single values are converted to slices if required. Each // element is weakly decoded. For example: "4" can become []int{4} // if the target type is an int slice. // WeaklyTypedInput bool // Squash will squash embedded structs. A squash tag may also be // added to an individual struct field using a tag. For example: // // type Parent struct { // Child `mapstructure:",squash"` // } Squash bool // Metadata is the struct that will contain extra metadata about // the decoding. If this is nil, then no metadata will be tracked. Metadata *Metadata // Result is a pointer to the struct that will contain the decoded // value. Result any // The tag name that mapstructure reads for field names. This // defaults to "mapstructure" TagName string // The option of the value in the tag that indicates a field should // be squashed. This defaults to "squash". SquashTagOption string // IgnoreUntaggedFields ignores all struct fields without explicit // TagName, comparable to `mapstructure:"-"` as default behaviour. IgnoreUntaggedFields bool // MatchName is the function used to match the map key to the struct // field name or tag. Defaults to `strings.EqualFold`. This can be used // to implement case-sensitive tag values, support snake casing, etc. MatchName func(mapKey, fieldName string) bool // DecodeNil, if set to true, will cause the DecodeHook (if present) to run // even if the input is nil. This can be used to provide default values. DecodeNil bool } // A Decoder takes a raw interface value and turns it into structured // data, keeping track of rich error information along the way in case // anything goes wrong. Unlike the basic top-level Decode method, you can // more finely control how the Decoder behaves using the DecoderConfig // structure. The top-level Decode method is just a convenience that sets // up the most basic Decoder. type Decoder struct { config *DecoderConfig cachedDecodeHook func(from reflect.Value, to reflect.Value) (any, error) } // Metadata contains information about decoding a structure that // is tedious or difficult to get otherwise. type Metadata struct { // Keys are the keys of the structure which were successfully decoded Keys []string // Unused is a slice of keys that were found in the raw value but // weren't decoded since there was no matching field in the result interface Unused []string // Unset is a slice of field names that were found in the result interface // but weren't set in the decoding process since there was no matching value // in the input Unset []string } // Decode takes an input structure and uses reflection to translate it to // the output structure. output must be a pointer to a map or struct. func Decode(input any, output any) error { config := &DecoderConfig{ Metadata: nil, Result: output, } decoder, err := NewDecoder(config) if err != nil { return err } return decoder.Decode(input) } // WeakDecode is the same as Decode but is shorthand to enable // WeaklyTypedInput. See DecoderConfig for more info. func WeakDecode(input, output any) error { config := &DecoderConfig{ Metadata: nil, Result: output, WeaklyTypedInput: true, } decoder, err := NewDecoder(config) if err != nil { return err } return decoder.Decode(input) } // DecodeMetadata is the same as Decode, but is shorthand to // enable metadata collection. See DecoderConfig for more info. func DecodeMetadata(input any, output any, metadata *Metadata) error { config := &DecoderConfig{ Metadata: metadata, Result: output, } decoder, err := NewDecoder(config) if err != nil { return err } return decoder.Decode(input) } // WeakDecodeMetadata is the same as Decode, but is shorthand to // enable both WeaklyTypedInput and metadata collection. See // DecoderConfig for more info. func WeakDecodeMetadata(input any, output any, metadata *Metadata) error { config := &DecoderConfig{ Metadata: metadata, Result: output, WeaklyTypedInput: true, } decoder, err := NewDecoder(config) if err != nil { return err } return decoder.Decode(input) } // NewDecoder returns a new decoder for the given configuration. Once // a decoder has been returned, the same configuration must not be used // again. func NewDecoder(config *DecoderConfig) (*Decoder, error) { val := reflect.ValueOf(config.Result) if val.Kind() != reflect.Ptr { return nil, errors.New("result must be a pointer") } val = val.Elem() if !val.CanAddr() { return nil, errors.New("result must be addressable (a pointer)") } if config.Metadata != nil { if config.Metadata.Keys == nil { config.Metadata.Keys = make([]string, 0) } if config.Metadata.Unused == nil { config.Metadata.Unused = make([]string, 0) } if config.Metadata.Unset == nil { config.Metadata.Unset = make([]string, 0) } } if config.TagName == "" { config.TagName = "mapstructure" } if config.SquashTagOption == "" { config.SquashTagOption = "squash" } if config.MatchName == nil { config.MatchName = strings.EqualFold } result := &Decoder{ config: config, } if config.DecodeHook != nil { result.cachedDecodeHook = cachedDecodeHook(config.DecodeHook) } return result, nil } // Decode decodes the given raw interface to the target pointer specified // by the configuration. func (d *Decoder) Decode(input any) error { err := d.decode("", input, reflect.ValueOf(d.config.Result).Elem()) // Retain some of the original behavior when multiple errors ocurr var joinedErr interface{ Unwrap() []error } if errors.As(err, &joinedErr) { return fmt.Errorf("decoding failed due to the following error(s):\n\n%w", err) } return err } // isNil returns true if the input is nil or a typed nil pointer. func isNil(input any) bool { if input == nil { return true } val := reflect.ValueOf(input) return val.Kind() == reflect.Ptr && val.IsNil() } // Decodes an unknown data type into a specific reflection value. func (d *Decoder) decode(name string, input any, outVal reflect.Value) error { var ( inputVal = reflect.ValueOf(input) outputKind = getKind(outVal) decodeNil = d.config.DecodeNil && d.cachedDecodeHook != nil ) if isNil(input) { // Typed nils won't match the "input == nil" below, so reset input. input = nil } if input == nil { // If the data is nil, then we don't set anything, unless ZeroFields is set // to true. if d.config.ZeroFields { outVal.Set(reflect.Zero(outVal.Type())) if d.config.Metadata != nil && name != "" { d.config.Metadata.Keys = append(d.config.Metadata.Keys, name) } } if !decodeNil { return nil } } if !inputVal.IsValid() { if !decodeNil { // If the input value is invalid, then we just set the value // to be the zero value. outVal.Set(reflect.Zero(outVal.Type())) if d.config.Metadata != nil && name != "" { d.config.Metadata.Keys = append(d.config.Metadata.Keys, name) } return nil } // Hooks need a valid inputVal, so reset it to zero value of outVal type. switch outputKind { case reflect.Struct, reflect.Map: var mapVal map[string]any inputVal = reflect.ValueOf(mapVal) // create nil map pointer case reflect.Slice, reflect.Array: var sliceVal []any inputVal = reflect.ValueOf(sliceVal) // create nil slice pointer default: inputVal = reflect.Zero(outVal.Type()) } } if d.cachedDecodeHook != nil { // We have a DecodeHook, so let's pre-process the input. var err error input, err = d.cachedDecodeHook(inputVal, outVal) if err != nil { return newDecodeError(name, err) } } if isNil(input) { return nil } var err error addMetaKey := true switch outputKind { case reflect.Bool: err = d.decodeBool(name, input, outVal) case reflect.Interface: err = d.decodeBasic(name, input, outVal) case reflect.String: err = d.decodeString(name, input, outVal) case reflect.Int: err = d.decodeInt(name, input, outVal) case reflect.Uint: err = d.decodeUint(name, input, outVal) case reflect.Float32: err = d.decodeFloat(name, input, outVal) case reflect.Complex64: err = d.decodeComplex(name, input, outVal) case reflect.Struct: err = d.decodeStruct(name, input, outVal) case reflect.Map: err = d.decodeMap(name, input, outVal) case reflect.Ptr: addMetaKey, err = d.decodePtr(name, input, outVal) case reflect.Slice: err = d.decodeSlice(name, input, outVal) case reflect.Array: err = d.decodeArray(name, input, outVal) case reflect.Func: err = d.decodeFunc(name, input, outVal) default: // If we reached this point then we weren't able to decode it return newDecodeError(name, fmt.Errorf("unsupported type: %s", outputKind)) } // If we reached here, then we successfully decoded SOMETHING, so // mark the key as used if we're tracking metainput. if addMetaKey && d.config.Metadata != nil && name != "" { d.config.Metadata.Keys = append(d.config.Metadata.Keys, name) } return err } // This decodes a basic type (bool, int, string, etc.) and sets the // value to "data" of that type. func (d *Decoder) decodeBasic(name string, data any, val reflect.Value) error { if val.IsValid() && val.Elem().IsValid() { elem := val.Elem() // If we can't address this element, then its not writable. Instead, // we make a copy of the value (which is a pointer and therefore // writable), decode into that, and replace the whole value. copied := false if !elem.CanAddr() { copied = true // Make *T copy := reflect.New(elem.Type()) // *T = elem copy.Elem().Set(elem) // Set elem so we decode into it elem = copy } // Decode. If we have an error then return. We also return right // away if we're not a copy because that means we decoded directly. if err := d.decode(name, data, elem); err != nil || !copied { return err } // If we're a copy, we need to set te final result val.Set(elem.Elem()) return nil } dataVal := reflect.ValueOf(data) // If the input data is a pointer, and the assigned type is the dereference // of that exact pointer, then indirect it so that we can assign it. // Example: *string to string if dataVal.Kind() == reflect.Ptr && dataVal.Type().Elem() == val.Type() { dataVal = reflect.Indirect(dataVal) } if !dataVal.IsValid() { dataVal = reflect.Zero(val.Type()) } dataValType := dataVal.Type() if !dataValType.AssignableTo(val.Type()) { return newDecodeError(name, &UnconvertibleTypeError{ Expected: val, Value: data, }) } val.Set(dataVal) return nil } func (d *Decoder) decodeString(name string, data any, val reflect.Value) error { dataVal := reflect.Indirect(reflect.ValueOf(data)) dataKind := getKind(dataVal) converted := true switch { case dataKind == reflect.String: val.SetString(dataVal.String()) case dataKind == reflect.Bool && d.config.WeaklyTypedInput: if dataVal.Bool() { val.SetString("1") } else { val.SetString("0") } case dataKind == reflect.Int && d.config.WeaklyTypedInput: val.SetString(strconv.FormatInt(dataVal.Int(), 10)) case dataKind == reflect.Uint && d.config.WeaklyTypedInput: val.SetString(strconv.FormatUint(dataVal.Uint(), 10)) case dataKind == reflect.Float32 && d.config.WeaklyTypedInput: val.SetString(strconv.FormatFloat(dataVal.Float(), 'f', -1, 64)) case dataKind == reflect.Slice && d.config.WeaklyTypedInput, dataKind == reflect.Array && d.config.WeaklyTypedInput: dataType := dataVal.Type() elemKind := dataType.Elem().Kind() switch elemKind { case reflect.Uint8: var uints []uint8 if dataKind == reflect.Array { uints = make([]uint8, dataVal.Len(), dataVal.Len()) for i := range uints { uints[i] = dataVal.Index(i).Interface().(uint8) } } else { uints = dataVal.Interface().([]uint8) } val.SetString(string(uints)) default: converted = false } default: converted = false } if !converted { return newDecodeError(name, &UnconvertibleTypeError{ Expected: val, Value: data, }) } return nil } func (d *Decoder) decodeInt(name string, data any, val reflect.Value) error { dataVal := reflect.Indirect(reflect.ValueOf(data)) dataKind := getKind(dataVal) dataType := dataVal.Type() switch { case dataKind == reflect.Int: val.SetInt(dataVal.Int()) case dataKind == reflect.Uint: val.SetInt(int64(dataVal.Uint())) case dataKind == reflect.Float32: val.SetInt(int64(dataVal.Float())) case dataKind == reflect.Bool && d.config.WeaklyTypedInput: if dataVal.Bool() { val.SetInt(1) } else { val.SetInt(0) } case dataKind == reflect.String && d.config.WeaklyTypedInput: str := dataVal.String() if str == "" { str = "0" } i, err := strconv.ParseInt(str, 0, val.Type().Bits()) if err == nil { val.SetInt(i) } else { return newDecodeError(name, &ParseError{ Expected: val, Value: data, Err: wrapStrconvNumError(err), }) } case dataType.PkgPath() == "encoding/json" && dataType.Name() == "Number": jn := data.(json.Number) i, err := jn.Int64() if err != nil { return newDecodeError(name, &ParseError{ Expected: val, Value: data, Err: err, }) } val.SetInt(i) default: return newDecodeError(name, &UnconvertibleTypeError{ Expected: val, Value: data, }) } return nil } func (d *Decoder) decodeUint(name string, data any, val reflect.Value) error { dataVal := reflect.Indirect(reflect.ValueOf(data)) dataKind := getKind(dataVal) dataType := dataVal.Type() switch { case dataKind == reflect.Int: i := dataVal.Int() if i < 0 && !d.config.WeaklyTypedInput { return newDecodeError(name, &ParseError{ Expected: val, Value: data, Err: fmt.Errorf("%d overflows uint", i), }) } val.SetUint(uint64(i)) case dataKind == reflect.Uint: val.SetUint(dataVal.Uint()) case dataKind == reflect.Float32: f := dataVal.Float() if f < 0 && !d.config.WeaklyTypedInput { 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: if dataVal.Bool() { val.SetUint(1) } else { val.SetUint(0) } case dataKind == reflect.String && d.config.WeaklyTypedInput: str := dataVal.String() if str == "" { str = "0" } i, err := strconv.ParseUint(str, 0, val.Type().Bits()) if err == nil { val.SetUint(i) } else { return newDecodeError(name, &ParseError{ Expected: val, Value: data, Err: wrapStrconvNumError(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 newDecodeError(name, &ParseError{ Expected: val, Value: data, Err: wrapStrconvNumError(err), }) } val.SetUint(i) default: return newDecodeError(name, &UnconvertibleTypeError{ Expected: val, Value: data, }) } return nil } func (d *Decoder) decodeBool(name string, data any, val reflect.Value) error { dataVal := reflect.Indirect(reflect.ValueOf(data)) dataKind := getKind(dataVal) switch { case dataKind == reflect.Bool: val.SetBool(dataVal.Bool()) case dataKind == reflect.Int && d.config.WeaklyTypedInput: val.SetBool(dataVal.Int() != 0) case dataKind == reflect.Uint && d.config.WeaklyTypedInput: val.SetBool(dataVal.Uint() != 0) case dataKind == reflect.Float32 && d.config.WeaklyTypedInput: val.SetBool(dataVal.Float() != 0) case dataKind == reflect.String && d.config.WeaklyTypedInput: b, err := strconv.ParseBool(dataVal.String()) if err == nil { val.SetBool(b) } else if dataVal.String() == "" { val.SetBool(false) } else { return newDecodeError(name, &ParseError{ Expected: val, Value: data, Err: wrapStrconvNumError(err), }) } default: return newDecodeError(name, &UnconvertibleTypeError{ Expected: val, Value: data, }) } return nil } func (d *Decoder) decodeFloat(name string, data any, val reflect.Value) error { dataVal := reflect.Indirect(reflect.ValueOf(data)) dataKind := getKind(dataVal) dataType := dataVal.Type() switch { case dataKind == reflect.Int: val.SetFloat(float64(dataVal.Int())) case dataKind == reflect.Uint: val.SetFloat(float64(dataVal.Uint())) case dataKind == reflect.Float32: val.SetFloat(dataVal.Float()) case dataKind == reflect.Bool && d.config.WeaklyTypedInput: if dataVal.Bool() { val.SetFloat(1) } else { val.SetFloat(0) } case dataKind == reflect.String && d.config.WeaklyTypedInput: str := dataVal.String() if str == "" { str = "0" } f, err := strconv.ParseFloat(str, val.Type().Bits()) if err == nil { val.SetFloat(f) } else { return newDecodeError(name, &ParseError{ Expected: val, Value: data, Err: wrapStrconvNumError(err), }) } case dataType.PkgPath() == "encoding/json" && dataType.Name() == "Number": jn := data.(json.Number) i, err := jn.Float64() if err != nil { return newDecodeError(name, &ParseError{ Expected: val, Value: data, Err: err, }) } val.SetFloat(i) default: return newDecodeError(name, &UnconvertibleTypeError{ Expected: val, Value: data, }) } return nil } func (d *Decoder) decodeComplex(name string, data any, val reflect.Value) error { dataVal := reflect.Indirect(reflect.ValueOf(data)) dataKind := getKind(dataVal) switch { case dataKind == reflect.Complex64: val.SetComplex(dataVal.Complex()) default: return newDecodeError(name, &UnconvertibleTypeError{ Expected: val, Value: data, }) } return nil } func (d *Decoder) decodeMap(name string, data any, val reflect.Value) error { valType := val.Type() valKeyType := valType.Key() valElemType := valType.Elem() // By default we overwrite keys in the current map valMap := val // If the map is nil or we're purposely zeroing fields, make a new map if valMap.IsNil() || d.config.ZeroFields { // Make a new map to hold our result mapType := reflect.MapOf(valKeyType, valElemType) valMap = reflect.MakeMap(mapType) } dataVal := reflect.ValueOf(data) // Resolve any levels of indirection for dataVal.Kind() == reflect.Pointer { dataVal = reflect.Indirect(dataVal) } // Check input type and based on the input type jump to the proper func switch dataVal.Kind() { case reflect.Map: return d.decodeMapFromMap(name, dataVal, val, valMap) case reflect.Struct: return d.decodeMapFromStruct(name, dataVal, val, valMap) case reflect.Array, reflect.Slice: if d.config.WeaklyTypedInput { return d.decodeMapFromSlice(name, dataVal, val, valMap) } fallthrough default: return newDecodeError(name, &UnconvertibleTypeError{ Expected: val, Value: data, }) } } func (d *Decoder) decodeMapFromSlice(name string, dataVal reflect.Value, val reflect.Value, valMap reflect.Value) error { // Special case for BC reasons (covered by tests) if dataVal.Len() == 0 { val.Set(valMap) return nil } for i := 0; i < dataVal.Len(); i++ { err := d.decode( name+"["+strconv.Itoa(i)+"]", dataVal.Index(i).Interface(), val) if err != nil { return err } } return nil } func (d *Decoder) decodeMapFromMap(name string, dataVal reflect.Value, val reflect.Value, valMap reflect.Value) error { valType := val.Type() valKeyType := valType.Key() valElemType := valType.Elem() // Accumulate errors var errs []error // If the input data is empty, then we just match what the input data is. if dataVal.Len() == 0 { if dataVal.IsNil() { if !val.IsNil() { val.Set(dataVal) } } else { // Set to empty allocated value val.Set(valMap) } return nil } for _, k := range dataVal.MapKeys() { fieldName := name + "[" + k.String() + "]" // First decode the key into the proper type currentKey := reflect.Indirect(reflect.New(valKeyType)) if err := d.decode(fieldName, k.Interface(), currentKey); err != nil { errs = append(errs, err) continue } // Next decode the data into the proper type v := dataVal.MapIndex(k).Interface() currentVal := reflect.Indirect(reflect.New(valElemType)) if err := d.decode(fieldName, v, currentVal); err != nil { errs = append(errs, err) continue } valMap.SetMapIndex(currentKey, currentVal) } // Set the built up map to the value val.Set(valMap) return errors.Join(errs...) } func (d *Decoder) decodeMapFromStruct(name string, dataVal reflect.Value, val reflect.Value, valMap reflect.Value) error { typ := dataVal.Type() for i := 0; i < typ.NumField(); i++ { // Get the StructField first since this is a cheap operation. If the // field is unexported, then ignore it. f := typ.Field(i) if f.PkgPath != "" { continue } // Next get the actual value of this field and verify it is assignable // to the map value. v := dataVal.Field(i) if !v.Type().AssignableTo(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) keyName := f.Name if tagValue == "" && d.config.IgnoreUntaggedFields { continue } // If Squash is set in the config, we squash the field down. squash := d.config.Squash && v.Kind() == reflect.Struct && f.Anonymous v = dereferencePtrToStructIfNeeded(v, d.config.TagName) // Determine the name of the key in the map if index := strings.Index(tagValue, ","); index != -1 { if tagValue[:index] == "-" { continue } // If "omitempty" is specified in the tag, it ignores empty values. if strings.Index(tagValue[index+1:], "omitempty") != -1 && isEmptyValue(v) { 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 { // When squashing, the embedded type can be a pointer to a struct. if v.Kind() == reflect.Ptr && v.Elem().Kind() == reflect.Struct { v = v.Elem() } // The final type must be a struct if v.Kind() != reflect.Struct { 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 newDecodeError( name+"."+f.Name, fmt.Errorf("error remain-tag field with invalid type: %q", v.Type()), ) } ptr := v.MapRange() for ptr.Next() { valMap.SetMapIndex(ptr.Key(), ptr.Value()) } continue } } if keyNameTagValue := tagValue[:index]; keyNameTagValue != "" { keyName = keyNameTagValue } } else if len(tagValue) > 0 { if tagValue == "-" { continue } keyName = tagValue } switch v.Kind() { // this is an embedded struct, so handle it differently case reflect.Struct: x := reflect.New(v.Type()) x.Elem().Set(v) vType := valMap.Type() vKeyType := vType.Key() vElemType := vType.Elem() mType := reflect.MapOf(vKeyType, vElemType) vMap := reflect.MakeMap(mType) // Creating a pointer to a map so that other methods can completely // overwrite the map if need be (looking at you decodeMapFromMap). The // indirection allows the underlying map to be settable (CanSet() == true) // where as reflect.MakeMap returns an unsettable map. addrVal := reflect.New(vMap.Type()) reflect.Indirect(addrVal).Set(vMap) err := d.decode(keyName, x.Interface(), reflect.Indirect(addrVal)) if err != nil { return err } // the underlying map may have been completely overwritten so pull // it indirectly out of the enclosing value. vMap = reflect.Indirect(addrVal) if squash { for _, k := range vMap.MapKeys() { valMap.SetMapIndex(k, vMap.MapIndex(k)) } } else { valMap.SetMapIndex(reflect.ValueOf(keyName), vMap) } default: valMap.SetMapIndex(reflect.ValueOf(keyName), v) } } if val.CanAddr() { val.Set(valMap) } return nil } func (d *Decoder) decodePtr(name string, data any, val reflect.Value) (bool, error) { // If the input data is nil, then we want to just set the output // pointer to be nil as well. isNil := data == nil if !isNil { switch v := reflect.Indirect(reflect.ValueOf(data)); v.Kind() { case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice: isNil = v.IsNil() } } if isNil { if !val.IsNil() && val.CanSet() { nilValue := reflect.New(val.Type()).Elem() val.Set(nilValue) } return true, nil } // Create an element of the concrete (non pointer) type and decode // into that. Then set the value of the pointer to this type. valType := val.Type() valElemType := valType.Elem() if val.CanSet() { realVal := val if realVal.IsNil() || d.config.ZeroFields { realVal = reflect.New(valElemType) } if err := d.decode(name, data, reflect.Indirect(realVal)); err != nil { return false, err } val.Set(realVal) } else { if err := d.decode(name, data, reflect.Indirect(val)); err != nil { return false, err } } return false, nil } func (d *Decoder) decodeFunc(name string, data any, val reflect.Value) error { // Create an element of the concrete (non pointer) type and decode // into that. Then set the value of the pointer to this type. dataVal := reflect.Indirect(reflect.ValueOf(data)) if val.Type() != dataVal.Type() { return newDecodeError(name, &UnconvertibleTypeError{ Expected: val, Value: data, }) } val.Set(dataVal) return nil } func (d *Decoder) decodeSlice(name string, data any, val reflect.Value) error { dataVal := reflect.Indirect(reflect.ValueOf(data)) dataValKind := dataVal.Kind() valType := val.Type() valElemType := valType.Elem() sliceType := reflect.SliceOf(valElemType) // If we have a non array/slice type then we first attempt to convert. if dataValKind != reflect.Array && dataValKind != reflect.Slice { if d.config.WeaklyTypedInput { switch { // Slice and array we use the normal logic case dataValKind == reflect.Slice, dataValKind == reflect.Array: break // Empty maps turn into empty slices case dataValKind == reflect.Map: if dataVal.Len() == 0 { val.Set(reflect.MakeSlice(sliceType, 0, 0)) return nil } // Create slice of maps of other sizes return d.decodeSlice(name, []any{data}, val) case dataValKind == reflect.String && valElemType.Kind() == reflect.Uint8: return d.decodeSlice(name, []byte(dataVal.String()), val) // All other types we try to convert to the slice type // and "lift" it into it. i.e. a string becomes a string slice. default: // Just re-try this function with data as a slice. return d.decodeSlice(name, []any{data}, val) } } 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 if dataValKind != reflect.Array && dataVal.IsNil() { return nil } valSlice := val if valSlice.IsNil() || d.config.ZeroFields { // Make a new slice to hold our result, same size as the original data. valSlice = reflect.MakeSlice(sliceType, dataVal.Len(), dataVal.Len()) } else if valSlice.Len() > dataVal.Len() { valSlice = valSlice.Slice(0, dataVal.Len()) } // Accumulate any errors var errs []error for i := 0; i < dataVal.Len(); i++ { currentData := dataVal.Index(i).Interface() for valSlice.Len() <= i { valSlice = reflect.Append(valSlice, reflect.Zero(valElemType)) } currentField := valSlice.Index(i) fieldName := name + "[" + strconv.Itoa(i) + "]" if err := d.decode(fieldName, currentData, currentField); err != nil { errs = append(errs, err) } } // Finally, set the value to the slice we built up val.Set(valSlice) return errors.Join(errs...) } func (d *Decoder) decodeArray(name string, data any, val reflect.Value) error { dataVal := reflect.Indirect(reflect.ValueOf(data)) dataValKind := dataVal.Kind() valType := val.Type() valElemType := valType.Elem() arrayType := reflect.ArrayOf(valType.Len(), valElemType) valArray := val if isComparable(valArray) && valArray.Interface() == reflect.Zero(valArray.Type()).Interface() || d.config.ZeroFields { // Check input type if dataValKind != reflect.Array && dataValKind != reflect.Slice { if d.config.WeaklyTypedInput { switch { // Empty maps turn into empty arrays case dataValKind == reflect.Map: if dataVal.Len() == 0 { val.Set(reflect.Zero(arrayType)) return nil } // All other types we try to convert to the array type // and "lift" it into it. i.e. a string becomes a string array. default: // Just re-try this function with data as a slice. return d.decodeArray(name, []any{data}, val) } } return newDecodeError(name, fmt.Errorf("source data must be an array or slice, got %s", dataValKind)) } if dataVal.Len() > arrayType.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. valArray = reflect.New(arrayType).Elem() } // Accumulate any errors var errs []error for i := 0; i < dataVal.Len(); i++ { currentData := dataVal.Index(i).Interface() currentField := valArray.Index(i) fieldName := name + "[" + strconv.Itoa(i) + "]" if err := d.decode(fieldName, currentData, currentField); err != nil { errs = append(errs, err) } } // Finally, set the value to the array we built up val.Set(valArray) return errors.Join(errs...) } func (d *Decoder) decodeStruct(name string, data any, val reflect.Value) error { dataVal := reflect.Indirect(reflect.ValueOf(data)) // If the type of the value to write to and the data match directly, // then we just set it directly instead of recursing into the structure. if dataVal.Type() == val.Type() { val.Set(dataVal) return nil } dataValKind := dataVal.Kind() switch dataValKind { case reflect.Map: return d.decodeStructFromMap(name, dataVal, val) case reflect.Struct: // Not the most efficient way to do this but we can optimize later if // we want to. To convert from struct to struct we go to map first // as an intermediary. // Make a new map to hold our result mapType := reflect.TypeOf((map[string]any)(nil)) mval := reflect.MakeMap(mapType) // Creating a pointer to a map so that other methods can completely // overwrite the map if need be (looking at you decodeMapFromMap). The // indirection allows the underlying map to be settable (CanSet() == true) // where as reflect.MakeMap returns an unsettable map. addrVal := reflect.New(mval.Type()) reflect.Indirect(addrVal).Set(mval) if err := d.decodeMapFromStruct(name, dataVal, reflect.Indirect(addrVal), mval); err != nil { return err } result := d.decodeStructFromMap(name, reflect.Indirect(addrVal), val) return result default: 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 newDecodeError(name, fmt.Errorf("needs a map with string keys, has %q keys", kind)) } dataValKeys := make(map[reflect.Value]struct{}) dataValKeysUnused := make(map[any]struct{}) for _, dataValKey := range dataVal.MapKeys() { dataValKeys[dataValKey] = struct{}{} dataValKeysUnused[dataValKey.Interface()] = struct{}{} } targetValKeysUnused := make(map[any]struct{}) var errs []error // This slice will keep track of all the structs we'll be decoding. // There can be more than one struct if there are embedded structs // that are squashed. structs := make([]reflect.Value, 1, 5) structs[0] = val // Compile the list of all the fields that we're going to be decoding // from all the structs. type field struct { field reflect.StructField val reflect.Value } // remainField is set to a valid field set with the "remain" tag if // we are keeping track of remaining values. var remainField *field fields := []field{} for len(structs) > 0 { structVal := structs[0] structs = structs[1:] structType := structVal.Type() for i := 0; i < structType.NumField(); i++ { fieldType := structType.Field(i) fieldVal := structVal.Field(i) if fieldVal.Kind() == reflect.Ptr && fieldVal.Elem().Kind() == reflect.Struct { // Handle embedded struct pointers as embedded structs. fieldVal = fieldVal.Elem() } // If "squash" is specified in the tag, we squash the field down. squash := d.config.Squash && fieldVal.Kind() == reflect.Struct && fieldType.Anonymous remain := false // We always parse the tags cause we're looking for other tags too tagParts := strings.Split(fieldType.Tag.Get(d.config.TagName), ",") for _, tag := range tagParts[1:] { if tag == d.config.SquashTagOption { squash = true break } if tag == "remain" { remain = true break } } if squash { switch fieldVal.Kind() { case reflect.Struct: structs = append(structs, fieldVal) case reflect.Interface: if !fieldVal.IsNil() { structs = append(structs, fieldVal.Elem().Elem()) } default: errs = append(errs, newDecodeError( name+"."+fieldType.Name, fmt.Errorf("unsupported type for squash: %s", fieldVal.Kind()), )) } continue } // Build our field if remain { remainField = &field{fieldType, fieldVal} } else { // Normal struct field, store it away fields = append(fields, field{fieldType, fieldVal}) } } } // for fieldType, field := range fields { for _, f := range fields { field, fieldValue := f.field, f.val fieldName := field.Name tagValue := field.Tag.Get(d.config.TagName) if tagValue == "" && d.config.IgnoreUntaggedFields { continue } tagValue = strings.SplitN(tagValue, ",", 2)[0] if tagValue != "" { fieldName = tagValue } rawMapKey := reflect.ValueOf(fieldName) rawMapVal := dataVal.MapIndex(rawMapKey) if !rawMapVal.IsValid() { // Do a slower search by iterating over each key and // doing case-insensitive search. for dataValKey := range dataValKeys { mK, ok := dataValKey.Interface().(string) if !ok { // Not a string key continue } if d.config.MatchName(mK, fieldName) { rawMapKey = dataValKey rawMapVal = dataVal.MapIndex(dataValKey) break } } 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{}{} } continue } } if !fieldValue.IsValid() { // This should never happen panic("field is not valid") } // If we can't set the field, then it is unexported or something, // and we just continue onwards. if !fieldValue.CanSet() { continue } // Delete the key we're using from the unused map so we stop tracking delete(dataValKeysUnused, rawMapKey.Interface()) // If the name is empty string, then we're at the root, and we // don't dot-join the fields. if name != "" { fieldName = name + "." + fieldName } if err := d.decode(fieldName, rawMapVal.Interface(), fieldValue); err != nil { errs = append(errs, err) } } // If we have a "remain"-tagged field and we have unused keys then // we put the unused keys directly into the remain field. if remainField != nil && len(dataValKeysUnused) > 0 { // Build a map of only the unused values remain := map[any]any{} for key := range dataValKeysUnused { remain[key] = dataVal.MapIndex(reflect.ValueOf(key)).Interface() } // Decode it as-if we were just decoding this map onto our map. if err := d.decodeMap(name, remain, remainField.val); err != nil { errs = append(errs, err) } // Set the map to nil so we have none so that the next check will // not error (ErrorUnused) dataValKeysUnused = nil } if d.config.ErrorUnused && len(dataValKeysUnused) > 0 { keys := make([]string, 0, len(dataValKeysUnused)) for rawKey := range dataValKeysUnused { keys = append(keys, rawKey.(string)) } sort.Strings(keys) errs = append(errs, newDecodeError( name, fmt.Errorf("has invalid keys: %s", strings.Join(keys, ", ")), )) } if d.config.ErrorUnset && len(targetValKeysUnused) > 0 { keys := make([]string, 0, len(targetValKeysUnused)) for rawKey := range targetValKeysUnused { keys = append(keys, rawKey.(string)) } sort.Strings(keys) errs = append(errs, newDecodeError( name, fmt.Errorf("has unset fields: %s", strings.Join(keys, ", ")), )) } if err := errors.Join(errs...); err != nil { return err } // Add the unused keys to the list of unused keys if we're tracking metadata if d.config.Metadata != nil { for rawKey := range dataValKeysUnused { key := rawKey.(string) if name != "" { key = name + "." + key } d.config.Metadata.Unused = append(d.config.Metadata.Unused, key) } for rawKey := range targetValKeysUnused { key := rawKey.(string) if name != "" { key = name + "." + key } d.config.Metadata.Unset = append(d.config.Metadata.Unset, key) } } return nil } func isEmptyValue(v reflect.Value) bool { switch getKind(v) { case reflect.Array, reflect.Map, reflect.Slice, reflect.String: return v.Len() == 0 case reflect.Bool: return !v.Bool() case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: return v.Int() == 0 case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: return v.Uint() == 0 case reflect.Float32, reflect.Float64: return v.Float() == 0 case reflect.Interface, reflect.Ptr: return v.IsNil() } return false } func getKind(val reflect.Value) reflect.Kind { kind := val.Kind() switch { case kind >= reflect.Int && kind <= reflect.Int64: return reflect.Int case kind >= reflect.Uint && kind <= reflect.Uint64: return reflect.Uint case kind >= reflect.Float32 && kind <= reflect.Float64: return reflect.Float32 case kind >= reflect.Complex64 && kind <= reflect.Complex128: return reflect.Complex64 default: return kind } } func isStructTypeConvertibleToMap(typ reflect.Type, checkMapstructureTags bool, tagName string) bool { for i := 0; i < typ.NumField(); i++ { f := typ.Field(i) if f.PkgPath == "" && !checkMapstructureTags { // check for unexported fields return true } if checkMapstructureTags && f.Tag.Get(tagName) != "" { // check for mapstructure tags inside return true } } return false } func dereferencePtrToStructIfNeeded(v reflect.Value, tagName string) reflect.Value { if v.Kind() != reflect.Ptr || v.Elem().Kind() != reflect.Struct { return v } deref := v.Elem() derefT := deref.Type() if isStructTypeConvertibleToMap(derefT, true, tagName) { return deref } return v } golang-github-go-viper-mapstructure-2.4.0/mapstructure_benchmark_test.go000066400000000000000000000136221510370305500266670ustar00rootroot00000000000000package mapstructure import ( "encoding/json" "testing" ) type Person struct { Name string Age int Emails []string Extra map[string]string } func Benchmark_Decode(b *testing.B) { input := map[string]any{ "name": "Mitchell", "age": 91, "emails": []string{"one", "two", "three"}, "extra": map[string]string{ "twitter": "mitchellh", }, } var result Person for i := 0; i < b.N; i++ { Decode(input, &result) } } // decodeViaJSON takes the map data and passes it through encoding/json to convert it into the // given Go native structure pointed to by v. v must be a pointer to a struct. func decodeViaJSON(data any, v any) error { // Perform the task by simply marshalling the input into JSON, // then unmarshalling it into target native Go struct. b, err := json.Marshal(data) if err != nil { return err } return json.Unmarshal(b, v) } func Benchmark_DecodeViaJSON(b *testing.B) { input := map[string]any{ "name": "Mitchell", "age": 91, "emails": []string{"one", "two", "three"}, "extra": map[string]string{ "twitter": "mitchellh", }, } var result Person for i := 0; i < b.N; i++ { decodeViaJSON(input, &result) } } func Benchmark_JSONUnmarshal(b *testing.B) { input := map[string]any{ "name": "Mitchell", "age": 91, "emails": []string{"one", "two", "three"}, "extra": map[string]string{ "twitter": "mitchellh", }, } inputB, err := json.Marshal(input) if err != nil { b.Fatal("Failed to marshal test input:", err) } var result Person for i := 0; i < b.N; i++ { json.Unmarshal(inputB, &result) } } func Benchmark_DecodeBasic(b *testing.B) { input := map[string]any{ "vstring": "foo", "vint": 42, "Vuint": 42, "vbool": true, "Vfloat": 42.42, "vsilent": true, "vdata": 42, "vjsonInt": json.Number("1234"), "vjsonFloat": json.Number("1234.5"), "vjsonNumber": json.Number("1234.5"), } for i := 0; i < b.N; i++ { var result Basic Decode(input, &result) } } func Benchmark_DecodeEmbedded(b *testing.B) { input := map[string]any{ "vstring": "foo", "Basic": map[string]any{ "vstring": "innerfoo", }, "vunique": "bar", } var result Embedded for i := 0; i < b.N; i++ { Decode(input, &result) } } func Benchmark_DecodeTypeConversion(b *testing.B) { input := map[string]any{ "IntToFloat": 42, "IntToUint": 42, "IntToBool": 1, "IntToString": 42, "UintToInt": 42, "UintToFloat": 42, "UintToBool": 42, "UintToString": 42, "BoolToInt": true, "BoolToUint": true, "BoolToFloat": true, "BoolToString": true, "FloatToInt": 42.42, "FloatToUint": 42.42, "FloatToBool": 42.42, "FloatToString": 42.42, "StringToInt": "42", "StringToUint": "42", "StringToBool": "1", "StringToFloat": "42.42", "SliceToMap": []any{}, "MapToSlice": map[string]any{}, } var resultStrict TypeConversionResult for i := 0; i < b.N; i++ { Decode(input, &resultStrict) } } func Benchmark_DecodeMap(b *testing.B) { input := map[string]any{ "vfoo": "foo", "vother": map[any]any{ "foo": "foo", "bar": "bar", }, } var result Map for i := 0; i < b.N; i++ { Decode(input, &result) } } func Benchmark_DecodeMapOfStruct(b *testing.B) { input := map[string]any{ "value": map[string]any{ "foo": map[string]string{"vstring": "one"}, "bar": map[string]string{"vstring": "two"}, }, } var result MapOfStruct for i := 0; i < b.N; i++ { Decode(input, &result) } } func Benchmark_DecodeSlice(b *testing.B) { input := map[string]any{ "vfoo": "foo", "vbar": []string{"foo", "bar", "baz"}, } var result Slice for i := 0; i < b.N; i++ { Decode(input, &result) } } func Benchmark_DecodeSliceOfStruct(b *testing.B) { input := map[string]any{ "value": []map[string]any{ {"vstring": "one"}, {"vstring": "two"}, }, } var result SliceOfStruct for i := 0; i < b.N; i++ { Decode(input, &result) } } func Benchmark_DecodeWeaklyTypedInput(b *testing.B) { // This input can come from anywhere, but typically comes from // something like decoding JSON, generated by a weakly typed language // such as PHP. input := map[string]any{ "name": 123, // number => string "age": "42", // string => number "emails": map[string]any{}, // empty map => empty array } var result Person config := &DecoderConfig{ WeaklyTypedInput: true, Result: &result, } decoder, err := NewDecoder(config) if err != nil { panic(err) } for i := 0; i < b.N; i++ { decoder.Decode(input) } } func Benchmark_DecodeMetadata(b *testing.B) { input := map[string]any{ "name": "Mitchell", "age": 91, "email": "foo@bar.com", } var md Metadata var result Person config := &DecoderConfig{ Metadata: &md, Result: &result, } decoder, err := NewDecoder(config) if err != nil { panic(err) } for i := 0; i < b.N; i++ { decoder.Decode(input) } } func Benchmark_DecodeMetadataEmbedded(b *testing.B) { input := map[string]any{ "vstring": "foo", "vunique": "bar", } var md Metadata var result EmbeddedSquash config := &DecoderConfig{ Metadata: &md, Result: &result, } decoder, err := NewDecoder(config) if err != nil { b.Fatalf("err: %s", err) } for i := 0; i < b.N; i++ { decoder.Decode(input) } } func Benchmark_DecodeTagged(b *testing.B) { input := map[string]any{ "foo": "bar", "bar": "value", } var result Tagged for i := 0; i < b.N; i++ { Decode(input, &result) } } func Benchmark_DecodeWithRemainingFields(b *testing.B) { type Person struct { Name string Other map[string]any `mapstructure:",remain"` } input := map[string]any{ "name": "Luffy", "age": 19, "powers": []string{ "Rubber Man", "Conqueror Haki", }, } for i := 0; i < b.N; i++ { // Decoding Map -> Struct var person Person _ = Decode(input, &person) // Decoding Struct -> Map result := make(map[string]any) _ = Decode(&person, &result) } } golang-github-go-viper-mapstructure-2.4.0/mapstructure_bugs_test.go000066400000000000000000000357001510370305500256760ustar00rootroot00000000000000package mapstructure import ( "reflect" "testing" "time" ) // GH-1, GH-10, GH-96 func TestDecode_NilValue(t *testing.T) { t.Parallel() tests := []struct { name string in any target any out any metaKeys []string metaUnused []string }{ { "all nil", &map[string]any{ "vfoo": nil, "vother": nil, }, &Map{Vfoo: "foo", Vother: map[string]string{"foo": "bar"}}, &Map{Vfoo: "", Vother: nil}, []string{"Vfoo", "Vother"}, []string{}, }, { "partial nil", &map[string]any{ "vfoo": "baz", "vother": nil, }, &Map{Vfoo: "foo", Vother: map[string]string{"foo": "bar"}}, &Map{Vfoo: "baz", Vother: nil}, []string{"Vfoo", "Vother"}, []string{}, }, { "partial decode", &map[string]any{ "vother": nil, }, &Map{Vfoo: "foo", Vother: map[string]string{"foo": "bar"}}, &Map{Vfoo: "foo", Vother: nil}, []string{"Vother"}, []string{}, }, { "unused values", &map[string]any{ "vbar": "bar", "vfoo": nil, "vother": nil, }, &Map{Vfoo: "foo", Vother: map[string]string{"foo": "bar"}}, &Map{Vfoo: "", Vother: nil}, []string{"Vfoo", "Vother"}, []string{"vbar"}, }, { "map interface all nil", &map[any]any{ "vfoo": nil, "vother": nil, }, &Map{Vfoo: "foo", Vother: map[string]string{"foo": "bar"}}, &Map{Vfoo: "", Vother: nil}, []string{"Vfoo", "Vother"}, []string{}, }, { "map interface partial nil", &map[any]any{ "vfoo": "baz", "vother": nil, }, &Map{Vfoo: "foo", Vother: map[string]string{"foo": "bar"}}, &Map{Vfoo: "baz", Vother: nil}, []string{"Vfoo", "Vother"}, []string{}, }, { "map interface partial decode", &map[any]any{ "vother": nil, }, &Map{Vfoo: "foo", Vother: map[string]string{"foo": "bar"}}, &Map{Vfoo: "foo", Vother: nil}, []string{"Vother"}, []string{}, }, { "map interface unused values", &map[any]any{ "vbar": "bar", "vfoo": nil, "vother": nil, }, &Map{Vfoo: "foo", Vother: map[string]string{"foo": "bar"}}, &Map{Vfoo: "", Vother: nil}, []string{"Vfoo", "Vother"}, []string{"vbar"}, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { config := &DecoderConfig{ Metadata: new(Metadata), Result: tc.target, ZeroFields: true, } decoder, err := NewDecoder(config) if err != nil { t.Fatalf("should not error: %s", err) } err = decoder.Decode(tc.in) if err != nil { t.Fatalf("should not error: %s", err) } if !reflect.DeepEqual(tc.out, tc.target) { t.Fatalf("%q: TestDecode_NilValue() expected: %#v, got: %#v", tc.name, tc.out, tc.target) } if !reflect.DeepEqual(tc.metaKeys, config.Metadata.Keys) { t.Fatalf("%q: Metadata.Keys mismatch expected: %#v, got: %#v", tc.name, tc.metaKeys, config.Metadata.Keys) } if !reflect.DeepEqual(tc.metaUnused, config.Metadata.Unused) { t.Fatalf("%q: Metadata.Unused mismatch expected: %#v, got: %#v", tc.name, tc.metaUnused, config.Metadata.Unused) } }) } } // #48 func TestNestedTypePointerWithDefaults(t *testing.T) { t.Parallel() input := map[string]any{ "vfoo": "foo", "vbar": map[string]any{ "vstring": "foo", "vint": 42, "vbool": true, }, } result := NestedPointer{ Vbar: &Basic{ Vuint: 42, }, } err := Decode(input, &result) if err != nil { t.Fatalf("got an err: %s", err.Error()) } if result.Vfoo != "foo" { t.Errorf("vfoo value should be 'foo': %#v", result.Vfoo) } if result.Vbar.Vstring != "foo" { t.Errorf("vstring value should be 'foo': %#v", result.Vbar.Vstring) } if result.Vbar.Vint != 42 { t.Errorf("vint value should be 42: %#v", result.Vbar.Vint) } if result.Vbar.Vbool != true { t.Errorf("vbool value should be true: %#v", result.Vbar.Vbool) } if result.Vbar.Vextra != "" { t.Errorf("vextra value should be empty: %#v", result.Vbar.Vextra) } // this is the error if result.Vbar.Vuint != 42 { t.Errorf("vuint value should be 42: %#v", result.Vbar.Vuint) } } type NestedSlice struct { Vfoo string Vbars []Basic Vempty []Basic } // #48 func TestNestedTypeSliceWithDefaults(t *testing.T) { t.Parallel() input := map[string]any{ "vfoo": "foo", "vbars": []map[string]any{ {"vstring": "foo", "vint": 42, "vbool": true}, {"vint": 42, "vbool": true}, }, "vempty": []map[string]any{ {"vstring": "foo", "vint": 42, "vbool": true}, {"vint": 42, "vbool": true}, }, } result := NestedSlice{ Vbars: []Basic{ {Vuint: 42}, {Vstring: "foo"}, }, } err := Decode(input, &result) if err != nil { t.Fatalf("got an err: %s", err.Error()) } if result.Vfoo != "foo" { t.Errorf("vfoo value should be 'foo': %#v", result.Vfoo) } if result.Vbars[0].Vstring != "foo" { t.Errorf("vstring value should be 'foo': %#v", result.Vbars[0].Vstring) } // this is the error if result.Vbars[0].Vuint != 42 { t.Errorf("vuint value should be 42: %#v", result.Vbars[0].Vuint) } } // #48 workaround func TestNestedTypeWithDefaults(t *testing.T) { t.Parallel() input := map[string]any{ "vfoo": "foo", "vbar": map[string]any{ "vstring": "foo", "vint": 42, "vbool": true, }, } result := Nested{ Vbar: Basic{ Vuint: 42, }, } err := Decode(input, &result) if err != nil { t.Fatalf("got an err: %s", err.Error()) } if result.Vfoo != "foo" { t.Errorf("vfoo value should be 'foo': %#v", result.Vfoo) } if result.Vbar.Vstring != "foo" { t.Errorf("vstring value should be 'foo': %#v", result.Vbar.Vstring) } if result.Vbar.Vint != 42 { t.Errorf("vint value should be 42: %#v", result.Vbar.Vint) } if result.Vbar.Vbool != true { t.Errorf("vbool value should be true: %#v", result.Vbar.Vbool) } if result.Vbar.Vextra != "" { t.Errorf("vextra value should be empty: %#v", result.Vbar.Vextra) } // this is the error if result.Vbar.Vuint != 42 { t.Errorf("vuint value should be 42: %#v", result.Vbar.Vuint) } } // #67 panic() on extending slices (decodeSlice with disabled ZeroValues) func TestDecodeSliceToEmptySliceWOZeroing(t *testing.T) { t.Parallel() type TestStruct struct { Vfoo []string } decode := func(m any, rawVal any) error { config := &DecoderConfig{ Metadata: nil, Result: rawVal, ZeroFields: false, } decoder, err := NewDecoder(config) if err != nil { return err } return decoder.Decode(m) } { input := map[string]any{ "vfoo": []string{"1"}, } result := &TestStruct{} err := decode(input, &result) if err != nil { t.Fatalf("got an err: %s", err.Error()) } } { input := map[string]any{ "vfoo": []string{"1"}, } result := &TestStruct{ Vfoo: []string{}, } err := decode(input, &result) if err != nil { t.Fatalf("got an err: %s", err.Error()) } } { input := map[string]any{ "vfoo": []string{"2", "3"}, } result := &TestStruct{ Vfoo: []string{"1"}, } err := decode(input, &result) if err != nil { t.Fatalf("got an err: %s", err.Error()) } } } // #70 func TestNextSquashMapstructure(t *testing.T) { data := &struct { Level1 struct { Level2 struct { Foo string } `mapstructure:",squash"` } `mapstructure:",squash"` }{} err := Decode(map[any]any{"foo": "baz"}, &data) if err != nil { t.Fatalf("should not error: %s", err) } if data.Level1.Level2.Foo != "baz" { t.Fatal("value should be baz") } } type ImplementsInterfacePointerReceiver struct { Name string } func (i *ImplementsInterfacePointerReceiver) DoStuff() {} type ImplementsInterfaceValueReceiver string func (i ImplementsInterfaceValueReceiver) DoStuff() {} // GH-140 Type error when using DecodeHook to decode into interface func TestDecode_DecodeHookInterface(t *testing.T) { t.Parallel() type Interface interface { DoStuff() } type DecodeIntoInterface struct { Test Interface } testData := map[string]string{"test": "test"} stringToPointerInterfaceDecodeHook := func(from, to reflect.Type, data any) (any, error) { if from.Kind() != reflect.String { return data, nil } if to != reflect.TypeOf((*Interface)(nil)).Elem() { return data, nil } // Ensure interface is satisfied var impl Interface = &ImplementsInterfacePointerReceiver{data.(string)} return impl, nil } stringToValueInterfaceDecodeHook := func(from, to reflect.Type, data any) (any, error) { if from.Kind() != reflect.String { return data, nil } if to != reflect.TypeOf((*Interface)(nil)).Elem() { return data, nil } // Ensure interface is satisfied var impl Interface = ImplementsInterfaceValueReceiver(data.(string)) return impl, nil } { decodeInto := new(DecodeIntoInterface) decoder, _ := NewDecoder(&DecoderConfig{ DecodeHook: stringToPointerInterfaceDecodeHook, Result: decodeInto, }) err := decoder.Decode(testData) if err != nil { t.Fatalf("Decode returned error: %s", err) } expected := &ImplementsInterfacePointerReceiver{"test"} if !reflect.DeepEqual(decodeInto.Test, expected) { t.Fatalf("expected: %#v (%T), got: %#v (%T)", decodeInto.Test, decodeInto.Test, expected, expected) } } { decodeInto := new(DecodeIntoInterface) decoder, _ := NewDecoder(&DecoderConfig{ DecodeHook: stringToValueInterfaceDecodeHook, Result: decodeInto, }) err := decoder.Decode(testData) if err != nil { t.Fatalf("Decode returned error: %s", err) } expected := ImplementsInterfaceValueReceiver("test") if !reflect.DeepEqual(decodeInto.Test, expected) { t.Fatalf("expected: %#v (%T), got: %#v (%T)", decodeInto.Test, decodeInto.Test, expected, expected) } } } // #103 Check for data type before trying to access its composants prevent a panic error // in decodeSlice func TestDecodeBadDataTypeInSlice(t *testing.T) { t.Parallel() input := map[string]any{ "Toto": "titi", } result := []struct { Toto string }{} if err := Decode(input, &result); err == nil { t.Error("An error was expected, got nil") } } // #202 Ensure that intermediate maps in the struct -> struct decode process are settable // and not just the elements within them. func TestDecodeIntermediateMapsSettable(t *testing.T) { type Timestamp struct { Seconds int64 Nanos int32 } type TsWrapper struct { Timestamp *Timestamp } type TimeWrapper struct { Timestamp time.Time } input := TimeWrapper{ Timestamp: time.Unix(123456789, 987654), } expected := TsWrapper{ Timestamp: &Timestamp{ Seconds: 123456789, Nanos: 987654, }, } timePtrType := reflect.TypeOf((*time.Time)(nil)) mapStrInfType := reflect.TypeOf((map[string]any)(nil)) var actual TsWrapper decoder, err := NewDecoder(&DecoderConfig{ Result: &actual, DecodeHook: func(from, to reflect.Type, data any) (any, error) { if from == timePtrType && to == mapStrInfType { ts := data.(*time.Time) nanos := ts.UnixNano() seconds := nanos / 1000000000 nanos = nanos % 1000000000 return &map[string]any{ "Seconds": seconds, "Nanos": int32(nanos), }, nil } return data, nil }, }) if err != nil { t.Fatalf("failed to create decoder: %v", err) } if err := decoder.Decode(&input); err != nil { t.Fatalf("failed to decode input: %v", err) } if !reflect.DeepEqual(expected, actual) { t.Fatalf("expected: %#[1]v (%[1]T), got: %#[2]v (%[2]T)", expected, actual) } } // GH-206: decodeInt throws an error for an empty string func TestDecode_weakEmptyStringToInt(t *testing.T) { input := map[string]any{ "StringToInt": "", "StringToUint": "", "StringToBool": "", "StringToFloat": "", } expectedResultWeak := TypeConversionResult{ StringToInt: 0, StringToUint: 0, StringToBool: false, StringToFloat: 0, } // Test weak type conversion var resultWeak TypeConversionResult err := WeakDecode(input, &resultWeak) if err != nil { t.Fatalf("got an err: %s", err) } if !reflect.DeepEqual(resultWeak, expectedResultWeak) { t.Errorf("expected \n%#v, got: \n%#v", expectedResultWeak, resultWeak) } } // GH-228: Squash cause *time.Time set to zero func TestMapSquash(t *testing.T) { type AA struct { T *time.Time } type A struct { AA } v := time.Now() in := &AA{ T: &v, } out := &A{} d, err := NewDecoder(&DecoderConfig{ Squash: true, Result: out, }) if err != nil { t.Fatalf("err: %s", err) } if err := d.Decode(in); err != nil { t.Fatalf("err: %s", err) } // these failed if !v.Equal(*out.T) { t.Fatal("expected equal") } if out.T.IsZero() { t.Fatal("expected false") } } // GH-238: Empty key name when decoding map from struct with only omitempty flag func TestMapOmitEmptyWithEmptyFieldnameInTag(t *testing.T) { type Struct struct { Username string `mapstructure:",omitempty"` Age int `mapstructure:",omitempty"` } s := Struct{ Username: "Joe", } var m map[string]any if err := Decode(s, &m); err != nil { t.Fatal(err) } if len(m) != 1 { t.Fatalf("fail: %#v", m) } if m["Username"] != "Joe" { t.Fatalf("fail: %#v", m) } } // GH-340: Decoding array of slices causes panic type HasNonComparableType struct { NonComparableType [2][]byte } func TestDecode_nonComparableType(t *testing.T) { decodeTo := &HasNonComparableType{} expected := [2][]byte{{1, 2}, {3, 4, 5}} if err := Decode(map[string]any{"NonComparableType": expected}, &decodeTo); err != nil { t.Fatal(err) } if !reflect.DeepEqual(expected, decodeTo.NonComparableType) { t.Fatalf("fail: %#v", decodeTo.NonComparableType) } } // GH-347: Decoding maps with multiple indirection results in an error func TestDecodeToMapWithMultipleIndirection(t *testing.T) { t.Run("Struct", func(t *testing.T) { type Struct struct { Foo string `mapstructure:"foo"` } v := Struct{ Foo: "bar", } i := &v ii := &i iii := &ii var actual map[string]any if err := Decode(iii, &actual); err != nil { t.Fatal(err) } expected := map[string]any{ "foo": "bar", } if !reflect.DeepEqual(actual, expected) { t.Fatalf("expected: %#v, got: %#v", expected, actual) } }) t.Run("Map", func(t *testing.T) { v := map[string]any{ "foo": "bar", } i := &v ii := &i iii := &ii var actual map[string]any if err := Decode(iii, &actual); err != nil { t.Fatal(err) } expected := map[string]any{ "foo": "bar", } if !reflect.DeepEqual(actual, expected) { t.Fatalf("expected: %#v, got: %#v", expected, actual) } }) t.Run("Array", func(t *testing.T) { v := [1]map[string]any{ { "foo": "bar", }, } i := &v ii := &i iii := &ii var actual map[string]any if err := WeakDecode(iii, &actual); err != nil { t.Fatal(err) } expected := map[string]any{ "foo": "bar", } if !reflect.DeepEqual(actual, expected) { t.Fatalf("expected: %#v, got: %#v", expected, actual) } }) t.Run("Slice", func(t *testing.T) { v := []map[string]any{ { "foo": "bar", }, } i := &v ii := &i iii := &ii var actual map[string]any if err := WeakDecode(iii, &actual); err != nil { t.Fatal(err) } expected := map[string]any{ "foo": "bar", } if !reflect.DeepEqual(actual, expected) { t.Fatalf("expected: %#v, got: %#v", expected, actual) } }) } golang-github-go-viper-mapstructure-2.4.0/mapstructure_examples_test.go000066400000000000000000000177271510370305500265650ustar00rootroot00000000000000package mapstructure import ( "fmt" "reflect" "strconv" "strings" ) func ExampleDecode() { type Person struct { Name string Age int Emails []string Extra map[string]string } // This input can come from anywhere, but typically comes from // something like decoding JSON where we're not quite sure of the // struct initially. input := map[string]any{ "name": "Mitchell", "age": 91, "emails": []string{"one", "two", "three"}, "extra": map[string]string{ "twitter": "mitchellh", }, } var result Person err := Decode(input, &result) if err != nil { panic(err) } fmt.Printf("%#v", result) // Output: // mapstructure.Person{Name:"Mitchell", Age:91, Emails:[]string{"one", "two", "three"}, Extra:map[string]string{"twitter":"mitchellh"}} } func ExampleDecode_errors() { type Person struct { Name string Age int Emails []string Extra map[string]string } // This input can come from anywhere, but typically comes from // something like decoding JSON where we're not quite sure of the // struct initially. input := map[string]any{ "name": 123, "age": "bad value", "emails": []int{1, 2, 3}, } var result Person err := Decode(input, &result) if err == nil { panic("should have an error") } fmt.Println(err.Error()) // Output: // decoding failed due to the following error(s): // // '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() { type Person struct { Name string Age int } // This input can come from anywhere, but typically comes from // something like decoding JSON where we're not quite sure of the // struct initially. input := map[string]any{ "name": "Mitchell", "age": 91, "email": "foo@bar.com", } // For metadata, we make a more advanced DecoderConfig so we can // more finely configure the decoder that is used. In this case, we // just tell the decoder we want to track metadata. var md Metadata var result Person config := &DecoderConfig{ Metadata: &md, Result: &result, } decoder, err := NewDecoder(config) if err != nil { panic(err) } if err := decoder.Decode(input); err != nil { panic(err) } fmt.Printf("Unused keys: %#v", md.Unused) // Output: // Unused keys: []string{"email"} } func ExampleDecode_weaklyTypedInput() { type Person struct { Name string Age int Emails []string } // This input can come from anywhere, but typically comes from // something like decoding JSON, generated by a weakly typed language // such as PHP. input := map[string]any{ "name": 123, // number => string "age": "42", // string => number "emails": map[string]any{}, // empty map => empty array } var result Person config := &DecoderConfig{ WeaklyTypedInput: true, Result: &result, } decoder, err := NewDecoder(config) if err != nil { panic(err) } err = decoder.Decode(input) if err != nil { panic(err) } fmt.Printf("%#v", result) // Output: mapstructure.Person{Name:"123", Age:42, Emails:[]string{}} } func ExampleDecode_tags() { // Note that the mapstructure tags defined in the struct type // can indicate which fields the values are mapped to. type Person struct { Name string `mapstructure:"person_name"` Age int `mapstructure:"person_age"` } input := map[string]any{ "person_name": "Mitchell", "person_age": 91, } var result Person err := Decode(input, &result) if err != nil { panic(err) } fmt.Printf("%#v", result) // Output: // mapstructure.Person{Name:"Mitchell", Age:91} } func ExampleDecode_embeddedStruct() { // Squashing multiple embedded structs is allowed using the squash tag. // This is demonstrated by creating a composite struct of multiple types // and decoding into it. In this case, a person can carry with it both // a Family and a Location, as well as their own FirstName. type Family struct { LastName string } type Location struct { City string } type Person struct { Family `mapstructure:",squash"` Location `mapstructure:",squash"` FirstName string } input := map[string]any{ "FirstName": "Mitchell", "LastName": "Hashimoto", "City": "San Francisco", } var result Person err := Decode(input, &result) if err != nil { panic(err) } fmt.Printf("%s %s, %s", result.FirstName, result.LastName, result.City) // Output: // Mitchell Hashimoto, San Francisco } func ExampleDecode_remainingData() { // Note that the mapstructure tags defined in the struct type // can indicate which fields the values are mapped to. type Person struct { Name string Age int Other map[string]any `mapstructure:",remain"` } input := map[string]any{ "name": "Mitchell", "age": 91, "email": "mitchell@example.com", } var result Person err := Decode(input, &result) if err != nil { panic(err) } fmt.Printf("%#v", result) // Output: // mapstructure.Person{Name:"Mitchell", Age:91, Other:map[string]interface {}{"email":"mitchell@example.com"}} } func ExampleDecode_remainingDataDecodeBackToMapInFlatFormat() { // Note that the mapstructure tags defined in the struct type // can indicate which fields the values are mapped to. type Person struct { Name string Age int Other map[string]any `mapstructure:",remain"` } input := map[string]any{ "name": "Luffy", "age": 19, "powers": []string{ "Rubber Man", "Conqueror Haki", }, } var person Person err := Decode(input, &person) if err != nil { panic(err) } result := make(map[string]any) err = Decode(&person, &result) if err != nil { panic(err) } fmt.Printf("%#v", result) // Output: // map[string]interface {}{"Age":19, "Name":"Luffy", "powers":[]string{"Rubber Man", "Conqueror Haki"}} } func ExampleDecode_omitempty() { // Add omitempty annotation to avoid map keys for empty values type Family struct { LastName string } type Location struct { City string } type Person struct { *Family `mapstructure:",omitempty"` *Location `mapstructure:",omitempty"` Age int FirstName string } result := &map[string]any{} input := Person{FirstName: "Somebody"} err := Decode(input, &result) if err != nil { panic(err) } fmt.Printf("%+v", result) // Output: // &map[Age:0 FirstName:Somebody] } func ExampleDecode_decodeHookFunc() { type PersonLocation struct { Latitude float64 Longtitude float64 } type Person struct { Name string Location PersonLocation } // Example of parsing messy input: here we have latitude, longitude squashed into // a single string field. We write a custom DecodeHookFunc to parse the '#' separated // values into a PersonLocation struct. input := map[string]any{ "name": "Mitchell", "location": "-35.2809#149.1300", } toPersonLocationHookFunc := func() DecodeHookFunc { return func(f reflect.Type, t reflect.Type, data any) (any, error) { if t != reflect.TypeOf(PersonLocation{}) { return data, nil } switch f.Kind() { case reflect.String: xs := strings.Split(data.(string), "#") if len(xs) == 2 { lat, errLat := strconv.ParseFloat(xs[0], 64) lon, errLon := strconv.ParseFloat(xs[1], 64) if errLat == nil && errLon == nil { return PersonLocation{Latitude: lat, Longtitude: lon}, nil } } else { return data, nil } } return data, nil } } var result Person decoder, errDecoder := NewDecoder(&DecoderConfig{ Metadata: nil, DecodeHook: toPersonLocationHookFunc(), // Here, use ComposeDecodeHookFunc to run multiple hooks. Result: &result, }) if errDecoder != nil { panic(errDecoder) } err := decoder.Decode(input) if err != nil { panic(err) } fmt.Printf("%#v", result) // Output: // mapstructure.Person{Name:"Mitchell", Location:mapstructure.PersonLocation{Latitude:-35.2809, Longtitude:149.13}} } golang-github-go-viper-mapstructure-2.4.0/mapstructure_test.go000066400000000000000000002214671510370305500246650ustar00rootroot00000000000000package mapstructure import ( "encoding/json" "errors" "fmt" "io" "reflect" "sort" "strings" "testing" "time" ) type Basic struct { Vstring string Vint int Vint8 int8 Vint16 int16 Vint32 int32 Vint64 int64 Vuint uint Vbool bool Vfloat float64 Vextra string vsilent bool Vdata any VjsonInt int VjsonUint uint VjsonUint64 uint64 VjsonFloat float64 VjsonNumber json.Number Vcomplex64 complex64 Vcomplex128 complex128 } type BasicPointer struct { Vstring *string Vint *int Vuint *uint Vbool *bool Vfloat *float64 Vextra *string vsilent *bool Vdata *any VjsonInt *int VjsonFloat *float64 VjsonNumber *json.Number } type BasicSquash struct { Test Basic `mapstructure:",squash"` } type BasicJSONInline struct { Test Basic `json:",inline"` } type Embedded struct { Basic Vunique string } type EmbeddedPointer struct { *Basic Vunique string } type EmbeddedSquash struct { Basic `mapstructure:",squash"` Vunique string } type EmbeddedPointerSquash struct { *Basic `mapstructure:",squash"` Vunique string } type BasicMapStructure struct { Vunique string `mapstructure:"vunique"` Vtime *time.Time `mapstructure:"time"` } type NestedPointerWithMapstructure struct { Vbar *BasicMapStructure `mapstructure:"vbar"` } type EmbeddedPointerSquashWithNestedMapstructure struct { *NestedPointerWithMapstructure `mapstructure:",squash"` Vunique string } type EmbeddedAndNamed struct { Basic Named Basic Vunique string } type SliceAlias []string type EmbeddedSlice struct { SliceAlias `mapstructure:"slice_alias"` Vunique string } type ArrayAlias [2]string type EmbeddedArray struct { ArrayAlias `mapstructure:"array_alias"` Vunique string } type SquashOnNonStructType struct { InvalidSquashType int `mapstructure:",squash"` } type TestInterface interface { GetVfoo() string GetVbarfoo() string GetVfoobar() string } type TestInterfaceImpl struct { Vfoo string } func (t *TestInterfaceImpl) GetVfoo() string { return t.Vfoo } func (t *TestInterfaceImpl) GetVbarfoo() string { return "" } func (t *TestInterfaceImpl) GetVfoobar() string { return "" } type TestNestedInterfaceImpl struct { SquashOnNestedInterfaceType `mapstructure:",squash"` Vfoo string } func (t *TestNestedInterfaceImpl) GetVfoo() string { return t.Vfoo } func (t *TestNestedInterfaceImpl) GetVbarfoo() string { return t.Vbarfoo } func (t *TestNestedInterfaceImpl) GetVfoobar() string { return t.NestedSquash.Vfoobar } type SquashOnInterfaceType struct { TestInterface `mapstructure:",squash"` Vbar string } type NestedSquash struct { SquashOnInterfaceType `mapstructure:",squash"` Vfoobar string } type SquashOnNestedInterfaceType struct { NestedSquash NestedSquash `mapstructure:",squash"` Vbarfoo string } type Map struct { Vfoo string Vother map[string]string } type MapOfStruct struct { Value map[string]Basic } type Nested struct { Vfoo string Vbar Basic } type NestedPointer struct { Vfoo string Vbar *Basic } type NilInterface struct { W io.Writer } type NilPointer struct { Value *string } type Slice struct { Vfoo string Vbar []string } type SliceOfByte struct { Vfoo string Vbar []byte } type SliceOfAlias struct { Vfoo string Vbar SliceAlias } type SliceOfStruct struct { Value []Basic } type SlicePointer struct { Vbar *[]string } type Array struct { Vfoo string Vbar [2]string } type ArrayOfStruct struct { Value [2]Basic } type Func struct { Foo func() string } type Tagged struct { Extra string `mapstructure:"bar,what,what"` Value string `mapstructure:"foo"` } type Remainder struct { A string Extra map[string]any `mapstructure:",remain"` } type StructWithOmitEmpty struct { VisibleStringField string `mapstructure:"visible-string"` OmitStringField string `mapstructure:"omittable-string,omitempty"` VisibleIntField int `mapstructure:"visible-int"` OmitIntField int `mapstructure:"omittable-int,omitempty"` VisibleFloatField float64 `mapstructure:"visible-float"` OmitFloatField float64 `mapstructure:"omittable-float,omitempty"` VisibleSliceField []any `mapstructure:"visible-slice"` OmitSliceField []any `mapstructure:"omittable-slice,omitempty"` VisibleMapField map[string]any `mapstructure:"visible-map"` OmitMapField map[string]any `mapstructure:"omittable-map,omitempty"` NestedField *Nested `mapstructure:"visible-nested"` 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 []any `mapstructure:"visible-slice"` OmitSliceField []any `mapstructure:"omittable-slice,omitzero"` VisibleMapField map[string]any `mapstructure:"visible-map"` OmitMapField map[string]any `mapstructure:"omittable-map,omitzero"` NestedField *Nested `mapstructure:"visible-nested"` OmitNestedField *Nested `mapstructure:"omittable-nested,omitzero"` } type TypeConversionResult struct { IntToFloat float32 IntToUint uint IntToBool bool IntToString string UintToInt int UintToFloat float32 UintToBool bool UintToString string BoolToInt int BoolToUint uint BoolToFloat float32 BoolToString string FloatToInt int FloatToUint uint FloatToBool bool FloatToString string SliceUint8ToString string StringToSliceUint8 []byte ArrayUint8ToString string StringToInt int StringToUint uint StringToBool bool StringToFloat float32 StringToStrSlice []string StringToIntSlice []int StringToStrArray [1]string StringToIntArray [1]int SliceToMap map[string]any MapToSlice []any ArrayToMap map[string]any MapToArray [1]any } func TestBasicTypes(t *testing.T) { t.Parallel() input := map[string]any{ "vstring": "foo", "vint": 42, "vint8": 42, "vint16": 42, "vint32": 42, "vint64": 42, "Vuint": 42, "vbool": true, "Vfloat": 42.42, "vsilent": true, "vdata": 42, "vjsonInt": json.Number("1234"), "vjsonUint": json.Number("1234"), "vjsonUint64": json.Number("9223372036854775809"), // 2^63 + 1 "vjsonFloat": json.Number("1234.5"), "vjsonNumber": json.Number("1234.5"), "vcomplex64": complex(float32(42), float32(42)), "vcomplex128": complex(42, 42), } var result Basic err := Decode(input, &result) if err != nil { t.Errorf("got an err: %s", err.Error()) t.FailNow() } if result.Vstring != "foo" { t.Errorf("vstring value should be 'foo': %#v", result.Vstring) } if result.Vint != 42 { t.Errorf("vint value should be 42: %#v", result.Vint) } if result.Vint8 != 42 { t.Errorf("vint8 value should be 42: %#v", result.Vint) } if result.Vint16 != 42 { t.Errorf("vint16 value should be 42: %#v", result.Vint) } if result.Vint32 != 42 { t.Errorf("vint32 value should be 42: %#v", result.Vint) } if result.Vint64 != 42 { t.Errorf("vint64 value should be 42: %#v", result.Vint) } if result.Vuint != 42 { t.Errorf("vuint value should be 42: %#v", result.Vuint) } if result.Vbool != true { t.Errorf("vbool value should be true: %#v", result.Vbool) } if result.Vfloat != 42.42 { t.Errorf("vfloat value should be 42.42: %#v", result.Vfloat) } if result.Vextra != "" { t.Errorf("vextra value should be empty: %#v", result.Vextra) } if result.vsilent != false { t.Error("vsilent should not be set, it is unexported") } if result.Vdata != 42 { t.Error("vdata should be valid") } if result.VjsonInt != 1234 { t.Errorf("vjsonint value should be 1234: %#v", result.VjsonInt) } if result.VjsonUint != 1234 { t.Errorf("vjsonuint value should be 1234: %#v", result.VjsonUint) } if result.VjsonUint64 != 9223372036854775809 { t.Errorf("vjsonuint64 value should be 9223372036854775809: %#v", result.VjsonUint64) } if result.VjsonFloat != 1234.5 { t.Errorf("vjsonfloat value should be 1234.5: %#v", result.VjsonFloat) } if !reflect.DeepEqual(result.VjsonNumber, json.Number("1234.5")) { t.Errorf("vjsonnumber value should be '1234.5': %T, %#v", result.VjsonNumber, result.VjsonNumber) } if real(result.Vcomplex64) != 42 || imag(result.Vcomplex64) != 42 { t.Errorf("vcomplex64 value shou be 42+42i: %#v", result.Vcomplex64) } if real(result.Vcomplex128) != 42 || imag(result.Vcomplex128) != 42 { t.Errorf("vcomplex64 value shou be 42+42i: %#v", result.Vcomplex128) } } func TestBasic_IntWithFloat(t *testing.T) { t.Parallel() input := map[string]any{ "vint": float64(42), } var result Basic err := Decode(input, &result) if err != nil { t.Fatalf("got an err: %s", err) } } func TestBasic_Merge(t *testing.T) { t.Parallel() input := map[string]any{ "vint": 42, } var result Basic result.Vuint = 100 err := Decode(input, &result) if err != nil { t.Fatalf("got an err: %s", err) } expected := Basic{ Vint: 42, Vuint: 100, } if !reflect.DeepEqual(result, expected) { t.Fatalf("bad: %#v", result) } } // Test for issue #46. func TestBasic_Struct(t *testing.T) { t.Parallel() input := map[string]any{ "vdata": map[string]any{ "vstring": "foo", }, } var result, inner Basic result.Vdata = &inner err := Decode(input, &result) if err != nil { t.Fatalf("got an err: %s", err) } expected := Basic{ Vdata: &Basic{ Vstring: "foo", }, } if !reflect.DeepEqual(result, expected) { t.Fatalf("bad: %#v", result) } } func TestBasic_interfaceStruct(t *testing.T) { t.Parallel() input := map[string]any{ "vstring": "foo", } var iface any = &Basic{} err := Decode(input, &iface) if err != nil { t.Fatalf("got an err: %s", err) } expected := &Basic{ Vstring: "foo", } if !reflect.DeepEqual(iface, expected) { t.Fatalf("bad: %#v", iface) } } // Issue 187 func TestBasic_interfaceStructNonPtr(t *testing.T) { t.Parallel() input := map[string]any{ "vstring": "foo", } var iface any = Basic{} err := Decode(input, &iface) if err != nil { t.Fatalf("got an err: %s", err) } expected := Basic{ Vstring: "foo", } if !reflect.DeepEqual(iface, expected) { t.Fatalf("bad: %#v", iface) } } func TestDecode_BasicSquash(t *testing.T) { t.Parallel() input := map[string]any{ "vstring": "foo", } var result BasicSquash err := Decode(input, &result) if err != nil { t.Fatalf("got an err: %s", err.Error()) } if result.Test.Vstring != "foo" { t.Errorf("vstring value should be 'foo': %#v", result.Test.Vstring) } } func TestDecodeFrom_BasicSquash(t *testing.T) { t.Parallel() var v any var ok bool input := BasicSquash{ Test: Basic{ Vstring: "foo", }, } var result map[string]any err := Decode(input, &result) if err != nil { t.Fatalf("got an err: %s", err.Error()) } if _, ok = result["Test"]; ok { t.Error("test should not be present in map") } v, ok = result["Vstring"] if !ok { t.Error("vstring should be present in map") } else if !reflect.DeepEqual(v, "foo") { t.Errorf("vstring value should be 'foo': %#v", v) } } func TestDecode_BasicJSONInline(t *testing.T) { t.Parallel() input := map[string]any{ "vstring": "foo", } var result BasicJSONInline d, err := NewDecoder(&DecoderConfig{TagName: "json", SquashTagOption: "inline", Result: &result}) if err != nil { t.Fatalf("got an err: %s", err.Error()) } if err := d.Decode(input); err != nil { t.Fatalf("got an err: %s", err.Error()) } if result.Test.Vstring != "foo" { t.Errorf("vstring value should be 'foo': %#v", result.Test.Vstring) } } func TestDecodeFrom_BasicJSONInline(t *testing.T) { t.Parallel() var v any var ok bool input := BasicJSONInline{ Test: Basic{ Vstring: "foo", }, } var result map[string]any d, err := NewDecoder(&DecoderConfig{TagName: "json", SquashTagOption: "inline", Result: &result}) if err != nil { t.Fatalf("got an err: %s", err.Error()) } if err := d.Decode(input); err != nil { t.Fatalf("got an err: %s", err.Error()) } if _, ok = result["Test"]; ok { t.Error("test should not be present in map") } v, ok = result["Vstring"] if !ok { t.Error("vstring should be present in map") } else if !reflect.DeepEqual(v, "foo") { t.Errorf("vstring value should be 'foo': %#v", v) } } func TestDecode_Embedded(t *testing.T) { t.Parallel() input := map[string]any{ "vstring": "foo", "Basic": map[string]any{ "vstring": "innerfoo", }, "vunique": "bar", } var result Embedded err := Decode(input, &result) if err != nil { t.Fatalf("got an err: %s", err.Error()) } if result.Vstring != "innerfoo" { t.Errorf("vstring value should be 'innerfoo': %#v", result.Vstring) } if result.Vunique != "bar" { t.Errorf("vunique value should be 'bar': %#v", result.Vunique) } } func TestDecode_EmbeddedPointer(t *testing.T) { t.Parallel() input := map[string]any{ "vstring": "foo", "Basic": map[string]any{ "vstring": "innerfoo", }, "vunique": "bar", } var result EmbeddedPointer err := Decode(input, &result) if err != nil { t.Fatalf("err: %s", err) } expected := EmbeddedPointer{ Basic: &Basic{ Vstring: "innerfoo", }, Vunique: "bar", } if !reflect.DeepEqual(result, expected) { t.Fatalf("bad: %#v", result) } } func TestDecode_EmbeddedSlice(t *testing.T) { t.Parallel() input := map[string]any{ "slice_alias": []string{"foo", "bar"}, "vunique": "bar", } var result EmbeddedSlice err := Decode(input, &result) if err != nil { t.Fatalf("got an err: %s", err.Error()) } if !reflect.DeepEqual(result.SliceAlias, SliceAlias([]string{"foo", "bar"})) { t.Errorf("slice value: %#v", result.SliceAlias) } if result.Vunique != "bar" { t.Errorf("vunique value should be 'bar': %#v", result.Vunique) } } func TestDecode_EmbeddedArray(t *testing.T) { t.Parallel() input := map[string]any{ "array_alias": [2]string{"foo", "bar"}, "vunique": "bar", } var result EmbeddedArray err := Decode(input, &result) if err != nil { t.Fatalf("got an err: %s", err.Error()) } if !reflect.DeepEqual(result.ArrayAlias, ArrayAlias([2]string{"foo", "bar"})) { t.Errorf("array value: %#v", result.ArrayAlias) } if result.Vunique != "bar" { t.Errorf("vunique value should be 'bar': %#v", result.Vunique) } } func TestDecode_decodeSliceWithArray(t *testing.T) { t.Parallel() var result []int input := [1]int{1} expected := []int{1} if err := Decode(input, &result); err != nil { t.Fatalf("got an err: %s", err.Error()) } if !reflect.DeepEqual(expected, result) { t.Errorf("wanted %+v, got %+v", expected, result) } } func TestDecode_EmbeddedNoSquash(t *testing.T) { t.Parallel() input := map[string]any{ "vstring": "foo", "vunique": "bar", } var result Embedded err := Decode(input, &result) if err != nil { t.Fatalf("got an err: %s", err.Error()) } if result.Vstring != "" { t.Errorf("vstring value should be empty: %#v", result.Vstring) } if result.Vunique != "bar" { t.Errorf("vunique value should be 'bar': %#v", result.Vunique) } } func TestDecode_EmbeddedPointerNoSquash(t *testing.T) { t.Parallel() input := map[string]any{ "vstring": "foo", "vunique": "bar", } result := EmbeddedPointer{ Basic: &Basic{}, } err := Decode(input, &result) if err != nil { t.Fatalf("err: %s", err) } if result.Vstring != "" { t.Errorf("vstring value should be empty: %#v", result.Vstring) } if result.Vunique != "bar" { t.Errorf("vunique value should be 'bar': %#v", result.Vunique) } } func TestDecode_EmbeddedSquash(t *testing.T) { t.Parallel() input := map[string]any{ "vstring": "foo", "vunique": "bar", } var result EmbeddedSquash err := Decode(input, &result) if err != nil { t.Fatalf("got an err: %s", err.Error()) } if result.Vstring != "foo" { t.Errorf("vstring value should be 'foo': %#v", result.Vstring) } if result.Vunique != "bar" { t.Errorf("vunique value should be 'bar': %#v", result.Vunique) } } func TestDecodeFrom_EmbeddedSquash(t *testing.T) { t.Parallel() var v any var ok bool input := EmbeddedSquash{ Basic: Basic{ Vstring: "foo", }, Vunique: "bar", } var result map[string]any err := Decode(input, &result) if err != nil { t.Fatalf("got an err: %s", err.Error()) } if _, ok = result["Basic"]; ok { t.Error("basic should not be present in map") } v, ok = result["Vstring"] if !ok { t.Error("vstring should be present in map") } else if !reflect.DeepEqual(v, "foo") { t.Errorf("vstring value should be 'foo': %#v", v) } v, ok = result["Vunique"] if !ok { t.Error("vunique should be present in map") } else if !reflect.DeepEqual(v, "bar") { t.Errorf("vunique value should be 'bar': %#v", v) } } func TestDecode_EmbeddedPointerSquash_FromStructToMap(t *testing.T) { t.Parallel() input := EmbeddedPointerSquash{ Basic: &Basic{ Vstring: "foo", }, Vunique: "bar", } var result map[string]any err := Decode(input, &result) if err != nil { t.Fatalf("got an err: %s", err.Error()) } if result["Vstring"] != "foo" { t.Errorf("vstring value should be 'foo': %#v", result["Vstring"]) } if result["Vunique"] != "bar" { t.Errorf("vunique value should be 'bar': %#v", result["Vunique"]) } } func TestDecode_EmbeddedPointerSquash_FromMapToStruct(t *testing.T) { t.Parallel() input := map[string]any{ "Vstring": "foo", "Vunique": "bar", } result := EmbeddedPointerSquash{ Basic: &Basic{}, } err := Decode(input, &result) if err != nil { t.Fatalf("got an err: %s", err.Error()) } if result.Vstring != "foo" { t.Errorf("vstring value should be 'foo': %#v", result.Vstring) } if result.Vunique != "bar" { t.Errorf("vunique value should be 'bar': %#v", result.Vunique) } } func TestDecode_EmbeddedPointerSquashWithNestedMapstructure_FromStructToMap(t *testing.T) { t.Parallel() vTime := time.Now() input := EmbeddedPointerSquashWithNestedMapstructure{ NestedPointerWithMapstructure: &NestedPointerWithMapstructure{ Vbar: &BasicMapStructure{ Vunique: "bar", Vtime: &vTime, }, }, Vunique: "foo", } var result map[string]any err := Decode(input, &result) if err != nil { t.Fatalf("got an err: %s", err.Error()) } expected := map[string]any{ "vbar": map[string]any{ "vunique": "bar", "time": &vTime, }, "Vunique": "foo", } if !reflect.DeepEqual(result, expected) { t.Errorf("result should be %#v: got %#v", expected, result) } } func TestDecode_EmbeddedPointerSquashWithNestedMapstructure_FromMapToStruct(t *testing.T) { t.Parallel() vTime := time.Now() input := map[string]any{ "vbar": map[string]any{ "vunique": "bar", "time": &vTime, }, "Vunique": "foo", } result := EmbeddedPointerSquashWithNestedMapstructure{ NestedPointerWithMapstructure: &NestedPointerWithMapstructure{}, } err := Decode(input, &result) if err != nil { t.Fatalf("got an err: %s", err.Error()) } expected := EmbeddedPointerSquashWithNestedMapstructure{ NestedPointerWithMapstructure: &NestedPointerWithMapstructure{ Vbar: &BasicMapStructure{ Vunique: "bar", Vtime: &vTime, }, }, Vunique: "foo", } if !reflect.DeepEqual(result, expected) { t.Errorf("result should be %#v: got %#v", expected, result) } } func TestDecode_EmbeddedSquashConfig(t *testing.T) { t.Parallel() input := map[string]any{ "vstring": "foo", "vunique": "bar", "Named": map[string]any{ "vstring": "baz", }, } var result EmbeddedAndNamed config := &DecoderConfig{ Squash: true, Result: &result, } decoder, err := NewDecoder(config) if err != nil { t.Fatalf("err: %s", err) } err = decoder.Decode(input) if err != nil { t.Fatalf("got an err: %s", err) } if result.Vstring != "foo" { t.Errorf("vstring value should be 'foo': %#v", result.Vstring) } if result.Vunique != "bar" { t.Errorf("vunique value should be 'bar': %#v", result.Vunique) } if result.Named.Vstring != "baz" { t.Errorf("Named.vstring value should be 'baz': %#v", result.Named.Vstring) } } func TestDecodeFrom_EmbeddedSquashConfig(t *testing.T) { t.Parallel() input := EmbeddedAndNamed{ Basic: Basic{Vstring: "foo"}, Named: Basic{Vstring: "baz"}, Vunique: "bar", } result := map[string]any{} config := &DecoderConfig{ Squash: true, Result: &result, } decoder, err := NewDecoder(config) if err != nil { t.Fatalf("got an err: %s", err.Error()) } err = decoder.Decode(input) if err != nil { t.Fatalf("got an err: %s", err.Error()) } if _, ok := result["Basic"]; ok { t.Error("basic should not be present in map") } v, ok := result["Vstring"] if !ok { t.Error("vstring should be present in map") } else if !reflect.DeepEqual(v, "foo") { t.Errorf("vstring value should be 'foo': %#v", v) } v, ok = result["Vunique"] if !ok { t.Error("vunique should be present in map") } else if !reflect.DeepEqual(v, "bar") { t.Errorf("vunique value should be 'bar': %#v", v) } v, ok = result["Named"] if !ok { t.Error("Named should be present in map") } else { named := v.(map[string]any) v, ok := named["Vstring"] if !ok { t.Error("Named: vstring should be present in map") } else if !reflect.DeepEqual(v, "baz") { t.Errorf("Named: vstring should be 'baz': %#v", v) } } } func TestDecodeFrom_EmbeddedSquashConfig_WithTags(t *testing.T) { t.Parallel() var v any var ok bool input := EmbeddedSquash{ Basic: Basic{ Vstring: "foo", }, Vunique: "bar", } result := map[string]any{} config := &DecoderConfig{ Squash: true, Result: &result, } decoder, err := NewDecoder(config) if err != nil { t.Fatalf("got an err: %s", err.Error()) } err = decoder.Decode(input) if err != nil { t.Fatalf("got an err: %s", err.Error()) } if _, ok = result["Basic"]; ok { t.Error("basic should not be present in map") } v, ok = result["Vstring"] if !ok { t.Error("vstring should be present in map") } else if !reflect.DeepEqual(v, "foo") { t.Errorf("vstring value should be 'foo': %#v", v) } v, ok = result["Vunique"] if !ok { t.Error("vunique should be present in map") } else if !reflect.DeepEqual(v, "bar") { t.Errorf("vunique value should be 'bar': %#v", v) } } func TestDecode_SquashOnNonStructType(t *testing.T) { t.Parallel() input := map[string]any{ "InvalidSquashType": 42, } var result SquashOnNonStructType err := Decode(input, &result) if err == nil { t.Fatal("unexpected success decoding invalid squash field type") } else if !strings.Contains(err.Error(), "unsupported type for squash") { t.Fatalf("unexpected error message for invalid squash field type: %s", err) } } func TestDecode_SquashOnInterfaceType(t *testing.T) { t.Parallel() input := map[string]any{ "VFoo": "42", "VBar": "43", } result := SquashOnInterfaceType{ TestInterface: &TestInterfaceImpl{}, } err := Decode(input, &result) if err != nil { t.Fatalf("got an err: %s", err) } res := result.GetVfoo() if res != "42" { t.Errorf("unexpected value for VFoo: %s", res) } res = result.Vbar if res != "43" { t.Errorf("unexpected value for Vbar: %s", res) } } func TestDecode_SquashOnOuterNestedInterfaceType(t *testing.T) { t.Parallel() input := map[string]any{ "VFoo": "42", "VBar": "43", "Vfoobar": "44", "Vbarfoo": "45", } result := SquashOnNestedInterfaceType{ NestedSquash: NestedSquash{ SquashOnInterfaceType: SquashOnInterfaceType{ TestInterface: &TestInterfaceImpl{}, }, }, } err := Decode(input, &result) if err != nil { t.Fatalf("got an err: %s", err) } res := result.NestedSquash.GetVfoo() if res != "42" { t.Errorf("unexpected value for VFoo: %s", res) } res = result.NestedSquash.Vbar if res != "43" { t.Errorf("unexpected value for Vbar: %s", res) } res = result.NestedSquash.Vfoobar if res != "44" { t.Errorf("unexpected value for Vfoobar: %s", res) } res = result.Vbarfoo if res != "45" { t.Errorf("unexpected value for Vbarfoo: %s", res) } } func TestDecode_SquashOnInnerNestedInterfaceType(t *testing.T) { t.Parallel() input := map[string]any{ "VFoo": "42", "VBar": "43", "Vfoobar": "44", "Vbarfoo": "45", } result := SquashOnInterfaceType{ TestInterface: &TestNestedInterfaceImpl{ SquashOnNestedInterfaceType: SquashOnNestedInterfaceType{ NestedSquash: NestedSquash{ SquashOnInterfaceType: SquashOnInterfaceType{ TestInterface: &TestInterfaceImpl{}, }, }, }, }, } err := Decode(input, &result) if err != nil { t.Fatalf("got an err: %s", err) } res := result.GetVfoo() if res != "42" { t.Errorf("unexpected value for VFoo: %s", res) } res = result.Vbar if res != "43" { t.Errorf("unexpected value for Vbar: %s", res) } res = result.GetVfoobar() if res != "44" { t.Errorf("unexpected value for Vfoobar: %s", res) } res = result.GetVbarfoo() if res != "45" { t.Errorf("unexpected value for Vbarfoo: %s", res) } } func TestDecode_SquashOnNilInterfaceType(t *testing.T) { t.Parallel() input := map[string]any{ "VFoo": "42", "VBar": "43", } result := SquashOnInterfaceType{ TestInterface: nil, } err := Decode(input, &result) if err != nil { t.Fatalf("got an err: %s", err) } res := result.Vbar if res != "43" { t.Errorf("unexpected value for Vbar: %s", res) } } func TestDecode_DecodeHook(t *testing.T) { t.Parallel() input := map[string]any{ "vint": "WHAT", } decodeHook := func(from reflect.Kind, to reflect.Kind, v any) (any, error) { if from == reflect.String && to != reflect.String { return 5, nil } return v, nil } var result Basic config := &DecoderConfig{ DecodeHook: decodeHook, Result: &result, } decoder, err := NewDecoder(config) if err != nil { t.Fatalf("err: %s", err) } err = decoder.Decode(input) if err != nil { t.Fatalf("got an err: %s", err) } if result.Vint != 5 { t.Errorf("vint should be 5: %#v", result.Vint) } } func TestDecode_DecodeHookType(t *testing.T) { t.Parallel() input := map[string]any{ "vint": "WHAT", } decodeHook := func(from reflect.Type, to reflect.Type, v any) (any, error) { if from.Kind() == reflect.String && to.Kind() != reflect.String { return 5, nil } return v, nil } var result Basic config := &DecoderConfig{ DecodeHook: decodeHook, Result: &result, } decoder, err := NewDecoder(config) if err != nil { t.Fatalf("err: %s", err) } err = decoder.Decode(input) if err != nil { t.Fatalf("got an err: %s", err) } if result.Vint != 5 { t.Errorf("vint should be 5: %#v", result.Vint) } } func TestDecode_Nil(t *testing.T) { t.Parallel() var input any result := Basic{ Vstring: "foo", } err := Decode(input, &result) if err != nil { t.Fatalf("err: %s", err) } if result.Vstring != "foo" { t.Fatalf("bad: %#v", result.Vstring) } } func TestDecode_NilInterfaceHook(t *testing.T) { t.Parallel() input := map[string]any{ "w": "", } decodeHook := func(f, t reflect.Type, v any) (any, error) { if t.String() == "io.Writer" { return nil, nil } return v, nil } var result NilInterface config := &DecoderConfig{ DecodeHook: decodeHook, Result: &result, } decoder, err := NewDecoder(config) if err != nil { t.Fatalf("err: %s", err) } err = decoder.Decode(input) if err != nil { t.Fatalf("got an err: %s", err) } if result.W != nil { t.Errorf("W should be nil: %#v", result.W) } } func TestDecode_NilPointerHook(t *testing.T) { t.Parallel() input := map[string]any{ "value": "", } decodeHook := func(f, t reflect.Type, v any) (any, error) { if typed, ok := v.(string); ok { if typed == "" { return nil, nil } } return v, nil } var result NilPointer config := &DecoderConfig{ DecodeHook: decodeHook, Result: &result, } decoder, err := NewDecoder(config) if err != nil { t.Fatalf("err: %s", err) } err = decoder.Decode(input) if err != nil { t.Fatalf("got an err: %s", err) } if result.Value != nil { t.Errorf("W should be nil: %#v", result.Value) } } func TestDecode_FuncHook(t *testing.T) { t.Parallel() input := map[string]any{ "foo": "baz", } decodeHook := func(f, t reflect.Type, v any) (any, error) { if t.Kind() != reflect.Func { return v, nil } val := v.(string) return func() string { return val }, nil } var result Func config := &DecoderConfig{ DecodeHook: decodeHook, Result: &result, } decoder, err := NewDecoder(config) if err != nil { t.Fatalf("err: %s", err) } err = decoder.Decode(input) if err != nil { t.Fatalf("got an err: %s", err) } if result.Foo() != "baz" { t.Errorf("Foo call result should be 'baz': %s", result.Foo()) } } func TestDecode_NonStruct(t *testing.T) { t.Parallel() input := map[string]any{ "foo": "bar", "bar": "baz", } var result map[string]string err := Decode(input, &result) if err != nil { t.Fatalf("err: %s", err) } if result["foo"] != "bar" { t.Fatal("foo is not bar") } } func TestDecode_StructMatch(t *testing.T) { t.Parallel() input := map[string]any{ "vbar": Basic{ Vstring: "foo", }, } var result Nested err := Decode(input, &result) if err != nil { t.Fatalf("got an err: %s", err.Error()) } if result.Vbar.Vstring != "foo" { t.Errorf("bad: %#v", result) } } func TestDecode_TypeConversion(t *testing.T) { input := map[string]any{ "IntToFloat": 42, "IntToUint": 42, "IntToBool": 1, "IntToString": 42, "UintToInt": 42, "UintToFloat": 42, "UintToBool": 42, "UintToString": 42, "BoolToInt": true, "BoolToUint": true, "BoolToFloat": true, "BoolToString": true, "FloatToInt": 42.42, "FloatToUint": 42.42, "FloatToBool": 42.42, "FloatToString": 42.42, "SliceUint8ToString": []uint8("foo"), "StringToSliceUint8": "foo", "ArrayUint8ToString": [3]uint8{'f', 'o', 'o'}, "StringToInt": "42", "StringToUint": "42", "StringToBool": "1", "StringToFloat": "42.42", "StringToStrSlice": "A", "StringToIntSlice": "42", "StringToStrArray": "A", "StringToIntArray": "42", "SliceToMap": []any{}, "MapToSlice": map[string]any{}, "ArrayToMap": []any{}, "MapToArray": map[string]any{}, } expectedResultStrict := TypeConversionResult{ IntToFloat: 42.0, IntToUint: 42, UintToInt: 42, UintToFloat: 42, BoolToInt: 0, BoolToUint: 0, BoolToFloat: 0, FloatToInt: 42, FloatToUint: 42, } expectedResultWeak := TypeConversionResult{ IntToFloat: 42.0, IntToUint: 42, IntToBool: true, IntToString: "42", UintToInt: 42, UintToFloat: 42, UintToBool: true, UintToString: "42", BoolToInt: 1, BoolToUint: 1, BoolToFloat: 1, BoolToString: "1", FloatToInt: 42, FloatToUint: 42, FloatToBool: true, FloatToString: "42.42", SliceUint8ToString: "foo", StringToSliceUint8: []byte("foo"), ArrayUint8ToString: "foo", StringToInt: 42, StringToUint: 42, StringToBool: true, StringToFloat: 42.42, StringToStrSlice: []string{"A"}, StringToIntSlice: []int{42}, StringToStrArray: [1]string{"A"}, StringToIntArray: [1]int{42}, SliceToMap: map[string]any{}, MapToSlice: []any{}, ArrayToMap: map[string]any{}, MapToArray: [1]any{}, } // Test strict type conversion var resultStrict TypeConversionResult err := Decode(input, &resultStrict) if err == nil { t.Errorf("should return an error") } if !reflect.DeepEqual(resultStrict, expectedResultStrict) { t.Errorf("expected %v, got: %v", expectedResultStrict, resultStrict) } // Test weak type conversion var decoder *Decoder var resultWeak TypeConversionResult config := &DecoderConfig{ WeaklyTypedInput: true, Result: &resultWeak, } decoder, err = NewDecoder(config) if err != nil { t.Fatalf("err: %s", err) } err = decoder.Decode(input) if err != nil { t.Fatalf("got an err: %s", err) } if !reflect.DeepEqual(resultWeak, expectedResultWeak) { t.Errorf("expected \n%#v, got: \n%#v", expectedResultWeak, resultWeak) } } func TestDecoder_ErrorUnused(t *testing.T) { t.Parallel() input := map[string]any{ "vstring": "hello", "foo": "bar", } var result Basic config := &DecoderConfig{ ErrorUnused: true, Result: &result, } decoder, err := NewDecoder(config) if err != nil { t.Fatalf("err: %s", err) } err = decoder.Decode(input) if err == nil { t.Fatal("expected error") } } func TestDecoder_ErrorUnused_NotSetable(t *testing.T) { t.Parallel() // lowercase vsilent is unexported and cannot be set input := map[string]any{ "vsilent": "false", } var result Basic config := &DecoderConfig{ ErrorUnused: true, Result: &result, } decoder, err := NewDecoder(config) if err != nil { t.Fatalf("err: %s", err) } err = decoder.Decode(input) if err == nil { t.Fatal("expected error") } } func TestDecoder_ErrorUnset(t *testing.T) { t.Parallel() input := map[string]any{ "vstring": "hello", "foo": "bar", } var result Basic config := &DecoderConfig{ ErrorUnset: true, Result: &result, } decoder, err := NewDecoder(config) if err != nil { t.Fatalf("err: %s", err) } err = decoder.Decode(input) if err == nil { t.Fatal("expected error") } } func TestDecoder_ErrorUnset_AllowUnsetPointer(t *testing.T) { t.Parallel() input := map[string]any{ "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() input := map[string]any{ "vfoo": "foo", "vother": map[any]any{ "foo": "foo", "bar": "bar", }, } var result Map err := Decode(input, &result) if err != nil { t.Fatalf("got an error: %s", err) } if result.Vfoo != "foo" { t.Errorf("vfoo value should be 'foo': %#v", result.Vfoo) } if result.Vother == nil { t.Fatal("vother should not be nil") } if len(result.Vother) != 2 { t.Error("vother should have two items") } if result.Vother["foo"] != "foo" { t.Errorf("'foo' key should be foo, got: %#v", result.Vother["foo"]) } if result.Vother["bar"] != "bar" { t.Errorf("'bar' key should be bar, got: %#v", result.Vother["bar"]) } } func TestMapMerge(t *testing.T) { t.Parallel() input := map[string]any{ "vfoo": "foo", "vother": map[any]any{ "foo": "foo", "bar": "bar", }, } var result Map result.Vother = map[string]string{"hello": "world"} err := Decode(input, &result) if err != nil { t.Fatalf("got an error: %s", err) } if result.Vfoo != "foo" { t.Errorf("vfoo value should be 'foo': %#v", result.Vfoo) } expected := map[string]string{ "foo": "foo", "bar": "bar", "hello": "world", } if !reflect.DeepEqual(result.Vother, expected) { t.Errorf("bad: %#v", result.Vother) } } func TestMapOfStruct(t *testing.T) { t.Parallel() input := map[string]any{ "value": map[string]any{ "foo": map[string]string{"vstring": "one"}, "bar": map[string]string{"vstring": "two"}, }, } var result MapOfStruct err := Decode(input, &result) if err != nil { t.Fatalf("got an err: %s", err) } if result.Value == nil { t.Fatal("value should not be nil") } if len(result.Value) != 2 { t.Error("value should have two items") } if result.Value["foo"].Vstring != "one" { t.Errorf("foo value should be 'one', got: %s", result.Value["foo"].Vstring) } if result.Value["bar"].Vstring != "two" { t.Errorf("bar value should be 'two', got: %s", result.Value["bar"].Vstring) } } func TestNestedType(t *testing.T) { t.Parallel() input := map[string]any{ "vfoo": "foo", "vbar": map[string]any{ "vstring": "foo", "vint": 42, "vbool": true, }, } var result Nested err := Decode(input, &result) if err != nil { t.Fatalf("got an err: %s", err.Error()) } if result.Vfoo != "foo" { t.Errorf("vfoo value should be 'foo': %#v", result.Vfoo) } if result.Vbar.Vstring != "foo" { t.Errorf("vstring value should be 'foo': %#v", result.Vbar.Vstring) } if result.Vbar.Vint != 42 { t.Errorf("vint value should be 42: %#v", result.Vbar.Vint) } if result.Vbar.Vbool != true { t.Errorf("vbool value should be true: %#v", result.Vbar.Vbool) } if result.Vbar.Vextra != "" { t.Errorf("vextra value should be empty: %#v", result.Vbar.Vextra) } } func TestNestedTypePointer(t *testing.T) { t.Parallel() input := map[string]any{ "vfoo": "foo", "vbar": &map[string]any{ "vstring": "foo", "vint": 42, "vbool": true, }, } var result NestedPointer err := Decode(input, &result) if err != nil { t.Fatalf("got an err: %s", err.Error()) } if result.Vfoo != "foo" { t.Errorf("vfoo value should be 'foo': %#v", result.Vfoo) } if result.Vbar.Vstring != "foo" { t.Errorf("vstring value should be 'foo': %#v", result.Vbar.Vstring) } if result.Vbar.Vint != 42 { t.Errorf("vint value should be 42: %#v", result.Vbar.Vint) } if result.Vbar.Vbool != true { t.Errorf("vbool value should be true: %#v", result.Vbar.Vbool) } if result.Vbar.Vextra != "" { t.Errorf("vextra value should be empty: %#v", result.Vbar.Vextra) } } // Test for issue #46. func TestNestedTypeInterface(t *testing.T) { t.Parallel() input := map[string]any{ "vfoo": "foo", "vbar": &map[string]any{ "vstring": "foo", "vint": 42, "vbool": true, "vdata": map[string]any{ "vstring": "bar", }, }, } var result NestedPointer result.Vbar = new(Basic) result.Vbar.Vdata = new(Basic) err := Decode(input, &result) if err != nil { t.Fatalf("got an err: %s", err.Error()) } if result.Vfoo != "foo" { t.Errorf("vfoo value should be 'foo': %#v", result.Vfoo) } if result.Vbar.Vstring != "foo" { t.Errorf("vstring value should be 'foo': %#v", result.Vbar.Vstring) } if result.Vbar.Vint != 42 { t.Errorf("vint value should be 42: %#v", result.Vbar.Vint) } if result.Vbar.Vbool != true { t.Errorf("vbool value should be true: %#v", result.Vbar.Vbool) } if result.Vbar.Vextra != "" { t.Errorf("vextra value should be empty: %#v", result.Vbar.Vextra) } if result.Vbar.Vdata.(*Basic).Vstring != "bar" { t.Errorf("vstring value should be 'bar': %#v", result.Vbar.Vdata.(*Basic).Vstring) } } func TestSlice(t *testing.T) { t.Parallel() inputStringSlice := map[string]any{ "vfoo": "foo", "vbar": []string{"foo", "bar", "baz"}, } inputStringSlicePointer := map[string]any{ "vfoo": "foo", "vbar": &[]string{"foo", "bar", "baz"}, } outputStringSlice := &Slice{ "foo", []string{"foo", "bar", "baz"}, } testSliceInput(t, inputStringSlice, outputStringSlice) testSliceInput(t, inputStringSlicePointer, outputStringSlice) } func TestNotEmptyByteSlice(t *testing.T) { t.Parallel() inputByteSlice := map[string]any{ "vfoo": "foo", "vbar": []byte(`{"bar": "bar"}`), } result := SliceOfByte{ Vfoo: "another foo", Vbar: []byte(`{"bar": "bar bar bar bar bar bar bar bar"}`), } err := Decode(inputByteSlice, &result) if err != nil { t.Fatalf("got unexpected error: %s", err) } expected := SliceOfByte{ Vfoo: "foo", Vbar: []byte(`{"bar": "bar"}`), } if !reflect.DeepEqual(result, expected) { t.Errorf("bad: %#v", result) } } func TestInvalidSlice(t *testing.T) { t.Parallel() input := map[string]any{ "vfoo": "foo", "vbar": 42, } result := Slice{} err := Decode(input, &result) if err == nil { t.Errorf("expected failure") } } func TestSliceOfStruct(t *testing.T) { t.Parallel() input := map[string]any{ "value": []map[string]any{ {"vstring": "one"}, {"vstring": "two"}, }, } var result SliceOfStruct err := Decode(input, &result) if err != nil { t.Fatalf("got unexpected error: %s", err) } if len(result.Value) != 2 { t.Fatalf("expected two values, got %d", len(result.Value)) } if result.Value[0].Vstring != "one" { t.Errorf("first value should be 'one', got: %s", result.Value[0].Vstring) } if result.Value[1].Vstring != "two" { t.Errorf("second value should be 'two', got: %s", result.Value[1].Vstring) } } func TestSliceCornerCases(t *testing.T) { t.Parallel() // Input with a map with zero values input := map[string]any{} var resultWeak []Basic err := WeakDecode(input, &resultWeak) if err != nil { t.Fatalf("got unexpected error: %s", err) } if len(resultWeak) != 0 { t.Errorf("length should be 0") } // Input with more values input = map[string]any{ "Vstring": "foo", } resultWeak = nil err = WeakDecode(input, &resultWeak) if err != nil { t.Fatalf("got unexpected error: %s", err) } if resultWeak[0].Vstring != "foo" { t.Errorf("value does not match") } } func TestSliceToMap(t *testing.T) { t.Parallel() input := []map[string]any{ { "foo": "bar", }, { "bar": "baz", }, } var result map[string]any err := WeakDecode(input, &result) if err != nil { t.Fatalf("got an error: %s", err) } expected := map[string]any{ "foo": "bar", "bar": "baz", } if !reflect.DeepEqual(result, expected) { t.Errorf("bad: %#v", result) } } func TestArray(t *testing.T) { t.Parallel() inputStringArray := map[string]any{ "vfoo": "foo", "vbar": [2]string{"foo", "bar"}, } inputStringArrayPointer := map[string]any{ "vfoo": "foo", "vbar": &[2]string{"foo", "bar"}, } outputStringArray := &Array{ "foo", [2]string{"foo", "bar"}, } testArrayInput(t, inputStringArray, outputStringArray) testArrayInput(t, inputStringArrayPointer, outputStringArray) } func TestInvalidArray(t *testing.T) { t.Parallel() input := map[string]any{ "vfoo": "foo", "vbar": 42, } result := Array{} err := Decode(input, &result) if err == nil { t.Errorf("expected failure") } } func TestArrayOfStruct(t *testing.T) { t.Parallel() input := map[string]any{ "value": []map[string]any{ {"vstring": "one"}, {"vstring": "two"}, }, } var result ArrayOfStruct err := Decode(input, &result) if err != nil { t.Fatalf("got unexpected error: %s", err) } if len(result.Value) != 2 { t.Fatalf("expected two values, got %d", len(result.Value)) } if result.Value[0].Vstring != "one" { t.Errorf("first value should be 'one', got: %s", result.Value[0].Vstring) } if result.Value[1].Vstring != "two" { t.Errorf("second value should be 'two', got: %s", result.Value[1].Vstring) } } func TestArrayToMap(t *testing.T) { t.Parallel() input := []map[string]any{ { "foo": "bar", }, { "bar": "baz", }, } var result map[string]any err := WeakDecode(input, &result) if err != nil { t.Fatalf("got an error: %s", err) } expected := map[string]any{ "foo": "bar", "bar": "baz", } if !reflect.DeepEqual(result, expected) { t.Errorf("bad: %#v", result) } } func TestDecodeTable(t *testing.T) { t.Parallel() // We need to make new types so that we don't get the short-circuit // copy functionality. We want to test the deep copying functionality. type BasicCopy Basic type NestedPointerCopy NestedPointer type MapCopy Map tests := []struct { name string in any target any out any wantErr bool }{ { "basic struct input", &Basic{ Vstring: "vstring", Vint: 2, Vint8: 2, Vint16: 2, Vint32: 2, Vint64: 2, Vuint: 3, Vbool: true, Vfloat: 4.56, Vextra: "vextra", vsilent: true, Vdata: []byte("data"), }, &map[string]any{}, &map[string]any{ "Vstring": "vstring", "Vint": 2, "Vint8": int8(2), "Vint16": int16(2), "Vint32": int32(2), "Vint64": int64(2), "Vuint": uint(3), "Vbool": true, "Vfloat": 4.56, "Vextra": "vextra", "Vdata": []byte("data"), "VjsonInt": 0, "VjsonUint": uint(0), "VjsonUint64": uint64(0), "VjsonFloat": 0.0, "VjsonNumber": json.Number(""), "Vcomplex64": complex64(0), "Vcomplex128": complex128(0), }, false, }, { "embedded struct input", &Embedded{ Vunique: "vunique", Basic: Basic{ Vstring: "vstring", Vint: 2, Vint8: 2, Vint16: 2, Vint32: 2, Vint64: 2, Vuint: 3, Vbool: true, Vfloat: 4.56, Vextra: "vextra", vsilent: true, Vdata: []byte("data"), }, }, &map[string]any{}, &map[string]any{ "Vunique": "vunique", "Basic": map[string]any{ "Vstring": "vstring", "Vint": 2, "Vint8": int8(2), "Vint16": int16(2), "Vint32": int32(2), "Vint64": int64(2), "Vuint": uint(3), "Vbool": true, "Vfloat": 4.56, "Vextra": "vextra", "Vdata": []byte("data"), "VjsonInt": 0, "VjsonUint": uint(0), "VjsonUint64": uint64(0), "VjsonFloat": 0.0, "VjsonNumber": json.Number(""), "Vcomplex64": complex64(0), "Vcomplex128": complex128(0), }, }, false, }, { "struct => struct", &Basic{ Vstring: "vstring", Vint: 2, Vuint: 3, Vbool: true, Vfloat: 4.56, Vextra: "vextra", Vdata: []byte("data"), vsilent: true, }, &BasicCopy{}, &BasicCopy{ Vstring: "vstring", Vint: 2, Vuint: 3, Vbool: true, Vfloat: 4.56, Vextra: "vextra", Vdata: []byte("data"), }, false, }, { "struct => struct with pointers", &NestedPointer{ Vfoo: "hello", Vbar: nil, }, &NestedPointerCopy{}, &NestedPointerCopy{ Vfoo: "hello", }, false, }, { "basic pointer to non-pointer", &BasicPointer{ Vstring: stringPtr("vstring"), Vint: intPtr(2), Vuint: uintPtr(3), Vbool: boolPtr(true), Vfloat: floatPtr(4.56), Vdata: interfacePtr([]byte("data")), }, &Basic{}, &Basic{ Vstring: "vstring", Vint: 2, Vuint: 3, Vbool: true, Vfloat: 4.56, Vdata: []byte("data"), }, false, }, { "slice non-pointer to pointer", &Slice{}, &SlicePointer{}, &SlicePointer{}, false, }, { "slice non-pointer to pointer, zero field", &Slice{}, &SlicePointer{ Vbar: &[]string{"yo"}, }, &SlicePointer{}, false, }, { "slice to slice alias", &Slice{}, &SliceOfAlias{}, &SliceOfAlias{}, false, }, { "nil map to map", &Map{}, &MapCopy{}, &MapCopy{}, false, }, { "nil map to non-empty map", &Map{}, &MapCopy{Vother: map[string]string{"foo": "bar"}}, &MapCopy{}, false, }, { "slice input - should error", []string{"foo", "bar"}, &map[string]any{}, &map[string]any{}, true, }, { "struct with slice property", &Slice{ Vfoo: "vfoo", Vbar: []string{"foo", "bar"}, }, &map[string]any{}, &map[string]any{ "Vfoo": "vfoo", "Vbar": []string{"foo", "bar"}, }, false, }, { "struct with empty slice", &map[string]any{ "Vbar": []string{}, }, &Slice{}, &Slice{ Vbar: []string{}, }, false, }, { "struct with slice of struct property", &SliceOfStruct{ Value: []Basic{ { Vstring: "vstring", Vint: 2, Vuint: 3, Vbool: true, Vfloat: 4.56, Vextra: "vextra", vsilent: true, Vdata: []byte("data"), }, }, }, &map[string]any{}, &map[string]any{ "Value": []Basic{ { Vstring: "vstring", Vint: 2, Vuint: 3, Vbool: true, Vfloat: 4.56, Vextra: "vextra", vsilent: true, Vdata: []byte("data"), }, }, }, false, }, { "struct with map property", &Map{ Vfoo: "vfoo", Vother: map[string]string{"vother": "vother"}, }, &map[string]any{}, &map[string]any{ "Vfoo": "vfoo", "Vother": map[string]string{ "vother": "vother", }, }, false, }, { "tagged struct", &Tagged{ Extra: "extra", Value: "value", }, &map[string]string{}, &map[string]string{ "bar": "extra", "foo": "value", }, false, }, { "omit tag struct", &struct { Value string `mapstructure:"value"` Omit string `mapstructure:"-"` }{ Value: "value", Omit: "omit", }, &map[string]string{}, &map[string]string{ "value": "value", }, false, }, { "decode to wrong map type", &struct { Value string }{ Value: "string", }, &map[string]int{}, &map[string]int{}, true, }, { "remainder", map[string]any{ "A": "hello", "B": "goodbye", "C": "yo", }, &Remainder{}, &Remainder{ A: "hello", Extra: map[string]any{ "B": "goodbye", "C": "yo", }, }, false, }, { "remainder with no extra", map[string]any{ "A": "hello", }, &Remainder{}, &Remainder{ A: "hello", Extra: nil, }, false, }, { "struct with omitempty tag return non-empty values", &struct { VisibleField any `mapstructure:"visible"` OmitField any `mapstructure:"omittable,omitempty"` }{ VisibleField: nil, OmitField: "string", }, &map[string]any{}, &map[string]any{"visible": nil, "omittable": "string"}, false, }, { "struct with omitempty tag ignore empty values", &struct { VisibleField any `mapstructure:"visible"` OmitField any `mapstructure:"omittable,omitempty"` }{ VisibleField: nil, OmitField: nil, }, &map[string]any{}, &map[string]any{"visible": nil}, false, }, { "remainder with decode to map", &Remainder{ A: "Alabasta", Extra: map[string]any{ "B": "Baratie", "C": "Cocoyasi", }, }, &map[string]any{}, &map[string]any{ "A": "Alabasta", "B": "Baratie", "C": "Cocoyasi", }, false, }, { "remainder with decode to map with non-map field", &struct { A string Extra *struct{} `mapstructure:",remain"` }{ A: "Alabasta", Extra: nil, }, &map[string]any{}, &map[string]any{ "A": "Alabasta", }, true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if err := Decode(tt.in, tt.target); (err != nil) != tt.wantErr { t.Fatalf("%q: TestMapOutputForStructuredInputs() unexpected error: %s", tt.name, err) } if !reflect.DeepEqual(tt.out, tt.target) { t.Fatalf("%q: TestMapOutputForStructuredInputs() expected: %#v, got: %#v", tt.name, tt.out, tt.target) } }) } } func TestInvalidType(t *testing.T) { t.Parallel() input := map[string]any{ "vstring": 42, } var result Basic err := Decode(input, &result) if err == nil { t.Fatal("error should exist") } var derr interface { Unwrap() []error } if !errors.As(err, &derr) { t.Fatalf("error should be a type implementing Unwrap() []error, instead: %#v", err) } errs := derr.Unwrap() var decoderErr *DecodeError if !errors.As(errs[0], &decoderErr) { t.Errorf("got unexpected error: %s", err) } else if errors.Is(decoderErr.Unwrap(), &UnconvertibleTypeError{}) { t.Errorf("error should be UnconvertibleTypeError, got: %s", decoderErr.Unwrap()) } inputNegIntUint := map[string]any{ "vuint": -42, } err = Decode(inputNegIntUint, &result) if err == nil { t.Fatal("error should exist") } if !errors.As(err, &derr) { t.Fatalf("error should be a type implementing Unwrap() []error, instead: %#v", err) } errs = derr.Unwrap() if !errors.As(errs[0], &decoderErr) { t.Errorf("got unexpected error: %s", err) } else if errors.Is(decoderErr.Unwrap(), &ParseError{}) { t.Errorf("error should be ParseError, got: %s", decoderErr.Unwrap()) } inputNegFloatUint := map[string]any{ "vuint": -42.0, } err = Decode(inputNegFloatUint, &result) if err == nil { t.Fatal("error should exist") } if !errors.As(err, &derr) { t.Fatalf("error should be a type implementing Unwrap() []error, instead: %#v", err) } errs = derr.Unwrap() if !errors.As(errs[0], &decoderErr) { t.Errorf("got unexpected error: %s", err) } } func TestDecodeMetadata(t *testing.T) { t.Parallel() input := map[string]any{ "vfoo": "foo", "vbar": map[string]any{ "vstring": "foo", "Vuint": 42, "vsilent": "false", "foo": "bar", }, "bar": "nil", } var md Metadata var result Nested err := DecodeMetadata(input, &result, &md) if err != nil { t.Fatalf("err: %s", err.Error()) } expectedKeys := []string{"Vbar", "Vbar.Vstring", "Vbar.Vuint", "Vfoo"} sort.Strings(md.Keys) if !reflect.DeepEqual(md.Keys, expectedKeys) { t.Fatalf("bad keys: %#v", md.Keys) } expectedUnused := []string{"Vbar.foo", "Vbar.vsilent", "bar"} sort.Strings(md.Unused) if !reflect.DeepEqual(md.Unused, expectedUnused) { t.Fatalf("bad unused: %#v", md.Unused) } } func TestMetadata(t *testing.T) { t.Parallel() type testResult struct { Vfoo string Vbar BasicPointer } input := map[string]any{ "vfoo": "foo", "vbar": map[string]any{ "vstring": "foo", "Vuint": 42, "vsilent": "false", "foo": "bar", }, "bar": "nil", } var md Metadata var result testResult config := &DecoderConfig{ Metadata: &md, Result: &result, } decoder, err := NewDecoder(config) if err != nil { t.Fatalf("err: %s", err) } err = decoder.Decode(input) if err != nil { t.Fatalf("err: %s", err.Error()) } expectedKeys := []string{"Vbar", "Vbar.Vstring", "Vbar.Vuint", "Vfoo"} sort.Strings(md.Keys) if !reflect.DeepEqual(md.Keys, expectedKeys) { t.Fatalf("bad keys: %#v", md.Keys) } expectedUnused := []string{"Vbar.foo", "Vbar.vsilent", "bar"} sort.Strings(md.Unused) if !reflect.DeepEqual(md.Unused, expectedUnused) { t.Fatalf("bad unused: %#v", md.Unused) } expectedUnset := []string{ "Vbar.Vbool", "Vbar.Vdata", "Vbar.Vextra", "Vbar.Vfloat", "Vbar.Vint", "Vbar.VjsonFloat", "Vbar.VjsonInt", "Vbar.VjsonNumber", } sort.Strings(md.Unset) if !reflect.DeepEqual(md.Unset, expectedUnset) { t.Fatalf("bad unset: %#v", md.Unset) } } func TestMetadata_Embedded(t *testing.T) { t.Parallel() input := map[string]any{ "vstring": "foo", "vunique": "bar", } var md Metadata var result EmbeddedSquash config := &DecoderConfig{ Metadata: &md, Result: &result, } decoder, err := NewDecoder(config) if err != nil { t.Fatalf("err: %s", err) } err = decoder.Decode(input) if err != nil { t.Fatalf("err: %s", err.Error()) } expectedKeys := []string{"Vstring", "Vunique"} sort.Strings(md.Keys) if !reflect.DeepEqual(md.Keys, expectedKeys) { t.Fatalf("bad keys: %#v", md.Keys) } expectedUnused := []string{} if !reflect.DeepEqual(md.Unused, expectedUnused) { t.Fatalf("bad unused: %#v", md.Unused) } } func TestNonPtrValue(t *testing.T) { t.Parallel() err := Decode(map[string]any{}, Basic{}) if err == nil { t.Fatal("error should exist") } if err.Error() != "result must be a pointer" { t.Errorf("got unexpected error: %s", err) } } func TestTagged(t *testing.T) { t.Parallel() input := map[string]any{ "foo": "bar", "bar": "value", } var result Tagged err := Decode(input, &result) if err != nil { t.Fatalf("unexpected error: %s", err) } if result.Value != "bar" { t.Errorf("value should be 'bar', got: %#v", result.Value) } if result.Extra != "value" { t.Errorf("extra should be 'value', got: %#v", result.Extra) } } func TestWeakDecode(t *testing.T) { t.Parallel() input := map[string]any{ "foo": "4", "bar": "value", } var result struct { Foo int Bar string } if err := WeakDecode(input, &result); err != nil { t.Fatalf("err: %s", err) } if result.Foo != 4 { t.Fatalf("bad: %#v", result) } if result.Bar != "value" { t.Fatalf("bad: %#v", result) } } func TestWeakDecodeMetadata(t *testing.T) { t.Parallel() input := map[string]any{ "foo": "4", "bar": "value", "unused": "value", "unexported": "value", } var md Metadata var result struct { Foo int Bar string unexported string } if err := WeakDecodeMetadata(input, &result, &md); err != nil { t.Fatalf("err: %s", err) } if result.Foo != 4 { t.Fatalf("bad: %#v", result) } if result.Bar != "value" { t.Fatalf("bad: %#v", result) } expectedKeys := []string{"Bar", "Foo"} sort.Strings(md.Keys) if !reflect.DeepEqual(md.Keys, expectedKeys) { t.Fatalf("bad keys: %#v", md.Keys) } expectedUnused := []string{"unexported", "unused"} sort.Strings(md.Unused) if !reflect.DeepEqual(md.Unused, expectedUnused) { t.Fatalf("bad unused: %#v", md.Unused) } } func TestDecode_StructTaggedWithOmitempty_OmitEmptyValues(t *testing.T) { t.Parallel() input := &StructWithOmitEmpty{} var emptySlice []any var emptyMap map[string]any var emptyNested *Nested expected := &map[string]any{ "visible-string": "", "visible-int": 0, "visible-float": 0.0, "visible-slice": emptySlice, "visible-map": emptyMap, "visible-nested": emptyNested, } actual := &map[string]any{} Decode(input, actual) if !reflect.DeepEqual(actual, expected) { t.Fatalf("Decode() expected: %#v, got: %#v", expected, actual) } } func TestDecode_StructTaggedWithOmitempty_KeepNonEmptyValues(t *testing.T) { t.Parallel() input := &StructWithOmitEmpty{ VisibleStringField: "", OmitStringField: "string", VisibleIntField: 0, OmitIntField: 1, VisibleFloatField: 0.0, OmitFloatField: 1.0, VisibleSliceField: nil, OmitSliceField: []any{1}, VisibleMapField: nil, OmitMapField: map[string]any{"k": "v"}, NestedField: nil, OmitNestedField: &Nested{}, } var emptySlice []any var emptyMap map[string]any var emptyNested *Nested expected := &map[string]any{ "visible-string": "", "omittable-string": "string", "visible-int": 0, "omittable-int": 1, "visible-float": 0.0, "omittable-float": 1.0, "visible-slice": emptySlice, "omittable-slice": []any{1}, "visible-map": emptyMap, "omittable-map": map[string]any{"k": "v"}, "visible-nested": emptyNested, "omittable-nested": &Nested{}, } actual := &map[string]any{} Decode(input, actual) if !reflect.DeepEqual(actual, expected) { t.Fatalf("Decode() expected: %#v, got: %#v", expected, actual) } } 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: []any{}, VisibleMapField: nil, OmitMapField: map[string]any{}, NestedField: nil, OmitNestedField: &Nested{}, } var emptySlice []any var emptyMap map[string]any var emptyNested *Nested expected := &map[string]any{ "visible-string": "", "omittable-string": "string", "visible-int": 0, "omittable-int": 1, "visible-float": 0.0, "omittable-float": 1.0, "visible-slice": emptySlice, "omittable-slice": []any{}, "visible-map": emptyMap, "omittable-map": map[string]any{}, "visible-nested": emptyNested, "omittable-nested": &Nested{}, } actual := &map[string]any{} 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 []any var emptyMap map[string]any var emptyNested *Nested expected := &map[string]any{ "visible-string": "", "visible-int": 0, "visible-float": 0.0, "visible-slice": emptySlice, "visible-map": emptyMap, "visible-nested": emptyNested, } actual := &map[string]any{} 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 StringPtr *string } expected := Target{ String: "hello", } var target Target err := Decode(map[string]any{ "string": "hello", "StringPtr": "goodbye", }, &target) if err != nil { t.Fatalf("got error: %s", err) } // Pointers fail reflect test so do those manually if target.StringPtr == nil || *target.StringPtr != "goodbye" { t.Fatalf("bad: %#v", target) } target.StringPtr = nil if !reflect.DeepEqual(target, expected) { t.Fatalf("bad: %#v", target) } } func TestDecoder_MatchName(t *testing.T) { t.Parallel() type Target struct { FirstMatch string `mapstructure:"first_match"` SecondMatch string NoMatch string `mapstructure:"no_match"` } input := map[string]any{ "first_match": "foo", "SecondMatch": "bar", "NO_MATCH": "baz", } expected := Target{ FirstMatch: "foo", SecondMatch: "bar", } var actual Target config := &DecoderConfig{ Result: &actual, MatchName: func(mapKey, fieldName string) bool { return mapKey == fieldName }, } decoder, err := NewDecoder(config) if err != nil { t.Fatalf("err: %s", err) } err = decoder.Decode(input) if err != nil { t.Fatalf("err: %s", err) } if !reflect.DeepEqual(expected, actual) { t.Fatalf("Decode() expected: %#v, got: %#v", expected, actual) } } func TestDecoder_IgnoreUntaggedFields(t *testing.T) { type Input struct { UntaggedNumber int TaggedNumber int `mapstructure:"tagged_number"` UntaggedString string TaggedString string `mapstructure:"tagged_string"` } input := &Input{ UntaggedNumber: 31, TaggedNumber: 42, UntaggedString: "hidden", TaggedString: "visible", } actual := make(map[string]any) config := &DecoderConfig{ Result: &actual, IgnoreUntaggedFields: true, } decoder, err := NewDecoder(config) if err != nil { t.Fatalf("err: %s", err) } err = decoder.Decode(input) if err != nil { t.Fatalf("err: %s", err) } expected := map[string]any{ "tagged_number": 42, "tagged_string": "visible", } if !reflect.DeepEqual(expected, actual) { t.Fatalf("Decode() expected: %#v\ngot: %#v", expected, actual) } } func TestDecoder_IgnoreUntaggedFieldsWithStruct(t *testing.T) { type Output struct { UntaggedInt int TaggedNumber int `mapstructure:"tagged_number"` UntaggedString string TaggedString string `mapstructure:"tagged_string"` } input := map[any]any{ "untaggedint": 31, "tagged_number": 41, "untagged_string": "hidden", "tagged_string": "visible", } actual := Output{} config := &DecoderConfig{ Result: &actual, IgnoreUntaggedFields: true, } decoder, err := NewDecoder(config) if err != nil { t.Fatalf("err: %s", err) } err = decoder.Decode(input) if err != nil { t.Fatalf("err: %s", err) } expected := Output{ TaggedNumber: 41, TaggedString: "visible", } if !reflect.DeepEqual(expected, actual) { t.Fatalf("Decode() expected: %#v\ngot: %#v", expected, actual) } } func TestDecoder_DecodeNilOption(t *testing.T) { t.Parallel() type Transformed struct { Message string When string } helloHook := func(reflect.Type, reflect.Type, any) (any, error) { return Transformed{Message: "hello"}, nil } goodbyeHook := func(reflect.Type, reflect.Type, any) (any, error) { return Transformed{Message: "goodbye"}, nil } appendHook := func(from reflect.Value, to reflect.Value) (any, error) { if from.Kind() == reflect.Map { stringMap := from.Interface().(map[string]any) if stringMap == nil { stringMap = make(map[string]any) } stringMap["when"] = "see you later" return stringMap, nil } return from.Interface(), nil } tests := []struct { name string decodeNil bool input any result Transformed expectedResult Transformed decodeHook DecodeHookFunc }{ { name: "decodeNil=true for nil input with hook", decodeNil: true, input: nil, decodeHook: helloHook, expectedResult: Transformed{Message: "hello"}, }, { name: "decodeNil=true for nil input without hook", decodeNil: true, input: nil, expectedResult: Transformed{Message: ""}, }, { name: "decodeNil=false for nil input with hook", decodeNil: false, input: nil, decodeHook: helloHook, expectedResult: Transformed{Message: ""}, }, { name: "decodeNil=false for nil input without hook", decodeNil: false, input: nil, expectedResult: Transformed{Message: ""}, }, { name: "decodeNil=true for non-nil input without hook", decodeNil: true, input: map[string]any{"message": "bar"}, expectedResult: Transformed{Message: "bar"}, }, { name: "decodeNil=true for non-nil input with hook", decodeNil: true, input: map[string]any{"message": "bar"}, decodeHook: goodbyeHook, expectedResult: Transformed{Message: "goodbye"}, }, { name: "decodeNil=false for non-nil input without hook", decodeNil: false, input: map[string]any{"message": "bar"}, expectedResult: Transformed{Message: "bar"}, }, { name: "decodeNil=false for non-nil input with hook", decodeNil: false, input: map[string]any{"message": "bar"}, decodeHook: goodbyeHook, expectedResult: Transformed{Message: "goodbye"}, }, { name: "decodeNil=true for nil input without hook and non-empty result", decodeNil: true, input: nil, result: Transformed{Message: "foo"}, expectedResult: Transformed{Message: "foo"}, }, { name: "decodeNil=true for nil input with hook and non-empty result", decodeNil: true, input: nil, result: Transformed{Message: "foo"}, decodeHook: helloHook, expectedResult: Transformed{Message: "hello"}, }, { name: "decodeNil=false for nil input without hook and non-empty result", decodeNil: false, input: nil, result: Transformed{Message: "foo"}, expectedResult: Transformed{Message: "foo"}, }, { name: "decodeNil=false for nil input with hook and non-empty result", decodeNil: false, input: nil, result: Transformed{Message: "foo"}, decodeHook: helloHook, expectedResult: Transformed{Message: "foo"}, }, { name: "decodeNil=false for non-nil input with hook that appends a value", decodeNil: false, input: map[string]any{"message": "bar"}, decodeHook: appendHook, expectedResult: Transformed{Message: "bar", When: "see you later"}, }, { name: "decodeNil=true for non-nil input with hook that appends a value", decodeNil: true, input: map[string]any{"message": "bar"}, decodeHook: appendHook, expectedResult: Transformed{Message: "bar", When: "see you later"}, }, { name: "decodeNil=true for nil input with hook that appends a value", decodeNil: true, decodeHook: appendHook, expectedResult: Transformed{When: "see you later"}, }, { name: "decodeNil=false for nil input with hook that appends a value", decodeNil: false, decodeHook: appendHook, expectedResult: Transformed{}, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { config := &DecoderConfig{ Result: &test.result, DecodeNil: test.decodeNil, DecodeHook: test.decodeHook, } decoder, err := NewDecoder(config) if err != nil { t.Fatalf("err: %s", err) } if err := decoder.Decode(test.input); err != nil { t.Fatalf("got an err: %s", err) } if test.result != test.expectedResult { t.Errorf("result should be: %#v, got %#v", test.expectedResult, test.result) } }) } } func TestDecoder_ExpandNilStructPointersHookFunc(t *testing.T) { // a decoder hook that expands nil pointers in a struct to their zero value // if the input map contains the corresponding key. decodeHook := func(from reflect.Value, to reflect.Value) (any, error) { if from.Kind() == reflect.Map && to.Kind() == reflect.Map { toElem := to.Type().Elem() if toElem.Kind() == reflect.Ptr && toElem.Elem().Kind() == reflect.Struct { fromRange := from.MapRange() for fromRange.Next() { fromKey := fromRange.Key() fromValue := fromRange.Value() if fromValue.IsNil() { newFromValue := reflect.New(toElem.Elem()) from.SetMapIndex(fromKey, newFromValue) } } } } return from.Interface(), nil } type Struct struct { Name string } type TestConfig struct { Boolean *bool `mapstructure:"boolean"` Struct *Struct `mapstructure:"struct"` MapStruct map[string]*Struct `mapstructure:"map_struct"` } stringMap := map[string]any{ "boolean": nil, "struct": nil, "map_struct": map[string]any{ "struct": nil, }, } var result TestConfig decoder, err := NewDecoder(&DecoderConfig{ Result: &result, DecodeNil: true, DecodeHook: decodeHook, }) if err != nil { t.Fatalf("err: %s", err) } if err := decoder.Decode(stringMap); err != nil { t.Fatalf("got an err: %s", err) } if result.Boolean != nil { t.Errorf("nil Boolean expected, got '%#v'", result.Boolean) } if result.Struct != nil { t.Errorf("nil Struct expected, got '%#v'", result.Struct) } if len(result.MapStruct) == 0 { t.Fatalf("not-empty MapStruct expected, got '%#v'", result.MapStruct) } if _, ok := result.MapStruct["struct"]; !ok { t.Errorf("MapStruct['struct'] expected") } } func TestErrorLeakageWeakDecode(t *testing.T) { cases := []struct { value any target any allowNilError bool }{ // case 0 {"testing", new(bool), false}, {"testing", new(int8), false}, {"testing", new(uint8), false}, {"testing", new(int16), false}, {"testing", new(uint16), false}, // case 5 {"testing", new(int32), false}, {"testing", new(uint32), false}, {"testing", new(int64), false}, {"testing", new(uint64), false}, {"testing", new(int), false}, // case 10 {"testing", new(uint), false}, {"testing", new(float32), false}, {"testing", new(float64), false}, {"testing", new(complex64), false}, {"testing", new(complex128), false}, // case 15 {"testing", new(time.Duration), true}, {"testing", new(time.Time), true}, {map[string]string{"key": "secret-testing"}, new(map[string]int), false}, {map[string]string{"key": "secret-testing"}, new(struct{ Key int }), false}, {"secret-testing", new([]int), false}, // case 20 {"secret-testing", new([4]int), false}, } for i, tc := range cases { err := WeakDecode(tc.value, tc.target) if err == nil { if tc.allowNilError { continue } t.Fatalf("case %d: expected error from input %v:\n\toutput (%T): %#v\n\toutput (string): %v", i, tc.value, tc.target, tc.target, tc.target) } strValue := fmt.Sprintf("%v", tc.value) if strings.Contains(strValue, "secret-testing") { strValue = "secret-testing" } if strings.Contains(err.Error(), strValue) { t.Errorf("case %d: error contains input value\n\terr: %v\n\tinput: %v", i, err, strValue) } else { t.Logf("case %d: got safe error: %v", i, err) } } } func testSliceInput(t *testing.T, input map[string]any, expected *Slice) { var result Slice err := Decode(input, &result) if err != nil { t.Fatalf("got error: %s", err) } if result.Vfoo != expected.Vfoo { t.Errorf("Vfoo expected '%s', got '%s'", expected.Vfoo, result.Vfoo) } if result.Vbar == nil { t.Fatalf("Vbar a slice, got '%#v'", result.Vbar) } if len(result.Vbar) != len(expected.Vbar) { t.Errorf("Vbar length should be %d, got %d", len(expected.Vbar), len(result.Vbar)) } for i, v := range result.Vbar { if v != expected.Vbar[i] { t.Errorf( "Vbar[%d] should be '%#v', got '%#v'", i, expected.Vbar[i], v) } } } func testArrayInput(t *testing.T, input map[string]any, expected *Array) { var result Array err := Decode(input, &result) if err != nil { t.Fatalf("got error: %s", err) } if result.Vfoo != expected.Vfoo { t.Errorf("Vfoo expected '%s', got '%s'", expected.Vfoo, result.Vfoo) } if result.Vbar == [2]string{} { t.Fatalf("Vbar a slice, got '%#v'", result.Vbar) } if len(result.Vbar) != len(expected.Vbar) { t.Errorf("Vbar length should be %d, got %d", len(expected.Vbar), len(result.Vbar)) } for i, v := range result.Vbar { if v != expected.Vbar[i] { t.Errorf( "Vbar[%d] should be '%#v', got '%#v'", i, expected.Vbar[i], v) } } } func stringPtr(v string) *string { return &v } func intPtr(v int) *int { return &v } func uintPtr(v uint) *uint { return &v } func boolPtr(v bool) *bool { return &v } func floatPtr(v float64) *float64 { return &v } func interfacePtr(v any) *any { return &v } golang-github-go-viper-mapstructure-2.4.0/reflect_go1_19.go000066400000000000000000000014371510370305500235640ustar00rootroot00000000000000//go:build !go1.20 package mapstructure import "reflect" func isComparable(v reflect.Value) bool { k := v.Kind() switch k { case reflect.Invalid: return false case reflect.Array: switch v.Type().Elem().Kind() { case reflect.Interface, reflect.Array, reflect.Struct: for i := 0; i < v.Type().Len(); i++ { // if !v.Index(i).Comparable() { if !isComparable(v.Index(i)) { return false } } return true } return v.Type().Comparable() case reflect.Interface: // return v.Elem().Comparable() return isComparable(v.Elem()) case reflect.Struct: for i := 0; i < v.NumField(); i++ { return false // if !v.Field(i).Comparable() { if !isComparable(v.Field(i)) { return false } } return true default: return v.Type().Comparable() } } golang-github-go-viper-mapstructure-2.4.0/reflect_go1_20.go000066400000000000000000000002601510370305500235450ustar00rootroot00000000000000//go:build go1.20 package mapstructure import "reflect" // TODO: remove once we drop support for Go <1.20 func isComparable(v reflect.Value) bool { return v.Comparable() }