├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── data.go ├── data_test.go ├── doc.go ├── test_file.txt ├── validator.go └── validator_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | coverage.out -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.7.x 5 | script: 6 | - go test -v ./... 7 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing 2 | ------------ 3 | 4 | Feedback, bug reports, and pull requests are greatly appreciated :) 5 | 6 | ### Issues 7 | 8 | The following are all great reasons to submit an issue: 9 | 10 | 1. You found a bug in the code. 11 | 2. Something is missing from the documentation or the existing documentation is unclear. 12 | 3. You have an idea for a new feature. 13 | 14 | If you are thinking about submitting an issue please remember to: 15 | 16 | 1. Describe the issue in detail. 17 | 2. If applicable, describe the steps to reproduce the error, which probably should include some example code. 18 | 3. Mention details about your platform: OS, version of Go and Redis, etc. 19 | 20 | ### Pull Requests 21 | 22 | In order to submit a pull request: 23 | 24 | Fork the repository. 25 | Create a new "feature branch" with a descriptive name (e.g. fix-database-error). 26 | Make your changes in the feature branch. 27 | Run the tests to make sure that they still pass. Updated the tests if needed. 28 | Submit a pull request to merge your feature branch into the master branch. 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Alex Browne 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 13 | all 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 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Forms 2 | ===== 3 | 4 | [![GoDoc](https://godoc.org/github.com/albrow/forms?status.svg)](https://godoc.org/github.com/albrow/forms) 5 | 6 | Forms is a lightweight, but incredibly useful go library for parsing 7 | form data from an http.Request. It supports multipart forms, url-encoded 8 | forms, json data, and url query parameters. It also provides helper methods 9 | for converting data into other types and a Validator object which can be 10 | used to validate the data. Forms is framework-agnostic and works directly 11 | with the http package. 12 | 13 | Version 0.4.0 14 | 15 | 16 | Development Status 17 | ------------------ 18 | 19 | Forms is no longer actively maintained, and therefore it is not recommended for 20 | use in mission-critical production applications at this time. That said, it is 21 | fairly well tested and is probably fine to use for low-traffic hobby 22 | sites. 23 | 24 | Forms follows semantic versioning but offers no guarantees of backwards 25 | compatibility until version 1.0. Keep in mind that breaking changes might occur. 26 | We will do our best to make the community aware of any non-trivial breaking 27 | changes beforehand. We recommend using a dependency vendoring tool such as 28 | [godep](https://github.com/tools/godep) to ensure that breaking changes will not 29 | break your application. 30 | 31 | Installation 32 | ------------ 33 | 34 | Install like you would any other package: 35 | ``` 36 | go get github.com/albrow/forms 37 | ``` 38 | 39 | Then include the package in your import statements: 40 | ``` go 41 | import "github.com/albrow/forms" 42 | ``` 43 | 44 | Example Usage 45 | ------------- 46 | 47 | Meant to be used inside the body of an http.HandlerFunc or any function that 48 | has access to an http.Request. 49 | 50 | ``` go 51 | func CreateUserHandler(res http.ResponseWriter, req *http.Request) { 52 | // Parse request data. 53 | userData, err := forms.Parse(req) 54 | if err != nil { 55 | // Handle err 56 | // ... 57 | } 58 | 59 | // Validate 60 | val := userData.Validator() 61 | val.Require("username") 62 | val.LengthRange("username", 4, 16) 63 | val.Require("email") 64 | val.MatchEmail("email") 65 | val.Require("password") 66 | val.MinLength("password", 8) 67 | val.Require("confirmPassword") 68 | val.Equal("password", "confirmPassword") 69 | val.RequireFile("profileImage") 70 | val.AcceptFileExts("profileImage", "jpg", "png", "gif") 71 | if val.HasErrors() { 72 | // Write the errors to the response 73 | // Maybe this means formatting the errors as json 74 | // or re-rendering the form with an error message 75 | // ... 76 | } 77 | 78 | // Use data to create a user object 79 | user := &models.User { 80 | Username: userData.Get("username"), 81 | Email: userData.Get("email"), 82 | HashedPassword: hash(userData.Get("password")), 83 | } 84 | 85 | // Continue by saving the user to the database and writing 86 | // to the response 87 | // ... 88 | 89 | 90 | // Get the contents of the profileImage file 91 | imageBytes, err := userData.GetFileBytes("profileImage") 92 | if err != nil { 93 | // Handle err 94 | } 95 | // Now you can either copy the file over to your server using io.Copy, 96 | // upload the file to something like amazon S3, or do whatever you want 97 | // with it. 98 | } 99 | ``` 100 | 101 | Contributing 102 | ------------ 103 | 104 | See [CONTRIBUTING.md](https://github.com/albrow/forms/blob/master/CONTRIBUTING.md) 105 | 106 | License 107 | ------- 108 | 109 | Forms is licensed under the MIT License. See the LICENSE file for more information. 110 | -------------------------------------------------------------------------------- /data.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Alex Browne. All rights reserved. 2 | // Use of this source code is governed by the MIT 3 | // license, which can be found in the LICENSE file. 4 | 5 | package forms 6 | 7 | import ( 8 | "encoding/json" 9 | "fmt" 10 | "io/ioutil" 11 | "mime/multipart" 12 | "net/http" 13 | "net/url" 14 | "strconv" 15 | "strings" 16 | ) 17 | 18 | // DefaultMaxFormSize is the default maximum form size (in bytes) used by the Parse function. 19 | const DefaultMaxFormSize = 500000 20 | 21 | // Data holds data obtained from the request body and url query parameters. 22 | // Because Data is built from multiple sources, sometimes there will be more 23 | // than one value for a given key. You can use Get, Set, Add, and Del to access 24 | // the first element for a given key or access the Values and Files properties directly 25 | // to access additional elements for a given key. You can also use helper methods to convert 26 | // the first value for a given key to a different type (e.g. bool or int). 27 | type Data struct { 28 | // Values holds any basic key-value string data 29 | // This includes all fields from a json body or 30 | // urlencoded form, and the form fields only (not 31 | // files) from a multipart form 32 | Values url.Values 33 | // Files holds files from a multipart form only. 34 | // For any other type of request, it will always 35 | // be empty. Files only supports one file per key, 36 | // since this is by far the most common use. If you 37 | // need to have more than one file per key, parse the 38 | // files manually using req.MultipartForm.File. 39 | Files map[string]*multipart.FileHeader 40 | // jsonBody holds the original body of the request. 41 | // Only available for json requests. 42 | jsonBody []byte 43 | } 44 | 45 | func newData() *Data { 46 | return &Data{ 47 | Values: url.Values{}, 48 | Files: map[string]*multipart.FileHeader{}, 49 | } 50 | } 51 | 52 | // ParseMax parses the request body and url query parameters into 53 | // Data. The content in the body of the request has a higher priority, 54 | // will be added to Data first, and will be the result of any operation 55 | // which gets the first element for a given key (e.g. Get, GetInt, or GetBool). 56 | func ParseMax(req *http.Request, max int64) (*Data, error) { 57 | data := newData() 58 | contentType := req.Header.Get("Content-Type") 59 | if strings.Contains(contentType, "multipart/form-data") { 60 | if err := req.ParseMultipartForm(max); err != nil { 61 | return nil, err 62 | } 63 | for key, vals := range req.MultipartForm.Value { 64 | for _, val := range vals { 65 | data.Add(key, val) 66 | } 67 | } 68 | for key, files := range req.MultipartForm.File { 69 | if len(files) != 0 { 70 | data.AddFile(key, files[0]) 71 | } 72 | } 73 | } else if strings.Contains(contentType, "form-urlencoded") { 74 | if err := req.ParseForm(); err != nil { 75 | return nil, err 76 | } 77 | for key, vals := range req.PostForm { 78 | for _, val := range vals { 79 | data.Add(key, val) 80 | } 81 | } 82 | } else if strings.Contains(contentType, "application/json") { 83 | body, err := ioutil.ReadAll(req.Body) 84 | if err != nil { 85 | return nil, err 86 | } 87 | data.jsonBody = body 88 | if err := parseJSON(data.Values, data.jsonBody); err != nil { 89 | return nil, err 90 | } 91 | } 92 | for key, vals := range req.URL.Query() { 93 | for _, val := range vals { 94 | data.Add(key, val) 95 | } 96 | } 97 | return data, nil 98 | } 99 | 100 | // Parse uses the default max form size defined above and calls ParseMax 101 | func Parse(req *http.Request) (*Data, error) { 102 | return ParseMax(req, DefaultMaxFormSize) 103 | } 104 | 105 | // CreateFromMap returns a Data object with keys and values matching 106 | // the map. 107 | func CreateFromMap(m map[string]string) *Data { 108 | data := newData() 109 | for key, value := range m { 110 | data.Add(key, value) 111 | } 112 | return data 113 | } 114 | 115 | func parseJSON(values url.Values, body []byte) error { 116 | if len(body) == 0 { 117 | // don't attempt to parse empty bodies 118 | return nil 119 | } 120 | rawData := map[string]interface{}{} 121 | if err := json.Unmarshal(body, &rawData); err != nil { 122 | return err 123 | } 124 | // Whatever the underlying type is, we need to convert it to a 125 | // string. There are only a few possible types, so we can just 126 | // do a type switch over the possibilities. 127 | for key, val := range rawData { 128 | switch val.(type) { 129 | case string, bool, float64: 130 | values.Add(key, fmt.Sprint(val)) 131 | case nil: 132 | values.Add(key, "") 133 | case map[string]interface{}, []interface{}: 134 | // for more complicated data structures, convert back to 135 | // a JSON string and let user decide how to unmarshal 136 | jsonVal, err := json.Marshal(val) 137 | if err != nil { 138 | return err 139 | } 140 | values.Add(key, string(jsonVal)) 141 | } 142 | } 143 | return nil 144 | } 145 | 146 | // Add adds the value to key. It appends to any existing values associated with key. 147 | func (d *Data) Add(key string, value string) { 148 | d.Values.Add(key, value) 149 | } 150 | 151 | // AddFile adds the multipart form file to data with the given key. 152 | func (d *Data) AddFile(key string, file *multipart.FileHeader) { 153 | d.Files[key] = file 154 | } 155 | 156 | // Del deletes the values associated with key. 157 | func (d *Data) Del(key string) { 158 | d.Values.Del(key) 159 | } 160 | 161 | // DelFile deletes the file associated with key (if any). 162 | // If there is no file associated with key, it does nothing. 163 | func (d *Data) DelFile(key string) { 164 | delete(d.Files, key) 165 | } 166 | 167 | // Encode encodes the values into “URL encoded” form ("bar=baz&foo=quux") sorted by key. 168 | // Any files in d will be ignored because there is no direct way to convert a file to a 169 | // URL encoded value. 170 | func (d *Data) Encode() string { 171 | return d.Values.Encode() 172 | } 173 | 174 | // Get gets the first value associated with the given key. If there are no values 175 | // associated with the key, Get returns the empty string. To access multiple values, 176 | // use the map directly. 177 | func (d Data) Get(key string) string { 178 | return d.Values.Get(key) 179 | } 180 | 181 | // GetFile returns the multipart form file associated with key, if any, as a *multipart.FileHeader. 182 | // If there is no file associated with key, it returns nil. If you just want the body of the 183 | // file, use GetFileBytes. 184 | func (d Data) GetFile(key string) *multipart.FileHeader { 185 | return d.Files[key] 186 | } 187 | 188 | // Set sets the key to value. It replaces any existing values. 189 | func (d *Data) Set(key string, value string) { 190 | d.Values.Set(key, value) 191 | } 192 | 193 | // KeyExists returns true iff data.Values[key] exists. When parsing a request body, the key 194 | // is considered to be in existence if it was provided in the request body, even if its value 195 | // is empty. 196 | func (d Data) KeyExists(key string) bool { 197 | _, found := d.Values[key] 198 | return found 199 | } 200 | 201 | // FileExists returns true iff data.Files[key] exists. When parsing a request body, the key 202 | // is considered to be in existence if it was provided in the request body, even if the file 203 | // is empty. 204 | func (d Data) FileExists(key string) bool { 205 | _, found := d.Files[key] 206 | return found 207 | } 208 | 209 | // GetInt returns the first element in data[key] converted to an int. 210 | func (d Data) GetInt(key string) int { 211 | if !d.KeyExists(key) || len(d.Values[key]) == 0 { 212 | return 0 213 | } 214 | str := d.Get(key) 215 | if result, err := strconv.Atoi(str); err != nil { 216 | panic(err) 217 | } else { 218 | return result 219 | } 220 | } 221 | 222 | // GetFloat returns the first element in data[key] converted to a float. 223 | func (d Data) GetFloat(key string) float64 { 224 | if !d.KeyExists(key) || len(d.Values[key]) == 0 { 225 | return 0.0 226 | } 227 | str := d.Get(key) 228 | if result, err := strconv.ParseFloat(str, 64); err != nil { 229 | panic(err) 230 | } else { 231 | return result 232 | } 233 | } 234 | 235 | // GetBool returns the first element in data[key] converted to a bool. 236 | func (d Data) GetBool(key string) bool { 237 | if !d.KeyExists(key) || len(d.Values[key]) == 0 { 238 | return false 239 | } 240 | str := d.Get(key) 241 | if result, err := strconv.ParseBool(str); err != nil { 242 | panic(err) 243 | } else { 244 | return result 245 | } 246 | } 247 | 248 | // GetBytes returns the first element in data[key] converted to a slice of bytes. 249 | func (d Data) GetBytes(key string) []byte { 250 | return []byte(d.Get(key)) 251 | } 252 | 253 | // GetFileBytes returns the body of the file associated with key. If there is no 254 | // file associated with key, it returns nil (not an error). It may return an error if 255 | // there was a problem reading the file. If you need to know whether or not the file 256 | // exists (i.e. whether it was provided in the request), use the FileExists method. 257 | func (d Data) GetFileBytes(key string) ([]byte, error) { 258 | fileHeader, found := d.Files[key] 259 | if !found { 260 | return nil, nil 261 | } else { 262 | file, err := fileHeader.Open() 263 | if err != nil { 264 | return nil, err 265 | } 266 | return ioutil.ReadAll(file) 267 | } 268 | } 269 | 270 | // GetStringsSplit returns the first element in data[key] split into a slice delimited by delim. 271 | func (d Data) GetStringsSplit(key string, delim string) []string { 272 | if !d.KeyExists(key) || len(d.Values[key]) == 0 { 273 | return nil 274 | } 275 | return strings.Split(d.Values[key][0], delim) 276 | } 277 | 278 | // BindJSON binds v to the json data in the request body. It calls json.Unmarshal and 279 | // sets the value of v. 280 | func (d Data) BindJSON(v interface{}) error { 281 | if len(d.jsonBody) == 0 { 282 | return nil 283 | } 284 | return json.Unmarshal(d.jsonBody, v) 285 | } 286 | 287 | // GetMapFromJSON assumes that the first element in data[key] is a json string, attempts to 288 | // unmarshal it into a map[string]interface{}, and if successful, returns the result. If 289 | // unmarshaling was not successful, returns an error. 290 | func (d Data) GetMapFromJSON(key string) (map[string]interface{}, error) { 291 | if !d.KeyExists(key) || len(d.Values[key]) == 0 { 292 | return nil, nil 293 | } 294 | result := map[string]interface{}{} 295 | if err := json.Unmarshal([]byte(d.Get(key)), &result); err != nil { 296 | return nil, err 297 | } else { 298 | return result, nil 299 | } 300 | } 301 | 302 | // GetSliceFromJSON assumes that the first element in data[key] is a json string, attempts to 303 | // unmarshal it into a []interface{}, and if successful, returns the result. If unmarshaling 304 | // was not successful, returns an error. 305 | func (d Data) GetSliceFromJSON(key string) ([]interface{}, error) { 306 | if !d.KeyExists(key) || len(d.Values[key]) == 0 { 307 | return nil, nil 308 | } 309 | result := []interface{}{} 310 | if err := json.Unmarshal([]byte(d.Get(key)), &result); err != nil { 311 | return nil, err 312 | } else { 313 | return result, nil 314 | } 315 | } 316 | 317 | // GetAndUnmarshalJSON assumes that the first element in data[key] is a json string and 318 | // attempts to unmarshal it into v. If unmarshaling was not successful, returns an error. 319 | // v should be a pointer to some data structure. 320 | func (d Data) GetAndUnmarshalJSON(key string, v interface{}) error { 321 | if err := json.Unmarshal([]byte(d.Get(key)), v); err != nil { 322 | return err 323 | } 324 | return nil 325 | } 326 | 327 | // Validator returns a Validator which can be used to easily validate data. 328 | func (d *Data) Validator() *Validator { 329 | return &Validator{ 330 | data: d, 331 | } 332 | } 333 | -------------------------------------------------------------------------------- /data_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Alex Browne. All rights reserved. 2 | // Use of this source code is governed by the MIT 3 | // license, which can be found in the LICENSE file. 4 | 5 | package forms 6 | 7 | import ( 8 | "bytes" 9 | "fmt" 10 | "io" 11 | "io/ioutil" 12 | "mime/multipart" 13 | "net/http" 14 | "net/url" 15 | "os" 16 | "reflect" 17 | "strings" 18 | "testing" 19 | ) 20 | 21 | func TestGet(t *testing.T) { 22 | data := newData() 23 | data.Values = map[string][]string{ 24 | "name": []string{"bob", "bill"}, 25 | "profession": []string{"plumber"}, 26 | } 27 | 28 | table := []struct { 29 | key string 30 | expected string 31 | }{ 32 | { 33 | key: "name", 34 | expected: "bob", 35 | }, 36 | { 37 | key: "profession", 38 | expected: "plumber", 39 | }, 40 | { 41 | key: "favoriteColor", 42 | expected: "", 43 | }, 44 | } 45 | 46 | for _, test := range table { 47 | got := data.Get(test.key) 48 | if got != test.expected { 49 | t.Errorf("%s was incorrect. Expected %s, but got %s.\n", test.key, test.expected, got) 50 | } 51 | } 52 | } 53 | 54 | func TestGetInt(t *testing.T) { 55 | data := newData() 56 | data.Values = map[string][]string{ 57 | "age": []string{"25", "33"}, 58 | "weight": []string{"155"}, 59 | } 60 | 61 | table := []struct { 62 | key string 63 | expected int 64 | }{ 65 | { 66 | key: "age", 67 | expected: 25, 68 | }, 69 | { 70 | key: "weight", 71 | expected: 155, 72 | }, 73 | { 74 | key: "height", 75 | expected: 0, 76 | }, 77 | } 78 | 79 | for _, test := range table { 80 | got := data.GetInt(test.key) 81 | if got != test.expected { 82 | t.Errorf("%s was incorrect. Expected %d, but got %d.\n", test.key, test.expected, got) 83 | } 84 | } 85 | } 86 | 87 | func TestGetFloat(t *testing.T) { 88 | data := newData() 89 | data.Values = map[string][]string{ 90 | "age": []string{"25.7", "33"}, 91 | "weight": []string{"42"}, 92 | } 93 | 94 | table := []struct { 95 | key string 96 | expected float64 97 | }{ 98 | { 99 | key: "age", 100 | expected: 25.7, 101 | }, 102 | { 103 | key: "weight", 104 | expected: 42.0, 105 | }, 106 | { 107 | key: "height", 108 | expected: 0.0, 109 | }, 110 | } 111 | 112 | for _, test := range table { 113 | got := data.GetFloat(test.key) 114 | if got != test.expected { 115 | t.Errorf("%s was incorrect. Expected %f, but got %f.\n", test.key, test.expected, got) 116 | } 117 | } 118 | } 119 | 120 | func TestGetBool(t *testing.T) { 121 | data := newData() 122 | data.Values = map[string][]string{ 123 | "retired": []string{"true", "false"}, 124 | "leftHanded": []string{"0"}, 125 | "collegeGraduate": []string{"1"}, 126 | } 127 | 128 | table := []struct { 129 | key string 130 | expected bool 131 | }{ 132 | { 133 | key: "retired", 134 | expected: true, 135 | }, 136 | { 137 | key: "leftHanded", 138 | expected: false, 139 | }, 140 | { 141 | key: "collegeGraduate", 142 | expected: true, 143 | }, 144 | { 145 | key: "sagittarius", 146 | expected: false, 147 | }, 148 | } 149 | 150 | for _, test := range table { 151 | got := data.GetBool(test.key) 152 | if got != test.expected { 153 | t.Errorf("%s was incorrect. Expected %t, but got %t.\n", test.key, test.expected, got) 154 | } 155 | } 156 | } 157 | 158 | func TestBytes(t *testing.T) { 159 | data := newData() 160 | data.Values = map[string][]string{ 161 | "name": []string{"bob", "bill"}, 162 | "profession": []string{"plumber"}, 163 | } 164 | 165 | table := []struct { 166 | key string 167 | expected []byte 168 | }{ 169 | { 170 | key: "name", 171 | expected: []byte("bob"), 172 | }, 173 | { 174 | key: "profession", 175 | expected: []byte("plumber"), 176 | }, 177 | { 178 | key: "favoriteColor", 179 | expected: []byte(""), 180 | }, 181 | } 182 | 183 | for _, test := range table { 184 | got := data.GetBytes(test.key) 185 | if len(got) == 0 && len(test.expected) == 0 { 186 | // do nothing 187 | // reflect.DeepEqual doesn't like when both lengths are zero, but it should pass. 188 | } else if !reflect.DeepEqual(got, test.expected) { 189 | t.Errorf("%s was incorrect. Expected %v, but got %v.\n", test.key, test.expected, got) 190 | } 191 | } 192 | } 193 | 194 | func TestCreateFromMap(t *testing.T) { 195 | m := map[string]string{ 196 | "name": "bob", 197 | "age": "25", 198 | "favoriteColor": "fuchsia", 199 | } 200 | data := CreateFromMap(m) 201 | 202 | table := []struct { 203 | key string 204 | expected string 205 | }{ 206 | { 207 | key: "name", 208 | expected: "bob", 209 | }, 210 | { 211 | key: "age", 212 | expected: "25", 213 | }, 214 | { 215 | key: "dreamJob", 216 | expected: "", 217 | }, 218 | } 219 | 220 | for _, test := range table { 221 | got := data.Get(test.key) 222 | if got != test.expected { 223 | t.Errorf("%s was incorrect. Expected %s, but got %s.\n", test.key, test.expected, got) 224 | } 225 | } 226 | } 227 | 228 | func TestGetStringsSplit(t *testing.T) { 229 | data := newData() 230 | data.Values = map[string][]string{ 231 | "children": []string{"martha,bill,jane", "adam,julia"}, 232 | "favoriteColors": []string{"blue%20green%20fuchsia"}, 233 | } 234 | 235 | table := []struct { 236 | key string 237 | delim string 238 | expecteds []string 239 | }{ 240 | { 241 | key: "children", 242 | delim: ",", 243 | expecteds: []string{"martha", "bill", "jane"}, 244 | }, 245 | { 246 | key: "favoriteColors", 247 | delim: "%20", 248 | expecteds: []string{"blue", "green", "fuchsia"}, 249 | }, 250 | { 251 | key: "height", 252 | delim: "-", 253 | expecteds: []string{}, 254 | }, 255 | } 256 | 257 | for _, test := range table { 258 | gots := data.GetStringsSplit(test.key, test.delim) 259 | if len(gots) == 0 && len(test.expecteds) == 0 { 260 | // do nothing 261 | // reflect.DeepEqual doesn't like when both lengths are zero, but it should pass. 262 | } else if !reflect.DeepEqual(gots, test.expecteds) { 263 | t.Errorf("%s was incorrect. Expected %v, but got %v.\n", test.key, test.expecteds, gots) 264 | } 265 | } 266 | } 267 | 268 | func TestParseUrlEncoded(t *testing.T) { 269 | // Construct a urlencoded form request 270 | // Add some simple key-value params to the form 271 | fieldData := map[string]string{ 272 | "name": "Bob", 273 | "age": "25", 274 | "favoriteNumber": "99.99", 275 | "leftHanded": "true", 276 | } 277 | values := url.Values{} 278 | for fieldname, value := range fieldData { 279 | values.Add(fieldname, value) 280 | } 281 | req, err := http.NewRequest("POST", "/", strings.NewReader(values.Encode())) 282 | if err != nil { 283 | t.Error(err) 284 | } 285 | req.Header.Add("Content-Type", "application/x-www-form-urlencoded") 286 | 287 | // Parse the request 288 | d, err := Parse(req) 289 | if err != nil { 290 | t.Error(err) 291 | } 292 | testBasicFormFields(t, d) 293 | } 294 | 295 | func TestParseMultipart(t *testing.T) { 296 | // Construct a multipart request 297 | body := bytes.NewBuffer([]byte{}) 298 | form := multipart.NewWriter(body) 299 | 300 | // Add some simple key-value params to the form 301 | fieldData := map[string]string{ 302 | "name": "Bob", 303 | "age": "25", 304 | "favoriteNumber": "99.99", 305 | "leftHanded": "true", 306 | } 307 | for fieldname, value := range fieldData { 308 | if err := form.WriteField(fieldname, value); err != nil { 309 | panic(err) 310 | } 311 | } 312 | 313 | // Add a file to the form 314 | testFile, err := os.Open("test_file.txt") 315 | if err != nil { 316 | t.Error(err) 317 | } 318 | defer testFile.Close() 319 | fileWriter, err := form.CreateFormFile("file", "test_file.txt") 320 | if err != nil { 321 | panic(err) 322 | } 323 | if _, err := io.Copy(fileWriter, testFile); err != nil { 324 | panic(err) 325 | } 326 | // Close the form to finish writing 327 | if err := form.Close(); err != nil { 328 | panic(err) 329 | } 330 | 331 | req, err := http.NewRequest("POST", "/", body) 332 | if err != nil { 333 | t.Error(err) 334 | } 335 | req.Header.Add("Content-Type", "multipart/form-data; boundary="+form.Boundary()) 336 | 337 | // Parse the request 338 | d, err := Parse(req) 339 | if err != nil { 340 | t.Error(err) 341 | } 342 | testBasicFormFields(t, d) 343 | 344 | // Next test that the file was parsed correctly 345 | if !d.FileExists("file") { 346 | t.Error("Expected FileExists() to return true but it returned false.") 347 | } 348 | header := d.GetFile("file") 349 | if header == nil { 350 | t.Error("Exected GetFile() to return a *multipart.FileHeader but got nil.") 351 | } 352 | if header.Filename != "test_file.txt" { 353 | t.Errorf(`Expected header.Filename to equal "test_file.txt" but got %s`, header.Filename) 354 | } 355 | file, err := header.Open() 356 | if err != nil { 357 | t.Error(err) 358 | } 359 | gotBytes, err := ioutil.ReadAll(file) 360 | if err != nil { 361 | t.Error(err) 362 | } 363 | if string(gotBytes) != "Hello!" { 364 | t.Errorf(`Expected file contents when read directly to be "Hello!" but got %s`, string(gotBytes)) 365 | } 366 | gotBytes, err = d.GetFileBytes("file") 367 | if err != nil { 368 | t.Error(err) 369 | } 370 | if string(gotBytes) != "Hello!" { 371 | t.Errorf(`Expected GetFileBytes("file") to return "Hello!" but got %s`, string(gotBytes)) 372 | } 373 | } 374 | 375 | // Used for testing multipart and urlencoded form data, since both tests expect the same data 376 | // to be present. 377 | func testBasicFormFields(t *testing.T, d *Data) { 378 | // use a table for testing 379 | fields := []struct { 380 | key string 381 | got interface{} 382 | expected interface{} 383 | }{ 384 | { 385 | key: "name", 386 | got: d.Get("name"), 387 | expected: "Bob", 388 | }, 389 | { 390 | key: "age", 391 | got: d.GetInt("age"), 392 | expected: 25, 393 | }, 394 | { 395 | key: "favoriteNumber", 396 | got: d.GetFloat("favoriteNumber"), 397 | expected: 99.99, 398 | }, 399 | { 400 | key: "leftHanded", 401 | got: d.GetBool("leftHanded"), 402 | expected: true, 403 | }, 404 | } 405 | for _, test := range fields { 406 | if !reflect.DeepEqual(test.got, test.expected) { 407 | t.Errorf("%s was incorrect. Expected %v, but got %v.\n", test.key, test.expected, test.got) 408 | } 409 | } 410 | } 411 | 412 | type jsonData struct { 413 | Name string `json:"name"` 414 | Age int `json:"age"` 415 | Cool bool `json:"cool"` 416 | Aptitude string `json:"aptitude"` 417 | Location map[string]float64 `json:"location"` 418 | Things []string `json:"things"` 419 | } 420 | 421 | func TestParseJSON(t *testing.T) { 422 | // Construct and parse a json request 423 | input := `{ 424 | "name": "bob", 425 | "age": 25, 426 | "cool": true, 427 | "aptitude": null, 428 | "location": {"latitude": 123.456, "longitude": 948.123}, 429 | "things": ["a", "b", "c"] 430 | }` 431 | body := bytes.NewBuffer([]byte(input)) 432 | req, err := http.NewRequest("POST", "/", body) 433 | if err != nil { 434 | t.Error(err) 435 | } 436 | req.Header.Set("Content-Type", "application/json") 437 | d, err := Parse(req) 438 | if err != nil { 439 | t.Error(err) 440 | } 441 | 442 | // use a table for testing 443 | table := []struct { 444 | key string 445 | got interface{} 446 | expected interface{} 447 | }{ 448 | { 449 | key: "name", 450 | got: d.Get("name"), 451 | expected: "bob", 452 | }, 453 | { 454 | key: "age", 455 | got: d.GetFloat("age"), 456 | expected: 25.0, 457 | }, 458 | { 459 | key: "cool", 460 | got: d.GetBool("cool"), 461 | expected: true, 462 | }, 463 | { 464 | key: "aptitude", 465 | got: d.Get("aptitude"), 466 | expected: "", 467 | }, 468 | } 469 | for _, test := range table { 470 | if !reflect.DeepEqual(test.got, test.expected) { 471 | t.Errorf("%s was incorrect. Expected %v, but got %v.\n", test.key, test.expected, test.got) 472 | } 473 | } 474 | 475 | // Test unmarshaling the entire body to a data structure. 476 | expected := jsonData{ 477 | Name: "bob", 478 | Age: 25, 479 | Cool: true, 480 | Aptitude: "", 481 | Location: map[string]float64{"latitude": 123.456, "longitude": 948.123}, 482 | Things: []string{"a", "b", "c"}, 483 | } 484 | var got jsonData 485 | if err := d.BindJSON(&got); err != nil { 486 | t.Error(err) 487 | } else if !reflect.DeepEqual(got, expected) { 488 | t.Errorf("Result of BindJSON was incorrect. Expected %+v, but got %+v.\n", expected, got) 489 | } 490 | 491 | // Test unmarshaling into data structures separately 492 | // For maps, both the GetMapFromJSON method and the GetAndUnmarshalJSON method 493 | expectedMap := map[string]interface{}{"latitude": 123.456, "longitude": 948.123} 494 | if got, err := d.GetMapFromJSON("location"); err != nil { 495 | t.Error(err) 496 | } else if !reflect.DeepEqual(got, expectedMap) { 497 | t.Errorf("location was incorrect. Expected %v, but got %v.\n", expectedMap, got) 498 | } 499 | gotMap := map[string]interface{}{} 500 | if err := d.GetAndUnmarshalJSON("location", &gotMap); err != nil { 501 | t.Error(err) 502 | } else if !reflect.DeepEqual(gotMap, expectedMap) { 503 | t.Errorf("location was incorrect. Expected %v, but got %v.\n", expectedMap, gotMap) 504 | } 505 | 506 | // For slices, both the GetSliceFromJSON method and the GetAndUnmarshalJSON method 507 | expectedSlice := []interface{}{"a", "b", "c"} 508 | if got, err := d.GetSliceFromJSON("things"); err != nil { 509 | t.Error(err) 510 | } else if !reflect.DeepEqual(got, expectedSlice) { 511 | t.Errorf("things was incorrect. Expected %v, but got %v.\n", expectedSlice, got) 512 | } 513 | gotSlice := []interface{}{} 514 | if err := d.GetAndUnmarshalJSON("things", &gotSlice); err != nil { 515 | t.Error(err) 516 | } else if !reflect.DeepEqual(gotSlice, expectedSlice) { 517 | t.Errorf("things was incorrect. Expected %v, but got %v.\n", expectedSlice, gotSlice) 518 | } 519 | } 520 | 521 | func ExampleParse() { 522 | // Construct a request object for example purposes only. 523 | // Typically you would be using this inside a http.HandlerFunc, 524 | // not constructing your own request. 525 | req, _ := http.NewRequest("GET", "/", nil) 526 | values := url.Values{} 527 | values.Add("name", "Bob") 528 | values.Add("age", "25") 529 | values.Add("retired", "false") 530 | req.PostForm = values 531 | req.Header.Set("Content-Type", "form-urlencoded") 532 | 533 | // Parse the form data. 534 | data, err := Parse(req) 535 | if err != nil { 536 | panic(err) 537 | } 538 | name := data.Get("name") 539 | age := data.GetInt("age") 540 | retired := data.GetBool("retired") 541 | if retired { 542 | fmt.Printf("%s is %d years old and he has retired.", name, age) 543 | } else { 544 | fmt.Printf("%s is %d years old and not yet retired.", name, age) 545 | } 546 | // Output: 547 | // Bob is 25 years old and not yet retired. 548 | } 549 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Alex Browne. All rights reserved. 2 | // Use of this source code is governed by the MIT 3 | // license, which can be found in the LICENSE file. 4 | 5 | // package forms is a lightweight, but incredibly useful library for parsing 6 | // form data from an http.Request. It supports multipart forms, url-encoded 7 | // forms, json data, and url query parameters. It also provides helper methods 8 | // for converting data into other types and a Validator object which can be 9 | // used to validate the data. Forms is framework-agnostic and works directly 10 | // with the http package. 11 | // 12 | // For the full source code, example usage, and more, visit 13 | // https://github.com/albrow/forms. 14 | // 15 | // Version 0.4.0 16 | package forms 17 | -------------------------------------------------------------------------------- /test_file.txt: -------------------------------------------------------------------------------- 1 | Hello! -------------------------------------------------------------------------------- /validator.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Alex Browne. All rights reserved. 2 | // Use of this source code is governed by the MIT 3 | // license, which can be found in the LICENSE file. 4 | 5 | package forms 6 | 7 | import ( 8 | "fmt" 9 | "path/filepath" 10 | "regexp" 11 | "strconv" 12 | "strings" 13 | ) 14 | 15 | // Validator has methods for validating its underlying Data. 16 | // A Validator stores any errors that occurred during validation, 17 | // and they can be accessed directly. In a typical workflow, you 18 | // will create a Validator from some Data, call some methods on 19 | // that validator (e.g. Require), check if the validator 20 | // has errors, then do something with the errors if it does. 21 | type Validator struct { 22 | data *Data 23 | results []*ValidationResult 24 | } 25 | 26 | // ValidationResult is returned from every validation method and can 27 | // be used to override the default field name or error message. If 28 | // you want to use the default fields and messages, simply discard 29 | // the ValidationResult. 30 | type ValidationResult struct { 31 | Ok bool 32 | field string 33 | message string 34 | } 35 | 36 | var validationOk = &ValidationResult{Ok: true} 37 | 38 | // Field changes the field name associated with the validation result. 39 | func (vr *ValidationResult) Field(field string) *ValidationResult { 40 | vr.field = field 41 | return vr 42 | } 43 | 44 | // Message changes the error message associated with the validation 45 | // result. msg should typically be a user-readable sentence, such as 46 | // "username is required." 47 | func (vr *ValidationResult) Message(msg string) *ValidationResult { 48 | vr.message = msg 49 | return vr 50 | } 51 | 52 | // AddError adds an error associated with field to the validator. msg 53 | // should typically be a user-readable sentence, such as "username 54 | // is required." 55 | func (v *Validator) AddError(field string, msg string) *ValidationResult { 56 | result := &ValidationResult{ 57 | field: field, 58 | message: msg, 59 | } 60 | v.results = append(v.results, result) 61 | return result 62 | } 63 | 64 | // HasErrors returns true iff the Validator has errors, i.e. 65 | // if any validation methods called on the Validator failed. 66 | func (v *Validator) HasErrors() bool { 67 | return len(v.results) > 0 68 | } 69 | 70 | // Messages returns the messages for all validation results for 71 | // the Validator, in order. 72 | func (v *Validator) Messages() []string { 73 | msgs := []string{} 74 | for _, vr := range v.results { 75 | msgs = append(msgs, vr.message) 76 | } 77 | return msgs 78 | } 79 | 80 | // Fields returns the fields for all validation results for 81 | // the Validator, in order. 82 | func (v *Validator) Fields() []string { 83 | fields := []string{} 84 | for _, vr := range v.results { 85 | fields = append(fields, vr.field) 86 | } 87 | return fields 88 | } 89 | 90 | // ErrorMap reutrns all the fields and error messages for 91 | // the validator in the form of a map. The keys of the map 92 | // are field names, and the values are any error messages 93 | // associated with that field name. 94 | func (v *Validator) ErrorMap() map[string][]string { 95 | errMap := map[string][]string{} 96 | for _, vr := range v.results { 97 | if _, found := errMap[vr.field]; found { 98 | errMap[vr.field] = append(errMap[vr.field], vr.message) 99 | } else { 100 | errMap[vr.field] = []string{vr.message} 101 | } 102 | } 103 | return errMap 104 | } 105 | 106 | // Require will add an error to the Validator if data.Values[field] 107 | // does not exist, is an empty string, or consists of only 108 | // whitespace. 109 | func (v *Validator) Require(field string) *ValidationResult { 110 | if strings.TrimSpace(v.data.Get(field)) == "" { 111 | return v.addRequiredError(field) 112 | } else { 113 | return validationOk 114 | } 115 | } 116 | 117 | // RequireFile will add an error to the Validator if data.Files[field] 118 | // does not exist or is an empty file 119 | func (v *Validator) RequireFile(field string) *ValidationResult { 120 | if !v.data.FileExists(field) { 121 | return v.addRequiredError(field) 122 | } 123 | bytes, err := v.data.GetFileBytes(field) 124 | if err != nil { 125 | return v.AddError(field, "Could not read file.") 126 | } 127 | if len(bytes) == 0 { 128 | return v.addFileEmptyError(field) 129 | } 130 | return validationOk 131 | } 132 | 133 | func (v *Validator) addRequiredError(field string) *ValidationResult { 134 | msg := fmt.Sprintf("%s is required.", field) 135 | return v.AddError(field, msg) 136 | } 137 | 138 | func (v *Validator) addFileEmptyError(field string) *ValidationResult { 139 | msg := fmt.Sprintf("%s is required and cannot be an empty file.", field) 140 | return v.AddError(field, msg) 141 | } 142 | 143 | // MinLength will add an error to the Validator if data.Values[field] 144 | // is shorter than length (if data.Values[field] has less than 145 | // length characters), not counting leading or trailing 146 | // whitespace. 147 | func (v *Validator) MinLength(field string, length int) *ValidationResult { 148 | val := v.data.Get(field) 149 | trimmed := strings.TrimSpace(val) 150 | if len(trimmed) < length { 151 | return v.addMinLengthError(field, length) 152 | } else { 153 | return validationOk 154 | } 155 | } 156 | 157 | func (v *Validator) addMinLengthError(field string, length int) *ValidationResult { 158 | msg := fmt.Sprintf("%s must be at least %d characters long.", field, length) 159 | return v.AddError(field, msg) 160 | } 161 | 162 | // MaxLength will add an error to the Validator if data.Values[field] 163 | // is longer than length (if data.Values[field] has more than 164 | // length characters), not counting leading or trailing 165 | // whitespace. 166 | func (v *Validator) MaxLength(field string, length int) *ValidationResult { 167 | val := v.data.Get(field) 168 | trimmed := strings.TrimSpace(val) 169 | if len(trimmed) > length { 170 | return v.addMaxLengthError(field, length) 171 | } else { 172 | return validationOk 173 | } 174 | } 175 | 176 | func (v *Validator) addMaxLengthError(field string, length int) *ValidationResult { 177 | msg := fmt.Sprintf("%s cannot be more than %d characters long.", field, length) 178 | return v.AddError(field, msg) 179 | } 180 | 181 | // LengthRange will add an error to the Validator if data.Values[field] 182 | // is shorter than min (if data.Values[field] has less than 183 | // min characters) or if data.Values[field] is longer than max 184 | // (if data.Values[field] has more than max characters), not 185 | // counting leading or trailing whitespace. 186 | func (v *Validator) LengthRange(field string, min int, max int) *ValidationResult { 187 | if val := v.data.Get(field); len(val) < min || len(val) > max { 188 | return v.addLengthRangeError(field, min, max) 189 | } else { 190 | return validationOk 191 | } 192 | } 193 | 194 | func (v *Validator) addLengthRangeError(field string, min int, max int) *ValidationResult { 195 | msg := fmt.Sprintf("%s must be between %d and %d characters long.", field, min, max) 196 | return v.AddError(field, msg) 197 | } 198 | 199 | // Equal will add an error to the Validator if data[field1] 200 | // is not equal to data[field2]. 201 | func (v *Validator) Equal(field1 string, field2 string) *ValidationResult { 202 | val1 := v.data.Get(field1) 203 | val2 := v.data.Get(field2) 204 | if val1 != val2 { 205 | return v.addEqualError(field1, field2) 206 | } else { 207 | return validationOk 208 | } 209 | } 210 | 211 | func (v *Validator) addEqualError(field1 string, field2 string) *ValidationResult { 212 | // note: "match" is a more natural colloquial term than "be equal" 213 | // not to be confused with "matching" a regular expression 214 | msg := fmt.Sprintf("%s and %s must match.", field1, field2) 215 | return v.AddError(field2, msg) 216 | } 217 | 218 | // Match will add an error to the Validator if data.Values[field] does 219 | // not match the regular expression regex. 220 | func (v *Validator) Match(field string, regex *regexp.Regexp) *ValidationResult { 221 | if !regex.MatchString(v.data.Get(field)) { 222 | return v.addMatchError(field) 223 | } else { 224 | return validationOk 225 | } 226 | } 227 | 228 | // MatchEmail will add an error to the Validator if data.Values[field] 229 | // does not match the formatting expected of an email. 230 | func (v *Validator) MatchEmail(field string) *ValidationResult { 231 | regex := regexp.MustCompile("^[\\w!#$%&'*+/=?^_`{|}~-]+(?:\\.[\\w!#$%&'*+/=?^_`{|}~-]+)*@(?:[\\w](?:[\\w-]*[\\w])?\\.)+[a-zA-Z0-9](?:[\\w-]*[\\w])?$") 232 | return v.Match(field, regex) 233 | } 234 | 235 | func (v *Validator) addMatchError(field string) *ValidationResult { 236 | msg := fmt.Sprintf("%s must be correctly formatted.", field) 237 | return v.AddError(field, msg) 238 | } 239 | 240 | // TypeInt will add an error to the Validator if the first 241 | // element of data.Values[field] cannot be converted to an int. 242 | func (v *Validator) TypeInt(field string) *ValidationResult { 243 | if _, err := strconv.Atoi(v.data.Get(field)); err != nil { 244 | return v.addTypeError(field, "integer") 245 | } else { 246 | return validationOk 247 | } 248 | } 249 | 250 | // TypeFloat will add an error to the Validator if the first 251 | // element of data.Values[field] cannot be converted to an float64. 252 | func (v *Validator) TypeFloat(field string) *ValidationResult { 253 | if _, err := strconv.ParseFloat(v.data.Get(field), 64); err != nil { 254 | // note: "number" is a more natural colloquial term than "float" 255 | return v.addTypeError(field, "number") 256 | } else { 257 | return validationOk 258 | } 259 | } 260 | 261 | // TypeBool will add an error to the Validator if the first 262 | // element of data.Values[field] cannot be converted to a bool. 263 | func (v *Validator) TypeBool(field string) *ValidationResult { 264 | if _, err := strconv.ParseBool(v.data.Get(field)); err != nil { 265 | // note: "true or false" is a more natural colloquial term than "bool" 266 | return v.addTypeError(field, "true or false") 267 | } else { 268 | return validationOk 269 | } 270 | } 271 | 272 | func (v *Validator) addTypeError(field string, typ string) *ValidationResult { 273 | article := "a" 274 | if strings.Contains("aeiou", string(typ[0])) { 275 | article = "an" 276 | } 277 | msg := fmt.Sprintf("%s must be %s %s", field, article, typ) 278 | return v.AddError(field, msg) 279 | } 280 | 281 | // Greater will add an error to the Validator if the first 282 | // element of data.Values[field] is not greater than value or if the first 283 | // element of data.Values[field] cannot be converted to a number. 284 | func (v *Validator) Greater(field string, value float64) *ValidationResult { 285 | return v.inequality(field, value, greater, "greater than") 286 | } 287 | 288 | // GreaterOrEqual will add an error to the Validator if the first 289 | // element of data.Values[field] is not greater than or equal to value or if 290 | // the first element of data.Values[field] cannot be converted to a number. 291 | func (v *Validator) GreaterOrEqual(field string, value float64) *ValidationResult { 292 | return v.inequality(field, value, greaterOrEqual, "greater than or equal to") 293 | } 294 | 295 | // Less will add an error to the Validator if the first 296 | // element of data.Values[field] is not less than value or if the first 297 | // element of data.Values[field] cannot be converted to a number. 298 | func (v *Validator) Less(field string, value float64) *ValidationResult { 299 | return v.inequality(field, value, less, "less than") 300 | } 301 | 302 | // LessOrEqual will add an error to the Validator if the first 303 | // element of data.Values[field] is not less than or equal to value or if 304 | // the first element of data.Values[field] cannot be converted to a number. 305 | func (v *Validator) LessOrEqual(field string, value float64) *ValidationResult { 306 | return v.inequality(field, value, lessOrEqual, "less than or equal to") 307 | } 308 | 309 | type conditional func(given float64, target float64) bool 310 | 311 | var greater conditional = func(given float64, target float64) bool { 312 | return given > target 313 | } 314 | 315 | var greaterOrEqual conditional = func(given float64, target float64) bool { 316 | return given >= target 317 | } 318 | 319 | var less conditional = func(given float64, target float64) bool { 320 | return given < target 321 | } 322 | 323 | var lessOrEqual conditional = func(given float64, target float64) bool { 324 | return given <= target 325 | } 326 | 327 | func (v *Validator) inequality(field string, value float64, condition conditional, explanation string) *ValidationResult { 328 | if valFloat, err := strconv.ParseFloat(v.data.Get(field), 64); err != nil { 329 | // note: "number" is a more natural colloquial term than "float" 330 | return v.addTypeError(field, "number") 331 | } else { 332 | if !condition(valFloat, value) { 333 | return v.AddError(field, fmt.Sprintf("%s must be %s %f.", field, explanation, value)) 334 | } else { 335 | return validationOk 336 | } 337 | } 338 | } 339 | 340 | // AcceptFileExts will add an error to the Validator if the extension 341 | // of the file identified by field is not in exts. exts should be one ore more 342 | // allowed file extensions, not including the preceding ".". If the file does not 343 | // exist, it does not add an error to the Validator. 344 | func (v *Validator) AcceptFileExts(field string, exts ...string) *ValidationResult { 345 | if !v.data.FileExists(field) { 346 | return validationOk 347 | } 348 | header := v.data.GetFile(field) 349 | gotExt := filepath.Ext(header.Filename) 350 | for _, ext := range exts { 351 | if ext == gotExt[1:] { 352 | return validationOk 353 | } 354 | } 355 | return v.addFileExtError(field, gotExt, exts...) 356 | } 357 | 358 | func (v *Validator) addFileExtError(field string, gotExt string, allowedExts ...string) *ValidationResult { 359 | msg := fmt.Sprintf("The file extension %s is not allowed. Allowed extensions include: ", gotExt) 360 | 361 | // Append each allowed extension to the message, in a human-readable list 362 | // e.g. "x, y, and z" 363 | for i, ext := range allowedExts { 364 | if i == len(allowedExts)-1 { 365 | // special case for the last element 366 | switch len(allowedExts) { 367 | case 1: 368 | msg += ext 369 | default: 370 | msg += fmt.Sprintf("and %s", ext) 371 | } 372 | } else { 373 | // default case for middle elements 374 | // we only reach here if there is at least 375 | // one element 376 | switch len(allowedExts) { 377 | case 2: 378 | msg += fmt.Sprintf("%s ", ext) 379 | default: 380 | msg += fmt.Sprintf("%s, ", ext) 381 | } 382 | } 383 | } 384 | return v.AddError(field, msg) 385 | } 386 | -------------------------------------------------------------------------------- /validator_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Alex Browne. All rights reserved. 2 | // Use of this source code is governed by the MIT 3 | // license, which can be found in the LICENSE file. 4 | 5 | package forms 6 | 7 | import ( 8 | "bytes" 9 | "fmt" 10 | "mime/multipart" 11 | "net/http" 12 | "net/url" 13 | "regexp" 14 | "strings" 15 | "testing" 16 | ) 17 | 18 | func TestCustomMessage(t *testing.T) { 19 | data := newData() 20 | val := data.Validator() 21 | customMsg := "You forgot to include name!" 22 | val.Require("name").Message(customMsg) 23 | 24 | if !val.HasErrors() { 25 | t.Error("Expected an error but got none.") 26 | } else if val.Messages()[0] != customMsg { 27 | t.Errorf("Expected custom error message \"%s\" but got \"%s\"", customMsg, val.Messages()[0]) 28 | } 29 | } 30 | 31 | func TestCustomField(t *testing.T) { 32 | data := newData() 33 | val := data.Validator() 34 | customField := "person.name" 35 | val.Require("name").Field(customField) 36 | 37 | if !val.HasErrors() { 38 | t.Error("Expected an error but got none.") 39 | } else if val.Fields()[0] != customField { 40 | t.Errorf("Expected custom field name \"%s\" but got \"%s\"", customField, val.Fields()[0]) 41 | } 42 | } 43 | 44 | func TestRequire(t *testing.T) { 45 | data := newData() 46 | data.Add("name", "Bob") 47 | data.Add("age", "25") 48 | data.Add("color", "") 49 | 50 | val := data.Validator() 51 | val.Require("name") 52 | val.Require("age") 53 | if val.HasErrors() { 54 | t.Errorf("Expected no errors but got errors: %v", val.Messages()) 55 | } 56 | 57 | val.Require("color") 58 | val.Require("a") 59 | if len(val.Messages()) != 2 { 60 | t.Errorf("Expected 2 validation errors but got %d.", len(val.Messages())) 61 | } 62 | } 63 | 64 | func TestRequireFile(t *testing.T) { 65 | data := newData() 66 | val := data.Validator() 67 | val.RequireFile("file") 68 | if !val.HasErrors() { 69 | t.Error("Expected val to have errors because file was not included but got none.") 70 | } 71 | 72 | fileHeader, err := createTestFileHeader("test_file.txt", []byte{}) 73 | if err != nil { 74 | t.Error(err) 75 | } 76 | data.AddFile("file", fileHeader) 77 | val = data.Validator() 78 | val.RequireFile("file") 79 | if len(val.ErrorMap()) != 1 { 80 | t.Errorf("Expected val to have exactly one error because file was empty but got %d.", len(val.ErrorMap())) 81 | } else { 82 | msg := val.ErrorMap()["file"][0] 83 | if !strings.Contains(msg, "empty") { 84 | t.Errorf("Expected message to say file was empty but got: %s.", msg) 85 | } 86 | } 87 | 88 | // Create the multipart file header 89 | // Write actual content to it this time 90 | fileHeaderWithContent, err := createTestFileHeader("test_file.txt", []byte("Hello!\n")) 91 | if err != nil { 92 | t.Error(err) 93 | } 94 | data.AddFile("file", fileHeaderWithContent) 95 | val = data.Validator() 96 | val.RequireFile("file") 97 | if val.HasErrors() { 98 | t.Errorf("Expected val to have no errors but got: %v\n", val.ErrorMap()) 99 | } 100 | } 101 | 102 | func createTestFileHeader(filename string, content []byte) (*multipart.FileHeader, error) { 103 | body := bytes.NewBuffer([]byte{}) 104 | partWriter := multipart.NewWriter(body) 105 | fileWriter, err := partWriter.CreateFormFile("file", filename) 106 | if err != nil { 107 | return nil, err 108 | } 109 | if _, err := fileWriter.Write(content); err != nil { 110 | return nil, err 111 | } 112 | if err := partWriter.Close(); err != nil { 113 | return nil, err 114 | } 115 | req, err := http.NewRequest("POST", "/", body) 116 | if err != nil { 117 | return nil, err 118 | } 119 | req.Header.Add("Content-Type", "multipart/form-data; boundary="+partWriter.Boundary()) 120 | _, fileHeader, err := req.FormFile("file") 121 | if err != nil { 122 | return nil, err 123 | } 124 | return fileHeader, nil 125 | } 126 | 127 | func TestMinLength(t *testing.T) { 128 | data := newData() 129 | data.Add("one", "A") 130 | data.Add("three", "ABC") 131 | data.Add("five", "ABC") 132 | 133 | val := data.Validator() 134 | val.MinLength("one", 1) 135 | val.MinLength("three", 3) 136 | if val.HasErrors() { 137 | t.Error("Expected no errors but got errors: %v", val.Messages()) 138 | } 139 | 140 | val.MinLength("five", 5) 141 | if len(val.Messages()) != 1 { 142 | t.Error("Expected a validation error.") 143 | } 144 | } 145 | 146 | func TestMaxLength(t *testing.T) { 147 | data := newData() 148 | data.Add("one", "A") 149 | data.Add("three", "ABC") 150 | data.Add("five", "ABCDEF") 151 | val := data.Validator() 152 | val.MaxLength("one", 1) 153 | val.MaxLength("three", 3) 154 | if val.HasErrors() { 155 | t.Errorf("Expected no errors but got errors: %v", val.Messages()) 156 | } 157 | 158 | val.MaxLength("five", 5) 159 | if len(val.Messages()) != 1 { 160 | t.Error("Expected a validation error.") 161 | } 162 | } 163 | 164 | func TestLengthRange(t *testing.T) { 165 | data := newData() 166 | data.Add("one-two", "a") 167 | data.Add("two-three", "abc") 168 | data.Add("three-four", "ab") 169 | data.Add("four-five", "abcdef") 170 | 171 | val := data.Validator() 172 | val.LengthRange("one-two", 1, 2) 173 | val.LengthRange("two-three", 2, 3) 174 | if val.HasErrors() { 175 | t.Errorf("Expected no errors but got errors: %v", val.Messages()) 176 | } 177 | 178 | val.LengthRange("three-four", 3, 4) 179 | val.LengthRange("four-five", 4, 5) 180 | if len(val.Messages()) != 2 { 181 | t.Errorf("Expected 2 validation errors but got %d.", len(val.Messages())) 182 | } 183 | } 184 | 185 | func TestEqual(t *testing.T) { 186 | data := newData() 187 | data.Add("password", "password123") 188 | data.Add("confirmPassword", "password123") 189 | data.Add("nonMatching", "password1234") 190 | 191 | val := data.Validator() 192 | val.Equal("password", "confirmPassword") 193 | if val.HasErrors() { 194 | t.Errorf("Expected no errors but got errors: %v", val.Messages()) 195 | } 196 | 197 | val.Equal("password", "nonMatching") 198 | if len(val.Messages()) != 1 { 199 | t.Error("Expected a validation error.") 200 | } 201 | } 202 | 203 | func TestMatch(t *testing.T) { 204 | data := newData() 205 | data.Add("numeric", "123") 206 | data.Add("alpha", "abc") 207 | data.Add("not-numeric", "123a") 208 | data.Add("not-alpha", "abc1") 209 | 210 | val := data.Validator() 211 | numericRegex := regexp.MustCompile("^[0-9]+$") 212 | alphaRegex := regexp.MustCompile("^[a-zA-Z]+$") 213 | val.Match("numeric", numericRegex) 214 | val.Match("alpha", alphaRegex) 215 | if val.HasErrors() { 216 | t.Errorf("Expected no errors but got errors: %v", val.Messages()) 217 | } 218 | 219 | val.Match("not-numeric", numericRegex) 220 | val.Match("not-alpha", alphaRegex) 221 | if len(val.Messages()) != 2 { 222 | t.Errorf("Expected 2 validation errors but got %d.", len(val.Messages())) 223 | } 224 | } 225 | 226 | func TestMatchEmail(t *testing.T) { 227 | data := newData() 228 | data.Add("email", "abc@example.com") 229 | data.Add("not-email", "abc.com") 230 | val := data.Validator() 231 | val.MatchEmail("email") 232 | if val.HasErrors() { 233 | t.Errorf("Expected no errors but got errors: %v", val.Messages()) 234 | } 235 | 236 | val.MatchEmail("not-email") 237 | val.MatchEmail("nothing") 238 | if len(val.Messages()) != 2 { 239 | t.Errorf("Expected 2 validation errors but got %d.", len(val.Messages())) 240 | } 241 | } 242 | 243 | func TestTypeInt(t *testing.T) { 244 | data := newData() 245 | data.Add("age", "23") 246 | data.Add("weight", "not a number") 247 | val := data.Validator() 248 | val.TypeInt("age") 249 | if val.HasErrors() { 250 | t.Errorf("Expected no errors but got errors: %v", val.Messages()) 251 | } 252 | 253 | val.TypeInt("weight") 254 | if len(val.Messages()) != 1 { 255 | t.Errorf("Expected 1 validation errors but got %d.", len(val.Messages())) 256 | } 257 | } 258 | 259 | func TestTypeFloat(t *testing.T) { 260 | data := newData() 261 | data.Add("age", "23") 262 | data.Add("weight", "155.8") 263 | data.Add("favoriteNumber", "not a number") 264 | val := data.Validator() 265 | val.TypeFloat("age") 266 | val.TypeFloat("weight") 267 | if val.HasErrors() { 268 | t.Errorf("Expected no errors but got errors: %v", val.Messages()) 269 | } 270 | 271 | val.TypeFloat("favoriteNumber") 272 | if len(val.Messages()) != 1 { 273 | t.Errorf("Expected 1 validation errors but got %d.", len(val.Messages())) 274 | } 275 | } 276 | 277 | func TestTypeBool(t *testing.T) { 278 | data := newData() 279 | data.Add("cool", "true") 280 | data.Add("fun", "false") 281 | data.Add("yes", "not a boolean") 282 | val := data.Validator() 283 | val.TypeBool("cool") 284 | val.TypeBool("fun") 285 | if val.HasErrors() { 286 | t.Errorf("Expected no errors but got errors: %v", val.Messages()) 287 | } 288 | 289 | val.TypeBool("yes") 290 | if len(val.Messages()) != 1 { 291 | t.Errorf("Expected 1 validation errors but got %d.", len(val.Messages())) 292 | } 293 | } 294 | 295 | func TestGreater(t *testing.T) { 296 | data := newData() 297 | data.Add("one", "1") 298 | data.Add("three", "3") 299 | val := data.Validator() 300 | val.Greater("one", -1) 301 | val.Greater("three", 2) 302 | if val.HasErrors() { 303 | t.Errorf("Expected no errors but got errors: %v", val.Messages()) 304 | } 305 | 306 | val.Greater("one", 1) 307 | val.Greater("three", 4) 308 | if len(val.Messages()) != 2 { 309 | t.Errorf("Expected 2 validation errors but got %d.", len(val.Messages())) 310 | } 311 | } 312 | 313 | func TestGreaterOrEqual(t *testing.T) { 314 | data := newData() 315 | data.Add("one", "1") 316 | data.Add("three", "3") 317 | val := data.Validator() 318 | val.GreaterOrEqual("one", 1) 319 | val.GreaterOrEqual("three", 2) 320 | if val.HasErrors() { 321 | t.Errorf("Expected no errors but got errors: %v", val.Messages()) 322 | } 323 | 324 | val.GreaterOrEqual("one", 2) 325 | val.GreaterOrEqual("three", 4) 326 | if len(val.Messages()) != 2 { 327 | t.Errorf("Expected 2 validation errors but got %d.", len(val.Messages())) 328 | } 329 | } 330 | 331 | func TestLess(t *testing.T) { 332 | data := newData() 333 | data.Add("one", "1") 334 | data.Add("three", "3") 335 | val := data.Validator() 336 | val.Less("one", 2) 337 | val.Less("three", 4) 338 | if val.HasErrors() { 339 | t.Errorf("Expected no errors but got errors: %v", val.Messages()) 340 | } 341 | 342 | val.Less("one", -1) 343 | val.Less("three", 3) 344 | if len(val.Messages()) != 2 { 345 | t.Errorf("Expected 2 validation errors but got %d.", len(val.Messages())) 346 | } 347 | } 348 | 349 | func TestLessOrEqual(t *testing.T) { 350 | data := newData() 351 | data.Add("one", "1") 352 | data.Add("three", "3") 353 | val := data.Validator() 354 | val.LessOrEqual("one", 1) 355 | val.LessOrEqual("three", 4) 356 | if val.HasErrors() { 357 | t.Errorf("Expected no errors but got errors: %v", val.Messages()) 358 | } 359 | 360 | val.LessOrEqual("one", -1) 361 | val.LessOrEqual("three", 2) 362 | if len(val.Messages()) != 2 { 363 | t.Errorf("Expected 2 validation errors but got %d.", len(val.Messages())) 364 | } 365 | } 366 | 367 | func TestAcceptFileExts(t *testing.T) { 368 | data := newData() 369 | fileHeader, err := createTestFileHeader("test_file.txt", []byte{}) 370 | if err != nil { 371 | t.Error(err) 372 | } 373 | data.AddFile("file", fileHeader) 374 | val := data.Validator() 375 | val.AcceptFileExts("file", "txt") 376 | if val.HasErrors() { 377 | t.Errorf("Expected no errors for the single allowed ext case but got %v\n", val.ErrorMap()) 378 | } 379 | val = data.Validator() 380 | val.AcceptFileExts("file", "txt", "jpg") 381 | if val.HasErrors() { 382 | t.Errorf("Expected no errors for the multiple allowed ext case but got %v\n", val.ErrorMap()) 383 | } 384 | val = data.Validator() 385 | val.AcceptFileExts("foo", "txt") 386 | if val.HasErrors() { 387 | t.Errorf("Expected no errors for the not-provided field case but got %v\n", val.ErrorMap()) 388 | } 389 | 390 | // use a table-driven test here 391 | table := []struct { 392 | allowedExts []string 393 | expectedMessageContains []string 394 | }{ 395 | { 396 | allowedExts: []string{"jpg"}, 397 | expectedMessageContains: []string{ 398 | "The file extension .txt is not allowed.", 399 | "include: jpg", 400 | }, 401 | }, 402 | { 403 | allowedExts: []string{"jpg", "png"}, 404 | expectedMessageContains: []string{ 405 | "The file extension .txt is not allowed.", 406 | "include: jpg and png", 407 | }, 408 | }, 409 | { 410 | allowedExts: []string{"jpg", "png", "gif"}, 411 | expectedMessageContains: []string{ 412 | "The file extension .txt is not allowed.", 413 | "include: jpg, png, and gif", 414 | }, 415 | }, 416 | } 417 | for i, test := range table { 418 | val = data.Validator() 419 | val.AcceptFileExts("file", test.allowedExts...) 420 | if !val.HasErrors() { 421 | t.Errorf("Expected val to have errors for test case %d but got none.", i) 422 | continue // avoid index out-of-bounds error in proceeding lines 423 | } 424 | gotMsg := val.ErrorMap()["file"][0] 425 | for _, expectedMsg := range test.expectedMessageContains { 426 | if !strings.Contains(gotMsg, expectedMsg) { 427 | t.Errorf(`Expected error in case %d to contain "%s" but it did not.%sGot: "%s"`, i, expectedMsg, "\n", gotMsg) 428 | } 429 | } 430 | } 431 | } 432 | 433 | func ExampleValidator() { 434 | // Construct a request object for example purposes only. 435 | // Typically you would be using this inside a http.HandlerFunc, 436 | // not constructing your own request. 437 | req, _ := http.NewRequest("GET", "/", nil) 438 | values := url.Values{} 439 | values.Add("name", "Bob") 440 | values.Add("age", "25") 441 | req.PostForm = values 442 | req.Header.Set("Content-Type", "form-urlencoded") 443 | 444 | // Parse the form data. 445 | data, _ := Parse(req) 446 | 447 | // Validate the data. 448 | val := data.Validator() 449 | val.Require("name") 450 | val.MinLength("name", 4) 451 | val.Require("age") 452 | 453 | // Here's how you can change the error message or field name 454 | val.Require("retired").Field("retired_status").Message("Must specify whether or not person is retired.") 455 | 456 | // Check for validation errors and print them if there are any. 457 | if val.HasErrors() { 458 | fmt.Printf("%#v\n", val.Messages()) 459 | } 460 | 461 | // Output: 462 | // []string{"name must be at least 4 characters long.", "Must specify whether or not person is retired."} 463 | } 464 | --------------------------------------------------------------------------------