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/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 } diff --git a/validator_test.go b/validator_test.go index 9c55576..034a70c 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,49 @@ 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 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 id sql.NullInt64 + err := v.Validate([]*v.Value{ + {Result: &id, Name: "id", Input: "42", TypeHandler: nullInt64TypeHandler}, + }) + assert.Nil(t, err) + assert.Equal(t, int64(42), id.Int64) + assert.Equal(t, true, id.Valid) + + // Test empty case + var emptyId sql.NullInt64 + err = v.Validate([]*v.Value{ + {Result: &emptyId, Name: "id", Input: "", TypeHandler: nullInt64TypeHandler}, + }) + assert.Nil(t, err) + 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) +}