├── .gitignore ├── .travis.yml ├── LICENSE.MD ├── README.MD ├── go.mod ├── parse.go ├── parse_test.go ├── parsers.go ├── parsers_test.go ├── setters.go ├── setters_internal_test.go └── setters_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | /Gopkg.lock 3 | /coverage.txt 4 | .idea/ 5 | go.sum -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - "1.13.x" 5 | 6 | go_import_path: github.com/tomwright/queryparam 7 | 8 | script: go test -race ./ -coverprofile=coverage.txt -covermode=atomic 9 | 10 | after_success: 11 | - bash <(curl -s https://codecov.io/bash) 12 | 13 | install: 14 | - go mod vendor 15 | -------------------------------------------------------------------------------- /LICENSE.MD: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2018 Tom Wright. http://www.tomwright.me 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. 22 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # Query Param 2 | 3 | [![Build Status](https://travis-ci.org/TomWright/queryparam.svg?branch=master)](https://travis-ci.org/TomWright/queryparam) 4 | [![codecov](https://codecov.io/gh/TomWright/queryparam/branch/master/graph/badge.svg)](https://codecov.io/gh/TomWright/queryparam) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/TomWright/queryparam)](https://goreportcard.com/report/github.com/TomWright/queryparam) 6 | [![Documentation](https://godoc.org/github.com/TomWright/queryparam?status.svg)](https://godoc.org/github.com/TomWright/queryparam) 7 | [![Mentioned in Awesome Go](https://awesome.re/mentioned-badge.svg)](https://github.com/avelino/awesome-go) 8 | 9 | Stop accessing query strings and repeatedly parsing them into your preferred values - `queryparam` can do that for you! 10 | 11 | ## Installation 12 | 13 | ``` 14 | go get -u github.com/tomwright/queryparam 15 | ``` 16 | 17 | ## Usage 18 | 19 | Please use the latest major version. This requires the `/v4` at the end of the import as per the go mod documentation. 20 | 21 | ``` 22 | import github.com/tomwright/queryparam/v4 23 | ``` 24 | 25 | ## Examples 26 | 27 | For examples see [godoc examples](https://godoc.org/github.com/TomWright/queryparam#example-Parse). 28 | 29 | ## Quickstart 30 | 31 | Transform your http handlers from this... 32 | ``` 33 | func searchUsersHandler(r *http.Request, rw http.ResponseWriter) { 34 | values := r.URL.Query() 35 | 36 | userIDs := make([]string, 0) 37 | if userIDsStr := values.Get("id"); userIDsStr != "" { 38 | userIDs = strings.Split(userIDsStr, ",") 39 | } 40 | teamIDs := make([]string, 0) 41 | if teamIDsStr := values.Get("team-id"); teamIDsStr != "" { 42 | teamIDs = strings.Split(teamIDsStr, ",") 43 | } 44 | mustBeActive := false 45 | switch strings.ToLower(values.Get("must-be-active")) { 46 | case "true", "yes", "y": 47 | mustBeActive = true 48 | case "": 49 | break 50 | default: 51 | // unhandled bool value... handle as 400 or ignore to default as false 52 | } 53 | createdAfter := time.Time{} 54 | if createdAfterStr := values.Get("must-be-active"); createdAfterStr != "" { 55 | var err error 56 | createdAfter, err = time.Parse(time.RFC3339, createdAfterStr) 57 | if err != nil { 58 | // bad time value 59 | } 60 | } 61 | 62 | users, err := searchUsers(userIDs, teamIDs, mustBeActive, createdAfter) 63 | 64 | // handle users and err... 65 | } 66 | ``` 67 | 68 | To this... 69 | ``` 70 | func searchUsersHandler(r *http.Request, rw http.ResponseWriter) { 71 | req := struct { 72 | UserIDs []string `queryparam:"id"` 73 | TeamIDs []string `queryparam:"team-id"` 74 | MustBeActive bool `queryparam:"must-be-active"` 75 | CreatedAfter time.Time `queryparam:"created-after"` 76 | }{} 77 | 78 | err := queryparam.Parse(r.URL.Query(), &req) 79 | switch err { 80 | case nil: 81 | break 82 | case queryparam.ErrInvalidBoolValue: // only necessary if the request contains a bool value 83 | // they have entered a non-bool value. 84 | // this can be handled this as a 400 or ignored to default to false. 85 | return 86 | default: 87 | // something went wrong when parsing a value. 88 | // send a 500. 89 | return 90 | } 91 | 92 | users, err := searchUsers(req.UserIDs, req.TeamIDs, req.MustBeActive, req.CreatedAfter) 93 | 94 | // handle users and err... 95 | } 96 | ``` 97 | 98 | ## Types 99 | 100 | By default `queryparam` can parse the following types. 101 | 102 | - `string` 103 | - `[]string` 104 | - `int` 105 | - `int32` 106 | - `int64` 107 | - `float32` 108 | - `float64` 109 | - `bool` 110 | - `time.Time` 111 | - `queryparam.Present` 112 | 113 | ### Custom Types 114 | 115 | You can add custom type parsers and setters with the following: 116 | 117 | ``` 118 | // your custom type. 119 | type MyCustomStringType string 120 | 121 | // add a value parser for the custom type. 122 | queryparam.DefaultParser.ValueParsers[reflect.TypeOf(MyCustomStringType(""))] = func(value string, _ string) (reflect.Value, error) { 123 | return reflect.ValueOf(MyCustomStringType(value)), nil 124 | } 125 | ``` 126 | 127 | You can override the default value parsers in a similar manner... 128 | ``` 129 | queryparam.DefaultParser.ValueParsers[reflect.TypeOf("")] = func(value string, _ string) (reflect.Value, error) { 130 | // my custom string parser 131 | return reflect.ValueOf(value), nil 132 | } 133 | ``` 134 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tomwright/queryparam/v4 2 | 3 | go 1.13 4 | -------------------------------------------------------------------------------- /parse.go: -------------------------------------------------------------------------------- 1 | package queryparam 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net/url" 7 | "reflect" 8 | ) 9 | 10 | var ( 11 | // ErrNonPointerTarget is returned when the given interface does not represent a pointer 12 | ErrNonPointerTarget = errors.New("invalid target. must be a non nil pointer") 13 | // ErrInvalidURLValues is returned when the given *url.URL is nil 14 | ErrInvalidURLValues = errors.New("invalid url provided") 15 | // ErrUnhandledFieldType is returned when a struct property is tagged but has an unhandled type. 16 | ErrUnhandledFieldType = errors.New("unhandled field type") 17 | // ErrInvalidTag is returned when the tag value is invalid 18 | ErrInvalidTag = errors.New("invalid tag") 19 | ) 20 | 21 | // ErrInvalidParameterValue is an error adds extra context to a parser error. 22 | type ErrInvalidParameterValue struct { 23 | Err error 24 | Parameter string 25 | Field string 26 | Value string 27 | Type reflect.Type 28 | } 29 | 30 | // Error returns the full error message. 31 | func (e *ErrInvalidParameterValue) Error() string { 32 | return fmt.Sprintf("invalid parameter value for field %s (%s) from parameter %s (%s): %s", e.Field, e.Type, e.Parameter, e.Value, e.Err.Error()) 33 | } 34 | 35 | // Unwrap returns the wrapped error. 36 | func (e *ErrInvalidParameterValue) Unwrap() error { 37 | return e.Err 38 | } 39 | 40 | // ErrCannotSetValue is an error adds extra context to a setter error. 41 | type ErrCannotSetValue struct { 42 | Err error 43 | Parameter string 44 | Field string 45 | Value string 46 | Type reflect.Type 47 | ParsedValue reflect.Value 48 | } 49 | 50 | // Error returns the full error message. 51 | func (e *ErrCannotSetValue) Error() string { 52 | return fmt.Sprintf("cannot set value for field %s (%s) from parameter %s (%s - %v): %s", e.Field, e.Type, e.Parameter, e.Value, e.ParsedValue, e.Err.Error()) 53 | } 54 | 55 | // Unwrap returns the wrapped error. 56 | func (e *ErrCannotSetValue) Unwrap() error { 57 | return e.Err 58 | } 59 | 60 | // Present allows you to determine whether or not a query parameter was present in a request. 61 | type Present bool 62 | 63 | // DefaultParser is a default parser. 64 | var DefaultParser = &Parser{ 65 | Tag: "queryparam", 66 | DelimiterTag: "queryparamdelim", 67 | Delimiter: ",", 68 | ValueParsers: DefaultValueParsers(), 69 | ValueSetters: DefaultValueSetters(), 70 | } 71 | 72 | // Parser is used to parse a URL. 73 | type Parser struct { 74 | // Tag is the name of the struct tag where the query parameter name is set. 75 | Tag string 76 | // Delimiter is the name of the struct tag where a string delimiter override is set. 77 | DelimiterTag string 78 | // Delimiter is the default string delimiter. 79 | Delimiter string 80 | // ValueParsers is a map[reflect.Type]ValueParser that defines how we parse query 81 | // parameters based on the destination variable type. 82 | ValueParsers map[reflect.Type]ValueParser 83 | // ValueSetters is a map[reflect.Type]ValueSetter that defines how we set values 84 | // onto target variables. 85 | ValueSetters map[reflect.Type]ValueSetter 86 | } 87 | 88 | // ValueParser is a func used to parse a value. 89 | type ValueParser func(value string, delimiter string) (reflect.Value, error) 90 | 91 | // ValueSetter is a func used to set a value on a target variable. 92 | type ValueSetter func(value reflect.Value, target reflect.Value) error 93 | 94 | // FieldDelimiter returns a delimiter to be used with the given field. 95 | func (p *Parser) FieldDelimiter(field reflect.StructField) string { 96 | if customDelimiter := field.Tag.Get(p.DelimiterTag); customDelimiter != "" { 97 | return customDelimiter 98 | } 99 | return p.Delimiter 100 | } 101 | 102 | // Parse attempts to parse query parameters from the specified URL and store any found values 103 | // into the given target interface. 104 | func (p *Parser) Parse(urlValues url.Values, target interface{}) error { 105 | if urlValues == nil { 106 | return ErrInvalidURLValues 107 | } 108 | targetValue := reflect.ValueOf(target) 109 | if targetValue.Kind() != reflect.Ptr || targetValue.IsNil() { 110 | return ErrNonPointerTarget 111 | } 112 | 113 | targetElement := targetValue.Elem() 114 | targetType := targetElement.Type() 115 | 116 | for i := 0; i < targetType.NumField(); i++ { 117 | if err := p.ParseField(targetType.Field(i), targetElement.Field(i), urlValues); err != nil { 118 | return err 119 | } 120 | } 121 | return nil 122 | } 123 | 124 | // ParseField parses the given field and sets the given value on the target. 125 | func (p *Parser) ParseField(field reflect.StructField, value reflect.Value, urlValues url.Values) error { 126 | queryParameterName, ok := field.Tag.Lookup(p.Tag) 127 | if !ok { 128 | return nil 129 | } 130 | if queryParameterName == "" { 131 | return fmt.Errorf("missing tag value for field: %s: %w", field.Name, ErrInvalidTag) 132 | } 133 | queryParameterValue := urlValues.Get(queryParameterName) 134 | 135 | valueParser, ok := p.ValueParsers[field.Type] 136 | if !ok { 137 | return fmt.Errorf("%w: %s: %v", ErrUnhandledFieldType, field.Name, field.Type.String()) 138 | } 139 | 140 | parsedValue, err := valueParser(queryParameterValue, p.FieldDelimiter(field)) 141 | if err != nil { 142 | return &ErrInvalidParameterValue{ 143 | Err: err, 144 | Value: queryParameterValue, 145 | Parameter: queryParameterName, 146 | Type: field.Type, 147 | Field: field.Name, 148 | } 149 | } 150 | 151 | valueSetter, ok := p.ValueSetters[field.Type] 152 | if !ok { 153 | valueSetter, ok = p.ValueSetters[GenericType] 154 | } 155 | if !ok { 156 | return &ErrCannotSetValue{ 157 | Err: ErrUnhandledFieldType, 158 | Value: queryParameterValue, 159 | ParsedValue: parsedValue, 160 | Parameter: queryParameterName, 161 | Type: field.Type, 162 | Field: field.Name, 163 | } 164 | } 165 | 166 | if err := valueSetter(parsedValue, value); err != nil { 167 | return &ErrCannotSetValue{ 168 | Err: err, 169 | Value: queryParameterValue, 170 | ParsedValue: parsedValue, 171 | Parameter: queryParameterName, 172 | Type: field.Type, 173 | Field: field.Name, 174 | } 175 | } 176 | 177 | return nil 178 | } 179 | 180 | // Parse attempts to parse query parameters from the specified URL and store any found values 181 | // into the given target interface. 182 | func Parse(urlValues url.Values, target interface{}) error { 183 | return DefaultParser.Parse(urlValues, target) 184 | } 185 | -------------------------------------------------------------------------------- /parse_test.go: -------------------------------------------------------------------------------- 1 | package queryparam_test 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/tomwright/queryparam/v4" 7 | "math" 8 | "net/url" 9 | "reflect" 10 | "testing" 11 | "time" 12 | ) 13 | 14 | var urlValues = url.Values{} 15 | var urlValuesNameAge = url.Values{ 16 | "name": []string{"tom"}, 17 | "age": []string{"26"}, 18 | } 19 | 20 | // ExampleParse creates a dummy http request and parses the data into a struct 21 | func ExampleParse() { 22 | urlValues := url.Values{} 23 | urlValues.Set("name", "Tom") 24 | urlValues.Set("names", "Tom,Jim,Frank") // list of names separated by , 25 | urlValues.Set("dash-names", "Tom-Jim-Frank") // list of names separated by - 26 | urlValues.Set("age", "123") 27 | urlValues.Set("age32", "123") 28 | urlValues.Set("age64", "123") 29 | urlValues.Set("float32", "123.45") 30 | urlValues.Set("float64", "123.45") 31 | urlValues.Set("created-at", "2019-02-05T13:32:02Z") 32 | urlValues.Set("bool-false", "false") 33 | urlValues.Set("bool-true", "true") 34 | urlValues.Set("bool-empty", "") // param set but no value means 35 | urlValues.Set("present-empty", "") // param set but no value 36 | urlValues.Set("present", "this is here") 37 | 38 | requestData := struct { 39 | Name string `queryparam:"name"` 40 | Names []string `queryparam:"names"` 41 | DashNames []string `queryparam:"dash-names" queryparamdelim:"-"` 42 | Age int `queryparam:"age"` 43 | Age32 int32 `queryparam:"age32"` 44 | Age64 int64 `queryparam:"age64"` 45 | Float32 float32 `queryparam:"float32"` 46 | Float64 float64 `queryparam:"float64"` 47 | CreatedAt time.Time `queryparam:"created-at"` 48 | UpdatedAt time.Time `queryparam:"updated-at"` 49 | BoolFalse bool `queryparam:"bool-false"` 50 | BoolTrue bool `queryparam:"bool-true"` 51 | BoolEmpty bool `queryparam:"bool-empty"` 52 | PresentEmpty queryparam.Present `queryparam:"present-empty"` 53 | Present queryparam.Present `queryparam:"present"` 54 | NotPresent queryparam.Present `queryparam:"not-present"` 55 | }{} 56 | 57 | if err := queryparam.Parse(urlValues, &requestData); err != nil { 58 | panic(err) 59 | } 60 | 61 | fmt.Printf("name: %s\n", requestData.Name) 62 | fmt.Printf("names: %v\n", requestData.Names) 63 | fmt.Printf("dash names: %v\n", requestData.DashNames) 64 | fmt.Printf("age: %d\n", requestData.Age) 65 | fmt.Printf("age32: %d\n", requestData.Age32) 66 | fmt.Printf("age64: %d\n", requestData.Age64) 67 | fmt.Printf("float32: %f\n", math.Round(float64(requestData.Float32))) // rounded to avoid floating point precision issues 68 | fmt.Printf("float64: %f\n", math.Round(requestData.Float64)) // rounded to avoid floating point precision issues 69 | fmt.Printf("created at: %s\n", requestData.CreatedAt.Format(time.RFC3339)) 70 | fmt.Printf("updated at: %s\n", requestData.UpdatedAt.Format(time.RFC3339)) 71 | fmt.Printf("bool false: %v\n", requestData.BoolFalse) 72 | fmt.Printf("bool true: %v\n", requestData.BoolTrue) 73 | fmt.Printf("bool empty: %v\n", requestData.BoolEmpty) 74 | fmt.Printf("present empty: %v\n", requestData.PresentEmpty) 75 | fmt.Printf("present: %v\n", requestData.Present) 76 | fmt.Printf("not present: %v\n", requestData.NotPresent) 77 | 78 | // Output: 79 | // name: Tom 80 | // names: [Tom Jim Frank] 81 | // dash names: [Tom Jim Frank] 82 | // age: 123 83 | // age32: 123 84 | // age64: 123 85 | // float32: 123.000000 86 | // float64: 123.000000 87 | // created at: 2019-02-05T13:32:02Z 88 | // updated at: 0001-01-01T00:00:00Z 89 | // bool false: false 90 | // bool true: true 91 | // bool empty: false 92 | // present empty: false 93 | // present: true 94 | // not present: false 95 | } 96 | 97 | func TestParse_FieldWithNoTagIsNotUsed(t *testing.T) { 98 | t.Parallel() 99 | 100 | req := &struct { 101 | Name string `` 102 | }{} 103 | 104 | if err := queryparam.Parse(urlValues, req); err != nil { 105 | t.Errorf("unexpected error: %v", err) 106 | } 107 | 108 | if exp, got := "", req.Name; exp != got { 109 | t.Errorf("unexpected name. expected `%v`, got `%v`", exp, got) 110 | } 111 | } 112 | 113 | func TestParse_InvalidURLValues(t *testing.T) { 114 | t.Parallel() 115 | 116 | req := &struct{}{} 117 | 118 | err := queryparam.Parse(nil, req) 119 | if exp, got := queryparam.ErrInvalidURLValues, err; exp != got { 120 | t.Errorf("unexpected error. expected `%v`, got `%v`", exp, got) 121 | } 122 | } 123 | 124 | func TestParse_NonPointerTarget(t *testing.T) { 125 | t.Parallel() 126 | 127 | req := struct{}{} 128 | 129 | err := queryparam.Parse(urlValues, req) 130 | if exp, got := queryparam.ErrNonPointerTarget, err; exp != got { 131 | t.Errorf("unexpected error. expected `%v`, got `%v`", exp, got) 132 | } 133 | } 134 | 135 | func TestParse_ParserUnhandledFieldType(t *testing.T) { 136 | t.Parallel() 137 | 138 | req := &struct { 139 | Age struct{} `queryparam:"age"` 140 | }{} 141 | 142 | err := queryparam.Parse(urlValuesNameAge, req) 143 | if !errors.Is(err, queryparam.ErrUnhandledFieldType) { 144 | t.Errorf("unexpected error: %v", err) 145 | } 146 | } 147 | 148 | func TestParse_SetterUnhandledFieldType(t *testing.T) { 149 | t.Parallel() 150 | 151 | req := &struct { 152 | Age int `queryparam:"age"` 153 | }{} 154 | 155 | p := &queryparam.Parser{ 156 | Tag: "queryparam", 157 | DelimiterTag: "queryparamdelim", 158 | Delimiter: ",", 159 | ValueParsers: queryparam.DefaultValueParsers(), 160 | ValueSetters: map[reflect.Type]queryparam.ValueSetter{}, 161 | } 162 | 163 | err := p.Parse(urlValuesNameAge, req) 164 | if !errors.Is(err, queryparam.ErrUnhandledFieldType) { 165 | t.Errorf("unexpected error: %v", err) 166 | } 167 | } 168 | 169 | func TestParse_ValueParserErrorReturned(t *testing.T) { 170 | t.Parallel() 171 | 172 | tmpErr := errors.New("something bad happened") 173 | 174 | p := &queryparam.Parser{ 175 | Tag: "queryparam", 176 | DelimiterTag: "queryparamdelim", 177 | Delimiter: ",", 178 | ValueParsers: queryparam.DefaultValueParsers(), 179 | ValueSetters: map[reflect.Type]queryparam.ValueSetter{ 180 | reflect.TypeOf(""): func(value reflect.Value, target reflect.Value) error { 181 | return tmpErr 182 | }, 183 | }, 184 | } 185 | 186 | req := &struct { 187 | Name string `queryparam:"name"` 188 | }{} 189 | 190 | err := p.Parse(urlValuesNameAge, req) 191 | if !errors.Is(err, tmpErr) { 192 | t.Errorf("unexpected error: %v", err) 193 | } 194 | } 195 | 196 | func TestParse_EmptyTag(t *testing.T) { 197 | t.Parallel() 198 | 199 | req := &struct { 200 | Name string `queryparam:""` 201 | }{} 202 | 203 | err := queryparam.Parse(urlValuesNameAge, req) 204 | if !errors.Is(err, queryparam.ErrInvalidTag) { 205 | t.Errorf("unexpected error: %v", err) 206 | } 207 | } 208 | 209 | func TestParse_ValueSetterErrorReturned(t *testing.T) { 210 | t.Parallel() 211 | 212 | tmpErr := errors.New("something bad happened") 213 | 214 | p := &queryparam.Parser{ 215 | Tag: "queryparam", 216 | DelimiterTag: "queryparamdelim", 217 | Delimiter: ",", 218 | ValueParsers: map[reflect.Type]queryparam.ValueParser{ 219 | reflect.TypeOf(""): func(value string, delimiter string) (reflect.Value, error) { 220 | return reflect.ValueOf(""), tmpErr 221 | }, 222 | }, 223 | } 224 | 225 | req := &struct { 226 | Name string `queryparam:"name"` 227 | }{} 228 | 229 | err := p.Parse(urlValuesNameAge, req) 230 | if !errors.Is(err, tmpErr) { 231 | t.Errorf("unexpected error: %v", err) 232 | } 233 | } 234 | 235 | func BenchmarkParse(b *testing.B) { 236 | b.ResetTimer() 237 | for i := 0; i < b.N; i++ { 238 | data := struct { 239 | Name string `queryparam:"name"` 240 | NameList []string `queryparam:"name-list"` 241 | NameListDash []string `queryparam:"name-list" queryparamdelim:"-"` 242 | Age int `queryparam:"age"` 243 | }{} 244 | err := queryparam.Parse(urlValuesNameAge, &data) 245 | if err != nil { 246 | b.FailNow() 247 | } 248 | } 249 | b.StopTimer() 250 | b.ReportAllocs() 251 | } 252 | 253 | func TestErrInvalidParameterValue_Unwrap(t *testing.T) { 254 | tmpErr := errors.New("something bad") 255 | e := &queryparam.ErrInvalidParameterValue{ 256 | Err: tmpErr, 257 | Parameter: "Name", 258 | Field: "name", 259 | Value: "asd", 260 | Type: reflect.TypeOf(""), 261 | } 262 | exp := "invalid parameter value for field name (string) from parameter Name (asd): something bad" 263 | if got := e.Error(); exp != got { 264 | t.Errorf("expected `%s`, got `%s`", exp, got) 265 | } 266 | if !errors.Is(e, tmpErr) { 267 | t.Error("expected is to return true") 268 | } 269 | } 270 | 271 | func TestCannotSetValue_Unwrap(t *testing.T) { 272 | tmpErr := errors.New("something bad") 273 | e := &queryparam.ErrCannotSetValue{ 274 | Err: tmpErr, 275 | Parameter: "Name", 276 | Field: "name", 277 | Value: "asd", 278 | Type: reflect.TypeOf(""), 279 | ParsedValue: reflect.ValueOf("asd"), 280 | } 281 | exp := "cannot set value for field name (string) from parameter Name (asd - asd): something bad" 282 | if got := e.Error(); exp != got { 283 | t.Errorf("expected `%s`, got `%s`", exp, got) 284 | } 285 | if !errors.Is(e, tmpErr) { 286 | t.Error("expected is to return true") 287 | } 288 | } 289 | -------------------------------------------------------------------------------- /parsers.go: -------------------------------------------------------------------------------- 1 | package queryparam 2 | 3 | import ( 4 | "errors" 5 | "reflect" 6 | "strconv" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | // ErrInvalidBoolValue is returned when an unhandled string is parsed. 12 | var ErrInvalidBoolValue = errors.New("unknown bool value") 13 | 14 | // DefaultValueParsers returns a set of default value parsers. 15 | func DefaultValueParsers() map[reflect.Type]ValueParser { 16 | return map[reflect.Type]ValueParser{ 17 | reflect.TypeOf(""): StringValueParser, 18 | reflect.TypeOf([]string{}): StringSliceValueParser, 19 | reflect.TypeOf(0): IntValueParser, 20 | reflect.TypeOf(int32(0)): Int32ValueParser, 21 | reflect.TypeOf(int64(0)): Int64ValueParser, 22 | reflect.TypeOf(float32(0)): Float32ValueParser, 23 | reflect.TypeOf(float64(0)): Float64ValueParser, 24 | reflect.TypeOf(time.Time{}): TimeValueParser, 25 | reflect.TypeOf(false): BoolValueParser, 26 | reflect.TypeOf(Present(false)): PresentValueParser, 27 | } 28 | } 29 | 30 | // StringValueParser parses a string into a string. 31 | func StringValueParser(value string, _ string) (reflect.Value, error) { 32 | return reflect.ValueOf(value), nil 33 | } 34 | 35 | // StringSliceValueParser parses a string into a []string. 36 | func StringSliceValueParser(value string, delimiter string) (reflect.Value, error) { 37 | if value == "" { 38 | // ignore blank values. 39 | return reflect.ValueOf([]string{}), nil 40 | } 41 | 42 | return reflect.ValueOf(strings.Split(value, delimiter)), nil 43 | } 44 | 45 | // IntValueParser parses a string into an int64. 46 | func IntValueParser(value string, _ string) (reflect.Value, error) { 47 | if value == "" { 48 | // ignore blank values. 49 | return reflect.ValueOf(int(0)), nil 50 | } 51 | 52 | i64, err := strconv.ParseInt(value, 10, 64) 53 | if err != nil { 54 | return reflect.ValueOf(int(0)), err 55 | } 56 | 57 | return reflect.ValueOf(int(i64)), nil 58 | } 59 | 60 | // Int64ValueParser parses a string into an int64. 61 | func Int64ValueParser(value string, _ string) (reflect.Value, error) { 62 | if value == "" { 63 | // ignore blank values. 64 | return reflect.ValueOf(int64(0)), nil 65 | } 66 | 67 | i64, err := strconv.ParseInt(value, 10, 64) 68 | if err != nil { 69 | return reflect.ValueOf(int64(0)), err 70 | } 71 | 72 | return reflect.ValueOf(i64), nil 73 | } 74 | 75 | // Int32ValueParser parses a string into an int32. 76 | func Int32ValueParser(value string, _ string) (reflect.Value, error) { 77 | if value == "" { 78 | // ignore blank values. 79 | return reflect.ValueOf(0), nil 80 | } 81 | 82 | i64, err := strconv.ParseInt(value, 10, 32) 83 | if err != nil { 84 | return reflect.ValueOf(0), err 85 | } 86 | 87 | return reflect.ValueOf(i64), nil 88 | } 89 | 90 | // TimeValueParser parses a string into an int64. 91 | func TimeValueParser(value string, _ string) (reflect.Value, error) { 92 | if value == "" { 93 | // ignore blank values. 94 | return reflect.ValueOf(time.Time{}), nil 95 | } 96 | 97 | t, err := time.Parse(time.RFC3339, value) 98 | if err != nil { 99 | return reflect.ValueOf(time.Time{}), err 100 | } 101 | return reflect.ValueOf(t), nil 102 | } 103 | 104 | // BoolValueParser parses a string into a bool. 105 | func BoolValueParser(value string, _ string) (reflect.Value, error) { 106 | switch strings.ToLower(value) { 107 | case "true", "1", "y", "yes": 108 | return reflect.ValueOf(true), nil 109 | case "", "false", "0", "n", "no": 110 | return reflect.ValueOf(false), nil 111 | default: 112 | return reflect.ValueOf(false), ErrInvalidBoolValue 113 | } 114 | } 115 | 116 | // PresentValueParser sets the target to true. 117 | // This parser will only be executed if the parameter is present. 118 | func PresentValueParser(value string, _ string) (reflect.Value, error) { 119 | return reflect.ValueOf(Present(value != "")), nil 120 | } 121 | 122 | // Float64ValueParser parses a string to a float64. 123 | func Float64ValueParser(value string, _ string) (reflect.Value, error) { 124 | if value == "" { 125 | // ignore blank values. 126 | return reflect.ValueOf(float64(0)), nil 127 | } 128 | 129 | i64, err := strconv.ParseFloat(value, 10) 130 | if err != nil { 131 | return reflect.ValueOf(float64(0)), err 132 | } 133 | 134 | return reflect.ValueOf(i64), nil 135 | } 136 | 137 | // Float32ValueParser parses a string to a float32. 138 | func Float32ValueParser(value string, _ string) (reflect.Value, error) { 139 | if value == "" { 140 | // ignore blank values. 141 | return reflect.ValueOf(float32(0)), nil 142 | } 143 | 144 | f64, err := strconv.ParseFloat(value, 10) 145 | if err != nil { 146 | return reflect.ValueOf(float32(0)), err 147 | } 148 | 149 | return reflect.ValueOf(f64), nil 150 | } 151 | -------------------------------------------------------------------------------- /parsers_test.go: -------------------------------------------------------------------------------- 1 | package queryparam_test 2 | 3 | import ( 4 | "errors" 5 | "github.com/tomwright/queryparam/v4" 6 | "reflect" 7 | "strconv" 8 | "testing" 9 | "time" 10 | ) 11 | 12 | func TestStringValueParser(t *testing.T) { 13 | t.Run("Empty", func(t *testing.T) { 14 | res, err := queryparam.StringValueParser("", "") 15 | if err != nil { 16 | t.Errorf("unexpected error: %s", err.Error()) 17 | return 18 | } 19 | if exp, got := "", res.String(); exp != got { 20 | t.Errorf("expected res `%v`, got `%v`", exp, got) 21 | } 22 | }) 23 | t.Run("Valid", func(t *testing.T) { 24 | res, err := queryparam.StringValueParser("hello", "") 25 | if err != nil { 26 | t.Errorf("unexpected error: %s", err.Error()) 27 | return 28 | } 29 | if exp, got := "hello", res.String(); exp != got { 30 | t.Errorf("expected res `%v`, got `%v`", exp, got) 31 | } 32 | }) 33 | } 34 | 35 | func TestStringSliceValueParser(t *testing.T) { 36 | t.Run("Empty", func(t *testing.T) { 37 | res, err := queryparam.StringSliceValueParser("", ",") 38 | if err != nil { 39 | t.Errorf("unexpected error: %s", err.Error()) 40 | return 41 | } 42 | exp := make([]string, 0) 43 | got, ok := res.Interface().([]string) 44 | if !ok || !reflect.DeepEqual(exp, got) { 45 | t.Errorf("expected res `%v`, got `%v`", exp, got) 46 | } 47 | }) 48 | t.Run("SingleValue", func(t *testing.T) { 49 | res, err := queryparam.StringSliceValueParser("hello", ",") 50 | if err != nil { 51 | t.Errorf("unexpected error: %s", err.Error()) 52 | return 53 | } 54 | exp := []string{"hello"} 55 | got, ok := res.Interface().([]string) 56 | if !ok || !reflect.DeepEqual(exp, got) { 57 | t.Errorf("expected res `%v`, got `%v`", exp, got) 58 | } 59 | }) 60 | t.Run("MultipleValueComma", func(t *testing.T) { 61 | res, err := queryparam.StringSliceValueParser("hello,hi,hey", ",") 62 | if err != nil { 63 | t.Errorf("unexpected error: %s", err.Error()) 64 | return 65 | } 66 | exp := []string{"hello", "hi", "hey"} 67 | got, ok := res.Interface().([]string) 68 | if !ok || !reflect.DeepEqual(exp, got) { 69 | t.Errorf("expected res `%v`, got `%v`", exp, got) 70 | } 71 | }) 72 | t.Run("MultipleValueDash", func(t *testing.T) { 73 | res, err := queryparam.StringSliceValueParser("hello-hi-hey", "-") 74 | if err != nil { 75 | t.Errorf("unexpected error: %s", err.Error()) 76 | return 77 | } 78 | exp := []string{"hello", "hi", "hey"} 79 | got, ok := res.Interface().([]string) 80 | if !ok || !reflect.DeepEqual(exp, got) { 81 | t.Errorf("expected res `%v`, got `%v`", exp, got) 82 | } 83 | }) 84 | } 85 | 86 | func TestTimeValueParser(t *testing.T) { 87 | t.Run("Empty", func(t *testing.T) { 88 | res, err := queryparam.TimeValueParser("", "") 89 | if err != nil { 90 | t.Errorf("unexpected error: %s", err.Error()) 91 | return 92 | } 93 | got, ok := res.Interface().(time.Time) 94 | if !ok || !got.IsZero() { 95 | t.Errorf("expected res to be zero, got `%v`", got) 96 | } 97 | }) 98 | t.Run("Valid", func(t *testing.T) { 99 | res, err := queryparam.TimeValueParser("2019-02-05T13:32:02Z", "") 100 | if err != nil { 101 | t.Errorf("unexpected error: %s", err.Error()) 102 | return 103 | } 104 | exp := time.Date(2019, 2, 5, 13, 32, 2, 0, time.UTC) 105 | got, ok := res.Interface().(time.Time) 106 | if !ok || !exp.Equal(got) { 107 | t.Errorf("expected res `%v`, got `%v`", exp, got) 108 | } 109 | }) 110 | t.Run("Invalid", func(t *testing.T) { 111 | res, err := queryparam.TimeValueParser("not-a-date", "") 112 | var timeErr *time.ParseError 113 | if err == nil || !errors.As(err, &timeErr) { 114 | t.Errorf("unexpected error: %v", err) 115 | } 116 | got, ok := res.Interface().(time.Time) 117 | if !ok || !got.IsZero() { 118 | t.Errorf("expected res to be zero, got `%v`", got) 119 | } 120 | }) 121 | } 122 | 123 | func TestIntValueParser(t *testing.T) { 124 | t.Run("Empty", func(t *testing.T) { 125 | res, err := queryparam.IntValueParser("", "") 126 | if err != nil { 127 | t.Errorf("unexpected error: %s", err.Error()) 128 | return 129 | } 130 | if exp, got := 0, int(res.Int()); exp != got { 131 | t.Errorf("expected res `%v`, got `%v`", exp, got) 132 | } 133 | }) 134 | t.Run("Valid", func(t *testing.T) { 135 | res, err := queryparam.IntValueParser("21323", "") 136 | if err != nil { 137 | t.Errorf("unexpected error: %s", err.Error()) 138 | return 139 | } 140 | if exp, got := 21323, int(res.Int()); exp != got { 141 | t.Errorf("expected res `%v`, got `%v`", exp, got) 142 | } 143 | }) 144 | t.Run("Invalid", func(t *testing.T) { 145 | res, err := queryparam.IntValueParser("asd", "") 146 | var numErr *strconv.NumError 147 | if err == nil || !errors.As(err, &numErr) || !errors.Is(numErr.Err, strconv.ErrSyntax) { 148 | t.Errorf("unexpected error: %v", err) 149 | } 150 | if exp, got := int(0), int(res.Int()); exp != got { 151 | t.Errorf("expected res `%v`, got `%v`", exp, got) 152 | } 153 | }) 154 | } 155 | 156 | func TestInt32ValueParser(t *testing.T) { 157 | t.Run("Empty", func(t *testing.T) { 158 | res, err := queryparam.Int32ValueParser("", "") 159 | if err != nil { 160 | t.Errorf("unexpected error: %s", err.Error()) 161 | return 162 | } 163 | if exp, got := int32(0), int32(res.Int()); exp != got { 164 | t.Errorf("expected res `%v`, got `%v`", exp, got) 165 | } 166 | }) 167 | t.Run("Valid", func(t *testing.T) { 168 | res, err := queryparam.Int32ValueParser("21323", "") 169 | if err != nil { 170 | t.Errorf("unexpected error: %s", err.Error()) 171 | return 172 | } 173 | if exp, got := int32(21323), int32(res.Int()); exp != got { 174 | t.Errorf("expected res `%v`, got `%v`", exp, got) 175 | } 176 | t.Run("Invalid", func(t *testing.T) { 177 | res, err := queryparam.Int32ValueParser("asd", "") 178 | var numErr *strconv.NumError 179 | if err == nil || !errors.As(err, &numErr) || !errors.Is(numErr.Err, strconv.ErrSyntax) { 180 | t.Errorf("unexpected error: %v", err) 181 | } 182 | if exp, got := int32(0), int32(res.Int()); exp != got { 183 | t.Errorf("expected res `%v`, got `%v`", exp, got) 184 | } 185 | }) 186 | }) 187 | } 188 | 189 | func TestInt64ValueParser(t *testing.T) { 190 | t.Run("Empty", func(t *testing.T) { 191 | res, err := queryparam.Int64ValueParser("", "") 192 | if err != nil { 193 | t.Errorf("unexpected error: %s", err.Error()) 194 | return 195 | } 196 | if exp, got := int64(0), int64(res.Int()); exp != got { 197 | t.Errorf("expected res `%v`, got `%v`", exp, got) 198 | } 199 | }) 200 | t.Run("Valid", func(t *testing.T) { 201 | res, err := queryparam.Int64ValueParser("21643", "") 202 | if err != nil { 203 | t.Errorf("unexpected error: %s", err.Error()) 204 | return 205 | } 206 | if exp, got := int64(21643), int64(res.Int()); exp != got { 207 | t.Errorf("expected res `%v`, got `%v`", exp, got) 208 | } 209 | }) 210 | t.Run("Invalid", func(t *testing.T) { 211 | res, err := queryparam.Int64ValueParser("asd", "") 212 | var numErr *strconv.NumError 213 | if err == nil || !errors.As(err, &numErr) || !errors.Is(numErr.Err, strconv.ErrSyntax) { 214 | t.Errorf("unexpected error: %v", err) 215 | } 216 | if exp, got := int64(0), int64(res.Int()); exp != got { 217 | t.Errorf("expected res `%v`, got `%v`", exp, got) 218 | } 219 | }) 220 | } 221 | 222 | func TestBoolValueParser(t *testing.T) { 223 | t.Run("Invalid", func(t *testing.T) { 224 | res, err := queryparam.BoolValueParser("non-bool-string", "") 225 | expErr := queryparam.ErrInvalidBoolValue 226 | if err == nil || !errors.Is(err, expErr) { 227 | t.Errorf("expected error `%v` but got `%v`", expErr, err) 228 | return 229 | } 230 | if exp, got := false, res.Bool(); exp != got { 231 | t.Errorf("expected res `%v`, got `%v`", exp, got) 232 | } 233 | }) 234 | 235 | checkBoolFn := func(input []string, exp bool) func(*testing.T) { 236 | return func(t *testing.T) { 237 | for _, in := range input { 238 | i := in 239 | t.Run(i, func(t *testing.T) { 240 | res, err := queryparam.BoolValueParser(i, "") 241 | if err != nil { 242 | t.Errorf("unexpected error: %s", err.Error()) 243 | return 244 | } 245 | if exp, got := exp, res.Bool(); exp != got { 246 | t.Errorf("expected res `%v`, got `%v`", exp, got) 247 | } 248 | }) 249 | } 250 | } 251 | } 252 | t.Run("True", checkBoolFn([]string{"true", "TRUE", "1", "yes", "YES", "y", "Y"}, true)) 253 | t.Run("False", checkBoolFn([]string{"", "false", "FALSE", "0", "no", "NO", "n", "N"}, false)) 254 | } 255 | 256 | func TestFloat32ValueParser(t *testing.T) { 257 | t.Run("Empty", func(t *testing.T) { 258 | res, err := queryparam.Float32ValueParser("", "") 259 | if err != nil { 260 | t.Errorf("unexpected error: %s", err.Error()) 261 | return 262 | } 263 | if exp, got := float32(0), float32(res.Float()); exp != got { 264 | t.Errorf("expected res `%v`, got `%v`", exp, got) 265 | } 266 | }) 267 | t.Run("Valid", func(t *testing.T) { 268 | res, err := queryparam.Float32ValueParser("21323", "") 269 | if err != nil { 270 | t.Errorf("unexpected error: %s", err.Error()) 271 | return 272 | } 273 | if exp, got := float32(21323), float32(res.Float()); exp != got { 274 | t.Errorf("expected res `%v`, got `%v`", exp, got) 275 | } 276 | t.Run("Invalid", func(t *testing.T) { 277 | res, err := queryparam.Float32ValueParser("asd", "") 278 | var numErr *strconv.NumError 279 | if err == nil || !errors.As(err, &numErr) || !errors.Is(numErr.Err, strconv.ErrSyntax) { 280 | t.Errorf("unexpected error: %v", err) 281 | } 282 | if exp, got := float32(0), float32(res.Float()); exp != got { 283 | t.Errorf("expected res `%v`, got `%v`", exp, got) 284 | } 285 | }) 286 | }) 287 | } 288 | 289 | func TestFloat64ValueParser(t *testing.T) { 290 | t.Run("Empty", func(t *testing.T) { 291 | res, err := queryparam.Float64ValueParser("", "") 292 | if err != nil { 293 | t.Errorf("unexpected error: %s", err.Error()) 294 | return 295 | } 296 | if exp, got := float64(0), float64(res.Float()); exp != got { 297 | t.Errorf("expected res `%v`, got `%v`", exp, got) 298 | } 299 | }) 300 | t.Run("Valid", func(t *testing.T) { 301 | res, err := queryparam.Float64ValueParser("21643", "") 302 | if err != nil { 303 | t.Errorf("unexpected error: %s", err.Error()) 304 | return 305 | } 306 | if exp, got := float64(21643), float64(res.Float()); exp != got { 307 | t.Errorf("expected res `%v`, got `%v`", exp, got) 308 | } 309 | }) 310 | t.Run("Invalid", func(t *testing.T) { 311 | res, err := queryparam.Float64ValueParser("asd", "") 312 | var numErr *strconv.NumError 313 | if err == nil || !errors.As(err, &numErr) || !errors.Is(numErr.Err, strconv.ErrSyntax) { 314 | t.Errorf("unexpected error: %v", err) 315 | } 316 | if exp, got := float64(0), float64(res.Float()); exp != got { 317 | t.Errorf("expected res `%v`, got `%v`", exp, got) 318 | } 319 | }) 320 | } 321 | -------------------------------------------------------------------------------- /setters.go: -------------------------------------------------------------------------------- 1 | package queryparam 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | ) 7 | 8 | // GenericType is a reflect.Type we can use to identity generic value setters. 9 | var GenericType = reflect.TypeOf(struct{}{}) 10 | 11 | // DefaultValueSetters returns a set of default value setters. 12 | func DefaultValueSetters() map[reflect.Type]ValueSetter { 13 | return map[reflect.Type]ValueSetter{ 14 | GenericType: GenericSetter, 15 | reflect.TypeOf(int32(0)): Int32ValueSetter, 16 | reflect.TypeOf(float32(0)): Float32ValueSetter, 17 | } 18 | } 19 | 20 | // recoverPanic recovers from a panic and sets the panic value into the given error. 21 | func recoverPanic(err *error) func() { 22 | return func() { 23 | rec := recover() 24 | if rec != nil { 25 | if e, ok := rec.(error); ok { 26 | *err = e 27 | } else { 28 | *err = fmt.Errorf("%s", rec) 29 | } 30 | } 31 | } 32 | } 33 | 34 | // GenericSetter sets the targets value. 35 | func GenericSetter(value reflect.Value, target reflect.Value) (err error) { 36 | defer recoverPanic(&err)() 37 | target.Set(value) 38 | return 39 | } 40 | 41 | // Int32ValueSetter sets the targets value to an int32. 42 | func Int32ValueSetter(value reflect.Value, target reflect.Value) (err error) { 43 | defer recoverPanic(&err)() 44 | target.SetInt(value.Int()) 45 | return nil 46 | } 47 | 48 | // Float32ValueSetter sets the targets value to a float32. 49 | func Float32ValueSetter(value reflect.Value, target reflect.Value) (err error) { 50 | defer recoverPanic(&err)() 51 | target.SetFloat(value.Float()) 52 | return nil 53 | } 54 | -------------------------------------------------------------------------------- /setters_internal_test.go: -------------------------------------------------------------------------------- 1 | package queryparam 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | ) 7 | 8 | func TestRecoverPanic(t *testing.T) { 9 | t.Run("NoPanic", func(t *testing.T) { 10 | got := func() (err error) { 11 | defer recoverPanic(&err)() 12 | return nil 13 | }() 14 | 15 | if got != nil { 16 | t.Errorf("unexpected error") 17 | } 18 | }) 19 | t.Run("PanicString", func(t *testing.T) { 20 | got := func() (err error) { 21 | defer recoverPanic(&err)() 22 | panic("something bad") 23 | return 24 | }() 25 | 26 | var exp error = errors.New("something bad") 27 | if got == nil || exp.Error() != got.Error() { 28 | t.Errorf("expected `%v`, got `%v`", exp, got) 29 | } 30 | }) 31 | t.Run("PanicError", func(t *testing.T) { 32 | exp := errors.New("something bad") 33 | got := func() (err error) { 34 | defer recoverPanic(&err)() 35 | panic(exp) 36 | return 37 | }() 38 | 39 | if got == nil || !errors.Is(got, exp) { 40 | t.Errorf("expected `%v`, got `%v`", exp, got) 41 | } 42 | }) 43 | } 44 | -------------------------------------------------------------------------------- /setters_test.go: -------------------------------------------------------------------------------- 1 | package queryparam_test 2 | 3 | import ( 4 | "errors" 5 | "github.com/tomwright/queryparam/v4" 6 | "reflect" 7 | "testing" 8 | ) 9 | 10 | func TestGenericSetter(t *testing.T) { 11 | t.Run("Valid", func(t *testing.T) { 12 | data := struct { 13 | Value string 14 | Target string 15 | }{} 16 | 17 | err := queryparam.GenericSetter(reflect.ValueOf(&data).Elem().FieldByName("Value"), reflect.ValueOf(&data).Elem().FieldByName("Target")) 18 | if err != nil { 19 | t.Errorf("unexpected error: %s", err) 20 | return 21 | } 22 | if data.Value != data.Target { 23 | t.Errorf("expected %v, got %v", data.Value, data.Target) 24 | } 25 | }) 26 | t.Run("BadValue", func(t *testing.T) { 27 | data := struct { 28 | Value string 29 | Target int 30 | }{} 31 | 32 | err := queryparam.GenericSetter(reflect.ValueOf(&data).Elem().FieldByName("Value"), reflect.ValueOf(&data).Elem().FieldByName("Target")) 33 | expErr := errors.New("reflect.Set: value of type string is not assignable to type int") 34 | if err == nil || err.Error() != expErr.Error() { 35 | t.Errorf("expected error `%v`, got `%v`", expErr, err) 36 | return 37 | } 38 | }) 39 | } 40 | --------------------------------------------------------------------------------