├── go.mod
├── .travis.yml
├── LICENSE
├── go.sum
├── README.md
├── form.go
└── form_test.go
/go.mod:
--------------------------------------------------------------------------------
1 | module clevergo.tech/form
2 |
3 | go 1.13
4 |
5 | require (
6 | github.com/gorilla/schema v1.1.0
7 | github.com/stretchr/testify v1.5.1
8 | )
9 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | os:
2 | - linux
3 | language: go
4 | go:
5 | - 1.13.x
6 | - 1.14.x
7 | - 1.15.x
8 | - master
9 | jobs:
10 | allow_failures:
11 | - go: master
12 | fast_finish: true
13 | before_install:
14 | - go get github.com/mattn/goveralls
15 | script:
16 | - go test -v -covermode=count -coverprofile=coverage.out
17 | - go vet ./...
18 | - test -z "$(gofmt -d -s . | tee /dev/stderr)"
19 | - $HOME/gopath/bin/goveralls -coverprofile=coverage.out -service=travis-ci
20 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 CleverGo
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3 | github.com/gorilla/schema v1.1.0 h1:CamqUDOFUBqzrvxuz2vEwo8+SUdwsluFh7IlzJh30LY=
4 | github.com/gorilla/schema v1.1.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU=
5 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
6 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
7 | github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
8 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
9 | github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
10 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
11 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
12 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
13 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
14 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
15 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Form Decoder
2 | [](https://travis-ci.org/clevergo/form)
3 | [](https://coveralls.io/github/clevergo/form?branch=master)
4 | [](https://pkg.go.dev/clevergo.tech/form?tab=doc)
5 | [](https://goreportcard.com/report/github.com/clevergo/form)
6 | [](https://github.com/clevergo/form/releases)
7 | [](https://pkg.clevergo.tech/)
8 | [](https://t.me/clevergotech)
9 | [](https://forum.clevergo.tech)
10 |
11 | A form decoder that decode request body of any types(xml, json, form, multipart form...) into a sctruct by same codebase.
12 |
13 | By default, form decoder can handles the following content types:
14 |
15 | - Form(application/x-www-form-urlencoded)
16 | - Multipart Form(multipart/form-data)
17 | - JSON(application/json)
18 | - XML(application/xml)
19 |
20 | > Form and multipart form are built on top of gorilla [schema](https://github.com/gorilla/schema), tag name is `schema`.
21 |
22 | [Register](https://pkg.go.dev/clevergo.tech/form?tab=doc#Decoders.Register) allow to register particular decoder or replace default decoder
23 | for the specified content type.
24 |
25 | ## Installation
26 |
27 | ```go
28 | $ go get clevergo.tech/form
29 | ```
30 |
31 | ## Usage
32 |
33 | ```go
34 | import (
35 | "net/http"
36 |
37 | "clevergo.tech/form"
38 | )
39 |
40 | var decoders = form.New()
41 |
42 | type user struct {
43 | Username string `schema:"username" json:"username" xml:"username"`
44 | Password string `schema:"password" json:"password" xml:"password"`
45 | }
46 |
47 | func init() {
48 | // replaces multipart form decoder.
49 | decoders.Register(form.ContentTypeMultipartForm, form.NewMultipartForm(10*1024*1024))
50 | // registers other decoder
51 | // decoders.Register(contentType, decoder)
52 | }
53 |
54 | func(w http.ResponseWriter, r *http.Request) {
55 | u := user{}
56 | if err := decoders.Decode(r, &u); err != nil {
57 | http.Error(w, http.StatusInternalServerError, err.Error())
58 | return
59 | }
60 | // ...
61 | }
62 | ```
63 |
64 | ### Example
65 |
66 | Checkout [example](https://github.com/clevergo/examples/tree/master/form) for details.
67 |
--------------------------------------------------------------------------------
/form.go:
--------------------------------------------------------------------------------
1 | // Copyright 2020 CleverGo. All rights reserved.
2 | // Use of this source code is governed by a MIT style license that can be found
3 | // in the LICENSE file.
4 |
5 | // Package form is a form decoder that decode request body into a struct.
6 | package form
7 |
8 | import (
9 | "bytes"
10 | "encoding/json"
11 | "encoding/xml"
12 | "errors"
13 | "io"
14 | "io/ioutil"
15 | "mime"
16 | "net/http"
17 |
18 | "github.com/gorilla/schema"
19 | )
20 |
21 | // Validatable indicates whether a value can be validated.
22 | type Validatable interface {
23 | Validate() error
24 | }
25 |
26 | // Content type constants.
27 | const (
28 | ContentType = "Content-Type"
29 | ContentTypeForm = "application/x-www-form-urlencoded"
30 | ContentTypeMultipartForm = "multipart/form-data"
31 | ContentTypeJSON = "application/json"
32 | ContentTypeXML = "application/xml"
33 | )
34 |
35 | var (
36 | defaultDecoders *Decoders
37 | defaultDecoder = schema.NewDecoder()
38 | defaultMaxMemory int64 = 10 * 1024 * 1024
39 | )
40 |
41 | func init() {
42 | defaultDecoder.IgnoreUnknownKeys(true)
43 |
44 | defaultDecoders = New()
45 | }
46 |
47 | // New returns a decoders.
48 | func New() *Decoders {
49 | d := &Decoders{}
50 | d.Register(ContentTypeForm, NewForm(defaultDecoder))
51 | d.Register(ContentTypeMultipartForm, NewMultipartForm(defaultMaxMemory))
52 | d.Register(ContentTypeJSON, JSON)
53 | d.Register(ContentTypeXML, XML)
54 | return d
55 | }
56 |
57 | // Register a decoder for the given content type.
58 | func Register(contentType string, decoder Decoder) {
59 | defaultDecoders.Register(contentType, decoder)
60 | }
61 |
62 | // Decode data from a request into v, v should be a pointer.
63 | func Decode(r *http.Request, v interface{}) error {
64 | return defaultDecoders.Decode(r, v)
65 | }
66 |
67 | // Decoders is a map that mapping from content type to decoder.
68 | type Decoders map[string]Decoder
69 |
70 | // Register a decoder for the given content type.
71 | func (d *Decoders) Register(contentType string, decoder Decoder) {
72 | (*d)[contentType] = decoder
73 | }
74 |
75 | // Decode data from a request into v, v should be a pointer.
76 | func (d *Decoders) Decode(r *http.Request, v interface{}) error {
77 | contentType, err := parseContentType(r)
78 | if err != nil {
79 | return err
80 | }
81 | decoder, ok := (*d)[contentType]
82 | if !ok {
83 | return errors.New("Unsupported content type: " + contentType)
84 | }
85 | if err = decoder(r, v); err != nil {
86 | return err
87 | }
88 | if vv, ok := v.(Validatable); ok {
89 | err = vv.Validate()
90 | }
91 |
92 | return err
93 | }
94 |
95 | // Decoder is a function that decode data from request into v.
96 | type Decoder func(req *http.Request, v interface{}) error
97 |
98 | func parseContentType(r *http.Request) (string, error) {
99 | header := r.Header.Get(ContentType)
100 | contentType, _, err := mime.ParseMediaType(header)
101 | if err != nil {
102 | return "", err
103 | }
104 |
105 | return contentType, nil
106 | }
107 |
108 | // JSON is a JSON decoder.
109 | func JSON(r *http.Request, v interface{}) error {
110 | var buf bytes.Buffer
111 | reader := io.TeeReader(r.Body, &buf)
112 | r.Body = ioutil.NopCloser(&buf)
113 | return json.NewDecoder(reader).Decode(v)
114 | }
115 |
116 | // XML is an XML decoder.
117 | func XML(r *http.Request, v interface{}) error {
118 | var buf bytes.Buffer
119 | reader := io.TeeReader(r.Body, &buf)
120 | r.Body = ioutil.NopCloser(&buf)
121 | return xml.NewDecoder(reader).Decode(v)
122 | }
123 |
124 | // NewForm returns a post form decoder with the given schema decoder.
125 | func NewForm(decoder *schema.Decoder) Decoder {
126 | return func(r *http.Request, v interface{}) error {
127 | err := r.ParseForm()
128 | if err != nil {
129 | return err
130 | }
131 |
132 | return decoder.Decode(v, r.PostForm)
133 | }
134 | }
135 |
136 | // NewMultipartForm returns a multipart form decoder with the given schema decoder.
137 | func NewMultipartForm(maxMemory int64) Decoder {
138 | return func(r *http.Request, v interface{}) error {
139 | err := r.ParseMultipartForm(maxMemory)
140 | if err != nil {
141 | return err
142 | }
143 |
144 | return defaultDecoder.Decode(v, r.MultipartForm.Value)
145 | }
146 | }
147 |
--------------------------------------------------------------------------------
/form_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2020 CleverGo. All rights reserved.
2 | // Use of this source code is governed by a MIT style license that can be found
3 | // in the LICENSE file.
4 |
5 | package form
6 |
7 | import (
8 | "bytes"
9 | "encoding/json"
10 | "encoding/xml"
11 | "net/http"
12 | "net/http/httptest"
13 | "strings"
14 | "testing"
15 |
16 | "github.com/stretchr/testify/assert"
17 | )
18 |
19 | type login struct {
20 | validated bool
21 | Username string `schema:"username" json:"username" xml:"username"`
22 | Password string `schema:"password" json:"password" xml:"password"`
23 | }
24 |
25 | func (l *login) Validate() error {
26 | l.validated = true
27 | return nil
28 | }
29 |
30 | func TestRegister(t *testing.T) {
31 | invoked := false
32 | decoder := func(r *http.Request, v interface{}) error {
33 | invoked = true
34 | return nil
35 | }
36 | contentType := "content/type"
37 | Register(contentType, decoder)
38 | actual, ok := (*defaultDecoders)[contentType]
39 | assert.True(t, ok)
40 | err := actual(httptest.NewRequest(http.MethodGet, "/", nil), nil)
41 | assert.Nil(t, err)
42 | assert.True(t, invoked)
43 | }
44 |
45 | func TestJSON(t *testing.T) {
46 | tests := []struct {
47 | shouldErr bool
48 | body []byte
49 | }{
50 | {true, []byte(`{"invalid json"}`)},
51 | {false, []byte(`{}`)},
52 | {false, []byte(`{"username": "foo"}`)},
53 | {false, []byte(`{"password": "bar"}`)},
54 | {false, []byte(`{"username": "foo","password": "bar"}`)},
55 | }
56 |
57 | for _, test := range tests {
58 | actual := login{}
59 | r := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(test.body))
60 | r.Header.Set("Content-Type", ContentTypeJSON)
61 | for i := 0; i < 3; i++ {
62 | err := Decode(r, &actual)
63 | if test.shouldErr {
64 | assert.NotNil(t, err)
65 | continue
66 | }
67 | assert.Nil(t, err)
68 | expected := login{validated: true}
69 | assert.Nil(t, json.Unmarshal(test.body, &expected))
70 | assert.Equal(t, expected, actual)
71 | }
72 | }
73 | }
74 |
75 | func TestXML(t *testing.T) {
76 | tests := []struct {
77 | shouldErr bool
78 | body []byte
79 | }{
80 | {true, []byte(`invalid xml`)},
81 | {false, []byte(``)},
82 | {false, []byte(`foo`)},
83 | {false, []byte(`bar>`)},
84 | {false, []byte(`foobar>`)},
85 | }
86 |
87 | for _, test := range tests {
88 | actual := login{}
89 | r := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(test.body))
90 | r.Header.Set("Content-Type", ContentTypeXML)
91 | for i := 0; i < 3; i++ {
92 | err := Decode(r, &actual)
93 | if test.shouldErr {
94 | assert.NotNil(t, err)
95 | continue
96 | }
97 | assert.Nil(t, err)
98 | expected := login{validated: true}
99 | assert.Nil(t, xml.Unmarshal(test.body, &expected))
100 | assert.Equal(t, expected, actual)
101 | }
102 | }
103 | }
104 |
105 | var formData = map[string][]string{
106 | "username": {"foo"},
107 | "password": {"bar"},
108 | }
109 |
110 | func TestForm(t *testing.T) {
111 | expected := login{validated: true}
112 | if err := defaultDecoder.Decode(&expected, formData); err != nil {
113 | t.Fatal(err.Error())
114 | }
115 |
116 | actual := login{validated: true}
117 | r := httptest.NewRequest(http.MethodPost, "/", strings.NewReader("username=foo&password=bar"))
118 | r.Header.Set("Content-Type", ContentTypeForm)
119 | assert.Nil(t, Decode(r, &actual))
120 | assert.Equal(t, expected, actual)
121 | }
122 |
123 | func TestMultipartForm(t *testing.T) {
124 | expected := login{}
125 | err := defaultDecoder.Decode(&expected, formData)
126 | assert.Nil(t, err)
127 |
128 | actual := login{}
129 | postData :=
130 | `--xxx
131 | Content-Disposition: form-data; name="username"
132 |
133 | foo
134 | --xxx
135 | Content-Disposition: form-data; name="password"
136 |
137 | bar
138 | --xxx--
139 | `
140 | r := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(postData))
141 | r.Header.Set("Content-Type", ContentTypeMultipartForm+"; boundary=xxx")
142 | assert.Nil(t, Decode(r, &actual))
143 | assert.True(t, actual.validated)
144 | assert.Equal(t, expected.Username, actual.Username)
145 | assert.Equal(t, expected.Password, actual.Password)
146 | }
147 |
148 | func TestParseContentType(t *testing.T) {
149 | tests := []struct {
150 | contentType string
151 | shouldError bool
152 | expectedContentType string
153 | }{
154 | {"application/json", false, ContentTypeJSON},
155 | {"application/json; charset=utf-8", false, ContentTypeJSON},
156 | {"application/xml", false, ContentTypeXML},
157 | {"application/xml; charset=utf-8", false, ContentTypeXML},
158 | {"application/x-www-form-urlencoded", false, ContentTypeForm},
159 | {"multipart/form-data", false, ContentTypeMultipartForm},
160 | {"", true, ""},
161 | }
162 |
163 | for _, test := range tests {
164 | r := httptest.NewRequest(http.MethodGet, "/", nil)
165 | r.Header.Set(ContentType, test.contentType)
166 | contentType, err := parseContentType(r)
167 | if test.shouldError {
168 | assert.NotNil(t, err)
169 | } else {
170 | assert.Equal(t, test.expectedContentType, contentType)
171 | }
172 | }
173 | }
174 |
175 | func TestDecode(t *testing.T) {
176 | delete((*defaultDecoders), "application/json")
177 | req := httptest.NewRequest(http.MethodPost, "/login", nil)
178 | req.Header.Set("Content-Type", "application/json")
179 | v := login{}
180 | err := Decode(req, &v)
181 | assert.NotNil(t, err)
182 | }
183 |
184 | func TestNewMultipartForm(t *testing.T) {
185 | tests := []struct {
186 | maxMemory int64
187 | contentType string
188 | shouldError bool
189 | }{
190 | {1024, ContentTypeMultipartForm + "; boundary=xxx", false},
191 | {1024, "", true},
192 | }
193 | for _, test := range tests {
194 | decoder := NewMultipartForm(test.maxMemory)
195 | v := login{}
196 | body := strings.NewReader(`--xxx--`)
197 | req := httptest.NewRequest(http.MethodPost, "/login", body)
198 | req.Header.Set("Content-Type", test.contentType)
199 | err := decoder(req, &v)
200 | if test.shouldError {
201 | assert.NotNil(t, err)
202 | }
203 | }
204 | }
205 |
--------------------------------------------------------------------------------