🧑💻 Improved recursive decoding
This commit is contained in:
parent
f9ed5be2c0
commit
9ef1398b80
|
@ -0,0 +1,40 @@
|
||||||
|
package form
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
UnmarshalTypeError struct {
|
||||||
|
Type reflect.Type
|
||||||
|
Value string
|
||||||
|
Struct string
|
||||||
|
Field string
|
||||||
|
Offset int64
|
||||||
|
}
|
||||||
|
|
||||||
|
InvalidUnmarshalError struct {
|
||||||
|
Type reflect.Type
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (e UnmarshalTypeError) Error() string {
|
||||||
|
if e.Struct != "" || e.Field != "" {
|
||||||
|
return "form: cannot unmarshal " + e.Value + " into Go struct field " + e.Struct + "." +
|
||||||
|
e.Field + " of type " + e.Type.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
return "form: cannot unmarshal " + e.Value + " into Go value of type " + e.Type.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e InvalidUnmarshalError) Error() string {
|
||||||
|
if e.Type == nil {
|
||||||
|
return "form: Unmarshal(nil)"
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.Type.Kind() != reflect.Pointer {
|
||||||
|
return "form: Unmarshal(non-pointer " + e.Type.String() + "}"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "form: Unmarshal(nil " + e.Type.String() + ")"
|
||||||
|
}
|
176
form.go
176
form.go
|
@ -3,9 +3,12 @@
|
||||||
package form
|
package form
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
http "github.com/valyala/fasthttp"
|
http "github.com/valyala/fasthttp"
|
||||||
)
|
)
|
||||||
|
@ -15,25 +18,32 @@ type (
|
||||||
// a form description of themselves. The input can be assumed to be a
|
// a form description of themselves. The input can be assumed to be a
|
||||||
// valid encoding of a form value. UnmarshalForm must copy the form data
|
// valid encoding of a form value. UnmarshalForm must copy the form data
|
||||||
// if it wishes to retain the data after returning.
|
// if it wishes to retain the data after returning.
|
||||||
//
|
|
||||||
// By convention, to approximate the behavior of Unmarshal itself,
|
|
||||||
// Unmarshalers implement UnmarshalForm([]byte("null")) as a no-op.
|
|
||||||
Unmarshaler interface {
|
Unmarshaler interface {
|
||||||
UnmarshalForm(v []byte) error
|
UnmarshalForm(v []byte) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// A Decoder reads and decodes form values from an *fasthttp.Args.
|
|
||||||
Decoder struct {
|
Decoder struct {
|
||||||
source *http.Args
|
tag string
|
||||||
|
args *http.Args
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const tagName string = "form"
|
const (
|
||||||
|
tagIgnore = "-"
|
||||||
|
methodName = "UnmarshalForm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewDecoder(r io.Reader) *Decoder {
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
defer buf.Reset()
|
||||||
|
|
||||||
|
_, _ = buf.ReadFrom(r)
|
||||||
|
|
||||||
|
args := http.AcquireArgs()
|
||||||
|
args.ParseBytes(buf.Bytes())
|
||||||
|
|
||||||
// NewDecoder returns a new decoder that reads from *fasthttp.Args.
|
|
||||||
func NewDecoder(args *http.Args) *Decoder {
|
|
||||||
return &Decoder{
|
return &Decoder{
|
||||||
source: args,
|
tag: "form",
|
||||||
|
args: args,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,21 +67,16 @@ func NewDecoder(args *http.Args) *Decoder {
|
||||||
// the keys (either the struct field name or its tag), preferring an exact match
|
// the keys (either the struct field name or its tag), preferring an exact match
|
||||||
// but also accepting a case-insensitive match. By default, object keys which
|
// but also accepting a case-insensitive match. By default, object keys which
|
||||||
// don't have a corresponding struct field are ignored.
|
// don't have a corresponding struct field are ignored.
|
||||||
func Unmarshal(src *http.Args, dst interface{}) error {
|
func Unmarshal(data []byte, v any) error {
|
||||||
if err := NewDecoder(src).Decode(dst); err != nil {
|
return NewDecoder(bytes.NewReader(data)).Decode(v)
|
||||||
return fmt.Errorf("unmarshal: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decode reads the next form-encoded value from its input and stores it in the
|
func (d Decoder) Decode(dst any) (err error) {
|
||||||
// value pointed to by v.
|
src := reflect.ValueOf(dst)
|
||||||
//nolint: funlen
|
if !src.IsValid() || src.Kind() != reflect.Pointer || src.Elem().Kind() != reflect.Struct {
|
||||||
func (dec *Decoder) Decode(src interface{}) (err error) {
|
return &InvalidUnmarshalError{
|
||||||
v := reflect.ValueOf(src).Elem()
|
Type: reflect.TypeOf(dst),
|
||||||
if !v.IsValid() {
|
}
|
||||||
return errors.New("invalid input")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
|
@ -84,54 +89,105 @@ func (dec *Decoder) Decode(src interface{}) (err error) {
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
t := reflect.TypeOf(src).Elem()
|
return d.decode("", src)
|
||||||
|
}
|
||||||
|
|
||||||
for i := 0; i < v.NumField(); i++ {
|
func (d Decoder) decode(key string, dst reflect.Value) error {
|
||||||
ft := t.Field(i)
|
src := http.AcquireArgs()
|
||||||
|
defer http.ReleaseArgs(src)
|
||||||
|
d.args.CopyTo(src)
|
||||||
|
|
||||||
// NOTE(toby3d): get tag value as query name
|
if keyIndex := strings.LastIndex(key, ","); keyIndex != -1 {
|
||||||
tagValue, ok := ft.Tag.Lookup(tagName)
|
if index, err := strconv.Atoi(key[keyIndex+1:]); err == nil {
|
||||||
if !ok || tagValue == "" || tagValue == "-" || !dec.source.Has(tagValue) {
|
key = key[:keyIndex]
|
||||||
continue
|
|
||||||
|
src.Reset()
|
||||||
|
src.SetBytesV(key, d.args.PeekMulti(key)[index])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch dst.Kind() {
|
||||||
|
case reflect.Bool:
|
||||||
|
dst.SetBool(src.GetBool(key))
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
dst.SetInt(int64(src.GetUfloatOrZero(key)))
|
||||||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||||
|
dst.SetUint(uint64(src.GetUintOrZero(key)))
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
dst.SetFloat(src.GetUfloatOrZero(key))
|
||||||
|
// case reflect.Array: // TODO(toby3d)
|
||||||
|
// case reflect.Interface: // TODO(toby3d)
|
||||||
|
case reflect.Slice:
|
||||||
|
// NOTE(toby3d): copy raw []byte value as is
|
||||||
|
if dst.Type().Elem().Kind() == reflect.Uint8 {
|
||||||
|
dst.SetBytes(src.Peek(key))
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
field := v.Field(i)
|
if dst.IsNil() {
|
||||||
|
slice := d.args.PeekMulti(key)
|
||||||
|
dst.Set(reflect.MakeSlice(dst.Type(), len(slice), cap(slice)))
|
||||||
|
}
|
||||||
|
|
||||||
// NOTE(toby3d): read struct field type
|
for i := 0; i < dst.Len(); i++ {
|
||||||
switch ft.Type.Kind() {
|
if err := d.decode(fmt.Sprintf("%s,%d", key, i), dst.Index(i)); err != nil {
|
||||||
case reflect.String:
|
return err
|
||||||
field.SetString(string(dec.source.Peek(tagValue)))
|
}
|
||||||
case reflect.Int:
|
}
|
||||||
field.SetInt(int64(dec.source.GetUintOrZero(tagValue)))
|
case reflect.String:
|
||||||
case reflect.Float64:
|
dst.SetString(string(src.Peek(key)))
|
||||||
field.SetFloat(dec.source.GetUfloatOrZero(tagValue))
|
case reflect.Pointer:
|
||||||
case reflect.Bool:
|
if dst.IsNil() {
|
||||||
field.SetBool(dec.source.GetBool(tagValue))
|
dst.Set(reflect.New(dst.Type().Elem()))
|
||||||
case reflect.Ptr: // NOTE(toby3d): pointer to another struct
|
}
|
||||||
// NOTE(toby3d): check what custom unmarshal method exists
|
|
||||||
unmarshalFunc := field.MethodByName("UnmarshalForm")
|
// NOTE(toby3d): if contains UnmarshalForm method
|
||||||
if unmarshalFunc.IsZero() {
|
for i := 0; i < dst.NumMethod(); i++ {
|
||||||
|
if dst.Type().Method(i).Name != methodName {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
field.Set(reflect.New(ft.Type.Elem())) // NOTE(toby3d): initialize zero value
|
in := make([]reflect.Value, 1)
|
||||||
unmarshalFunc.Call([]reflect.Value{reflect.ValueOf(dec.source.Peek(tagValue))})
|
in[0] = reflect.ValueOf(src.Peek(key))
|
||||||
case reflect.Slice:
|
|
||||||
switch ft.Type.Elem().Kind() {
|
|
||||||
case reflect.Uint8: // NOTE(toby3d): bytes slice
|
|
||||||
field.SetBytes(dec.source.Peek(tagValue))
|
|
||||||
case reflect.String: // NOTE(toby3d): string slice
|
|
||||||
values := dec.source.PeekMulti(tagValue)
|
|
||||||
slice := reflect.MakeSlice(ft.Type, len(values), len(values))
|
|
||||||
|
|
||||||
for j, vv := range values {
|
out := dst.Method(i).Call(in)
|
||||||
slice.Index(j).SetString(string(vv))
|
if len(out) > 0 && out[0].Interface() != nil {
|
||||||
|
return out[0].Interface().(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := d.decode(key, dst.Elem()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case reflect.Struct:
|
||||||
|
// NOTE(toby3d): if contains UnmarshalForm method
|
||||||
|
for i := 0; i < dst.Addr().NumMethod(); i++ {
|
||||||
|
if dst.Addr().Type().Method(i).Name != methodName {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
in := make([]reflect.Value, 1)
|
||||||
|
in[0] = reflect.ValueOf(src.Peek(key))
|
||||||
|
|
||||||
|
out := dst.Addr().Method(i).Call(in)
|
||||||
|
if len(out) > 0 && out[0].Interface() != nil {
|
||||||
|
return out[0].Interface().(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < dst.NumField(); i++ {
|
||||||
|
if name, _ := parseTag(string(dst.Type().Field(i).Tag.Get(d.tag))); name != tagIgnore {
|
||||||
|
if err := d.decode(name, dst.Field(i)); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
field.Set(slice)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
|
|
140
form_test.go
140
form_test.go
|
@ -1,83 +1,111 @@
|
||||||
package form_test
|
package form_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/google/go-cmp/cmp"
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
http "github.com/valyala/fasthttp"
|
http "github.com/valyala/fasthttp"
|
||||||
|
|
||||||
"source.toby3d.me/toby3d/form"
|
"source.toby3d.me/toby3d/form"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
ResponseType string
|
TestResult struct {
|
||||||
|
ArrayStruct []Struct `form:"arrayStruct[]"`
|
||||||
URI struct {
|
ArrayPtrStruct []*Struct `form:"arrayPtrStruct[]"`
|
||||||
*http.URI `form:"-"`
|
Bytes []byte `form:"bytes"` // TODO(toby3d)
|
||||||
|
Ints []int `form:"ints[]"`
|
||||||
|
Struct Struct `form:"struct"`
|
||||||
|
PtrStruct *Struct `form:"ptrStruct"`
|
||||||
|
Skip any `form:"-"`
|
||||||
|
// Interface any `form:"interface"` // TODO(toby3d)
|
||||||
|
Empty string `form:"empty"`
|
||||||
|
NotFormTag string `json:"notFormTag"`
|
||||||
|
String string `form:"string"`
|
||||||
|
Float float32 `form:"float"`
|
||||||
|
Uint uint `form:"uint"`
|
||||||
|
Int int `form:"int"`
|
||||||
|
Bool bool `form:"bool"`
|
||||||
}
|
}
|
||||||
|
|
||||||
TestResult struct {
|
Struct struct {
|
||||||
State []byte `form:"state"`
|
uid string `form:"-"`
|
||||||
Scope []string `form:"scope[]"`
|
|
||||||
ClientID *URI `form:"client_id"`
|
|
||||||
RedirectURI *URI `form:"redirect_uri"`
|
|
||||||
Me *URI `form:"me"`
|
|
||||||
ResponseType ResponseType `form:"response_type"`
|
|
||||||
CodeChallenge string `form:"code_challenge"`
|
|
||||||
CodeChallengeMethod string `form:"code_challenge_method"`
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const testData string = `response_type=code` + // NOTE(toby3d): string type alias
|
const testData string = `skip=dontTouchMe` +
|
||||||
`&state=1234567890` + // NOTE(toby3d): raw value
|
`&bool=true` +
|
||||||
// NOTE(toby3d): custom URL types
|
`&string=hello+world` +
|
||||||
`&client_id=https://app.example.com/` +
|
`&int=42` +
|
||||||
`&redirect_uri=https://app.example.com/redirect` +
|
`&uint=420` +
|
||||||
`&me=https://user.example.net/` +
|
`&float=4.2` +
|
||||||
// NOTE(toby3d): plain strings
|
// `&interface=a1b2c3` + // TODO(toby3d)
|
||||||
`&code_challenge=OfYAxt8zU2dAPDWQxTAUIteRzMsoj9QBdMIVEDOErUo` +
|
`&struct=abc` +
|
||||||
`&code_challenge_method=S256` +
|
`&ptrStruct=123` +
|
||||||
// NOTE(toby3d): multiple values
|
`&arrayStruct[]=abc` +
|
||||||
`&scope[]=profile` +
|
`&arrayStruct[]=123` +
|
||||||
`&scope[]=create` +
|
`&arrayPtrStruct[]=321` +
|
||||||
`&scope[]=update` +
|
`&arrayPtrStruct[]=bca` +
|
||||||
`&scope[]=delete`
|
`&ints[]=240` +
|
||||||
|
`&ints[]=420` +
|
||||||
|
`&bytes=sampletext` +
|
||||||
|
`¬FormTag=dontParseMe`
|
||||||
|
|
||||||
func TestUnmarshal(t *testing.T) {
|
func TestUnmarshal(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
args := http.AcquireArgs()
|
args := http.AcquireArgs()
|
||||||
clientId, redirectUri, me := http.AcquireURI(), http.AcquireURI(), http.AcquireURI()
|
|
||||||
|
|
||||||
t.Cleanup(func() {
|
|
||||||
http.ReleaseURI(me)
|
|
||||||
http.ReleaseURI(redirectUri)
|
|
||||||
http.ReleaseURI(clientId)
|
|
||||||
http.ReleaseArgs(args)
|
|
||||||
})
|
|
||||||
|
|
||||||
require.NoError(t, clientId.Parse(nil, []byte("https://app.example.com/")))
|
|
||||||
require.NoError(t, redirectUri.Parse(nil, []byte("https://app.example.com/redirect")))
|
|
||||||
require.NoError(t, me.Parse(nil, []byte("https://user.example.net/")))
|
|
||||||
args.Parse(testData)
|
args.Parse(testData)
|
||||||
|
|
||||||
result := new(TestResult)
|
var in TestResult
|
||||||
require.NoError(t, form.Unmarshal(args, result))
|
if err := form.Unmarshal(args.QueryString(), &in); err != nil {
|
||||||
assert.Equal(t, &TestResult{
|
t.Fatal(err)
|
||||||
ClientID: &URI{URI: clientId},
|
}
|
||||||
Me: &URI{URI: me},
|
|
||||||
RedirectURI: &URI{URI: redirectUri},
|
out := TestResult{
|
||||||
State: []byte("1234567890"),
|
Skip: nil,
|
||||||
Scope: []string{"profile", "create", "update", "delete"},
|
Bool: true,
|
||||||
CodeChallengeMethod: "S256",
|
Float: 4.2,
|
||||||
CodeChallenge: "OfYAxt8zU2dAPDWQxTAUIteRzMsoj9QBdMIVEDOErUo",
|
Int: 42,
|
||||||
ResponseType: "code",
|
// Interface: []byte("a1b2c3"), // TODO(toby3d)
|
||||||
}, result)
|
PtrStruct: &Struct{uid: "123"},
|
||||||
|
String: "hello world",
|
||||||
|
Struct: Struct{uid: "abc"},
|
||||||
|
Uint: 420,
|
||||||
|
ArrayStruct: []Struct{
|
||||||
|
{uid: "abc"},
|
||||||
|
{uid: "123"},
|
||||||
|
},
|
||||||
|
ArrayPtrStruct: []*Struct{
|
||||||
|
{uid: "321"},
|
||||||
|
{uid: "bca"},
|
||||||
|
},
|
||||||
|
Ints: []int{240, 420},
|
||||||
|
Empty: "",
|
||||||
|
Bytes: []byte("sampletext"),
|
||||||
|
NotFormTag: "",
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := []cmp.Option{
|
||||||
|
cmp.AllowUnexported(Struct{}),
|
||||||
|
}
|
||||||
|
|
||||||
|
if !cmp.Equal(out, in, opts...) {
|
||||||
|
t.Errorf("Unmarshal(%s, &in)\n%+s", args.QueryString(), cmp.Diff(out, in, opts...))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (src *URI) UnmarshalForm(v []byte) error {
|
func (s *Struct) UnmarshalForm(v []byte) error {
|
||||||
src.URI = http.AcquireURI()
|
src := string(v)
|
||||||
|
switch src {
|
||||||
|
case "123", "abc", "321", "bca":
|
||||||
|
s.uid = string(v)
|
||||||
|
default:
|
||||||
|
return errors.New("Struct: dough!")
|
||||||
|
}
|
||||||
|
|
||||||
return src.Parse(nil, v)
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s Struct) GoString() string { return "Struct(" + s.uid + ")" }
|
||||||
|
|
11
go.mod
11
go.mod
|
@ -1,17 +1,14 @@
|
||||||
module source.toby3d.me/toby3d/form
|
module source.toby3d.me/toby3d/form
|
||||||
|
|
||||||
go 1.17
|
go 1.18
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/stretchr/testify v1.7.0
|
github.com/google/go-cmp v0.5.8
|
||||||
github.com/valyala/fasthttp v1.34.0
|
github.com/valyala/fasthttp v1.37.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/andybalholm/brotli v1.0.4 // indirect
|
github.com/andybalholm/brotli v1.0.4 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.0 // indirect
|
github.com/klauspost/compress v1.15.4 // indirect
|
||||||
github.com/klauspost/compress v1.15.1 // indirect
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
|
||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
|
|
||||||
)
|
)
|
||||||
|
|
21
go.sum
21
go.sum
|
@ -1,19 +1,14 @@
|
||||||
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
|
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
|
||||||
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
||||||
github.com/klauspost/compress v1.15.1 h1:y9FcTHGyrebwfP0ZZqFiaxTaiDnUrGkJkI+f583BL1A=
|
github.com/klauspost/compress v1.15.4 h1:1kn4/7MepF/CHmYub99/nNX8az0IJjfSOU/jbnTVfqQ=
|
||||||
github.com/klauspost/compress v1.15.1/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
github.com/klauspost/compress v1.15.4/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
|
||||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
|
||||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
github.com/valyala/fasthttp v1.34.0 h1:d3AAQJ2DRcxJYHm7OXNXtXt2as1vMDfxeIcFvhmGGm4=
|
github.com/valyala/fasthttp v1.37.0 h1:7WHCyI7EAkQMVmrfBhWTCOaeROb1aCBiTopx63LkMbE=
|
||||||
github.com/valyala/fasthttp v1.34.0/go.mod h1:epZA5N+7pY6ZaEKRmstzOuYJx9HI8DI1oaCGZpdH4h0=
|
github.com/valyala/fasthttp v1.37.0/go.mod h1:t/G+3rLek+CyY9bnIE+YlMRddxVAAGjhxndDB4i4C0I=
|
||||||
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
||||||
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
|
@ -28,7 +23,3 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
package form
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type tagOptions string
|
||||||
|
|
||||||
|
const delim rune = ','
|
||||||
|
|
||||||
|
func parseTag(tag string) (string, tagOptions) {
|
||||||
|
tag, opt, _ := strings.Cut(tag, string(delim))
|
||||||
|
|
||||||
|
return tag, tagOptions(opt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o tagOptions) Contains(optionName string) bool {
|
||||||
|
if len(o) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
s := string(o)
|
||||||
|
for s != "" {
|
||||||
|
var name string
|
||||||
|
if name, s, _ = strings.Cut(s, string(delim)); name == optionName {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
Loading…
Reference in New Issue