From 91cd021b382d2c64789dff14cdfb4a969ac2a5ab Mon Sep 17 00:00:00 2001 From: BrandonRomano Date: Wed, 21 Jun 2017 17:38:12 -0400 Subject: [PATCH 1/3] Switch over to type handlers --- type_handlers.go | 133 +++++++++++++++++++++++++++++++++++++++++ validator.go | 151 ++++++++++++++++++----------------------------- 2 files changed, 191 insertions(+), 93 deletions(-) create mode 100644 type_handlers.go diff --git a/type_handlers.go b/type_handlers.go new file mode 100644 index 0000000..03b8f76 --- /dev/null +++ b/type_handlers.go @@ -0,0 +1,133 @@ +package validator + +import ( + "errors" + "fmt" + "strconv" +) + +func stringHandler(input string, value *Value) error { + *value.Result.(*string) = input + return nil +} + +func float32Handler(input string, value *Value) error { + res, err := strconv.ParseFloat(input, 32) + if err != nil { + return errors.New(invalidParam(value.Name, "a float32")) + } + *value.Result.(*float32) = float32(res) + return nil +} + +func float64Handler(input string, value *Value) error { + res, err := strconv.ParseFloat(input, 64) + if err != nil { + return errors.New(invalidParam(value.Name, "a float64")) + } + *value.Result.(*float64) = float64(res) + return nil +} + +func boolHandler(input string, value *Value) error { + res, err := strconv.ParseBool(input) + if err != nil { + return errors.New(invalidParam(value.Name, "a bool")) + } + *value.Result.(*bool) = res + return nil +} + +func intHandler(input string, value *Value) error { + res, err := strconv.ParseInt(input, 10, 0) + if err != nil { + return errors.New(invalidParam(value.Name, "an int")) + } + *value.Result.(*int) = int(res) + return nil +} + +func int8Handler(input string, value *Value) error { + res, err := strconv.ParseInt(input, 10, 8) + if err != nil { + return errors.New(invalidParam(value.Name, "an int8")) + } + *value.Result.(*int8) = int8(res) + return nil +} + +func int16Handler(input string, value *Value) error { + res, err := strconv.ParseInt(input, 10, 16) + if err != nil { + return errors.New(invalidParam(value.Name, "an int16")) + } + *value.Result.(*int16) = int16(res) + return nil +} + +func int32Handler(input string, value *Value) error { + res, err := strconv.ParseInt(input, 10, 32) + if err != nil { + return errors.New(invalidParam(value.Name, "an int32")) + } + *value.Result.(*int32) = int32(res) + return nil +} + +func int64Handler(input string, value *Value) error { + res, err := strconv.ParseInt(input, 10, 64) + if err != nil { + return errors.New(invalidParam(value.Name, "an int64")) + } + *value.Result.(*int64) = int64(res) + return nil +} + +func uintHandler(input string, value *Value) error { + res, err := strconv.ParseUint(input, 10, 0) + if err != nil { + return errors.New(invalidParam(value.Name, "a uint")) + } + *value.Result.(*uint) = uint(res) + return nil +} + +func uint8Handler(input string, value *Value) error { + res, err := strconv.ParseUint(input, 10, 8) + if err != nil { + return errors.New(invalidParam(value.Name, "a uint8")) + } + *value.Result.(*uint8) = uint8(res) + return nil +} + +func uint16Handler(input string, value *Value) error { + res, err := strconv.ParseUint(input, 10, 16) + if err != nil { + return errors.New(invalidParam(value.Name, "a uint16")) + } + *value.Result.(*uint16) = uint16(res) + return nil +} + +func uint32Handler(input string, value *Value) error { + res, err := strconv.ParseUint(input, 10, 32) + if err != nil { + return errors.New(invalidParam(value.Name, "a uint32")) + } + *value.Result.(*uint32) = uint32(res) + return nil +} + +func uint64Handler(input string, value *Value) error { + res, err := strconv.ParseUint(input, 10, 64) + if err != nil { + return errors.New(invalidParam(value.Name, "a uint64")) + } + *value.Result.(*uint64) = uint64(res) + return nil +} + +func invalidParam(name string, mustBe string) string { + return fmt.Sprintf("Invalid `%v` parameter, `%v` must be %v", name, name, mustBe) +} diff --git a/validator.go b/validator.go index 94d7377..7fc73a6 100644 --- a/validator.go +++ b/validator.go @@ -4,18 +4,23 @@ import ( "errors" "fmt" "reflect" - "strconv" ) // Value is the definition of a parameter that you would like to perform validation against. type Value struct { - Result interface{} - Default string - Name string - Input string - Rules []Rule + Result interface{} + Default string + Name string + Input string + Rules []Rule + TypeHandler TypeHandler } +// TypeHandler is a function that is responsible for +// are responsible for validating the input +// matches the type, and also to stuff the result into the *value.Result +type TypeHandler func(input string, value *Value) error + // Rule is a function that defines logic you would expect a Value to pass. // The parameters passed in to this function are their respective // values as they were set in the Value struct. @@ -40,97 +45,57 @@ func Validate(values []*Value) error { } } - // Sticking the value of result into result - if resolvedInput != "" { - switch i := (value.Result).(type) { - default: - panic(fmt.Sprintf("go-carrot/validator cannot handle a Value with Result of type %v", reflect.TypeOf(i))) - case *string: - *i = resolvedInput - case *float32: - res, err := strconv.ParseFloat(resolvedInput, 32) - if err != nil { - return errors.New(invalidParam(value.Name, "a float32")) - } - *i = float32(res) - case *float64: - res, err := strconv.ParseFloat(resolvedInput, 64) - if err != nil { - return errors.New(invalidParam(value.Name, "a float64")) - } - *i = float64(res) - case *bool: - res, err := strconv.ParseBool(resolvedInput) - if err != nil { - return errors.New(invalidParam(value.Name, "a bool")) - } - *i = res - case *int: - res, err := strconv.ParseInt(resolvedInput, 10, 0) - if err != nil { - return errors.New(invalidParam(value.Name, "an int")) - } - *i = int(res) - case *int8: - res, err := strconv.ParseInt(resolvedInput, 10, 8) - if err != nil { - return errors.New(invalidParam(value.Name, "an int8")) - } - *i = int8(res) - case *int16: - res, err := strconv.ParseInt(resolvedInput, 10, 16) - if err != nil { - return errors.New(invalidParam(value.Name, "an int16")) - } - *i = int16(res) - case *int32: - res, err := strconv.ParseInt(resolvedInput, 10, 32) - if err != nil { - return errors.New(invalidParam(value.Name, "an int32")) - } - *i = int32(res) - case *int64: - res, err := strconv.ParseInt(resolvedInput, 10, 64) - if err != nil { - return errors.New(invalidParam(value.Name, "an int64")) - } - *i = int64(res) - case *uint: - res, err := strconv.ParseUint(resolvedInput, 10, 0) - if err != nil { - return errors.New(invalidParam(value.Name, "a uint")) - } - *i = uint(res) - case *uint8: - res, err := strconv.ParseUint(resolvedInput, 10, 8) - if err != nil { - return errors.New(invalidParam(value.Name, "a uint8")) - } - *i = uint8(res) - case *uint16: - res, err := strconv.ParseUint(resolvedInput, 10, 16) - if err != nil { - return errors.New(invalidParam(value.Name, "a uint16")) - } - *i = uint16(res) - case *uint32: - res, err := strconv.ParseUint(resolvedInput, 10, 32) - if err != nil { - return errors.New(invalidParam(value.Name, "a uint32")) - } - *i = uint32(res) - case *uint64: - res, err := strconv.ParseUint(resolvedInput, 10, 64) - if err != nil { - return errors.New(invalidParam(value.Name, "a uint64")) - } - *i = uint64(res) + // Set primitive type handlers + if value.TypeHandler == nil { + err := applyTypeHandler(value) + if err != nil { + panic(err.Error()) + } + } + + // Validate against type + if value.Input != "" { + err := value.TypeHandler(resolvedInput, value) + if err != nil { + return err } } } return nil } -func invalidParam(name string, mustBe string) string { - return fmt.Sprintf("Invalid `%v` parameter, `%v` must be %v", name, name, mustBe) +func applyTypeHandler(value *Value) error { + switch i := (value.Result).(type) { + default: + return errors.New(fmt.Sprintf("go-carrot/validator cannot by default handle a Value with Result of type %v. Must set a custom TypeHandler for %v.", reflect.TypeOf(i), value.Name)) + case *string: + value.TypeHandler = stringHandler + case *float32: + value.TypeHandler = float32Handler + case *float64: + value.TypeHandler = float64Handler + case *bool: + value.TypeHandler = boolHandler + case *int: + value.TypeHandler = intHandler + case *int8: + value.TypeHandler = int8Handler + case *int16: + value.TypeHandler = int16Handler + case *int32: + value.TypeHandler = int32Handler + case *int64: + value.TypeHandler = int64Handler + case *uint: + value.TypeHandler = uintHandler + case *uint8: + value.TypeHandler = uint8Handler + case *uint16: + value.TypeHandler = uint16Handler + case *uint32: + value.TypeHandler = uint32Handler + case *uint64: + value.TypeHandler = uint64Handler + } + return nil } From 54352a98eca95e036c2e819c4b795477c3326068 Mon Sep 17 00:00:00 2001 From: BrandonRomano Date: Wed, 21 Jun 2017 19:09:07 -0400 Subject: [PATCH 2/3] Write custom type handler test --- validator_test.go | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/validator_test.go b/validator_test.go index 9c55576..0e74ae4 100644 --- a/validator_test.go +++ b/validator_test.go @@ -1,6 +1,7 @@ package validator_test import ( + "database/sql" "errors" "fmt" v "github.com/go-carrot/validator" @@ -649,3 +650,33 @@ func TestUint64(t *testing.T) { assert.NotNil(t, err) assert.Equal(t, uint64(0), stringId) } + +// TestCustomTypeHandler tests that we can create a new type handler +// and use it as expected +func TestCustomTypeHandler(t *testing.T) { + // Create type handler + var nullStringTypeHandler = func(input string, value *v.Value) error { + nullString := value.Result.(*sql.NullString) + (*nullString).String = input + (*nullString).Valid = (input != "") + return nil + } + + // Test valid case + var slug sql.NullString + err := v.Validate([]*v.Value{ + {Result: &slug, Name: "slug", Input: "wow", TypeHandler: nullStringTypeHandler}, + }) + assert.Nil(t, err) + assert.Equal(t, "wow", slug.String) + assert.Equal(t, true, slug.Valid) + + // Test invalid case + var emptySlug sql.NullString + err = v.Validate([]*v.Value{ + {Result: &emptySlug, Name: "slug", Input: "", TypeHandler: nullStringTypeHandler}, + }) + assert.Nil(t, err) + assert.Equal(t, "", emptySlug.String) + assert.Equal(t, false, emptySlug.Valid) +} From 8009ad90547ccec937fb3fd5a366f6166113f55e Mon Sep 17 00:00:00 2001 From: BrandonRomano Date: Thu, 22 Jun 2017 16:22:26 -0400 Subject: [PATCH 3/3] Add documentation, update tests --- README.md | 59 ++++++++++++++++++++++++++++++++++++++++++----- validator_test.go | 42 ++++++++++++++++++++++----------- 2 files changed, 82 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 060aec3..3d3491c 100644 --- a/README.md +++ b/README.md @@ -41,10 +41,12 @@ Let's check out the Value struct. ```go type Value struct { - Result interface{} - Name string - Input string - Rules []Rule + Result interface{} + Default string + Name string + Input string + Rules []Rule + TypeHandler TypeHandler } ``` @@ -52,9 +54,15 @@ type Value struct { Result must be a pointer to the variable you want to store the parsed input in. -Valid types for this are `*string`, `*float32`, `*float64`, `*bool`, `*int`, `*int8`, `*int16`, `*int32`, `*int64`, `*uint`, `*uint8`, `*uint16`, `*uint32`, `*uint64`. +By default, supported types for this are `*string`, `*float32`, `*float64`, `*bool`, `*int`, `*int8`, `*int16`, `*int32`, `*int64`, `*uint`, `*uint8`, `*uint16`, `*uint32`, `*uint64`. -It is expected that the value of the `Input` parameter can be parsed into the decided type using their respective [strconv](https://golang.org/pkg/strconv/) function, else an error will be thrown by [the Validate function](#the-validate-function) when it is called. +For the default supported types, it is expected that the value of the `Input` parameter can be parsed into the decided type using their respective [strconv](https://golang.org/pkg/strconv/) function, else an error will be thrown by [the Validate function](#the-validate-function) when it is called. + +If you need to use another type, `TypeHandler` must also be set to the Value struct. + +#### Default + +This is the optional default value of `Input` that will be set, if the value of `Input` ends up being an empty string. #### Name @@ -70,6 +78,12 @@ This is a slice of rules that you require a particular value to pass. This is optional, and can be not set if you don't have any rules for your value to pass. The value will still go through the type check if the Input is a non-empty string. +#### TypeHandler + +TypeHandler is a function that defines how the input string is parsed. + +For basic types, it's not necessary to implement your own TypeHandler, as they have already been implemented and will be attached to Values automatically. + ## Rules A Rule is a very simple type of function: @@ -113,6 +127,39 @@ Both of these strategies should feel very fluent in use: > You won't find any prebuilt rules in [go-carrot/validator](https://github.com/go-carrot/validator). If you're looking for those check out the [go-carrot/rules](https://github.com/go-carrot/rules) repository. +## TypeHandlers + +A TypeHandler is a function that follows the following definition: + +```go +type TypeHandler func(input string, value *Value) error +``` + +A TypeHandler is responsible for: + +- Validation of the non-null string input + - (TypeHandlers aren't called if the string is not set - you can mandate that a value is required via `Rules`) +- Converting string input to the desired type +- Passing the converted type into the `value.Result` + +TypeHandlers are best explained by example. This is a TypeHandler for a `*sql.NullInt64` + +```go +func NullInt64TypeHandler = func(input string, value *v.Value) error { + // Get int64 + res, err := strconv.ParseInt(input, 10, 64) + if err != nil { + return errors.New("Invalid parameter, must be an int64") + } + + // Update nullInt + nullInt := value.Result.(*sql.NullInt64) + (*nullInt).Int64 = int64(res) + (*nullInt).Valid = true + return nil +} +``` + ## The Validate Function The validate function is the function that will actually perform your input validation. This function will throw an error if any of your values fail validation. diff --git a/validator_test.go b/validator_test.go index 0e74ae4..034a70c 100644 --- a/validator_test.go +++ b/validator_test.go @@ -655,28 +655,44 @@ func TestUint64(t *testing.T) { // and use it as expected func TestCustomTypeHandler(t *testing.T) { // Create type handler - var nullStringTypeHandler = func(input string, value *v.Value) error { - nullString := value.Result.(*sql.NullString) - (*nullString).String = input - (*nullString).Valid = (input != "") + var nullInt64TypeHandler = func(input string, value *v.Value) error { + // Get int64 + res, err := strconv.ParseInt(input, 10, 64) + if err != nil { + return errors.New("Invalid parameter, must be an int64") + } + + // Update nullInt + nullInt := value.Result.(*sql.NullInt64) + (*nullInt).Int64 = int64(res) + (*nullInt).Valid = true return nil } // Test valid case - var slug sql.NullString + var id sql.NullInt64 err := v.Validate([]*v.Value{ - {Result: &slug, Name: "slug", Input: "wow", TypeHandler: nullStringTypeHandler}, + {Result: &id, Name: "id", Input: "42", TypeHandler: nullInt64TypeHandler}, }) assert.Nil(t, err) - assert.Equal(t, "wow", slug.String) - assert.Equal(t, true, slug.Valid) + assert.Equal(t, int64(42), id.Int64) + assert.Equal(t, true, id.Valid) - // Test invalid case - var emptySlug sql.NullString + // Test empty case + var emptyId sql.NullInt64 err = v.Validate([]*v.Value{ - {Result: &emptySlug, Name: "slug", Input: "", TypeHandler: nullStringTypeHandler}, + {Result: &emptyId, Name: "id", Input: "", TypeHandler: nullInt64TypeHandler}, }) assert.Nil(t, err) - assert.Equal(t, "", emptySlug.String) - assert.Equal(t, false, emptySlug.Valid) + assert.Equal(t, int64(0), emptyId.Int64) + assert.Equal(t, false, emptyId.Valid) + + // Test error case + var errorId sql.NullInt64 + err = v.Validate([]*v.Value{ + {Result: &errorId, Name: "id", Input: "abcd", TypeHandler: nullInt64TypeHandler}, + }) + assert.NotNil(t, err) + assert.Equal(t, int64(0), errorId.Int64) + assert.Equal(t, false, errorId.Valid) }