diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index d3dab1a..0000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,70 +0,0 @@ -version: 2.1 - -jobs: - "test": - parameters: - version: - type: string - default: "latest" - golint: - type: boolean - default: true - modules: - type: boolean - default: true - goproxy: - type: string - default: "" - docker: - - image: "circleci/golang:<< parameters.version >>" - working_directory: /go/src/github.com/gorilla/schema - environment: - GO111MODULE: "on" - GOPROXY: "<< parameters.goproxy >>" - steps: - - checkout - - run: - name: "Print the Go version" - command: > - go version - - run: - name: "Fetch dependencies" - command: > - if [[ << parameters.modules >> = true ]]; then - go mod download - export GO111MODULE=on - else - go get -v ./... - fi - # Only run gofmt, vet & lint against the latest Go version - - run: - name: "Run golint" - command: > - if [ << parameters.version >> = "latest" ] && [ << parameters.golint >> = true ]; then - go get -u golang.org/x/lint/golint - golint ./... - fi - - run: - name: "Run gofmt" - command: > - if [[ << parameters.version >> = "latest" ]]; then - diff -u <(echo -n) <(gofmt -d -e .) - fi - - run: - name: "Run go vet" - command: > - if [[ << parameters.version >> = "latest" ]]; then - go vet -v ./... - fi - - run: - name: "Run go test (+ race detector)" - command: > - go test -v -race ./... - -workflows: - tests: - jobs: - - test: - matrix: - parameters: - version: ["latest", "1.15", "1.14", "1.13", "1.12", "1.11"] diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..c6b74c3 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,20 @@ +; https://editorconfig.org/ + +root = true + +[*] +insert_final_newline = true +charset = utf-8 +trim_trailing_whitespace = true +indent_style = space +indent_size = 2 + +[{Makefile,go.mod,go.sum,*.go,.gitmodules}] +indent_style = tab +indent_size = 4 + +[*.md] +indent_size = 4 +trim_trailing_whitespace = false + +eclint_indent_style = unset \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..67a8340 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,38 @@ +--- +name: 🐛 Bug report +about: Create a report to help us improve +title: '' +labels: bug +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** +- OS: [e.g. iOS] +- Browser [e.g. chrome, safari] +- Version [e.g. 22] + +**Smartphone (please complete the following information):** +- Device: [e.g. iPhone6] +- OS: [e.g. iOS8.1] +- Browser [e.g. stock browser, safari] +- Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..0086358 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: true diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..118d30e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: 🚀 Feature request +about: Suggest an idea for this project +title: '' +labels: enhancement +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..dc0b8bc --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,16 @@ +version: 2 +updates: +- package-ecosystem: gomod + directory: "/" + schedule: + interval: daily + time: '04:00' + open-pull-requests-limit: 10 + commit-message: + prefix: chore + include: scope +- package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: weekly + \ No newline at end of file diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml deleted file mode 100644 index 2db2e13..0000000 --- a/.github/release-drafter.yml +++ /dev/null @@ -1,8 +0,0 @@ -# Config for https://github.com/apps/release-drafter -template: | - - - - ## CHANGELOG - - $CHANGES diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000..e9e58f5 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,54 @@ +name: "Code scanning - action" + +on: + push: + branches: [main,next] + pull_request: + # The branches below must be a subset of the branches above + branches: [main,next] + schedule: + - cron: '0 11 * * 0' + +jobs: + CodeQL-Build: + + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v5 + with: + # We must fetch at least the immediate parents so that if this is + # a pull request then we can checkout the head. + fetch-depth: 2 + + # If this run was triggered by a pull request event, then checkout + # the head of the pull request instead of the merge commit. + - run: git checkout HEAD^2 + if: ${{ github.event_name == 'pull_request' }} + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v4 + # Override language selection by uncommenting this and choosing your languages + with: + languages: go + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v4 + + # â„šī¸ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # âœī¸ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v4 diff --git a/.github/workflows/issue.yml b/.github/workflows/issue.yml new file mode 100644 index 0000000..3b3b5f9 --- /dev/null +++ b/.github/workflows/issue.yml @@ -0,0 +1,18 @@ +name: Add new issues to product management project + +on: + issues: + types: + - opened + +jobs: + add-to-project: + name: Add issue to project + runs-on: ubuntu-latest + steps: + - uses: actions/add-to-project@v1.0.2 + with: + # You can target a repository in a different organization + # to the issue + project-url: https://github.com/orgs/zitadel/projects/2 + github-token: ${{ secrets.ADD_TO_PROJECT_PAT }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..dfad3cc --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,47 @@ +name: Release +on: + push: + branches: + - main + - next + tags-ignore: + - '**' + pull_request: + branches: + - '**' + workflow_dispatch: + +jobs: + test: + runs-on: ubuntu-24.04 + strategy: + matrix: + go: ['1.24', '1.25'] + name: Go ${{ matrix.go }} test + steps: + - uses: actions/checkout@v5 + - name: Setup go + uses: actions/setup-go@v6 + with: + go-version-file: 'go.mod' + - run: go test -race -v -coverprofile=profile.cov . + - uses: codecov/codecov-action@v5.5.1 + with: + file: ./profile.cov + name: codecov-go + release: + runs-on: ubuntu-24.04 + needs: [test] + if: ${{ github.event_name == 'workflow_dispatch' || github.ref == 'refs/heads/main' || github.ref == 'refs/heads/next' }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - name: Source checkout + uses: actions/checkout@v5 + - name: Semantic Release + uses: cycjimmy/semantic-release-action@v5 + with: + dry_run: false + semantic_version: 18.0.1 + extra_plugins: | + @semantic-release/exec@6.0.3 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..84039fe --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +coverage.coverprofile diff --git a/.releaserc.js b/.releaserc.js new file mode 100644 index 0000000..e8eea8e --- /dev/null +++ b/.releaserc.js @@ -0,0 +1,11 @@ +module.exports = { + branches: [ + {name: "main"}, + {name: "next", prerelease: true}, + ], + plugins: [ + "@semantic-release/commit-analyzer", + "@semantic-release/release-notes-generator", + "@semantic-release/github" + ] +}; diff --git a/LICENSE b/LICENSE index 0e5fb87..bb9d80b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2012 Rodrigo Moraes. All rights reserved. +Copyright (c) 2023 The Gorilla Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..98f5ab7 --- /dev/null +++ b/Makefile @@ -0,0 +1,34 @@ +GO_LINT=$(shell which golangci-lint 2> /dev/null || echo '') +GO_LINT_URI=github.com/golangci/golangci-lint/cmd/golangci-lint@latest + +GO_SEC=$(shell which gosec 2> /dev/null || echo '') +GO_SEC_URI=github.com/securego/gosec/v2/cmd/gosec@latest + +GO_VULNCHECK=$(shell which govulncheck 2> /dev/null || echo '') +GO_VULNCHECK_URI=golang.org/x/vuln/cmd/govulncheck@latest + +.PHONY: golangci-lint +golangci-lint: + $(if $(GO_LINT), ,go install $(GO_LINT_URI)) + @echo "##### Running golangci-lint" + golangci-lint run -v + +.PHONY: gosec +gosec: + $(if $(GO_SEC), ,go install $(GO_SEC_URI)) + @echo "##### Running gosec" + gosec ./... + +.PHONY: govulncheck +govulncheck: + $(if $(GO_VULNCHECK), ,go install $(GO_VULNCHECK_URI)) + @echo "##### Running govulncheck" + govulncheck ./... + +.PHONY: verify +verify: golangci-lint gosec govulncheck + +.PHONY: test +test: + @echo "##### Running tests" + go test -race -cover -coverprofile=coverage.coverprofile -covermode=atomic -v ./... \ No newline at end of file diff --git a/README.md b/README.md index 82b0411..4ceaf09 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,14 @@ -schema -====== -[![GoDoc](https://godoc.org/github.com/gorilla/schema?status.svg)](https://godoc.org/github.com/gorilla/schema) -[![CircleCI](https://circleci.com/gh/gorilla/mux.svg?style=svg)](https://circleci.com/gh/gorilla/schema) -[![Sourcegraph](https://sourcegraph.com/github.com/gorilla/schema/-/badge.svg)](https://sourcegraph.com/github.com/gorilla/schema?badge) +# schema ---- +[![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release) +[![Release](https://github.com/zitadel/schema/workflows/Release/badge.svg)](https://github.com/zitadel/schema/actions) +[![Go Reference](https://pkg.go.dev/badge/github.com/zitadel/schema.svg)](https://pkg.go.dev/github.com/zitadel/schema) +[![license](https://badgen.net/github/license/zitadel/schema/)](https://github.com/zitadel/schema/blob/master/LICENSE) +[![release](https://badgen.net/github/release/zitadel/schema/stable)](https://github.com/zitadel/schema/releases) +[![Go Report Card](https://goreportcard.com/badge/github.com/zitadel/schema)](https://goreportcard.com/report/github.com/zitadel/schema) +[![codecov](https://codecov.io/gh/zitadel/schema/branch/main/graph/badge.svg?token=5QS2VEMCt2)](https://codecov.io/gh/zitadel/schema) -**The Gorilla project has been archived, and is no longer under active maintainenance. You can read more here: https://github.com/gorilla#gorilla-toolkit** - ---- - -Package gorilla/schema converts structs to and from form values. +Package zitadel/schema converts structs to and from form values. This is a maintained fork of [gorilla/schema](https://github.com/gorilla/schema) ## Example @@ -89,7 +87,32 @@ The supported field types in the struct are: Unsupported types are simply ignored, however custom types can be registered to be converted. -More examples are available on the Gorilla website: https://www.gorillatoolkit.org/pkg/schema +## Setting Defaults + +It is possible to set default values when encoding/decoding by using the `default` tag option. The value of `default` is applied when a field has a zero value, a pointer has a nil value, or a slice is empty. + +```go +type Person struct { + Phone string `schema:"phone,default:+123456"` // custom name + Age int `schema:"age,default:21"` + Admin bool `schema:"admin,default:false"` + Balance float64 `schema:"balance,default:10.0"` + Friends []string `schema:friends,default:john|bob` +} +``` + +The `default` tag option is supported for the following types: + +* bool +* float variants (float32, float64) +* int variants (int, int8, int16, int32, int64) +* uint variants (uint, uint8, uint16, uint32, uint64) +* string +* a slice of the above types. As shown in the example above, `|` should be used to separate between slice items. +* a pointer to one of the above types (pointer to slice and slice of pointers are not supported). + +> [!NOTE] +> Because primitive types like int, float, bool, unint and their variants have their default (or zero) values set by Golang, it is not possible to distinguish them from a provided value when decoding/encoding form values. In this case, the value provided by the `default` option tag will be always applied. For example, let's assume that the value submitted in the form for `balance` is `0.0` then the default of `10.0` will be applied, even if `0.0` is part of the form data for the `balance` field. In such cases, it is highly recommended to use pointers to allow schema to distinguish between when a form field has no provided value and when a form has a value equal to the corresponding default set by Golang for a particular type. If the type of the `Balance` field above is changed to `*float64`, then the zero value would be `nil`. In this case, if the form data value for `balance` is `0.0`, then the default will not be applied. ## License diff --git a/cache.go b/cache.go index 0746c12..065b8d6 100644 --- a/cache.go +++ b/cache.go @@ -12,7 +12,7 @@ import ( "sync" ) -var invalidPath = errors.New("schema: invalid path") +var errInvalidPath = errors.New("schema: invalid path") // newCache returns a new cache. func newCache() *cache { @@ -53,13 +53,13 @@ func (c *cache) parsePath(p string, t reflect.Type) ([]pathPart, error) { keys := strings.Split(p, ".") for i := 0; i < len(keys); i++ { if t.Kind() != reflect.Struct { - return nil, invalidPath + return nil, errInvalidPath } if struc = c.get(t); struc == nil { - return nil, invalidPath + return nil, errInvalidPath } if field = struc.get(keys[i]); field == nil { - return nil, invalidPath + return nil, errInvalidPath } // Valid field. Append index. path = append(path, field.name) @@ -72,10 +72,10 @@ func (c *cache) parsePath(p string, t reflect.Type) ([]pathPart, error) { // So checking i+2 is not necessary anymore. i++ if i+1 > len(keys) { - return nil, invalidPath + return nil, errInvalidPath } if index64, err = strconv.ParseInt(keys[i], 10, 0); err != nil { - return nil, invalidPath + return nil, errInvalidPath } parts = append(parts, pathPart{ path: path, @@ -197,6 +197,7 @@ func (c *cache) createField(field reflect.StructField, parentAlias string) *fiel isSliceOfStructs: isSlice && isStruct, isAnonymous: field.Anonymous, isRequired: options.Contains("required"), + defaultValue: options.getDefaultOptionValue(), } } @@ -246,8 +247,9 @@ type fieldInfo struct { // isSliceOfStructs indicates if the field type is a slice of structs. isSliceOfStructs bool // isAnonymous indicates whether the field is embedded in the struct. - isAnonymous bool - isRequired bool + isAnonymous bool + isRequired bool + defaultValue string } func (f *fieldInfo) paths(prefix string) []string { @@ -303,3 +305,13 @@ func (o tagOptions) Contains(option string) bool { } return false } + +func (o tagOptions) getDefaultOptionValue() string { + for _, s := range o { + if strings.HasPrefix(s, "default:") { + return strings.Split(s, ":")[1] + } + } + + return "" +} diff --git a/converter.go b/converter.go index 4f2116a..4bae6df 100644 --- a/converter.go +++ b/converter.go @@ -143,3 +143,80 @@ func convertUint64(value string) reflect.Value { } return invalidValue } + +func convertPointer(k reflect.Kind, value string) reflect.Value { + switch k { + case boolType: + if v := convertBool(value); v.IsValid() { + converted := v.Bool() + return reflect.ValueOf(&converted) + } + case float32Type: + if v := convertFloat32(value); v.IsValid() { + converted := float32(v.Float()) + return reflect.ValueOf(&converted) + } + case float64Type: + if v := convertFloat64(value); v.IsValid() { + converted := float64(v.Float()) + return reflect.ValueOf(&converted) + } + case intType: + if v := convertInt(value); v.IsValid() { + converted := int(v.Int()) + return reflect.ValueOf(&converted) + } + case int8Type: + if v := convertInt8(value); v.IsValid() { + converted := int8(v.Int()) + return reflect.ValueOf(&converted) + } + case int16Type: + if v := convertInt16(value); v.IsValid() { + converted := int16(v.Int()) + return reflect.ValueOf(&converted) + } + case int32Type: + if v := convertInt32(value); v.IsValid() { + converted := int32(v.Int()) + return reflect.ValueOf(&converted) + } + case int64Type: + if v := convertInt64(value); v.IsValid() { + converted := int64(v.Int()) + return reflect.ValueOf(&converted) + } + case stringType: + if v := convertString(value); v.IsValid() { + converted := v.String() + return reflect.ValueOf(&converted) + } + case uintType: + if v := convertUint(value); v.IsValid() { + converted := uint(v.Uint()) + return reflect.ValueOf(&converted) + } + case uint8Type: + if v := convertUint8(value); v.IsValid() { + converted := uint8(v.Uint()) + return reflect.ValueOf(&converted) + } + case uint16Type: + if v := convertUint16(value); v.IsValid() { + converted := uint16(v.Uint()) + return reflect.ValueOf(&converted) + } + case uint32Type: + if v := convertUint32(value); v.IsValid() { + converted := uint32(v.Uint()) + return reflect.ValueOf(&converted) + } + case uint64Type: + if v := convertUint64(value); v.IsValid() { + converted := uint64(v.Uint()) + return reflect.ValueOf(&converted) + } + } + + return invalidValue +} diff --git a/decoder.go b/decoder.go index 025e438..54c88ec 100644 --- a/decoder.go +++ b/decoder.go @@ -12,9 +12,13 @@ import ( "strings" ) +const ( + defaultMaxSize = 16000 +) + // NewDecoder returns a new Decoder. func NewDecoder() *Decoder { - return &Decoder{cache: newCache()} + return &Decoder{cache: newCache(), maxSize: defaultMaxSize} } // Decoder decodes values from a map[string][]string to a struct. @@ -22,6 +26,7 @@ type Decoder struct { cache *cache zeroEmpty bool ignoreUnknownKeys bool + maxSize int } // SetAliasTag changes the tag used to locate custom field aliases. @@ -54,6 +59,13 @@ func (d *Decoder) IgnoreUnknownKeys(i bool) { d.ignoreUnknownKeys = i } +// MaxSize limits the size of slices for URL nested arrays or object arrays. +// Choose MaxSize carefully; large values may create many zero-value slice elements. +// Example: "items.100000=apple" would create a slice with 100,000 empty strings. +func (d *Decoder) MaxSize(size int) { + d.maxSize = size +} + // RegisterConverter registers a converter function for a custom type. func (d *Decoder) RegisterConverter(value interface{}, converterFunc Converter) { d.cache.registerConverter(value, converterFunc) @@ -84,6 +96,7 @@ func (d *Decoder) Decode(dst interface{}, src map[string][]string) error { errors[path] = UnknownKeyError{Key: path} } } + errors.merge(d.setDefaults(t, v)) errors.merge(d.checkRequired(t, src)) if len(errors) > 0 { return errors @@ -91,6 +104,88 @@ func (d *Decoder) Decode(dst interface{}, src map[string][]string) error { return nil } +// setDefaults sets the default values when the `default` tag is specified, +// default is supported on basic/primitive types and their pointers, +// nested structs can also have default tags +func (d *Decoder) setDefaults(t reflect.Type, v reflect.Value) MultiError { + struc := d.cache.get(t) + if struc == nil { + // unexpect, cache.get never return nil + return MultiError{"default-" + t.Name(): errors.New("cache fail")} + } + + errs := MultiError{} + + if v.Type().Kind() == reflect.Struct { + for i := 0; i < v.NumField(); i++ { + field := v.Field(i) + if field.Type().Kind() == reflect.Ptr && field.IsNil() && v.Type().Field(i).Anonymous { + field.Set(reflect.New(field.Type().Elem())) + } + } + } + + for _, f := range struc.fields { + vCurrent := v.FieldByName(f.name) + + if vCurrent.Type().Kind() == reflect.Struct && f.defaultValue == "" { + errs.merge(d.setDefaults(vCurrent.Type(), vCurrent)) + } else if isPointerToStruct(vCurrent) && f.defaultValue == "" { + errs.merge(d.setDefaults(vCurrent.Elem().Type(), vCurrent.Elem())) + } + + if f.defaultValue != "" && f.isRequired { + errs.merge(MultiError{"default-" + f.name: errors.New("required fields cannot have a default value")}) + } else if f.defaultValue != "" && vCurrent.IsZero() && !f.isRequired { + if f.typ.Kind() == reflect.Struct { + errs.merge(MultiError{"default-" + f.name: errors.New("default option is supported only on: bool, float variants, string, unit variants types or their corresponding pointers or slices")}) + } else if f.typ.Kind() == reflect.Slice { + vals := strings.Split(f.defaultValue, "|") + + // check if slice has one of the supported types for defaults + if _, ok := builtinConverters[f.typ.Elem().Kind()]; !ok { + errs.merge(MultiError{"default-" + f.name: errors.New("default option is supported only on: bool, float variants, string, unit variants types or their corresponding pointers or slices")}) + continue + } + + defaultSlice := reflect.MakeSlice(f.typ, 0, cap(vals)) + for _, val := range vals { + // this check is to handle if the wrong value is provided + convertedVal := builtinConverters[f.typ.Elem().Kind()](val) + if !convertedVal.IsValid() { + errs.merge(MultiError{"default-" + f.name: fmt.Errorf("failed setting default: %s is not compatible with field %s type", val, f.name)}) + break + } + defaultSlice = reflect.Append(defaultSlice, convertedVal) + } + vCurrent.Set(defaultSlice) + } else if f.typ.Kind() == reflect.Ptr { + t1 := f.typ.Elem() + + if t1.Kind() == reflect.Struct || t1.Kind() == reflect.Slice { + errs.merge(MultiError{"default-" + f.name: errors.New("default option is supported only on: bool, float variants, string, unit variants types or their corresponding pointers or slices")}) + } + + // this check is to handle if the wrong value is provided + if convertedVal := convertPointer(t1.Kind(), f.defaultValue); convertedVal.IsValid() { + vCurrent.Set(convertedVal) + } + } else { + // this check is to handle if the wrong value is provided + if convertedVal := builtinConverters[f.typ.Kind()](f.defaultValue); convertedVal.IsValid() { + vCurrent.Set(builtinConverters[f.typ.Kind()](f.defaultValue)) + } + } + } + } + + return errs +} + +func isPointerToStruct(v reflect.Value) bool { + return !v.IsZero() && v.Type().Kind() == reflect.Ptr && v.Elem().Type().Kind() == reflect.Struct +} + // checkRequired checks whether required fields are empty // // check type t recursively if t has struct fields. @@ -193,7 +288,7 @@ func (d *Decoder) decode(v reflect.Value, path string, parts []pathPart, values if v.Type().Kind() == reflect.Struct { for i := 0; i < v.NumField(); i++ { field := v.Field(i) - if field.Type().Kind() == reflect.Ptr && field.IsNil() && v.Type().Field(i).Anonymous == true { + if field.Type().Kind() == reflect.Ptr && field.IsNil() && v.Type().Field(i).Anonymous { field.Set(reflect.New(field.Type().Elem())) } } @@ -219,6 +314,10 @@ func (d *Decoder) decode(v reflect.Value, path string, parts []pathPart, values // Slice of structs. Let's go recursive. if len(parts) > 1 { idx := parts[0].index + // a defensive check to avoid creating a large slice based on user input index + if idx > d.maxSize { + return fmt.Errorf("%v index %d is larger than the configured maxSize %d", v.Kind(), idx, d.maxSize) + } if v.IsNil() || v.Len() < idx+1 { value := reflect.MakeSlice(t, idx+1, idx+1) if v.Len() < idx+1 { diff --git a/decoder_test.go b/decoder_test.go index 863891f..d01569e 100644 --- a/decoder_test.go +++ b/decoder_test.go @@ -7,6 +7,7 @@ package schema import ( "encoding/hex" "errors" + "fmt" "reflect" "strings" "testing" @@ -69,9 +70,7 @@ func (id *S19) UnmarshalText(text []byte) error { if len(buf) > len(*id) { return errors.New("out of range") } - for i := range buf { - (*id)[i] = buf[i] - } + copy((*id)[:], buf) return nil } @@ -673,7 +672,10 @@ func TestEmptyValue(t *testing.T) { "F01": {"", "foo"}, } s := &S5{} - NewDecoder().Decode(s, data) + err := NewDecoder().Decode(s, data) + if err != nil { + t.Fatalf("Failed to decode: %v", err) + } if len(s.F01) != 1 { t.Errorf("Expected 1 values in F01") } @@ -706,7 +708,10 @@ func TestUnexportedField(t *testing.T) { "id": {"identifier"}, } s := &S6{} - NewDecoder().Decode(s, data) + err := NewDecoder().Decode(s, data) + if err != nil { + t.Fatalf("Failed to decode: %v", err) + } if s.id != "" { t.Errorf("Unexported field expected to be ignored") } @@ -724,7 +729,10 @@ func TestMultipleValues(t *testing.T) { } s := S7{} - NewDecoder().Decode(&s, data) + err := NewDecoder().Decode(&s, data) + if err != nil { + t.Fatalf("Failed to decode: %v", err) + } if s.ID != "1" { t.Errorf("Last defined value must be used when multiple values for same field are provided") } @@ -742,7 +750,10 @@ func TestSetAliasTag(t *testing.T) { s := S8{} dec := NewDecoder() dec.SetAliasTag("json") - dec.Decode(&s, data) + err := dec.Decode(&s, data) + if err != nil { + t.Fatalf("Failed to decode: %v", err) + } if s.ID != "foo" { t.Fatalf("Bad value: got %q, want %q", s.ID, "foo") } @@ -813,7 +824,10 @@ func TestEmbeddedField(t *testing.T) { "Id": {"identifier"}, } s := &S10{} - NewDecoder().Decode(s, data) + err := NewDecoder().Decode(s, data) + if err != nil { + t.Fatalf("Failed to decode: %v", err) + } if s.Id != "identifier" { t.Errorf("Missing support for embedded fields") } @@ -1148,7 +1162,10 @@ func TestCSVSlice(t *testing.T) { } s := S12A{} - NewDecoder().Decode(&s, data) + err := NewDecoder().Decode(&s, data) + if err != nil { + t.Fatalf("Failed to decode: %v", err) + } if len(s.ID) != 2 { t.Errorf("Expected two values in the result list, got %+v", s.ID) } @@ -1161,14 +1178,17 @@ type S12B struct { ID []string } -//Decode should not split on , into a slice for string only +// Decode should not split on , into a slice for string only func TestCSVStringSlice(t *testing.T) { data := map[string][]string{ "ID": {"0,1"}, } s := S12B{} - NewDecoder().Decode(&s, data) + err := NewDecoder().Decode(&s, data) + if err != nil { + t.Fatalf("Failed to decode: %v", err) + } if len(s.ID) != 1 { t.Errorf("Expected one value in the result list, got %+v", s.ID) } @@ -1177,7 +1197,7 @@ func TestCSVStringSlice(t *testing.T) { } } -//Invalid data provided by client should not panic (github issue 33) +// Invalid data provided by client should not panic (github issue 33) func TestInvalidDataProvidedByClient(t *testing.T) { defer func() { if r := recover(); r != nil { @@ -1186,7 +1206,7 @@ func TestInvalidDataProvidedByClient(t *testing.T) { }() type S struct { - f string + f string // nolint:unused } data := map[string][]string{ @@ -1202,7 +1222,7 @@ func TestInvalidDataProvidedByClient(t *testing.T) { // underlying cause of error in issue 33 func TestInvalidPathInCacheParsePath(t *testing.T) { type S struct { - f string + f string // nolint:unused } typ := reflect.ValueOf(new(S)).Elem().Type() @@ -1218,7 +1238,10 @@ func TestDecodeToTypedField(t *testing.T) { type Aa bool s1 := &struct{ Aa }{} v1 := map[string][]string{"Aa": {"true"}} - NewDecoder().Decode(s1, v1) + err := NewDecoder().Decode(s1, v1) + if err != nil { + t.Fatalf("Failed to decode: %v", err) + } if s1.Aa != Aa(true) { t.Errorf("s1: expected %v, got %v", true, s1.Aa) } @@ -1238,7 +1261,10 @@ func TestRegisterConverter(t *testing.T) { decoder.RegisterConverter(s1.Bb, func(s string) reflect.Value { return reflect.ValueOf(2) }) v1 := map[string][]string{"Aa": {"4"}, "Bb": {"5"}} - decoder.Decode(s1, v1) + err := decoder.Decode(s1, v1) + if err != nil { + t.Fatalf("Failed to decode: %v", err) + } if s1.Aa != Aa(1) { t.Errorf("s1.Aa: expected %v, got %v", 1, s1.Aa) @@ -1260,9 +1286,12 @@ func TestRegisterConverterSlice(t *testing.T) { }{} expected := []string{"one", "two", "three"} - decoder.Decode(&result, map[string][]string{ + err := decoder.Decode(&result, map[string][]string{ "multiple": []string{"one,two,three"}, }) + if err != nil { + t.Fatalf("Failed to decode: %v", err) + } for i := range expected { if got, want := expected[i], result.Multiple[i]; got != want { t.Errorf("%d: got %s, want %s", i, got, want) @@ -1868,8 +1897,11 @@ func TestRegisterConverterOverridesTextUnmarshaler(t *testing.T) { ts := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC) decoder.RegisterConverter(s1.MyTime, func(s string) reflect.Value { return reflect.ValueOf(ts) }) - v1 := map[string][]string{"MyTime": {"4"}, "Bb": {"5"}} - decoder.Decode(s1, v1) + v1 := map[string][]string{"MyTime": {"4"}} + err := decoder.Decode(s1, v1) + if err != nil { + t.Fatalf("Failed to decode: %v", err) + } if s1.MyTime != MyTime(ts) { t.Errorf("s1.Aa: expected %v, got %v", ts, s1.MyTime) @@ -1951,7 +1983,7 @@ func TestTextUnmarshalerTypeSliceOfStructs(t *testing.T) { sb := struct { Value S21B }{} - if err := decoder.Decode(&sb, data); err == invalidPath { + if err := decoder.Decode(&sb, data); err == errInvalidPath { t.Fatal("Expecting invalid path error", err) } } @@ -2024,3 +2056,474 @@ func TestUnmashalPointerToEmbedded(t *testing.T) { t.Errorf("Expected %v errors, got %v", expected, s.Value) } } + +type S24 struct { + F1 string `schema:"F1"` +} + +type S24e struct { + *S24 + F2 string `schema:"F2"` +} + +func TestUnmarshallToEmbeddedNoData(t *testing.T) { + data := map[string][]string{ + "F3": {"raw a"}, + } + + s := &S24e{} + + decoder := NewDecoder() + err := decoder.Decode(s, data) + + expectedErr := `schema: invalid path "F3"` + if err.Error() != expectedErr { + t.Fatalf("got %q, want %q", err, expectedErr) + } +} + +type S25ee struct { + F3 string `schema:"F3"` +} + +type S25e struct { + S25ee + F2 string `schema:"F2"` +} + +type S25 struct { + S25e + F1 string `schema:"F1"` +} + +func TestDoubleEmbedded(t *testing.T) { + data := map[string][]string{ + "F1": {"raw a"}, + "F2": {"raw b"}, + "F3": {"raw c"}, + } + + s := S25{} + decoder := NewDecoder() + + if err := decoder.Decode(&s, data); err != nil { + t.Fatal("Error while decoding:", err) + } + + expected := S25{ + F1: "raw a", + S25e: S25e{ + F2: "raw b", + S25ee: S25ee{ + F3: "raw c", + }, + }, + } + if !reflect.DeepEqual(expected, s) { + t.Errorf("Expected %v errors, got %v", expected, s) + } + +} + +func TestDefaultValuesAreSet(t *testing.T) { + type N struct { + S1 string `schema:"s1,default:test1"` + I2 int `schema:"i2,default:22"` + R2 []float64 `schema:"r2,default:2|3.5|11.01"` + } + + type D struct { + N + S string `schema:"s,default:test1"` + I int `schema:"i,default:21"` + J int8 `schema:"j,default:2"` + K int16 `schema:"k,default:-455"` + L int32 `schema:"l,default:899"` + M int64 `schema:"m,default:12455"` + B bool `schema:"b,default:false"` + F float64 `schema:"f,default:3.14"` + G float32 `schema:"g,default:19.12"` + U uint `schema:"u,default:1"` + V uint8 `schema:"v,default:190"` + W uint16 `schema:"w,default:20000"` + Y uint32 `schema:"y,default:156666666"` + Z uint64 `schema:"z,default:1545465465465546"` + X []string `schema:"x,default:x1|x2"` + } + + data := map[string][]string{} + + d := D{} + + decoder := NewDecoder() + + if err := decoder.Decode(&d, data); err != nil { + t.Fatal("Error while decoding:", err) + } + + expected := D{ + N: N{ + S1: "test1", + I2: 22, + R2: []float64{2, 3.5, 11.01}, + }, + S: "test1", + I: 21, + J: 2, + K: -455, + L: 899, + M: 12455, + B: false, + F: 3.14, + G: 19.12, + U: 1, + V: 190, + W: 20000, + Y: 156666666, + Z: 1545465465465546, + X: []string{"x1", "x2"}, + } + + if !reflect.DeepEqual(expected, d) { + t.Errorf("Expected %v, got %v", expected, d) + } + + type P struct { + *N + S *string `schema:"s,default:test1"` + I *int `schema:"i,default:21"` + J *int8 `schema:"j,default:2"` + K *int16 `schema:"k,default:-455"` + L *int32 `schema:"l,default:899"` + M *int64 `schema:"m,default:12455"` + B *bool `schema:"b,default:false"` + F *float64 `schema:"f,default:3.14"` + G *float32 `schema:"g,default:19.12"` + U *uint `schema:"u,default:1"` + V *uint8 `schema:"v,default:190"` + W *uint16 `schema:"w,default:20000"` + Y *uint32 `schema:"y,default:156666666"` + Z *uint64 `schema:"z,default:1545465465465546"` + X []string `schema:"x,default:x1|x2"` + } + + p := P{N: &N{}} + + if err := decoder.Decode(&p, data); err != nil { + t.Fatal("Error while decoding:", err) + } + + vExpected := reflect.ValueOf(expected) + vActual := reflect.ValueOf(p) + + i := 0 + + for i < vExpected.NumField() { + if !reflect.DeepEqual(vExpected.Field(i).Interface(), reflect.Indirect(vActual.Field(i)).Interface()) { + t.Errorf("Expected %v, got %v", vExpected.Field(i).Interface(), reflect.Indirect(vActual.Field(i)).Interface()) + } + i++ + } +} + +func TestDefaultValuesAreIgnoredIfValuesAreProvided(t *testing.T) { + type D struct { + S string `schema:"s,default:test1"` + I int `schema:"i,default:21"` + B bool `schema:"b,default:false"` + F float64 `schema:"f,default:3.14"` + U uint `schema:"u,default:1"` + } + + data := map[string][]string{"s": {"s"}, "i": {"1"}, "b": {"true"}, "f": {"0.22"}, "u": {"14"}} + + d := D{} + + decoder := NewDecoder() + + if err := decoder.Decode(&d, data); err != nil { + t.Fatal("Error while decoding:", err) + } + + expected := D{ + S: "s", + I: 1, + B: true, + F: 0.22, + U: 14, + } + + if !reflect.DeepEqual(expected, d) { + t.Errorf("Expected %v, got %v", expected, d) + } +} + +func TestRequiredFieldsCannotHaveDefaults(t *testing.T) { + type D struct { + S string `schema:"s,required,default:test1"` + I int `schema:"i,required,default:21"` + B bool `schema:"b,required,default:false"` + F float64 `schema:"f,required,default:3.14"` + U uint `schema:"u,required,default:1"` + } + + data := map[string][]string{"s": {"s"}, "i": {"1"}, "b": {"true"}, "f": {"0.22"}, "u": {"14"}} + + d := D{} + + decoder := NewDecoder() + + err := decoder.Decode(&d, data) + + expected := "required fields cannot have a default value" + + if err == nil || !strings.Contains(err.Error(), expected) { + t.Errorf("decoding should fail with error msg %s got %q", expected, err) + } + +} + +func TestInvalidDefaultElementInSliceRaiseError(t *testing.T) { + type D struct { + A []int `schema:"a,default:0|notInt"` + B []bool `schema:"b,default:true|notInt"` + // //uint types + D []uint `schema:"d,default:1|notInt"` + E []uint8 `schema:"e,default:2|notInt"` + F []uint16 `schema:"f,default:3|notInt"` + G []uint32 `schema:"g,default:4|notInt"` + H []uint64 `schema:"h,default:5|notInt"` + // // int types + N []int `schema:"n,default:11|notInt"` + O []int8 `schema:"o,default:12|notInt"` + P []int16 `schema:"p,default:13|notInt"` + Q []int32 `schema:"q,default:14|notInt"` + R []int64 `schema:"r,default:15|notInt"` + // // float + X []float32 `schema:"c,default:2.2|notInt"` + Y []float64 `schema:"c,default:3.3|notInt"` + } + d := D{} + + data := map[string][]string{} + + decoder := NewDecoder() + + err := decoder.Decode(&d, data) + + if err == nil { + t.Error("if a different type exists, error should be raised") + } + + dType := reflect.TypeOf(d) + + e, ok := err.(MultiError) + if !ok || len(e) != dType.NumField() { + t.Errorf("Expected %d errors, got %#v", dType.NumField(), err) + } + + for i := 0; i < dType.NumField(); i++ { + v := dType.Field(i) + fieldKey := "default-" + string(v.Name) + errMsg := fmt.Sprintf("failed setting default: notInt is not compatible with field %s type", string(v.Name)) + ferr := e[fieldKey] + if strings.Compare(ferr.Error(), errMsg) != 0 { + t.Errorf("%s: expected %s, got %#v\n", fieldKey, ferr.Error(), errMsg) + } + } +} + +func TestInvalidDefaultsValuesHaveNoEffect(t *testing.T) { + type D struct { + B bool `schema:"b,default:invalid"` + C *float32 `schema:"c,default:notAFloat"` + //uint types + D uint `schema:"d,default:notUint"` + E uint8 `schema:"e,default:notUint"` + F uint16 `schema:"f,default:notUint"` + G uint32 `schema:"g,default:notUint"` + H uint64 `schema:"h,default:notUint"` + // uint types pointers + I *uint `schema:"i,default:notUint"` + J *uint8 `schema:"j,default:notUint"` + K *uint16 `schema:"k,default:notUint"` + L *uint32 `schema:"l,default:notUint"` + M *uint64 `schema:"m,default:notUint"` + // int types + N int `schema:"n,default:notInt"` + O int8 `schema:"o,default:notInt"` + P int16 `schema:"p,default:notInt"` + Q int32 `schema:"q,default:notInt"` + R int64 `schema:"r,default:notInt"` + // int types pointers + S *int `schema:"s,default:notInt"` + T *int8 `schema:"t,default:notInt"` + U *int16 `schema:"u,default:notInt"` + V *int32 `schema:"v,default:notInt"` + W *int64 `schema:"w,default:notInt"` + // float + X float32 `schema:"c,default:notAFloat"` + Y float64 `schema:"c,default:notAFloat"` + Z *float64 `schema:"c,default:notAFloat"` + } + + d := D{} + + expected := D{} + + data := map[string][]string{} + + decoder := NewDecoder() + + err := decoder.Decode(&d, data) + + if err != nil { + t.Errorf("decoding should succeed but got error: %q", err) + } + + if !reflect.DeepEqual(expected, d) { + t.Errorf("expected %v but got %v", expected, d) + } +} + +func TestDefaultsAreNotSupportedForStructsAndStructSlices(t *testing.T) { + type C struct { + C string `schema:"c"` + } + + type D struct { + S S1 `schema:"s,default:{f1:0}"` + A []C `schema:"a,default:{c:test1}|{c:test2}"` + B []*int `schema:"b,default:12"` + E *C `schema:"e,default:{c:test3}"` + } + + d := D{} + + data := map[string][]string{} + + decoder := NewDecoder() + + err := decoder.Decode(&d, data) + + expected := "default option is supported only on: bool, float variants, string, unit variants types or their corresponding pointers or slices" + + if err == nil || !strings.Contains(err.Error(), expected) { + t.Errorf("decoding should fail with error msg %s got %q", expected, err) + } +} + +func TestDecoder_MaxSize(t *testing.T) { + t.Parallel() + + type Nested struct { + Val int + NestedValues []struct { + NVal int + } + } + type NestedSlices struct { + Values []Nested + } + + testcases := []struct { + name string + maxSize int + decoderInput func() (dst NestedSlices, src map[string][]string) + expectedDecoded NestedSlices + expectedErr MultiError + }{ + { + name: "no error on decoding under max size", + maxSize: 10, + decoderInput: func() (dst NestedSlices, src map[string][]string) { + return dst, map[string][]string{ + "Values.1.Val": {"132"}, + "Values.1.NestedValues.1.NVal": {"1"}, + "Values.1.NestedValues.2.NVal": {"2"}, + "Values.1.NestedValues.3.NVal": {"3"}, + } + }, + expectedDecoded: NestedSlices{ + Values: []Nested{ + { + Val: 0, + NestedValues: nil, + }, + { + Val: 132, NestedValues: []struct{ NVal int }{ + {NVal: 0}, + {NVal: 1}, + {NVal: 2}, + {NVal: 3}, + }, + }, + }, + }, + expectedErr: nil, + }, + { + name: "error on decoding above max size", + maxSize: 1, + decoderInput: func() (dst NestedSlices, src map[string][]string) { + return dst, map[string][]string{ + "Values.1.Val": {"132"}, + "Values.1.NestedValues.1.NVal": {"1"}, + "Values.1.NestedValues.2.NVal": {"2"}, + "Values.1.NestedValues.3.NVal": {"3"}, + } + }, + expectedErr: MultiError{ + "Values.1.NestedValues.2.NVal": errors.New("slice index 2 is larger than the configured maxSize 1"), + "Values.1.NestedValues.3.NVal": errors.New("slice index 3 is larger than the configured maxSize 1"), + }, + }, + } + + for _, tc := range testcases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + dec := NewDecoder() + dec.MaxSize(tc.maxSize) + dst, src := tc.decoderInput() + err := dec.Decode(&dst, src) + + if tc.expectedErr != nil { + var gotErr MultiError + if !errors.As(err, &gotErr) { + t.Errorf("decoder error is not of type %T", gotErr) + } + if !reflect.DeepEqual(gotErr, tc.expectedErr) { + t.Errorf("expected %v, got %v", tc.expectedErr, gotErr) + } + } else { + if !reflect.DeepEqual(dst, tc.expectedDecoded) { + t.Errorf("expected %v, got %v", tc.expectedDecoded, dst) + } + } + }) + } +} + +func TestDecoder_SetMaxSize(t *testing.T) { + + t.Run("default maxsize should be equal to given constant", func(t *testing.T) { + t.Parallel() + dec := NewDecoder() + if !reflect.DeepEqual(dec.maxSize, defaultMaxSize) { + t.Errorf("unexpected default max size") + } + }) + + t.Run("configured maxsize should be set properly", func(t *testing.T) { + t.Parallel() + configuredMaxSize := 50 + limitedMaxSizeDecoder := NewDecoder() + limitedMaxSizeDecoder.MaxSize(configuredMaxSize) + if !reflect.DeepEqual(limitedMaxSizeDecoder.maxSize, configuredMaxSize) { + t.Errorf("invalid decoder maxsize, expected: %d, got: %d", + configuredMaxSize, limitedMaxSizeDecoder.maxSize) + } + }) +} diff --git a/doc.go b/doc.go index aae9f33..d6e689e 100644 --- a/doc.go +++ b/doc.go @@ -3,7 +3,7 @@ // license that can be found in the LICENSE file. /* -Package gorilla/schema fills a struct with form values. +Package zitadel/schema fills a struct with form values. The basic usage is really simple. Given this struct: @@ -60,14 +60,14 @@ certain fields, use a dash for the name and it will be ignored: The supported field types in the destination struct are: - * bool - * float variants (float32, float64) - * int variants (int, int8, int16, int32, int64) - * string - * uint variants (uint, uint8, uint16, uint32, uint64) - * struct - * a pointer to one of the above types - * a slice or a pointer to a slice of one of the above types + - bool + - float variants (float32, float64) + - int variants (int, int8, int16, int32, int64) + - string + - uint variants (uint, uint8, uint16, uint32, uint64) + - struct + - a pointer to one of the above types + - a slice or a pointer to a slice of one of the above types Non-supported types are simply ignored, however custom types can be registered to be converted. diff --git a/encoder.go b/encoder.go index 8f9b9e0..52f2c10 100644 --- a/encoder.go +++ b/encoder.go @@ -94,7 +94,10 @@ func (e *Encoder) encode(v reflect.Value, dst map[string][]string) error { // Encode struct pointer types if the field is a valid pointer and a struct. if isValidStructPointer(v.Field(i)) && !e.hasCustomEncoder(v.Field(i).Type()) { - e.encode(v.Field(i).Elem(), dst) + err := e.encode(v.Field(i).Elem(), dst) + if err != nil { + errors[v.Field(i).Elem().Type().String()] = err + } continue } @@ -112,7 +115,10 @@ func (e *Encoder) encode(v reflect.Value, dst map[string][]string) error { } if v.Field(i).Type().Kind() == reflect.Struct { - e.encode(v.Field(i), dst) + err := e.encode(v.Field(i), dst) + if err != nil { + errors[v.Field(i).Type().String()] = err + } continue } diff --git a/encoder_test.go b/encoder_test.go index 49f7fa4..092f0de 100644 --- a/encoder_test.go +++ b/encoder_test.go @@ -205,7 +205,7 @@ func TestCompatSlices(t *testing.T) { t.Errorf("Dncoder has non-nil error: %v", err) } - if len(src.Ints) != len(dst.Ints) || len(src.Ones) != len(src.Ones) { + if len(src.Ints) != len(dst.Ints) || len(src.Ones) != len(dst.Ones) { t.Fatalf("Expected %v, got %v", src, dst) } @@ -315,7 +315,7 @@ func valsExist(t *testing.T, key string, expect []string, result map[string][]st func valNotExists(t *testing.T, key string, result map[string][]string) { if val, ok := result[key]; ok { - t.Error("Key not ommited. Expected: empty; got: " + val[0] + ".") + t.Error("Key not omitted. Expected: empty; got: " + val[0] + ".") } } @@ -344,7 +344,10 @@ func TestEncoderSetAliasTag(t *testing.T) { } encoder := NewEncoder() encoder.SetAliasTag("json") - encoder.Encode(&s, data) + err := encoder.Encode(&s, data) + if err != nil { + t.Fatalf("Failed to encode: %v", err) + } valExists(t, "id", "foo", data) } @@ -376,7 +379,10 @@ func TestEncoderWithOmitempty(t *testing.T) { } encoder := NewEncoder() - encoder.Encode(&s, vals) + err := encoder.Encode(&s, vals) + if err != nil { + t.Fatalf("Failed to encode: %v", err) + } valNotExists(t, "f01", vals) valExists(t, "f02", "test", vals) @@ -402,7 +408,10 @@ func TestStructPointer(t *testing.T) { } encoder := NewEncoder() - encoder.Encode(&s, vals) + err := encoder.Encode(&s, vals) + if err != nil { + t.Fatalf("Failed to encode: %v", err) + } valExists(t, "F12", "2", vals) valExists(t, "F02", "null", vals) valNotExists(t, "F03", vals) @@ -428,7 +437,10 @@ func TestRegisterEncoderCustomArrayType(t *testing.T) { return fmt.Sprint(value.Interface()) }) - encoder.Encode(s, vals) + err := encoder.Encode(ss[s], vals) + if err != nil { + t.Fatalf("Failed to encode: %v", err) + } } } @@ -466,7 +478,7 @@ func TestRegisterEncoderStructIsZero(t *testing.T) { t.Error("expected tim1 to be present") } - if "2020-08-04T13:30:01Z" != ta[0] { + if ta[0] != "2020-08-04T13:30:01Z" { t.Error("expected correct tim1 time") } diff --git a/go.mod b/go.mod index 200e4fa..9747360 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ -module github.com/gorilla/schema +module github.com/zitadel/schema -go 1.14 +go 1.20