Created simple form values decoding

This commit is contained in:
Maxim Lebedev 2021-12-22 06:38:54 +05:00
parent 4da2f775ba
commit 59909aef58
Signed by: toby3d
GPG Key ID: 1F14E25B7C119FC5
4 changed files with 265 additions and 0 deletions

137
form.go Normal file
View File

@ -0,0 +1,137 @@
// Package form implements encoding and decoding of urlencoded form. The mapping
// between form and Go values is described by `form:"query_name"` struct tags.
package form
import (
"errors"
"fmt"
"reflect"
http "github.com/valyala/fasthttp"
)
type (
// Unmarshaler is the interface implemented by types that can unmarshal
// 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
// 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 {
UnmarshalForm(v []byte) error
}
// A Decoder reads and decodes form values from an *fasthttp.Args.
Decoder struct {
source *http.Args
}
)
const tagName string = "form"
// NewDecoder returns a new decoder that reads from *fasthttp.Args.
func NewDecoder(args *http.Args) *Decoder {
return &Decoder{
source: args,
}
}
// Unmarshal parses the form-encoded data and stores the result in the value
// pointed to by v. If v is nil or not a pointer, Unmarshal returns error.
//
// Unmarshal uses the reflection, allocating maps, slices, and pointers as
// necessary, with the following additional rules:
//
// To unmarshal form into a pointer, Unmarshal first handles the case of the
// form being the form literal null. In that case, Unmarshal sets the pointer to
// nil. Otherwise, Unmarshal unmarshals the form into the value pointed at by
// the pointer. If the pointer is nil, Unmarshal allocates a new value for it to
// point to.
//
// To unmarshal form into a value implementing the Unmarshaler interface,
// Unmarshal calls that value's UnmarshalForm method, including when the input
// is a form null.
//
// To unmarshal form into a struct, Unmarshal matches incoming object keys to
// 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
// don't have a corresponding struct field are ignored.
func Unmarshal(src *http.Args, dst interface{}) error {
if err := NewDecoder(src).Decode(dst); err != nil {
return fmt.Errorf("unmarshal: %w", err)
}
return nil
}
// Decode reads the next form-encoded value from its input and stores it in the
// value pointed to by v.
//nolint: funlen
func (dec *Decoder) Decode(src interface{}) (err error) {
v := reflect.ValueOf(src).Elem()
if !v.IsValid() {
return errors.New("invalid input")
}
defer func() {
if r := recover(); r != nil {
if ve, ok := r.(*reflect.ValueError); ok {
err = fmt.Errorf("recovered: %w", ve)
} else {
panic(r)
}
}
}()
t := reflect.TypeOf(src).Elem()
for i := 0; i < v.NumField(); i++ {
ft := t.Field(i)
// NOTE(toby3d): get tag value as query name
tagValue, ok := ft.Tag.Lookup(tagName)
if !ok || tagValue == "" || tagValue == "-" || !dec.source.Has(tagValue) {
continue
}
field := v.Field(i)
// NOTE(toby3d): read struct field type
switch ft.Type.Kind() {
case reflect.String:
field.SetString(string(dec.source.Peek(tagValue)))
case reflect.Int:
field.SetInt(int64(dec.source.GetUintOrZero(tagValue)))
case reflect.Float64:
field.SetFloat(dec.source.GetUfloatOrZero(tagValue))
case reflect.Bool:
field.SetBool(dec.source.GetBool(tagValue))
case reflect.Ptr: // NOTE(toby3d): pointer to another struct
// NOTE(toby3d): check what custom unmarshal method exists
unmarshalFunc := field.MethodByName("UnmarshalForm")
if unmarshalFunc.IsZero() {
continue
}
field.Set(reflect.New(ft.Type.Elem())) // NOTE(toby3d): initialize zero value
unmarshalFunc.Call([]reflect.Value{reflect.ValueOf(dec.source.Peek(tagValue))})
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 {
slice.Index(j).SetString(string(vv))
}
field.Set(slice)
}
}
}
return
}

83
form_test.go Normal file
View File

@ -0,0 +1,83 @@
package form_test
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
http "github.com/valyala/fasthttp"
"source.toby3d.me/toby3d/form"
)
type (
ResponseType string
URI struct {
*http.URI `form:"-"`
}
TestResult struct {
State []byte `form:"state"`
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
`&state=1234567890` + // NOTE(toby3d): raw value
// NOTE(toby3d): custom URL types
`&client_id=https://app.example.com/` +
`&redirect_uri=https://app.example.com/redirect` +
`&me=https://user.example.net/` +
// NOTE(toby3d): plain strings
`&code_challenge=OfYAxt8zU2dAPDWQxTAUIteRzMsoj9QBdMIVEDOErUo` +
`&code_challenge_method=S256` +
// NOTE(toby3d): multiple values
`&scope[]=profile` +
`&scope[]=create` +
`&scope[]=update` +
`&scope[]=delete`
func TestUnmarshal(t *testing.T) {
t.Parallel()
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)
result := new(TestResult)
require.NoError(t, form.Unmarshal(args, result))
assert.Equal(t, &TestResult{
ClientID: &URI{URI: clientId},
Me: &URI{URI: me},
RedirectURI: &URI{URI: redirectUri},
State: []byte("1234567890"),
Scope: []string{"profile", "create", "update", "delete"},
CodeChallengeMethod: "S256",
CodeChallenge: "OfYAxt8zU2dAPDWQxTAUIteRzMsoj9QBdMIVEDOErUo",
ResponseType: "code",
}, result)
}
func (src *URI) UnmarshalForm(v []byte) error {
src.URI = http.AcquireURI()
return src.Parse(nil, v)
}

14
go.mod
View File

@ -1,3 +1,17 @@
module source.toby3d.me/toby3d/form
go 1.17
require (
github.com/stretchr/testify v1.7.0
github.com/valyala/fasthttp v1.31.0
)
require (
github.com/andybalholm/brotli v1.0.2 // indirect
github.com/davecgh/go-spew v1.1.0 // indirect
github.com/klauspost/compress v1.13.4 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
)

31
go.sum Normal file
View File

@ -0,0 +1,31 @@
github.com/andybalholm/brotli v1.0.2 h1:JKnhI/XQ75uFBTiuzXpzFrUriDPiZjlOSzh6wXogP0E=
github.com/andybalholm/brotli v1.0.2/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/klauspost/compress v1.13.4 h1:0zhec2I8zGnjWcKyLl6i3gPqKANCCn5e9xmviEEeX6s=
github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
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/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.31.0 h1:lrauRLII19afgCs2fnWRJ4M5IkV0lo2FqA61uGkNBfE=
github.com/valyala/fasthttp v1.31.0/go.mod h1:2rsYD01CKFrjjsvFxx75KlEUNpWNBY9JWD3K/7o2Cus=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
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=