diff --git a/internal/encoding/form/form.go b/internal/encoding/form/form.go deleted file mode 100644 index 27dce20..0000000 --- a/internal/encoding/form/form.go +++ /dev/null @@ -1,137 +0,0 @@ -// 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. -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 - field.Set(reflect.New(ft.Type.Elem())) - - // NOTE(toby3d): check what custom unmarshal method exists - unmarshalFunc := field.MethodByName("UnmarshalForm") - if unmarshalFunc.IsZero() { - continue - } - - 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 -} diff --git a/internal/encoding/form/form_test.go b/internal/encoding/form/form_test.go deleted file mode 100644 index bff1f42..0000000 --- a/internal/encoding/form/form_test.go +++ /dev/null @@ -1,83 +0,0 @@ -package form_test - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - http "github.com/valyala/fasthttp" - - "source.toby3d.me/website/oauth/internal/encoding/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) -}