[feature] Add support for profile fields (#1483)

* Add go-playground/form pkg

* [feature] Add support for profile fields

* Add field attributes test

* Validate profile fields form

* Add profile field validation tests

* Add Field Attributes definition to swagger

---------

Co-authored-by: tobi <31960611+tsmethurst@users.noreply.github.com>
This commit is contained in:
zowhoey 2023-03-06 09:30:19 +00:00 committed by GitHub
parent 24f6a447f3
commit f518f649f8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 2399 additions and 2 deletions

1
go.mod
View File

@ -26,6 +26,7 @@ require (
github.com/gin-contrib/sessions v0.0.5 github.com/gin-contrib/sessions v0.0.5
github.com/gin-gonic/gin v1.9.0 github.com/gin-gonic/gin v1.9.0
github.com/go-fed/httpsig v1.1.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/go-playground/validator/v10 v10.11.2
github.com/google/uuid v1.3.0 github.com/google/uuid v1.3.0
github.com/gorilla/feeds v1.1.1 github.com/gorilla/feeds v1.1.1

2
go.sum
View File

@ -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-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.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 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.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 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=

View File

@ -25,6 +25,7 @@ import (
"strconv" "strconv"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/go-playground/form/v4"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
"github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtserror"
@ -115,6 +116,13 @@ import (
// in: formData // in: formData
// description: Enable RSS feed for this account's Public posts at `/[username]/feed.rss` // description: Enable RSS feed for this account's Public posts at `/[username]/feed.rss`
// type: boolean // type: boolean
// -
// name: fields_attributes
// in: formData
// description: Profile fields to be added to this account's profile
// type: array
// items:
// type: object
// //
// security: // security:
// - OAuth2 Bearer: // - OAuth2 Bearer:
@ -162,6 +170,28 @@ func (m *Module) AccountUpdateCredentialsPATCHHandler(c *gin.Context) {
c.JSON(http.StatusOK, acctSensitive) 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) { func parseUpdateAccountForm(c *gin.Context) (*apimodel.UpdateCredentialsRequest, error) {
form := &apimodel.UpdateCredentialsRequest{ form := &apimodel.UpdateCredentialsRequest{
Source: &apimodel.UpdateSource{}, 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) 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 // parse source field-by-field
sourceMap := c.PostFormMap("source") sourceMap := c.PostFormMap("source")

View File

@ -38,12 +38,14 @@ type AccountUpdateTestSuite struct {
func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandler() { func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandler() {
// set up the request // 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" newBio := "this is my new bio read it and weep"
requestBody, w, err := testrig.CreateMultipartFormData( requestBody, w, err := testrig.CreateMultipartFormData(
"", "", "", "",
map[string]string{ map[string]string{
"note": newBio, "note": newBio,
"fields_attributes[0][name]": "pronouns",
"fields_attributes[0][value]": "they/them",
}) })
if err != nil { if err != nil {
panic(err) panic(err)
@ -74,6 +76,7 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandler()
// check the returned api model account // check the returned api model account
// fields should be updated // fields should be updated
suite.Equal("<p>this is my new bio read it and weep</p>", apimodelAccount.Note) suite.Equal("<p>this is my new bio read it and weep</p>", apimodelAccount.Note)
suite.Equal("they/them", apimodelAccount.Fields[0].Value)
suite.Equal(newBio, apimodelAccount.Source.Note) suite.Equal(newBio, apimodelAccount.Source.Note)
} }

View File

@ -165,6 +165,26 @@ func (p *Processor) Update(ctx context.Context, account *gtsmodel.Account, form
account.EnableRSS = form.EnableRSS 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) err := p.state.DB.UpdateAccount(ctx, account)
if err != nil { if err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("could not update account %s: %s", account.ID, err)) return nil, gtserror.NewErrorInternalError(fmt.Errorf("could not update account %s: %s", account.ID, err))

View File

@ -43,6 +43,8 @@ const (
maximumUsernameLength = 64 maximumUsernameLength = 64
maximumCustomCSSLength = 5000 maximumCustomCSSLength = 5000
maximumEmojiCategoryLength = 64 maximumEmojiCategoryLength = 64
maximumProfileFieldLength = 255
maximumProfileFields = 4
) )
// NewPassword returns an error if the given password is not sufficiently strong, or nil if it's ok. // 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 { func ULID(i string) bool {
return regexes.ULID.MatchString(i) 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)
}

View File

@ -25,6 +25,7 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
"github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/validate" "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) { func TestValidationTestSuite(t *testing.T) {
suite.Run(t, new(ValidationTestSuite)) suite.Run(t, new(ValidationTestSuite))
} }

28
vendor/github.com/go-playground/form/v4/.gitignore generated vendored Normal file
View File

@ -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

26
vendor/github.com/go-playground/form/v4/.travis.yml generated vendored Normal file
View File

@ -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

21
vendor/github.com/go-playground/form/v4/LICENSE generated vendored Normal file
View File

@ -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.

333
vendor/github.com/go-playground/form/v4/README.md generated vendored Normal file
View File

@ -0,0 +1,333 @@
Package form
============
<img align="right" src="https://raw.githubusercontent.com/go-playground/form/master/logo.jpg">![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
<form method="POST">
<input type="text" name="Name" value="joeybloggs"/>
<input type="text" name="Age" value="3"/>
<input type="text" name="Gender" value="Male"/>
<input type="text" name="Address[0].Name" value="26 Here Blvd."/>
<input type="text" name="Address[0].Phone" value="9(999)999-9999"/>
<input type="text" name="Address[1].Name" value="26 There Blvd."/>
<input type="text" name="Address[1].Phone" value="1(111)111-1111"/>
<input type="text" name="active" value="true"/>
<input type="text" name="MapExample[key]" value="value"/>
<input type="text" name="NestedMap[key][key]" value="value"/>
<input type="text" name="NestedArray[0][0]" value="value"/>
<input type="submit"/>
</form>
```
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.

133
vendor/github.com/go-playground/form/v4/cache.go generated vendored Normal file
View File

@ -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
}

748
vendor/github.com/go-playground/form/v4/decoder.go generated vendored Normal file
View File

@ -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
}

275
vendor/github.com/go-playground/form/v4/doc.go generated vendored Normal file
View File

@ -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
<form method="POST">
<input type="text" name="Name" value="joeybloggs"/>
<input type="text" name="Age" value="3"/>
<input type="text" name="Gender" value="Male"/>
<input type="text" name="Address[0].Name" value="26 Here Blvd."/>
<input type="text" name="Address[0].Phone" value="9(999)999-9999"/>
<input type="text" name="Address[1].Name" value="26 There Blvd."/>
<input type="text" name="Address[1].Phone" value="1(111)111-1111"/>
<input type="text" name="active" value="true"/>
<input type="text" name="MapExample[key]" value="value"/>
<input type="text" name="NestedMap[key][key]" value="value"/>
<input type="text" name="NestedArray[0][0]" value="value"/>
<input type="submit"/>
</form>
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

261
vendor/github.com/go-playground/form/v4/encoder.go generated vendored Normal file
View File

@ -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
}
}

49
vendor/github.com/go-playground/form/v4/form.go generated vendored Normal file
View File

@ -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
)

187
vendor/github.com/go-playground/form/v4/form_decoder.go generated vendored Normal file
View File

@ -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
}

157
vendor/github.com/go-playground/form/v4/form_encoder.go generated vendored Normal file
View File

@ -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
}

BIN
vendor/github.com/go-playground/form/v4/logo.jpg generated vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

62
vendor/github.com/go-playground/form/v4/util.go generated vendored Normal file
View File

@ -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()
}
}

3
vendor/modules.txt vendored
View File

@ -200,6 +200,9 @@ github.com/go-fed/httpsig
github.com/go-jose/go-jose/v3 github.com/go-jose/go-jose/v3
github.com/go-jose/go-jose/v3/cipher github.com/go-jose/go-jose/v3/cipher
github.com/go-jose/go-jose/v3/json 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 # github.com/go-playground/locales v0.14.1
## explicit; go 1.17 ## explicit; go 1.17
github.com/go-playground/locales github.com/go-playground/locales