diff --git a/go.mod b/go.mod index fd084ad59..dc3de6f19 100644 --- a/go.mod +++ b/go.mod @@ -26,6 +26,7 @@ require ( github.com/gin-contrib/sessions v0.0.5 github.com/gin-gonic/gin v1.9.0 github.com/go-fed/httpsig v1.1.0 + github.com/go-playground/form/v4 v4.2.0 github.com/go-playground/validator/v10 v10.11.2 github.com/google/uuid v1.3.0 github.com/gorilla/feeds v1.1.1 diff --git a/go.sum b/go.sum index 387808d50..a6ba5eef4 100644 --- a/go.sum +++ b/go.sum @@ -210,6 +210,8 @@ github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vb github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/form/v4 v4.2.0 h1:N1wh+Goz61e6w66vo8vJkQt+uwZSoLz50kZPJWR8eic= +github.com/go-playground/form/v4 v4.2.0/go.mod h1:q1a2BY+AQUUzhl6xA/6hBetay6dEIhMHjgvJiGo6K7U= github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= diff --git a/internal/api/client/accounts/accountupdate.go b/internal/api/client/accounts/accountupdate.go index b5d0dd5f9..55e544298 100644 --- a/internal/api/client/accounts/accountupdate.go +++ b/internal/api/client/accounts/accountupdate.go @@ -25,6 +25,7 @@ import ( "strconv" "github.com/gin-gonic/gin" + "github.com/go-playground/form/v4" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/gtserror" @@ -115,6 +116,13 @@ import ( // in: formData // description: Enable RSS feed for this account's Public posts at `/[username]/feed.rss` // type: boolean +// - +// name: fields_attributes +// in: formData +// description: Profile fields to be added to this account's profile +// type: array +// items: +// type: object // // security: // - OAuth2 Bearer: @@ -162,6 +170,28 @@ func (m *Module) AccountUpdateCredentialsPATCHHandler(c *gin.Context) { c.JSON(http.StatusOK, acctSensitive) } +type fieldAttributesBinding struct{} + +func (fieldAttributesBinding) Name() string { + return "FieldAttributes" +} + +func (fieldAttributesBinding) Bind(req *http.Request, obj any) error { + if err := req.ParseForm(); err != nil { + return err + } + + decoder := form.NewDecoder() + // change default namespace prefix and suffix to allow correct parsing of the field attributes + decoder.SetNamespacePrefix("[") + decoder.SetNamespaceSuffix("]") + if err := decoder.Decode(obj, req.Form); err != nil { + return err + } + + return nil +} + func parseUpdateAccountForm(c *gin.Context) (*apimodel.UpdateCredentialsRequest, error) { form := &apimodel.UpdateCredentialsRequest{ Source: &apimodel.UpdateSource{}, @@ -171,6 +201,11 @@ func parseUpdateAccountForm(c *gin.Context) (*apimodel.UpdateCredentialsRequest, return nil, fmt.Errorf("could not parse form from request: %s", err) } + // use custom form binding to support field attributes in the form data + if err := c.ShouldBindWith(&form, fieldAttributesBinding{}); err != nil { + return nil, fmt.Errorf("could not parse form from request: %s", err) + } + // parse source field-by-field sourceMap := c.PostFormMap("source") diff --git a/internal/api/client/accounts/accountupdate_test.go b/internal/api/client/accounts/accountupdate_test.go index f898e64da..4e4e03fa4 100644 --- a/internal/api/client/accounts/accountupdate_test.go +++ b/internal/api/client/accounts/accountupdate_test.go @@ -38,12 +38,14 @@ type AccountUpdateTestSuite struct { func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandler() { // set up the request - // we're updating the note of zork + // we're updating the note and profile fields of zork newBio := "this is my new bio read it and weep" requestBody, w, err := testrig.CreateMultipartFormData( "", "", map[string]string{ - "note": newBio, + "note": newBio, + "fields_attributes[0][name]": "pronouns", + "fields_attributes[0][value]": "they/them", }) if err != nil { panic(err) @@ -74,6 +76,7 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandler() // check the returned api model account // fields should be updated suite.Equal("

this is my new bio read it and weep

", apimodelAccount.Note) + suite.Equal("they/them", apimodelAccount.Fields[0].Value) suite.Equal(newBio, apimodelAccount.Source.Note) } diff --git a/internal/processing/account/update.go b/internal/processing/account/update.go index a96c17eeb..76f7a5a54 100644 --- a/internal/processing/account/update.go +++ b/internal/processing/account/update.go @@ -165,6 +165,26 @@ func (p *Processor) Update(ctx context.Context, account *gtsmodel.Account, form account.EnableRSS = form.EnableRSS } + if form.FieldsAttributes != nil && len(*form.FieldsAttributes) != 0 { + if err := validate.ProfileFieldsCount(*form.FieldsAttributes); err != nil { + return nil, gtserror.NewErrorBadRequest(err) + } + + account.Fields = make([]gtsmodel.Field, 0) // reset fields + for _, f := range *form.FieldsAttributes { + if f.Name != nil && f.Value != nil { + if *f.Name != "" && *f.Value != "" { + field := gtsmodel.Field{} + + field.Name = validate.ProfileField(f.Name) + field.Value = validate.ProfileField(f.Value) + + account.Fields = append(account.Fields, field) + } + } + } + } + err := p.state.DB.UpdateAccount(ctx, account) if err != nil { return nil, gtserror.NewErrorInternalError(fmt.Errorf("could not update account %s: %s", account.ID, err)) diff --git a/internal/validate/formvalidation.go b/internal/validate/formvalidation.go index 32aa1fd1e..e7839d1a3 100644 --- a/internal/validate/formvalidation.go +++ b/internal/validate/formvalidation.go @@ -43,6 +43,8 @@ const ( maximumUsernameLength = 64 maximumCustomCSSLength = 5000 maximumEmojiCategoryLength = 64 + maximumProfileFieldLength = 255 + maximumProfileFields = 4 ) // NewPassword returns an error if the given password is not sufficiently strong, or nil if it's ok. @@ -231,3 +233,20 @@ func SiteTerms(t string) error { func ULID(i string) bool { return regexes.ULID.MatchString(i) } + +func ProfileFieldsCount(fields []apimodel.UpdateField) error { + if length := len(fields); length > maximumProfileFields { + return fmt.Errorf("cannot have more than %d profile fields", maximumProfileFields) + } + + return nil +} + +func ProfileField(f *string) string { + s := []rune(*f) + if len(s) > maximumProfileFieldLength { + return string(s[:maximumProfileFieldLength]) // trim profile field to maximum allowed length + } + + return string(*f) +} diff --git a/internal/validate/formvalidation_test.go b/internal/validate/formvalidation_test.go index 61f505412..f59bbf753 100644 --- a/internal/validate/formvalidation_test.go +++ b/internal/validate/formvalidation_test.go @@ -25,6 +25,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" + "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/validate" ) @@ -284,6 +285,39 @@ func (suite *ValidationTestSuite) TestValidateReason() { } } +func (suite *ValidationTestSuite) TestValidateProfileFieldsCount() { + noFields := []model.UpdateField{} + fewFields := []model.UpdateField{{}, {}} + tooManyFields := []model.UpdateField{{}, {}, {}, {}, {}} + err := validate.ProfileFieldsCount(tooManyFields) + if assert.Error(suite.T(), err) { + assert.Equal(suite.T(), errors.New("cannot have more than 4 profile fields"), err) + } + + err = validate.ProfileFieldsCount(noFields) + assert.NoError(suite.T(), err) + + err = validate.ProfileFieldsCount(fewFields) + assert.NoError(suite.T(), err) +} + +func (suite *ValidationTestSuite) TestValidateProfileField() { + shortProfileField := "pronouns" + tooLongProfileField := "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer eu bibendum elit. Sed ac interdum nisi. Vestibulum vulputate eros quis euismod imperdiet. Nulla sit amet dui sit amet lorem consectetur iaculis. Mauris eget lacinia metus. Curabitur nec dui eleifend massa nunc." + trimmedProfileField := "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer eu bibendum elit. Sed ac interdum nisi. Vestibulum vulputate eros quis euismod imperdiet. Nulla sit amet dui sit amet lorem consectetur iaculis. Mauris eget lacinia metus. Curabitur nec dui " + + validated := validate.ProfileField(&shortProfileField) + assert.Equal(suite.T(), shortProfileField, validated) + + validated = validate.ProfileField(&tooLongProfileField) + assert.Len(suite.T(), validated, 255) + assert.Equal(suite.T(), trimmedProfileField, validated) + + validated = validate.ProfileField(&trimmedProfileField) + assert.Len(suite.T(), validated, 255) + assert.Equal(suite.T(), trimmedProfileField, validated) +} + func TestValidationTestSuite(t *testing.T) { suite.Run(t, new(ValidationTestSuite)) } diff --git a/vendor/github.com/go-playground/form/v4/.gitignore b/vendor/github.com/go-playground/form/v4/.gitignore new file mode 100644 index 000000000..aa19ee287 --- /dev/null +++ b/vendor/github.com/go-playground/form/v4/.gitignore @@ -0,0 +1,28 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof +old.txt +new.txt + +/.idea \ No newline at end of file diff --git a/vendor/github.com/go-playground/form/v4/.travis.yml b/vendor/github.com/go-playground/form/v4/.travis.yml new file mode 100644 index 000000000..fbc61d1b8 --- /dev/null +++ b/vendor/github.com/go-playground/form/v4/.travis.yml @@ -0,0 +1,26 @@ +language: go +go: + - 1.13.4 + - tip +matrix: + allow_failures: + - go: tip + +notifications: + email: + recipients: dean.karn@gmail.com + on_success: change + on_failure: always + +before_install: + - go install github.com/mattn/goveralls + +# Only clone the most recent commit. +git: + depth: 1 + +script: + - go test -v -race -covermode=atomic -coverprofile=coverage.coverprofile ./... + +after_success: | + goveralls -coverprofile=coverage.coverprofile -service travis-ci -repotoken $COVERALLS_TOKEN \ No newline at end of file diff --git a/vendor/github.com/go-playground/form/v4/LICENSE b/vendor/github.com/go-playground/form/v4/LICENSE new file mode 100644 index 000000000..8d8aba15b --- /dev/null +++ b/vendor/github.com/go-playground/form/v4/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 Go Playground + +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. diff --git a/vendor/github.com/go-playground/form/v4/README.md b/vendor/github.com/go-playground/form/v4/README.md new file mode 100644 index 000000000..647100f6f --- /dev/null +++ b/vendor/github.com/go-playground/form/v4/README.md @@ -0,0 +1,333 @@ +Package form +============ +![Project status](https://img.shields.io/badge/version-4.2.0-green.svg) +[![Build Status](https://github.com/go-playground/form/actions/workflows/workflow.yml/badge.svg)](https://github.com/go-playground/form/actions/workflows/workflow.yml) +[![Coverage Status](https://coveralls.io/repos/github/go-playground/form/badge.svg?branch=master)](https://coveralls.io/github/go-playground/form?branch=master) +[![Go Report Card](https://goreportcard.com/badge/github.com/go-playground/form)](https://goreportcard.com/report/github.com/go-playground/form) +[![GoDoc](https://godoc.org/github.com/go-playground/form?status.svg)](https://godoc.org/github.com/go-playground/form) +![License](https://img.shields.io/dub/l/vibe-d.svg) +[![Gitter](https://badges.gitter.im/go-playground/form.svg)](https://gitter.im/go-playground/form?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) + +Package form Decodes url.Values into Go value(s) and Encodes Go value(s) into url.Values. + +It has the following features: + +- Supports map of almost all types. +- Supports both Numbered and Normal arrays eg. `"Array[0]"` and just `"Array"` with multiple values passed. +- Slice honours the specified index. eg. if "Slice[2]" is the only Slice value passed down, it will be put at index 2; if slice isn't big enough it will be expanded. +- Array honours the specified index. eg. if "Array[2]" is the only Array value passed down, it will be put at index 2; if array isn't big enough a warning will be printed and value ignored. +- Only creates objects as necessary eg. if no `array` or `map` values are passed down, the `array` and `map` are left as their default values in the struct. +- Allows for Custom Type registration. +- Handles time.Time using RFC3339 time format by default, but can easily be changed by registering a Custom Type, see below. +- Handles Encoding & Decoding of almost all Go types eg. can Decode into struct, array, map, int... and Encode a struct, array, map, int... + +Common Questions + +- Does it support encoding.TextUnmarshaler? No because TextUnmarshaler only accepts []byte but posted values can have multiple values, so is not suitable. +- Mixing `array/slice` with `array[idx]/slice[idx]`, in which order are they parsed? `array/slice` then `array[idx]/slice[idx]` + +Supported Types ( out of the box ) +---------- + +* `string` +* `bool` +* `int`, `int8`, `int16`, `int32`, `int64` +* `uint`, `uint8`, `uint16`, `uint32`, `uint64` +* `float32`, `float64` +* `struct` and `anonymous struct` +* `interface{}` +* `time.Time` - by default using RFC3339 +* a `pointer` to one of the above types +* `slice`, `array` +* `map` +* `custom types` can override any of the above types +* many other types may be supported inherently + +**NOTE**: `map`, `struct` and `slice` nesting are ad infinitum. + +Installation +------------ + +Use go get. + + go get github.com/go-playground/form + +Then import the form package into your own code. + + import "github.com/go-playground/form/v4" + +Usage +----- + +- Use symbol `.` for separating fields/structs. (eg. `structfield.field`) +- Use `[index or key]` for access to index of a slice/array or key for map. (eg. `arrayfield[0]`, `mapfield[keyvalue]`) + +```html +
+ + + + + + + + + + + + +
+``` + +Examples +------- + +Decoding +```go +package main + +import ( + "fmt" + "log" + "net/url" + + "github.com/go-playground/form/v4" +) + +// Address contains address information +type Address struct { + Name string + Phone string +} + +// User contains user information +type User struct { + Name string + Age uint8 + Gender string + Address []Address + Active bool `form:"active"` + MapExample map[string]string + NestedMap map[string]map[string]string + NestedArray [][]string +} + +// use a single instance of Decoder, it caches struct info +var decoder *form.Decoder + +func main() { + decoder = form.NewDecoder() + + // this simulates the results of http.Request's ParseForm() function + values := parseForm() + + var user User + + // must pass a pointer + err := decoder.Decode(&user, values) + if err != nil { + log.Panic(err) + } + + fmt.Printf("%#v\n", user) +} + +// this simulates the results of http.Request's ParseForm() function +func parseForm() url.Values { + return url.Values{ + "Name": []string{"joeybloggs"}, + "Age": []string{"3"}, + "Gender": []string{"Male"}, + "Address[0].Name": []string{"26 Here Blvd."}, + "Address[0].Phone": []string{"9(999)999-9999"}, + "Address[1].Name": []string{"26 There Blvd."}, + "Address[1].Phone": []string{"1(111)111-1111"}, + "active": []string{"true"}, + "MapExample[key]": []string{"value"}, + "NestedMap[key][key]": []string{"value"}, + "NestedArray[0][0]": []string{"value"}, + } +} +``` + +Encoding +```go +package main + +import ( + "fmt" + "log" + + "github.com/go-playground/form/v4" +) + +// Address contains address information +type Address struct { + Name string + Phone string +} + +// User contains user information +type User struct { + Name string + Age uint8 + Gender string + Address []Address + Active bool `form:"active"` + MapExample map[string]string + NestedMap map[string]map[string]string + NestedArray [][]string +} + +// use a single instance of Encoder, it caches struct info +var encoder *form.Encoder + +func main() { + encoder = form.NewEncoder() + + user := User{ + Name: "joeybloggs", + Age: 3, + Gender: "Male", + Address: []Address{ + {Name: "26 Here Blvd.", Phone: "9(999)999-9999"}, + {Name: "26 There Blvd.", Phone: "1(111)111-1111"}, + }, + Active: true, + MapExample: map[string]string{"key": "value"}, + NestedMap: map[string]map[string]string{"key": {"key": "value"}}, + NestedArray: [][]string{{"value"}}, + } + + // must pass a pointer + values, err := encoder.Encode(&user) + if err != nil { + log.Panic(err) + } + + fmt.Printf("%#v\n", values) +} +``` + +Registering Custom Types +-------------- + +Decoder +```go +decoder.RegisterCustomTypeFunc(func(vals []string) (interface{}, error) { + return time.Parse("2006-01-02", vals[0]) +}, time.Time{}) +``` +ADDITIONAL: if a struct type is registered, the function will only be called if a url.Value exists for +the struct and not just the struct fields eg. url.Values{"User":"Name%3Djoeybloggs"} will call the +custom type function with 'User' as the type, however url.Values{"User.Name":"joeybloggs"} will not. + + +Encoder +```go +encoder.RegisterCustomTypeFunc(func(x interface{}) ([]string, error) { + return []string{x.(time.Time).Format("2006-01-02")}, nil +}, time.Time{}) +``` + +Ignoring Fields +-------------- +you can tell form to ignore fields using `-` in the tag +```go +type MyStruct struct { + Field string `form:"-"` +} +``` + +Omitempty +-------------- +you can tell form to omit empty fields using `,omitempty` or `FieldName,omitempty` in the tag +```go +type MyStruct struct { + Field string `form:",omitempty"` + Field2 string `form:"CustomFieldName,omitempty"` +} +``` + +Notes +------ +To maximize compatibility with other systems the Encoder attempts +to avoid using array indexes in url.Values if at all possible. + +eg. +```go +// A struct field of +Field []string{"1", "2", "3"} + +// will be output a url.Value as +"Field": []string{"1", "2", "3"} + +and not +"Field[0]": []string{"1"} +"Field[1]": []string{"2"} +"Field[2]": []string{"3"} + +// however there are times where it is unavoidable, like with pointers +i := int(1) +Field []*string{nil, nil, &i} + +// to avoid index 1 and 2 must use index +"Field[2]": []string{"1"} +``` + +Benchmarks +------ +###### Run on MacBook Pro (15-inch, 2017) using go version go1.10.1 darwin/amd64 + +NOTE: the 1 allocation and B/op in the first 4 decodes is actually the struct allocating when passing it in, so primitives are actually zero allocation. + +```go +go test -run=NONE -bench=. -benchmem=true +goos: darwin +goarch: amd64 +pkg: github.com/go-playground/form/benchmarks + +BenchmarkSimpleUserDecodeStruct-8 5000000 236 ns/op 64 B/op 1 allocs/op +BenchmarkSimpleUserDecodeStructParallel-8 20000000 82.1 ns/op 64 B/op 1 allocs/op +BenchmarkSimpleUserEncodeStruct-8 2000000 627 ns/op 485 B/op 10 allocs/op +BenchmarkSimpleUserEncodeStructParallel-8 10000000 223 ns/op 485 B/op 10 allocs/op +BenchmarkPrimitivesDecodeStructAllPrimitivesTypes-8 2000000 724 ns/op 96 B/op 1 allocs/op +BenchmarkPrimitivesDecodeStructAllPrimitivesTypesParallel-8 10000000 246 ns/op 96 B/op 1 allocs/op +BenchmarkPrimitivesEncodeStructAllPrimitivesTypes-8 500000 3187 ns/op 2977 B/op 36 allocs/op +BenchmarkPrimitivesEncodeStructAllPrimitivesTypesParallel-8 1000000 1106 ns/op 2977 B/op 36 allocs/op +BenchmarkComplexArrayDecodeStructAllTypes-8 100000 13748 ns/op 2248 B/op 121 allocs/op +BenchmarkComplexArrayDecodeStructAllTypesParallel-8 500000 4313 ns/op 2249 B/op 121 allocs/op +BenchmarkComplexArrayEncodeStructAllTypes-8 200000 10758 ns/op 7113 B/op 104 allocs/op +BenchmarkComplexArrayEncodeStructAllTypesParallel-8 500000 3532 ns/op 7113 B/op 104 allocs/op +BenchmarkComplexMapDecodeStructAllTypes-8 100000 17644 ns/op 5305 B/op 130 allocs/op +BenchmarkComplexMapDecodeStructAllTypesParallel-8 300000 5470 ns/op 5308 B/op 130 allocs/op +BenchmarkComplexMapEncodeStructAllTypes-8 200000 11155 ns/op 6971 B/op 129 allocs/op +BenchmarkComplexMapEncodeStructAllTypesParallel-8 500000 3768 ns/op 6971 B/op 129 allocs/op +BenchmarkDecodeNestedStruct-8 500000 2462 ns/op 384 B/op 14 allocs/op +BenchmarkDecodeNestedStructParallel-8 2000000 814 ns/op 384 B/op 14 allocs/op +BenchmarkEncodeNestedStruct-8 1000000 1483 ns/op 693 B/op 16 allocs/op +BenchmarkEncodeNestedStructParallel-8 3000000 525 ns/op 693 B/op 16 allocs/op +``` + +Competitor benchmarks can be found [here](https://github.com/go-playground/form/blob/master/benchmarks/benchmarks.md) + +Complimentary Software +---------------------- + +Here is a list of software that compliments using this library post decoding. + +* [Validator](https://github.com/go-playground/validator) - Go Struct and Field validation, including Cross Field, Cross Struct, Map, Slice and Array diving. +* [mold](https://github.com/go-playground/mold) - Is a general library to help modify or set data within data structures and other objects. + +Package Versioning +---------- +I'm jumping on the vendoring bandwagon, you should vendor this package as I will not +be creating different version with gopkg.in like allot of my other libraries. + +Why? because my time is spread pretty thin maintaining all of the libraries I have + LIFE, +it is so freeing not to worry about it and will help me keep pouring out bigger and better +things for you the community. + +License +------ +Distributed under MIT License, please see license file in code for more details. diff --git a/vendor/github.com/go-playground/form/v4/cache.go b/vendor/github.com/go-playground/form/v4/cache.go new file mode 100644 index 000000000..47e5fce96 --- /dev/null +++ b/vendor/github.com/go-playground/form/v4/cache.go @@ -0,0 +1,133 @@ +package form + +import ( + "reflect" + "sort" + "strings" + "sync" + "sync/atomic" +) + +type cacheFields []cachedField + +func (s cacheFields) Len() int { + return len(s) +} + +func (s cacheFields) Less(i, j int) bool { + return !s[i].isAnonymous +} + +func (s cacheFields) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +type cachedField struct { + idx int + name string + isAnonymous bool + isOmitEmpty bool +} + +type cachedStruct struct { + fields cacheFields +} + +type structCacheMap struct { + m atomic.Value // map[reflect.Type]*cachedStruct + lock sync.Mutex + tagFn TagNameFunc +} + +// TagNameFunc allows for adding of a custom tag name parser +type TagNameFunc func(field reflect.StructField) string + +func newStructCacheMap() *structCacheMap { + + sc := new(structCacheMap) + sc.m.Store(make(map[reflect.Type]*cachedStruct)) + + return sc +} + +func (s *structCacheMap) Get(key reflect.Type) (value *cachedStruct, ok bool) { + value, ok = s.m.Load().(map[reflect.Type]*cachedStruct)[key] + return +} + +func (s *structCacheMap) Set(key reflect.Type, value *cachedStruct) { + + m := s.m.Load().(map[reflect.Type]*cachedStruct) + + nm := make(map[reflect.Type]*cachedStruct, len(m)+1) + for k, v := range m { + nm[k] = v + } + nm[key] = value + s.m.Store(nm) +} + +func (s *structCacheMap) parseStruct(mode Mode, current reflect.Value, key reflect.Type, tagName string) *cachedStruct { + + s.lock.Lock() + + // could have been multiple trying to access, but once first is done this ensures struct + // isn't parsed again. + cs, ok := s.Get(key) + if ok { + s.lock.Unlock() + return cs + } + + typ := current.Type() + cs = &cachedStruct{fields: make([]cachedField, 0, 4)} // init 4, betting most structs decoding into have at aleast 4 fields. + + numFields := current.NumField() + + var fld reflect.StructField + var name string + var idx int + var isOmitEmpty bool + + for i := 0; i < numFields; i++ { + isOmitEmpty = false + fld = typ.Field(i) + + if fld.PkgPath != blank && !fld.Anonymous { + continue + } + + if s.tagFn != nil { + name = s.tagFn(fld) + } else { + name = fld.Tag.Get(tagName) + } + + if name == ignore { + continue + } + + if mode == ModeExplicit && len(name) == 0 { + continue + } + + // check for omitempty + if idx = strings.LastIndexByte(name, ','); idx != -1 { + isOmitEmpty = name[idx+1:] == "omitempty" + name = name[:idx] + } + + if len(name) == 0 { + name = fld.Name + } + + cs.fields = append(cs.fields, cachedField{idx: i, name: name, isAnonymous: fld.Anonymous, isOmitEmpty: isOmitEmpty}) + } + + sort.Sort(cs.fields) + s.Set(typ, cs) + + s.lock.Unlock() + + return cs +} diff --git a/vendor/github.com/go-playground/form/v4/decoder.go b/vendor/github.com/go-playground/form/v4/decoder.go new file mode 100644 index 000000000..e71c97794 --- /dev/null +++ b/vendor/github.com/go-playground/form/v4/decoder.go @@ -0,0 +1,748 @@ +package form + +import ( + "fmt" + "log" + "net/url" + "reflect" + "strconv" + "time" +) + +const ( + errArraySize = "Array size of '%d' is larger than the maximum currently set on the decoder of '%d'. To increase this limit please see, SetMaxArraySize(size uint)" + errMissingStartBracket = "Invalid formatting for key '%s' missing '[' bracket" + errMissingEndBracket = "Invalid formatting for key '%s' missing ']' bracket" +) + +type decoder struct { + d *Decoder + errs DecodeErrors + dm dataMap + values url.Values + maxKeyLen int + namespace []byte +} + +func (d *decoder) setError(namespace []byte, err error) { + if d.errs == nil { + d.errs = make(DecodeErrors) + } + d.errs[string(namespace)] = err +} + +func (d *decoder) findAlias(ns string) *recursiveData { + for i := 0; i < len(d.dm); i++ { + if d.dm[i].alias == ns { + return d.dm[i] + } + } + return nil +} + +func (d *decoder) parseMapData() { + // already parsed + if len(d.dm) > 0 { + return + } + + d.maxKeyLen = 0 + d.dm = d.dm[0:0] + + var i int + var idx int + var l int + var insideBracket bool + var rd *recursiveData + var isNum bool + + for k := range d.values { + + if len(k) > d.maxKeyLen { + d.maxKeyLen = len(k) + } + + for i = 0; i < len(k); i++ { + + switch k[i] { + case '[': + idx = i + insideBracket = true + isNum = true + case ']': + + if !insideBracket { + log.Panicf(errMissingStartBracket, k) + } + + if rd = d.findAlias(k[:idx]); rd == nil { + + l = len(d.dm) + 1 + + if l > cap(d.dm) { + dm := make(dataMap, l) + copy(dm, d.dm) + rd = new(recursiveData) + dm[len(d.dm)] = rd + d.dm = dm + } else { + l = len(d.dm) + d.dm = d.dm[:l+1] + rd = d.dm[l] + rd.sliceLen = 0 + rd.keys = rd.keys[0:0] + } + + rd.alias = k[:idx] + } + + // is map + key + ke := key{ + ivalue: -1, + value: k[idx+1 : i], + searchValue: k[idx : i+1], + } + + // is key is number, most likely array key, keep track of just in case an array/slice. + if isNum { + + // no need to check for error, it will always pass + // as we have done the checking to ensure + // the value is a number ahead of time. + ke.ivalue, _ = strconv.Atoi(ke.value) + + if ke.ivalue > rd.sliceLen { + rd.sliceLen = ke.ivalue + + } + } + + rd.keys = append(rd.keys, ke) + + insideBracket = false + default: + // checking if not a number, 0-9 is 48-57 in byte, see for yourself fmt.Println('0', '1', '2', '3', '4', '5', '6', '7', '8', '9') + if insideBracket && (k[i] > 57 || k[i] < 48) { + isNum = false + } + } + } + + // if still inside bracket, that means no ending bracket was ever specified + if insideBracket { + log.Panicf(errMissingEndBracket, k) + } + } +} + +func (d *decoder) traverseStruct(v reflect.Value, typ reflect.Type, namespace []byte) (set bool) { + + l := len(namespace) + first := l == 0 + + // anonymous structs will still work for caching as the whole definition is stored + // including tags + s, ok := d.d.structCache.Get(typ) + if !ok { + s = d.d.structCache.parseStruct(d.d.mode, v, typ, d.d.tagName) + } + + for _, f := range s.fields { + namespace = namespace[:l] + + if f.isAnonymous { + if d.setFieldByType(v.Field(f.idx), namespace, 0) { + set = true + } + } + + if first { + namespace = append(namespace, f.name...) + } else { + namespace = append(namespace, d.d.namespacePrefix...) + namespace = append(namespace, f.name...) + namespace = append(namespace, d.d.namespaceSuffix...) + } + + if d.setFieldByType(v.Field(f.idx), namespace, 0) { + set = true + } + } + + return +} + +func (d *decoder) setFieldByType(current reflect.Value, namespace []byte, idx int) (set bool) { + + var err error + v, kind := ExtractType(current) + + arr, ok := d.values[string(namespace)] + + if d.d.customTypeFuncs != nil { + + if ok { + if cf, ok := d.d.customTypeFuncs[v.Type()]; ok { + val, err := cf(arr[idx:]) + if err != nil { + d.setError(namespace, err) + return + } + + v.Set(reflect.ValueOf(val)) + set = true + return + } + } + } + switch kind { + case reflect.Interface: + if !ok || idx == len(arr) { + return + } + v.Set(reflect.ValueOf(arr[idx])) + set = true + + case reflect.Ptr: + newVal := reflect.New(v.Type().Elem()) + if set = d.setFieldByType(newVal.Elem(), namespace, idx); set { + v.Set(newVal) + } + + case reflect.String: + if !ok || idx == len(arr) { + return + } + v.SetString(arr[idx]) + set = true + + case reflect.Uint, reflect.Uint64: + if !ok || idx == len(arr) || len(arr[idx]) == 0 { + return + } + var u64 uint64 + if u64, err = strconv.ParseUint(arr[idx], 10, 64); err != nil { + d.setError(namespace, fmt.Errorf("Invalid Unsigned Integer Value '%s' Type '%v' Namespace '%s'", arr[idx], v.Type(), string(namespace))) + return + } + v.SetUint(u64) + set = true + + case reflect.Uint8: + if !ok || idx == len(arr) || len(arr[idx]) == 0 { + return + } + var u64 uint64 + if u64, err = strconv.ParseUint(arr[idx], 10, 8); err != nil { + d.setError(namespace, fmt.Errorf("Invalid Unsigned Integer Value '%s' Type '%v' Namespace '%s'", arr[idx], v.Type(), string(namespace))) + return + } + v.SetUint(u64) + set = true + + case reflect.Uint16: + if !ok || idx == len(arr) || len(arr[idx]) == 0 { + return + } + var u64 uint64 + if u64, err = strconv.ParseUint(arr[idx], 10, 16); err != nil { + d.setError(namespace, fmt.Errorf("Invalid Unsigned Integer Value '%s' Type '%v' Namespace '%s'", arr[idx], v.Type(), string(namespace))) + return + } + v.SetUint(u64) + set = true + + case reflect.Uint32: + if !ok || idx == len(arr) || len(arr[idx]) == 0 { + return + } + var u64 uint64 + if u64, err = strconv.ParseUint(arr[idx], 10, 32); err != nil { + d.setError(namespace, fmt.Errorf("Invalid Unsigned Integer Value '%s' Type '%v' Namespace '%s'", arr[idx], v.Type(), string(namespace))) + return + } + v.SetUint(u64) + set = true + + case reflect.Int, reflect.Int64: + if !ok || idx == len(arr) || len(arr[idx]) == 0 { + return + } + var i64 int64 + if i64, err = strconv.ParseInt(arr[idx], 10, 64); err != nil { + d.setError(namespace, fmt.Errorf("Invalid Integer Value '%s' Type '%v' Namespace '%s'", arr[idx], v.Type(), string(namespace))) + return + } + v.SetInt(i64) + set = true + + case reflect.Int8: + if !ok || idx == len(arr) || len(arr[idx]) == 0 { + return + } + var i64 int64 + if i64, err = strconv.ParseInt(arr[idx], 10, 8); err != nil { + d.setError(namespace, fmt.Errorf("Invalid Integer Value '%s' Type '%v' Namespace '%s'", arr[idx], v.Type(), string(namespace))) + return + } + v.SetInt(i64) + set = true + + case reflect.Int16: + if !ok || idx == len(arr) || len(arr[idx]) == 0 { + return + } + var i64 int64 + if i64, err = strconv.ParseInt(arr[idx], 10, 16); err != nil { + d.setError(namespace, fmt.Errorf("Invalid Integer Value '%s' Type '%v' Namespace '%s'", arr[idx], v.Type(), string(namespace))) + return + } + v.SetInt(i64) + set = true + + case reflect.Int32: + if !ok || idx == len(arr) || len(arr[idx]) == 0 { + return + } + var i64 int64 + if i64, err = strconv.ParseInt(arr[idx], 10, 32); err != nil { + d.setError(namespace, fmt.Errorf("Invalid Integer Value '%s' Type '%v' Namespace '%s'", arr[idx], v.Type(), string(namespace))) + return + } + v.SetInt(i64) + set = true + + case reflect.Float32: + if !ok || idx == len(arr) || len(arr[idx]) == 0 { + return + } + var f float64 + if f, err = strconv.ParseFloat(arr[idx], 32); err != nil { + d.setError(namespace, fmt.Errorf("Invalid Float Value '%s' Type '%v' Namespace '%s'", arr[idx], v.Type(), string(namespace))) + return + } + v.SetFloat(f) + set = true + + case reflect.Float64: + if !ok || idx == len(arr) || len(arr[idx]) == 0 { + return + } + var f float64 + if f, err = strconv.ParseFloat(arr[idx], 64); err != nil { + d.setError(namespace, fmt.Errorf("Invalid Float Value '%s' Type '%v' Namespace '%s'", arr[idx], v.Type(), string(namespace))) + return + } + v.SetFloat(f) + set = true + + case reflect.Bool: + if !ok || idx == len(arr) { + return + } + var b bool + if b, err = parseBool(arr[idx]); err != nil { + d.setError(namespace, fmt.Errorf("Invalid Boolean Value '%s' Type '%v' Namespace '%s'", arr[idx], v.Type(), string(namespace))) + return + } + v.SetBool(b) + set = true + + case reflect.Slice: + d.parseMapData() + // slice elements could be mixed eg. number and non-numbers Value[0]=[]string{"10"} and Value=[]string{"10","20"} + + if ok && len(arr) > 0 { + var varr reflect.Value + + var ol int + l := len(arr) + + if v.IsNil() { + varr = reflect.MakeSlice(v.Type(), len(arr), len(arr)) + } else { + + ol = v.Len() + l += ol + + if v.Cap() <= l { + varr = reflect.MakeSlice(v.Type(), l, l) + } else { + // preserve predefined capacity, possibly for reuse after decoding + varr = reflect.MakeSlice(v.Type(), l, v.Cap()) + } + reflect.Copy(varr, v) + } + + for i := ol; i < l; i++ { + newVal := reflect.New(v.Type().Elem()).Elem() + + if d.setFieldByType(newVal, namespace, i-ol) { + set = true + varr.Index(i).Set(newVal) + } + } + + v.Set(varr) + } + + // maybe it's an numbered array i.e. Phone[0].Number + if rd := d.findAlias(string(namespace)); rd != nil { + + var varr reflect.Value + var kv key + + sl := rd.sliceLen + 1 + + // checking below for maxArraySize, but if array exists and already + // has sufficient capacity allocated then we do not check as the code + // obviously allows a capacity greater than the maxArraySize. + + if v.IsNil() { + + if sl > d.d.maxArraySize { + d.setError(namespace, fmt.Errorf(errArraySize, sl, d.d.maxArraySize)) + return + } + + varr = reflect.MakeSlice(v.Type(), sl, sl) + + } else if v.Len() < sl { + + if v.Cap() <= sl { + + if sl > d.d.maxArraySize { + d.setError(namespace, fmt.Errorf(errArraySize, sl, d.d.maxArraySize)) + return + } + + varr = reflect.MakeSlice(v.Type(), sl, sl) + } else { + varr = reflect.MakeSlice(v.Type(), sl, v.Cap()) + } + + reflect.Copy(varr, v) + + } else { + varr = v + } + + for i := 0; i < len(rd.keys); i++ { + + kv = rd.keys[i] + newVal := reflect.New(varr.Type().Elem()).Elem() + + if kv.ivalue == -1 { + d.setError(namespace, fmt.Errorf("invalid slice index '%s'", kv.value)) + continue + } + + if d.setFieldByType(newVal, append(namespace, kv.searchValue...), 0) { + set = true + varr.Index(kv.ivalue).Set(newVal) + } + } + + if !set { + return + } + + v.Set(varr) + } + + case reflect.Array: + d.parseMapData() + + // array elements could be mixed eg. number and non-numbers Value[0]=[]string{"10"} and Value=[]string{"10","20"} + + if ok && len(arr) > 0 { + var varr reflect.Value + l := len(arr) + overCapacity := v.Len() < l + if overCapacity { + // more values than array capacity, ignore values over capacity as it's possible some would just want + // to grab the first x number of elements; in the future strict mode logic should return an error + fmt.Println("warning number of post form array values is larger than array capacity, ignoring overflow values") + } + varr = reflect.Indirect(reflect.New(reflect.ArrayOf(v.Len(), v.Type().Elem()))) + reflect.Copy(varr, v) + + if v.Len() < len(arr) { + l = v.Len() + } + for i := 0; i < l; i++ { + newVal := reflect.New(v.Type().Elem()).Elem() + + if d.setFieldByType(newVal, namespace, i) { + set = true + varr.Index(i).Set(newVal) + } + } + v.Set(varr) + } + + // maybe it's an numbered array i.e. Phone[0].Number + if rd := d.findAlias(string(namespace)); rd != nil { + var varr reflect.Value + var kv key + + overCapacity := rd.sliceLen >= v.Len() + if overCapacity { + // more values than array capacity, ignore values over capacity as it's possible some would just want + // to grab the first x number of elements; in the future strict mode logic should return an error + fmt.Println("warning number of post form array values is larger than array capacity, ignoring overflow values") + } + varr = reflect.Indirect(reflect.New(reflect.ArrayOf(v.Len(), v.Type().Elem()))) + reflect.Copy(varr, v) + + for i := 0; i < len(rd.keys); i++ { + kv = rd.keys[i] + if kv.ivalue >= v.Len() { + continue + } + newVal := reflect.New(varr.Type().Elem()).Elem() + + if kv.ivalue == -1 { + d.setError(namespace, fmt.Errorf("invalid array index '%s'", kv.value)) + continue + } + + if d.setFieldByType(newVal, append(namespace, kv.searchValue...), 0) { + set = true + varr.Index(kv.ivalue).Set(newVal) + } + } + + if !set { + return + } + v.Set(varr) + } + + case reflect.Map: + var rd *recursiveData + + d.parseMapData() + + // no natural map support so skip directly to dm lookup + if rd = d.findAlias(string(namespace)); rd == nil { + return + } + + var existing bool + var kv key + var mp reflect.Value + var mk reflect.Value + + typ := v.Type() + + if v.IsNil() { + mp = reflect.MakeMap(typ) + } else { + existing = true + mp = v + } + + for i := 0; i < len(rd.keys); i++ { + newVal := reflect.New(typ.Elem()).Elem() + mk = reflect.New(typ.Key()).Elem() + kv = rd.keys[i] + + if err := d.getMapKey(kv.value, mk, namespace); err != nil { + d.setError(namespace, err) + continue + } + + if d.setFieldByType(newVal, append(namespace, kv.searchValue...), 0) { + set = true + mp.SetMapIndex(mk, newVal) + } + } + + if !set || existing { + return + } + + v.Set(mp) + + case reflect.Struct: + typ := v.Type() + + // if we get here then no custom time function declared so use RFC3339 by default + if typ == timeType { + + if !ok || len(arr[idx]) == 0 { + return + } + + t, err := time.Parse(time.RFC3339, arr[idx]) + if err != nil { + d.setError(namespace, err) + } + + v.Set(reflect.ValueOf(t)) + set = true + return + } + + d.parseMapData() + + // we must be recursing infinitly...but that's ok we caught it on the very first overun. + if len(namespace) > d.maxKeyLen { + return + } + + set = d.traverseStruct(v, typ, namespace) + } + return +} + +func (d *decoder) getMapKey(key string, current reflect.Value, namespace []byte) (err error) { + + v, kind := ExtractType(current) + + if d.d.customTypeFuncs != nil { + if cf, ok := d.d.customTypeFuncs[v.Type()]; ok { + + val, er := cf([]string{key}) + if er != nil { + err = er + return + } + + v.Set(reflect.ValueOf(val)) + return + } + } + + switch kind { + case reflect.Interface: + // If interface would have been set on the struct before decoding, + // say to a struct value we would not get here but kind would be struct. + v.Set(reflect.ValueOf(key)) + return + case reflect.Ptr: + newVal := reflect.New(v.Type().Elem()) + if err = d.getMapKey(key, newVal.Elem(), namespace); err == nil { + v.Set(newVal) + } + + case reflect.String: + v.SetString(key) + + case reflect.Uint, reflect.Uint64: + + u64, e := strconv.ParseUint(key, 10, 64) + if e != nil { + err = fmt.Errorf("Invalid Unsigned Integer Value '%s' Type '%v' Namespace '%s'", key, v.Type(), string(namespace)) + return + } + + v.SetUint(u64) + + case reflect.Uint8: + + u64, e := strconv.ParseUint(key, 10, 8) + if e != nil { + err = fmt.Errorf("Invalid Unsigned Integer Value '%s' Type '%v' Namespace '%s'", key, v.Type(), string(namespace)) + return + } + + v.SetUint(u64) + + case reflect.Uint16: + + u64, e := strconv.ParseUint(key, 10, 16) + if e != nil { + err = fmt.Errorf("Invalid Unsigned Integer Value '%s' Type '%v' Namespace '%s'", key, v.Type(), string(namespace)) + return + } + + v.SetUint(u64) + + case reflect.Uint32: + + u64, e := strconv.ParseUint(key, 10, 32) + if e != nil { + err = fmt.Errorf("Invalid Unsigned Integer Value '%s' Type '%v' Namespace '%s'", key, v.Type(), string(namespace)) + return + } + + v.SetUint(u64) + + case reflect.Int, reflect.Int64: + + i64, e := strconv.ParseInt(key, 10, 64) + if e != nil { + err = fmt.Errorf("Invalid Integer Value '%s' Type '%v' Namespace '%s'", key, v.Type(), string(namespace)) + return + } + + v.SetInt(i64) + + case reflect.Int8: + + i64, e := strconv.ParseInt(key, 10, 8) + if e != nil { + err = fmt.Errorf("Invalid Integer Value '%s' Type '%v' Namespace '%s'", key, v.Type(), string(namespace)) + return + } + + v.SetInt(i64) + + case reflect.Int16: + + i64, e := strconv.ParseInt(key, 10, 16) + if e != nil { + err = fmt.Errorf("Invalid Integer Value '%s' Type '%v' Namespace '%s'", key, v.Type(), string(namespace)) + return + } + + v.SetInt(i64) + + case reflect.Int32: + + i64, e := strconv.ParseInt(key, 10, 32) + if e != nil { + err = fmt.Errorf("Invalid Integer Value '%s' Type '%v' Namespace '%s'", key, v.Type(), string(namespace)) + return + } + + v.SetInt(i64) + + case reflect.Float32: + + f, e := strconv.ParseFloat(key, 32) + if e != nil { + err = fmt.Errorf("Invalid Float Value '%s' Type '%v' Namespace '%s'", key, v.Type(), string(namespace)) + return + } + + v.SetFloat(f) + + case reflect.Float64: + + f, e := strconv.ParseFloat(key, 64) + if e != nil { + err = fmt.Errorf("Invalid Float Value '%s' Type '%v' Namespace '%s'", key, v.Type(), string(namespace)) + return + } + + v.SetFloat(f) + + case reflect.Bool: + + b, e := parseBool(key) + if e != nil { + err = fmt.Errorf("Invalid Boolean Value '%s' Type '%v' Namespace '%s'", key, v.Type(), string(namespace)) + return + } + + v.SetBool(b) + + default: + err = fmt.Errorf("Unsupported Map Key '%s', Type '%v' Namespace '%s'", key, v.Type(), string(namespace)) + } + + return +} diff --git a/vendor/github.com/go-playground/form/v4/doc.go b/vendor/github.com/go-playground/form/v4/doc.go new file mode 100644 index 000000000..f553dac2c --- /dev/null +++ b/vendor/github.com/go-playground/form/v4/doc.go @@ -0,0 +1,275 @@ +/* +Package form Decodes url.Values into Go value(s) and Encodes Go value(s) into url.Values. + + +It has the following features: + + - Primitives types cause zero allocations. + - Supports map of almost all types. + - Supports both Numbered and Normal arrays eg. "Array[0]" and just "Array" + with multiple values passed. + - Slice honours the specified index. eg. if "Slice[2]" is the only Slice + value passed down, it will be put at index 2; if slice isn't big enough + it will be expanded. + - Array honours the specified index. eg. if "Array[2]" is the only Array + value passed down, it will be put at index 2; if array isn't big enough + a warning will be printed and value ignored. + - Only creates objects as necessary eg. if no `array` or `map` values are + passed down, the `array` and `map` are left as their default values in + the struct. + - Allows for Custom Type registration. + - Handles time.Time using RFC3339 time format by default, + but can easily be changed by registering a Custom Type, see below. + - Handles Encoding & Decoding of almost all Go types eg. can Decode into + struct, array, map, int... and Encode a struct, array, map, int... + +Common Questions + +Questions + + Does it support encoding.TextUnmarshaler? + No because TextUnmarshaler only accepts []byte but posted values can have + multiple values, so is not suitable. + + Mixing array/slice with array[idx]/slice[idx], in which order are they parsed? + array/slice then array[idx]/slice[idx] + +Supported Types + +out of the box supported types + + - string + - bool + - int, int8, int16, int32, int64 + - uint, uint8, uint16, uint32, uint64 + - float32, float64 + - struct and anonymous struct + - interface{} + - time.Time` - by default using RFC3339 + - a `pointer` to one of the above types + - slice, array + - map + - `custom types` can override any of the above types + - many other types may be supported inherently (eg. bson.ObjectId is + type ObjectId string, which will get populated by the string type + + **NOTE**: map, struct and slice nesting are ad infinitum. + +Usage + +symbols + + - Use symbol `.` for separating fields/structs. (eg. `structfield.field`) + - Use `[index or key]` for access to index of a slice/array or key for map. + (eg. `arrayfield[0]`, `mapfield[keyvalue]`) + +html + +
+ + + + + + + + + + + + +
+ +Example + +example decoding the above HTML + + package main + + import ( + "fmt" + "log" + "net/url" + + "github.com/go-playground/form/v4" + ) + + // Address contains address information + type Address struct { + Name string + Phone string + } + + // User contains user information + type User struct { + Name string + Age uint8 + Gender string + Address []Address + Active bool `form:"active"` + MapExample map[string]string + NestedMap map[string]map[string]string + NestedArray [][]string + } + + // use a single instance of Decoder, it caches struct info + var decoder *form.Decoder + + func main() { + decoder = form.NewDecoder() + + // this simulates the results of http.Request's ParseForm() function + values := parseForm() + + var user User + + // must pass a pointer + err := decoder.Decode(&user, values) + if err != nil { + log.Panic(err) + } + + fmt.Printf("%#v\n", user) + } + + // this simulates the results of http.Request's ParseForm() function + func parseForm() url.Values { + return url.Values{ + "Name": []string{"joeybloggs"}, + "Age": []string{"3"}, + "Gender": []string{"Male"}, + "Address[0].Name": []string{"26 Here Blvd."}, + "Address[0].Phone": []string{"9(999)999-9999"}, + "Address[1].Name": []string{"26 There Blvd."}, + "Address[1].Phone": []string{"1(111)111-1111"}, + "active": []string{"true"}, + "MapExample[key]": []string{"value"}, + "NestedMap[key][key]": []string{"value"}, + "NestedArray[0][0]": []string{"value"}, + } + } + +example encoding + + package main + + import ( + "fmt" + "log" + + "github.com/go-playground/form/v4" + ) + + // Address contains address information + type Address struct { + Name string + Phone string + } + + // User contains user information + type User struct { + Name string + Age uint8 + Gender string + Address []Address + Active bool `form:"active"` + MapExample map[string]string + NestedMap map[string]map[string]string + NestedArray [][]string + } + + // use a single instance of Encoder, it caches struct info + var encoder *form.Encoder + + func main() { + encoder = form.NewEncoder() + + user := User{ + Name: "joeybloggs", + Age: 3, + Gender: "Male", + Address: []Address{ + {Name: "26 Here Blvd.", Phone: "9(999)999-9999"}, + {Name: "26 There Blvd.", Phone: "1(111)111-1111"}, + }, + Active: true, + MapExample: map[string]string{"key": "value"}, + NestedMap: map[string]map[string]string{"key": {"key": "value"}}, + NestedArray: [][]string{{"value"}}, + } + + // must pass a pointer + values, err := encoder.Encode(&user) + if err != nil { + log.Panic(err) + } + + fmt.Printf("%#v\n", values) + } + + +Registering Custom Types + +Decoder + + decoder.RegisterCustomTypeFunc(func(vals []string) (interface{}, error) { + return time.Parse("2006-01-02", vals[0]) + }, time.Time{}) + + ADDITIONAL: if a struct type is registered, the function will only be called + if a url.Value exists for the struct and not just the struct fields + eg. url.Values{"User":"Name%3Djoeybloggs"} will call the custom type function + with 'User' as the type, however url.Values{"User.Name":"joeybloggs"} will not. + +Encoder + + encoder.RegisterCustomTypeFunc(func(x interface{}) ([]string, error) { + return []string{x.(time.Time).Format("2006-01-02")}, nil + }, time.Time{}) + + +Ignoring Fields + +you can tell form to ignore fields using `-` in the tag + + type MyStruct struct { + Field string `form:"-"` + } + +Omitempty + +you can tell form to omit empty fields using `,omitempty` or `FieldName,omitempty` in the tag + + type MyStruct struct { + Field string `form:",omitempty"` + Field2 string `form:"CustomFieldName,omitempty"` + } + + +Notes + +To maximize compatibility with other systems the Encoder attempts +to avoid using array indexes in url.Values if at all possible. + + eg. + + // A struct field of + Field []string{"1", "2", "3"} + + // will be output a url.Value as + "Field": []string{"1", "2", "3"} + + and not + "Field[0]": []string{"1"} + "Field[1]": []string{"2"} + "Field[2]": []string{"3"} + + // however there are times where it is unavoidable, like with pointers + i := int(1) + Field []*string{nil, nil, &i} + + // to avoid index 1 and 2 must use index + "Field[2]": []string{"1"} + +*/ +package form diff --git a/vendor/github.com/go-playground/form/v4/encoder.go b/vendor/github.com/go-playground/form/v4/encoder.go new file mode 100644 index 000000000..eea0bd9f3 --- /dev/null +++ b/vendor/github.com/go-playground/form/v4/encoder.go @@ -0,0 +1,261 @@ +package form + +import ( + "fmt" + "net/url" + "reflect" + "strconv" + "time" +) + +type encoder struct { + e *Encoder + errs EncodeErrors + values url.Values + namespace []byte +} + +func (e *encoder) setError(namespace []byte, err error) { + if e.errs == nil { + e.errs = make(EncodeErrors) + } + + e.errs[string(namespace)] = err +} + +func (e *encoder) setVal(namespace []byte, idx int, vals ...string) { + + arr, ok := e.values[string(namespace)] + if ok { + arr = append(arr, vals...) + } else { + arr = vals + } + + e.values[string(namespace)] = arr +} + +func (e *encoder) traverseStruct(v reflect.Value, namespace []byte, idx int) { + + typ := v.Type() + l := len(namespace) + first := l == 0 + + // anonymous structs will still work for caching as the whole definition is stored + // including tags + s, ok := e.e.structCache.Get(typ) + if !ok { + s = e.e.structCache.parseStruct(e.e.mode, v, typ, e.e.tagName) + } + + for _, f := range s.fields { + namespace = namespace[:l] + + if f.isAnonymous && e.e.embedAnonymous { + e.setFieldByType(v.Field(f.idx), namespace, idx, f.isOmitEmpty) + continue + } + + if first { + namespace = append(namespace, f.name...) + } else { + namespace = append(namespace, e.e.namespacePrefix...) + namespace = append(namespace, f.name...) + namespace = append(namespace, e.e.namespaceSuffix...) + } + + e.setFieldByType(v.Field(f.idx), namespace, idx, f.isOmitEmpty) + } +} + +func (e *encoder) setFieldByType(current reflect.Value, namespace []byte, idx int, isOmitEmpty bool) { + + if idx > -1 && current.Kind() == reflect.Ptr { + namespace = append(namespace, '[') + namespace = strconv.AppendInt(namespace, int64(idx), 10) + namespace = append(namespace, ']') + idx = -2 + } + + if isOmitEmpty && !hasValue(current) { + return + } + v, kind := ExtractType(current) + + if e.e.customTypeFuncs != nil { + + if cf, ok := e.e.customTypeFuncs[v.Type()]; ok { + + arr, err := cf(v.Interface()) + if err != nil { + e.setError(namespace, err) + return + } + + if idx > -1 { + namespace = append(namespace, '[') + namespace = strconv.AppendInt(namespace, int64(idx), 10) + namespace = append(namespace, ']') + } + + e.setVal(namespace, idx, arr...) + return + } + } + + switch kind { + case reflect.Ptr, reflect.Interface, reflect.Invalid: + return + + case reflect.String: + + e.setVal(namespace, idx, v.String()) + + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + + e.setVal(namespace, idx, strconv.FormatUint(v.Uint(), 10)) + + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + + e.setVal(namespace, idx, strconv.FormatInt(v.Int(), 10)) + + case reflect.Float32: + + e.setVal(namespace, idx, strconv.FormatFloat(v.Float(), 'f', -1, 32)) + + case reflect.Float64: + + e.setVal(namespace, idx, strconv.FormatFloat(v.Float(), 'f', -1, 64)) + + case reflect.Bool: + + e.setVal(namespace, idx, strconv.FormatBool(v.Bool())) + + case reflect.Slice, reflect.Array: + + if idx == -1 { + + for i := 0; i < v.Len(); i++ { + e.setFieldByType(v.Index(i), namespace, i, false) + } + + return + } + + if idx > -1 { + namespace = append(namespace, '[') + namespace = strconv.AppendInt(namespace, int64(idx), 10) + namespace = append(namespace, ']') + } + + namespace = append(namespace, '[') + l := len(namespace) + + for i := 0; i < v.Len(); i++ { + namespace = namespace[:l] + namespace = strconv.AppendInt(namespace, int64(i), 10) + namespace = append(namespace, ']') + e.setFieldByType(v.Index(i), namespace, -2, false) + } + + case reflect.Map: + + if idx > -1 { + namespace = append(namespace, '[') + namespace = strconv.AppendInt(namespace, int64(idx), 10) + namespace = append(namespace, ']') + } + + var valid bool + var s string + l := len(namespace) + + for _, key := range v.MapKeys() { + + namespace = namespace[:l] + + if s, valid = e.getMapKey(key, namespace); !valid { + continue + } + + namespace = append(namespace, '[') + namespace = append(namespace, s...) + namespace = append(namespace, ']') + + e.setFieldByType(v.MapIndex(key), namespace, -2, false) + } + + case reflect.Struct: + + // if we get here then no custom time function declared so use RFC3339 by default + if v.Type() == timeType { + + if idx > -1 { + namespace = append(namespace, '[') + namespace = strconv.AppendInt(namespace, int64(idx), 10) + namespace = append(namespace, ']') + } + + e.setVal(namespace, idx, v.Interface().(time.Time).Format(time.RFC3339)) + return + } + + if idx == -1 { + e.traverseStruct(v, namespace, idx) + return + } + + if idx > -1 { + namespace = append(namespace, '[') + namespace = strconv.AppendInt(namespace, int64(idx), 10) + namespace = append(namespace, ']') + } + + e.traverseStruct(v, namespace, -2) + } +} + +func (e *encoder) getMapKey(key reflect.Value, namespace []byte) (string, bool) { + + v, kind := ExtractType(key) + + if e.e.customTypeFuncs != nil { + + if cf, ok := e.e.customTypeFuncs[v.Type()]; ok { + arr, err := cf(v.Interface()) + if err != nil { + e.setError(namespace, err) + return "", false + } + + return arr[0], true + } + } + + switch kind { + case reflect.Interface, reflect.Ptr: + return "", false + + case reflect.String: + return v.String(), true + + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return strconv.FormatUint(v.Uint(), 10), true + + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return strconv.FormatInt(v.Int(), 10), true + + case reflect.Float32: + return strconv.FormatFloat(v.Float(), 'f', -1, 32), true + + case reflect.Float64: + return strconv.FormatFloat(v.Float(), 'f', -1, 64), true + + case reflect.Bool: + return strconv.FormatBool(v.Bool()), true + + default: + e.setError(namespace, fmt.Errorf("Unsupported Map Key '%v' Namespace '%s'", v.String(), namespace)) + return "", false + } +} diff --git a/vendor/github.com/go-playground/form/v4/form.go b/vendor/github.com/go-playground/form/v4/form.go new file mode 100644 index 000000000..b1b4e7dc2 --- /dev/null +++ b/vendor/github.com/go-playground/form/v4/form.go @@ -0,0 +1,49 @@ +package form + +import ( + "reflect" + "time" +) + +const ( + blank = "" + ignore = "-" + fieldNS = "Field Namespace:" + errorText = " ERROR:" +) + +var ( + timeType = reflect.TypeOf(time.Time{}) +) + +// Mode specifies which mode the form decoder is to run +type Mode uint8 + +const ( + + // ModeImplicit tries to parse values for all + // fields that do not have an ignore '-' tag + ModeImplicit Mode = iota + + // ModeExplicit only parses values for field with a field tag + // and that tag is not the ignore '-' tag + ModeExplicit +) + +// AnonymousMode specifies how data should be rolled up +// or separated from anonymous structs +type AnonymousMode uint8 + +const ( + // AnonymousEmbed embeds anonymous data when encoding + // eg. type A struct { Field string } + // type B struct { A, Field string } + // encode results: url.Values{"Field":[]string{"B FieldVal", "A FieldVal"}} + AnonymousEmbed AnonymousMode = iota + + // AnonymousSeparate does not embed anonymous data when encoding + // eg. type A struct { Field string } + // type B struct { A, Field string } + // encode results: url.Values{"Field":[]string{"B FieldVal"}, "A.Field":[]string{"A FieldVal"}} + AnonymousSeparate +) diff --git a/vendor/github.com/go-playground/form/v4/form_decoder.go b/vendor/github.com/go-playground/form/v4/form_decoder.go new file mode 100644 index 000000000..ac131ea8b --- /dev/null +++ b/vendor/github.com/go-playground/form/v4/form_decoder.go @@ -0,0 +1,187 @@ +package form + +import ( + "bytes" + "net/url" + "reflect" + "strings" + "sync" +) + +// DecodeCustomTypeFunc allows for registering/overriding types to be parsed. +type DecodeCustomTypeFunc func([]string) (interface{}, error) + +// DecodeErrors is a map of errors encountered during form decoding +type DecodeErrors map[string]error + +func (d DecodeErrors) Error() string { + buff := bytes.NewBufferString(blank) + + for k, err := range d { + buff.WriteString(fieldNS) + buff.WriteString(k) + buff.WriteString(errorText) + buff.WriteString(err.Error()) + buff.WriteString("\n") + } + + return strings.TrimSpace(buff.String()) +} + +// An InvalidDecoderError describes an invalid argument passed to Decode. +// (The argument passed to Decode must be a non-nil pointer.) +type InvalidDecoderError struct { + Type reflect.Type +} + +func (e *InvalidDecoderError) Error() string { + + if e.Type == nil { + return "form: Decode(nil)" + } + + if e.Type.Kind() != reflect.Ptr { + return "form: Decode(non-pointer " + e.Type.String() + ")" + } + + return "form: Decode(nil " + e.Type.String() + ")" +} + +type key struct { + ivalue int + value string + searchValue string +} + +type recursiveData struct { + alias string + sliceLen int + keys []key +} + +type dataMap []*recursiveData + +// Decoder is the main decode instance +type Decoder struct { + tagName string + mode Mode + structCache *structCacheMap + customTypeFuncs map[reflect.Type]DecodeCustomTypeFunc + maxArraySize int + dataPool *sync.Pool + namespacePrefix string + namespaceSuffix string +} + +// NewDecoder creates a new decoder instance with sane defaults +func NewDecoder() *Decoder { + + d := &Decoder{ + tagName: "form", + mode: ModeImplicit, + structCache: newStructCacheMap(), + maxArraySize: 10000, + namespacePrefix: ".", + } + + d.dataPool = &sync.Pool{New: func() interface{} { + return &decoder{ + d: d, + namespace: make([]byte, 0, 64), + } + }} + + return d +} + +// SetTagName sets the given tag name to be used by the decoder. +// Default is "form" +func (d *Decoder) SetTagName(tagName string) { + d.tagName = tagName +} + +// SetMode sets the mode the decoder should run +// Default is ModeImplicit +func (d *Decoder) SetMode(mode Mode) { + d.mode = mode +} + +// SetNamespacePrefix sets a struct namespace prefix. +func (d *Decoder) SetNamespacePrefix(namespacePrefix string) { + d.namespacePrefix = namespacePrefix +} + +// SetNamespaceSuffix sets a struct namespace suffix. +func (d *Decoder) SetNamespaceSuffix(namespaceSuffix string) { + d.namespaceSuffix = namespaceSuffix +} + +// SetMaxArraySize sets maximum array size that can be created. +// This limit is for the array indexing this library supports to +// avoid potential DOS or man-in-the-middle attacks using an unusually +// high number. +// DEFAULT: 10000 +func (d *Decoder) SetMaxArraySize(size uint) { + d.maxArraySize = int(size) +} + +// RegisterTagNameFunc registers a custom tag name parser function +// NOTE: This method is not thread-safe it is intended that these all be registered prior to any parsing +// +// ADDITIONAL: once a custom function has been registered the default, or custom set, tag name is ignored +// and relies 100% on the function for the name data. The return value WILL BE CACHED and so return value +// must be consistent. +func (d *Decoder) RegisterTagNameFunc(fn TagNameFunc) { + d.structCache.tagFn = fn +} + +// RegisterCustomTypeFunc registers a CustomTypeFunc against a number of types. +// NOTE: This method is not thread-safe it is intended that these all be registered prior to any parsing +// +// ADDITIONAL: if a struct type is registered, the function will only be called if a url.Value exists for +// the struct and not just the struct fields eg. url.Values{"User":"Name%3Djoeybloggs"} will call the +// custom type function with `User` as the type, however url.Values{"User.Name":"joeybloggs"} will not. +func (d *Decoder) RegisterCustomTypeFunc(fn DecodeCustomTypeFunc, types ...interface{}) { + + if d.customTypeFuncs == nil { + d.customTypeFuncs = map[reflect.Type]DecodeCustomTypeFunc{} + } + + for _, t := range types { + d.customTypeFuncs[reflect.TypeOf(t)] = fn + } +} + +// Decode parses the given values and sets the corresponding struct and/or type values +// +// Decode returns an InvalidDecoderError if interface passed is invalid. +func (d *Decoder) Decode(v interface{}, values url.Values) (err error) { + + val := reflect.ValueOf(v) + + if val.Kind() != reflect.Ptr || val.IsNil() { + return &InvalidDecoderError{reflect.TypeOf(v)} + } + + dec := d.dataPool.Get().(*decoder) + dec.values = values + dec.dm = dec.dm[0:0] + + val = val.Elem() + typ := val.Type() + + if val.Kind() == reflect.Struct && typ != timeType { + dec.traverseStruct(val, typ, dec.namespace[0:0]) + } else { + dec.setFieldByType(val, dec.namespace[0:0], 0) + } + + if len(dec.errs) > 0 { + err = dec.errs + dec.errs = nil + } + + d.dataPool.Put(dec) + + return +} diff --git a/vendor/github.com/go-playground/form/v4/form_encoder.go b/vendor/github.com/go-playground/form/v4/form_encoder.go new file mode 100644 index 000000000..48960c9a1 --- /dev/null +++ b/vendor/github.com/go-playground/form/v4/form_encoder.go @@ -0,0 +1,157 @@ +package form + +import ( + "bytes" + "net/url" + "reflect" + "strings" + "sync" +) + +// EncodeCustomTypeFunc allows for registering/overriding types to be parsed. +type EncodeCustomTypeFunc func(x interface{}) ([]string, error) + +// EncodeErrors is a map of errors encountered during form encoding +type EncodeErrors map[string]error + +func (e EncodeErrors) Error() string { + buff := bytes.NewBufferString(blank) + + for k, err := range e { + buff.WriteString(fieldNS) + buff.WriteString(k) + buff.WriteString(errorText) + buff.WriteString(err.Error()) + buff.WriteString("\n") + } + + return strings.TrimSpace(buff.String()) +} + +// An InvalidEncodeError describes an invalid argument passed to Encode. +type InvalidEncodeError struct { + Type reflect.Type +} + +func (e *InvalidEncodeError) Error() string { + + if e.Type == nil { + return "form: Encode(nil)" + } + + return "form: Encode(nil " + e.Type.String() + ")" +} + +// Encoder is the main encode instance +type Encoder struct { + tagName string + structCache *structCacheMap + customTypeFuncs map[reflect.Type]EncodeCustomTypeFunc + dataPool *sync.Pool + mode Mode + embedAnonymous bool + namespacePrefix string + namespaceSuffix string +} + +// NewEncoder creates a new encoder instance with sane defaults +func NewEncoder() *Encoder { + + e := &Encoder{ + tagName: "form", + mode: ModeImplicit, + structCache: newStructCacheMap(), + embedAnonymous: true, + namespacePrefix: ".", + } + + e.dataPool = &sync.Pool{New: func() interface{} { + return &encoder{ + e: e, + namespace: make([]byte, 0, 64), + } + }} + + return e +} + +// SetTagName sets the given tag name to be used by the encoder. +// Default is "form" +func (e *Encoder) SetTagName(tagName string) { + e.tagName = tagName +} + +// SetMode sets the mode the encoder should run +// Default is ModeImplicit +func (e *Encoder) SetMode(mode Mode) { + e.mode = mode +} + +// SetNamespacePrefix sets a struct namespace prefix. +func (e *Encoder) SetNamespacePrefix(namespacePrefix string) { + e.namespacePrefix = namespacePrefix +} + +// SetNamespaceSuffix sets a struct namespace suffix. +func (e *Encoder) SetNamespaceSuffix(namespaceSuffix string) { + e.namespaceSuffix = namespaceSuffix +} + +// SetAnonymousMode sets the mode the encoder should run +// Default is AnonymousEmbed +func (e *Encoder) SetAnonymousMode(mode AnonymousMode) { + e.embedAnonymous = mode == AnonymousEmbed +} + +// RegisterTagNameFunc registers a custom tag name parser function +// NOTE: This method is not thread-safe it is intended that these all be registered prior to any parsing +// +// ADDITIONAL: once a custom function has been registered the default, or custom set, tag name is ignored +// and relies 100% on the function for the name data. The return value WILL BE CACHED and so return value +// must be consistent. +func (e *Encoder) RegisterTagNameFunc(fn TagNameFunc) { + e.structCache.tagFn = fn +} + +// RegisterCustomTypeFunc registers a CustomTypeFunc against a number of types +// NOTE: this method is not thread-safe it is intended that these all be registered prior to any parsing +func (e *Encoder) RegisterCustomTypeFunc(fn EncodeCustomTypeFunc, types ...interface{}) { + + if e.customTypeFuncs == nil { + e.customTypeFuncs = map[reflect.Type]EncodeCustomTypeFunc{} + } + + for _, t := range types { + e.customTypeFuncs[reflect.TypeOf(t)] = fn + } +} + +// Encode encodes the given values and sets the corresponding struct values +func (e *Encoder) Encode(v interface{}) (values url.Values, err error) { + + val, kind := ExtractType(reflect.ValueOf(v)) + + if kind == reflect.Ptr || kind == reflect.Interface || kind == reflect.Invalid { + return nil, &InvalidEncodeError{reflect.TypeOf(v)} + } + + enc := e.dataPool.Get().(*encoder) + enc.values = make(url.Values) + + if kind == reflect.Struct && val.Type() != timeType { + enc.traverseStruct(val, enc.namespace[0:0], -1) + } else { + enc.setFieldByType(val, enc.namespace[0:0], -1, false) + } + + if len(enc.errs) > 0 { + err = enc.errs + enc.errs = nil + } + + values = enc.values + + e.dataPool.Put(enc) + + return +} diff --git a/vendor/github.com/go-playground/form/v4/logo.jpg b/vendor/github.com/go-playground/form/v4/logo.jpg new file mode 100644 index 000000000..2ef34f87e Binary files /dev/null and b/vendor/github.com/go-playground/form/v4/logo.jpg differ diff --git a/vendor/github.com/go-playground/form/v4/util.go b/vendor/github.com/go-playground/form/v4/util.go new file mode 100644 index 000000000..02c86af72 --- /dev/null +++ b/vendor/github.com/go-playground/form/v4/util.go @@ -0,0 +1,62 @@ +package form + +import ( + "reflect" + "strconv" +) + +// ExtractType gets the actual underlying type of field value. +// it is exposed for use within you Custom Functions +func ExtractType(current reflect.Value) (reflect.Value, reflect.Kind) { + + switch current.Kind() { + case reflect.Ptr: + + if current.IsNil() { + return current, reflect.Ptr + } + + return ExtractType(current.Elem()) + + case reflect.Interface: + + if current.IsNil() { + return current, reflect.Interface + } + + return ExtractType(current.Elem()) + + default: + return current, current.Kind() + } +} + +func parseBool(str string) (bool, error) { + + switch str { + case "1", "t", "T", "true", "TRUE", "True", "on", "yes", "ok": + return true, nil + case "", "0", "f", "F", "false", "FALSE", "False", "off", "no": + return false, nil + } + + // strconv.NumError mimicing exactly the strconv.ParseBool(..) error and type + // to ensure compatibility with std library and beyond. + return false, &strconv.NumError{Func: "ParseBool", Num: str, Err: strconv.ErrSyntax} +} + +// hasValue determines if a reflect.Value is it's default value +func hasValue(field reflect.Value) bool { + switch field.Kind() { + case reflect.Slice, reflect.Map, reflect.Ptr, reflect.Interface, reflect.Chan, reflect.Func: + return !field.IsNil() + default: + if !field.IsValid() { + return false + } + if !field.Type().Comparable() { + return true + } + return field.Interface() != reflect.Zero(field.Type()).Interface() + } +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 510ca69b4..fc4bfef1c 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -200,6 +200,9 @@ github.com/go-fed/httpsig github.com/go-jose/go-jose/v3 github.com/go-jose/go-jose/v3/cipher github.com/go-jose/go-jose/v3/json +# github.com/go-playground/form/v4 v4.2.0 +## explicit; go 1.13 +github.com/go-playground/form/v4 # github.com/go-playground/locales v0.14.1 ## explicit; go 1.17 github.com/go-playground/locales