Skip to content

Commit

Permalink
Add ValueSetters
Browse files Browse the repository at this point in the history
  • Loading branch information
TomWright committed Dec 6, 2019
1 parent 57b100d commit 74acb62
Show file tree
Hide file tree
Showing 7 changed files with 288 additions and 111 deletions.
74 changes: 2 additions & 72 deletions README.MD
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
[![Go Report Card](https://goreportcard.com/badge/github.com/TomWright/queryparam)](https://goreportcard.com/report/github.com/TomWright/queryparam)
[![Documentation](https://godoc.org/github.com/TomWright/queryparam?status.svg)](https://godoc.org/github.com/TomWright/queryparam)

Query param makes it easy to access the query parameters stored in a URL.
Stop accessing query strings and parsing repeatedly parsing them into your preferred values - `queryparam` can do that for you!

## Installation

Expand All @@ -15,74 +15,4 @@ go get -u github.com/tomwright/queryparam

## Examples

For more complete examples see [godoc examples](https://godoc.org/github.com/TomWright/queryparam#example-Parse).

### Parsing a simple URL
```
type GetUsersRequest struct {
Name string `queryparam:"name"`
Age int `queryparam:"age"`
Gender string `queryparam:"gender"`
}
u, err := url.Parse("https://example.com/users?name=Tom&age=23")
if err != nil {
panic(err)
}
req := &GetUsersRequest{}
err = queryparam.Parse(u, req)
if err != nil {
panic(err)
}
fmt.Println(req.Name) // "Tom"
fmt.Println(req.Age) // 23
fmt.Println(req.Gender) // ""
```

### Parsing a URL with delimited parameters
```
type GetUsersRequest struct {
Name []string `queryparam:"name"`
}
u, err := url.Parse("https://example.com/users?name=Tom,Jim")
if err != nil {
panic(err)
}
req := &GetUsersRequest{}
err = queryparam.Parse(u, req)
if err != nil {
panic(err)
}
fmt.Println(req.Name) // { "Tom", "Jim" }
```

If you want to change the delimiter you can do so by changing the value of `queryparam.Delimiter`.

The default value is `,`.

### Parsing a URL from a HTTP Request
```
var r *http.Request
// Receive r in a http handler...
type GetUsersRequest struct {
Name string `queryparam:"name"`
Age int `queryparam:"age"`
Gender string `queryparam:"gender"`
}
req := &GetUsersRequest{}
err = queryparam.Parse(r.URL, req)
if err != nil {
panic(err)
}
// Do something with req...
```
For examples see [godoc examples](https://godoc.org/github.com/TomWright/queryparam#example-Parse).
97 changes: 61 additions & 36 deletions parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"fmt"
"net/url"
"reflect"
"time"
)

var (
Expand Down Expand Up @@ -38,49 +37,60 @@ func (e *ErrInvalidParameterValue) Unwrap() error {
return e.Err
}

// ErrCannotSetValue is an error adds extra context to a setter error.
type ErrCannotSetValue struct {
Err error
Parameter string
Field string
Value string
Type reflect.Type
ParsedValue reflect.Value
}

// Error returns the full error message.
func (e *ErrCannotSetValue) Error() string {
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())
}

// Unwrap returns the wrapped error.
func (e *ErrCannotSetValue) Unwrap() error {
return e.Err
}

// Present allows you to determine whether or not a query parameter was present in a request.
type Present bool

// DefaultParser is a default parser.
var DefaultParser = &Parser{
// Tag is the name of the struct tag where the query parameter name is set.
Tag: "queryparam",
// Delimiter is the name of the struct tag where a string delimiter override is set.
Tag: "queryparam",
DelimiterTag: "queryparamdelim",
// Delimiter is the default string delimiter.
Delimiter: ",",
// ValueParsers is a map[reflect.Type]ValueParser that defines how we parse query
// parameters based on the destination variable type.
Delimiter: ",",
ValueParsers: DefaultValueParsers(),
}

// DefaultValueParsers returns a set of default value parsers.
func DefaultValueParsers() map[reflect.Type]ValueParser {
return map[reflect.Type]ValueParser{
reflect.TypeOf(""): StringValueParser,
reflect.TypeOf([]string{}): StringSliceValueParser,
reflect.TypeOf(0): IntValueParser,
reflect.TypeOf(int32(0)): Int32ValueParser,
reflect.TypeOf(int64(0)): Int64ValueParser,
reflect.TypeOf(float32(0)): Float32ValueParser,
reflect.TypeOf(float64(0)): Float64ValueParser,
reflect.TypeOf(time.Time{}): TimeValueParser,
reflect.TypeOf(false): BoolValueParser,
reflect.TypeOf(Present(false)): PresentValueParser,
}
ValueSetters: DefaultValueSetters(),
}

// Parser is used to parse a URL.
type Parser struct {
Tag string
// Tag is the name of the struct tag where the query parameter name is set.
Tag string
// Delimiter is the name of the struct tag where a string delimiter override is set.
DelimiterTag string
Delimiter string
// Delimiter is the default string delimiter.
Delimiter string
// ValueParsers is a map[reflect.Type]ValueParser that defines how we parse query
// parameters based on the destination variable type.
ValueParsers map[reflect.Type]ValueParser
// ValueSetters is a map[reflect.Type]ValueSetter that defines how we set values
// onto target variables.
ValueSetters map[reflect.Type]ValueSetter
}

// ValueParser is a func used to parse a value.
type ValueParser func(value string, delimiter string) (reflect.Value, error)

// ValueSetter is a func used to set a value on a target variable.
type ValueSetter func(value reflect.Value, target reflect.Value) error

// FieldDelimiter returns a delimiter to be used with the given field.
func (p *Parser) FieldDelimiter(field reflect.StructField) string {
if customDelimiter := field.Tag.Get(p.DelimiterTag); customDelimiter != "" {
Expand Down Expand Up @@ -129,24 +139,39 @@ func (p *Parser) ParseField(field reflect.StructField, value reflect.Value, urlV

parsedValue, err := valueParser(queryParameterValue, p.FieldDelimiter(field))
if err != nil {
err = &ErrInvalidParameterValue{
return &ErrInvalidParameterValue{
Err: err,
Value: queryParameterValue,
Parameter: queryParameterName,
Type: field.Type,
Field: field.Name,
}
return err
}

// handle edge case value types
switch field.Type {
case reflect.TypeOf(int32(0)):
value.SetInt(parsedValue.Int())
case reflect.TypeOf(float32(0)):
value.SetFloat(parsedValue.Float())
default:
value.Set(parsedValue)
valueSetter, ok := p.ValueSetters[field.Type]
if !ok {
valueSetter, ok = p.ValueSetters[GenericType]
}
if !ok {
return &ErrCannotSetValue{
Err: ErrUnhandledFieldType,
Value: queryParameterValue,
ParsedValue: parsedValue,
Parameter: queryParameterName,
Type: field.Type,
Field: field.Name,
}
}

if err := valueSetter(parsedValue, value); err != nil {
return &ErrCannotSetValue{
Err: err,
Value: queryParameterValue,
ParsedValue: parsedValue,
Parameter: queryParameterName,
Type: field.Type,
Field: field.Name,
}
}

return nil
Expand Down
77 changes: 74 additions & 3 deletions parse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ func TestParse_NonPointerTarget(t *testing.T) {
}
}

func TestParse_UnhandledFieldType(t *testing.T) {
func TestParse_ParserUnhandledFieldType(t *testing.T) {
t.Parallel()

req := &struct {
Expand All @@ -145,6 +145,54 @@ func TestParse_UnhandledFieldType(t *testing.T) {
}
}

func TestParse_SetterUnhandledFieldType(t *testing.T) {
t.Parallel()

req := &struct {
Age int `queryparam:"age"`
}{}

p := &queryparam.Parser{
Tag: "queryparam",
DelimiterTag: "queryparamdelim",
Delimiter: ",",
ValueParsers: queryparam.DefaultValueParsers(),
ValueSetters: map[reflect.Type]queryparam.ValueSetter{},
}

err := p.Parse(urlValuesNameAge, req)
if !errors.Is(err, queryparam.ErrUnhandledFieldType) {
t.Errorf("unexpected error: %v", err)
}
}

func TestParse_ValueParserErrorReturned(t *testing.T) {
t.Parallel()

tmpErr := errors.New("something bad happened")

p := &queryparam.Parser{
Tag: "queryparam",
DelimiterTag: "queryparamdelim",
Delimiter: ",",
ValueParsers: queryparam.DefaultValueParsers(),
ValueSetters: map[reflect.Type]queryparam.ValueSetter{
reflect.TypeOf(""): func(value reflect.Value, target reflect.Value) error {
return tmpErr
},
},
}

req := &struct {
Name string `queryparam:"name"`
}{}

err := p.Parse(urlValuesNameAge, req)
if !errors.Is(err, tmpErr) {
t.Errorf("unexpected error: %v", err)
}
}

func TestParse_EmptyTag(t *testing.T) {
t.Parallel()

Expand All @@ -158,7 +206,7 @@ func TestParse_EmptyTag(t *testing.T) {
}
}

func TestParse_ValueParserErrorReturned(t *testing.T) {
func TestParse_ValueSetterErrorReturned(t *testing.T) {
t.Parallel()

tmpErr := errors.New("something bad happened")
Expand Down Expand Up @@ -203,8 +251,9 @@ func BenchmarkParse(b *testing.B) {
}

func TestErrInvalidParameterValue_Unwrap(t *testing.T) {
tmpErr := errors.New("something bad")
e := &queryparam.ErrInvalidParameterValue{
Err: errors.New("something bad"),
Err: tmpErr,
Parameter: "Name",
Field: "name",
Value: "asd",
Expand All @@ -214,4 +263,26 @@ func TestErrInvalidParameterValue_Unwrap(t *testing.T) {
if got := e.Error(); exp != got {
t.Errorf("expected `%s`, got `%s`", exp, got)
}
if !errors.Is(e, tmpErr) {
t.Error("expected is to return true")
}
}

func TestCannotSetValue_Unwrap(t *testing.T) {
tmpErr := errors.New("something bad")
e := &queryparam.ErrCannotSetValue{
Err: tmpErr,
Parameter: "Name",
Field: "name",
Value: "asd",
Type: reflect.TypeOf(""),
ParsedValue: reflect.ValueOf("asd"),
}
exp := "cannot set value for field name (string) from parameter Name (asd - asd): something bad"
if got := e.Error(); exp != got {
t.Errorf("expected `%s`, got `%s`", exp, got)
}
if !errors.Is(e, tmpErr) {
t.Error("expected is to return true")
}
}
16 changes: 16 additions & 0 deletions parsers.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,22 @@ import (
// ErrInvalidBoolValue is returned when an unhandled string is parsed.
var ErrInvalidBoolValue = errors.New("unknown bool value")

// DefaultValueParsers returns a set of default value parsers.
func DefaultValueParsers() map[reflect.Type]ValueParser {
return map[reflect.Type]ValueParser{
reflect.TypeOf(""): StringValueParser,
reflect.TypeOf([]string{}): StringSliceValueParser,
reflect.TypeOf(0): IntValueParser,
reflect.TypeOf(int32(0)): Int32ValueParser,
reflect.TypeOf(int64(0)): Int64ValueParser,
reflect.TypeOf(float32(0)): Float32ValueParser,
reflect.TypeOf(float64(0)): Float64ValueParser,
reflect.TypeOf(time.Time{}): TimeValueParser,
reflect.TypeOf(false): BoolValueParser,
reflect.TypeOf(Present(false)): PresentValueParser,
}
}

// StringValueParser parses a string into a string.
func StringValueParser(value string, _ string) (reflect.Value, error) {
return reflect.ValueOf(value), nil
Expand Down
Loading

0 comments on commit 74acb62

Please sign in to comment.