Skip to content

Commit

Permalink
Don't panic, return errors instead. Closes go-validator#2
Browse files Browse the repository at this point in the history
  • Loading branch information
robteix committed May 4, 2014
1 parent b07e045 commit 422656f
Show file tree
Hide file tree
Showing 5 changed files with 153 additions and 56 deletions.
126 changes: 94 additions & 32 deletions builtins.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
package validator

import (
"errors"
"reflect"
"regexp"
"strconv"
Expand Down Expand Up @@ -46,7 +45,7 @@ func nonzero(v interface{}, param string) error {
case reflect.Invalid:
valid = false // always invalid
default:
panic("Unsupported type " + st.String())
return ErrUnsupported
}

if !valid {
Expand All @@ -63,17 +62,37 @@ func length(v interface{}, param string) error {
valid := true
switch st.Kind() {
case reflect.String:
valid = int64(len(st.String())) == asInt(param)
p, err := asInt(param)
if err != nil {
return ErrBadParameter
}
valid = int64(len(st.String())) == p
case reflect.Slice, reflect.Map, reflect.Array:
valid = int64(st.Len()) == asInt(param)
p, err := asInt(param)
if err != nil {
return ErrBadParameter
}
valid = int64(st.Len()) == p
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
valid = st.Int() == asInt(param)
p, err := asInt(param)
if err != nil {
return ErrBadParameter
}
valid = st.Int() == p
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
valid = st.Uint() == asUint(param)
p, err := asUint(param)
if err != nil {
return ErrBadParameter
}
valid = st.Uint() == p
case reflect.Float32, reflect.Float64:
valid = st.Float() == asFloat(param)
p, err := asFloat(param)
if err != nil {
return ErrBadParameter
}
valid = st.Float() == p
default:
panic("length is not a valid validation tag for type " + st.String())
return ErrUnsupported
}
if !valid {
return ErrLen
Expand All @@ -90,17 +109,37 @@ func min(v interface{}, param string) error {
invalid := false
switch st.Kind() {
case reflect.String:
invalid = int64(len(st.String())) < asInt(param)
p, err := asInt(param)
if err != nil {
return ErrBadParameter
}
invalid = int64(len(st.String())) < p
case reflect.Slice, reflect.Map, reflect.Array:
invalid = int64(st.Len()) < asInt(param)
p, err := asInt(param)
if err != nil {
return ErrBadParameter
}
invalid = int64(st.Len()) < p
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
invalid = st.Int() < asInt(param)
p, err := asInt(param)
if err != nil {
return ErrBadParameter
}
invalid = st.Int() < p
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
invalid = st.Uint() < asUint(param)
p, err := asUint(param)
if err != nil {
return ErrBadParameter
}
invalid = st.Uint() < p
case reflect.Float32, reflect.Float64:
invalid = st.Float() < asFloat(param)
p, err := asFloat(param)
if err != nil {
return ErrBadParameter
}
invalid = st.Float() < p
default:
panic("min is not a valid validation tag for type " + st.String())
return ErrUnsupported
}
if invalid {
return ErrMin
Expand All @@ -117,17 +156,37 @@ func max(v interface{}, param string) error {
var invalid bool
switch st.Kind() {
case reflect.String:
invalid = int64(len(st.String())) > asInt(param)
p, err := asInt(param)
if err != nil {
return ErrBadParameter
}
invalid = int64(len(st.String())) > p
case reflect.Slice, reflect.Map, reflect.Array:
invalid = int64(st.Len()) > asInt(param)
p, err := asInt(param)
if err != nil {
return ErrBadParameter
}
invalid = int64(st.Len()) > p
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
invalid = st.Int() > asInt(param)
p, err := asInt(param)
if err != nil {
return ErrBadParameter
}
invalid = st.Int() > p
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
invalid = st.Uint() > asUint(param)
p, err := asUint(param)
if err != nil {
return ErrBadParameter
}
invalid = st.Uint() > p
case reflect.Float32, reflect.Float64:
invalid = st.Float() > asFloat(param)
p, err := asFloat(param)
if err != nil {
return ErrBadParameter
}
invalid = st.Float() > p
default:
panic("max is not a valid validation tag for type " + st.String())
return ErrUnsupported
}
if invalid {
return ErrMax
Expand All @@ -140,43 +199,46 @@ func max(v interface{}, param string) error {
func regex(v interface{}, param string) error {
s, ok := v.(string)
if !ok {
panic("regexp requires a string")
return ErrUnsupported
}

re := regexp.MustCompile(param)
re, err := regexp.Compile(param)
if err != nil {
return ErrBadParameter
}

if !re.MatchString(s) {
return errors.New("regexp does not match")
return ErrRegexp
}
return nil
}

// asInt retuns the parameter as a int64
// or panics if it can't convert
func asInt(param string) int64 {
func asInt(param string) (int64, error) {
i, err := strconv.ParseInt(param, 0, 64)
if err != nil {
panic("Invalid param " + param + ", should be an integer")
return 0, ErrBadParameter
}
return i
return i, nil
}

// asUint retuns the parameter as a uint64
// or panics if it can't convert
func asUint(param string) uint64 {
func asUint(param string) (uint64, error) {
i, err := strconv.ParseUint(param, 0, 64)
if err != nil {
panic("Invalid param " + param + ", should be an unsigned integer")
return 0, ErrBadParameter
}
return i
return i, nil
}

// asFloat retuns the parameter as a float64
// or panics if it can't convert
func asFloat(param string) float64 {
func asFloat(param string) (float64, error) {
i, err := strconv.ParseFloat(param, 64)
if err != nil {
panic("Invalid param " + param + ", should be a float")
return 0.0, ErrBadParameter
}
return i
return i, nil
}
9 changes: 5 additions & 4 deletions doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ Note that there are no tests to prevent conflicting validator parameters. For
instance, these fields will never be valid.
...
A int `validate:"max=0,nonzero"`
A int `validate:"max=0,min=1"`
B string `validate:"len=10,regexp=^$"
...
Expand All @@ -98,7 +98,7 @@ First, one needs to create a validation function.
func notZZ(v interface{}, param string) error {
st := reflect.ValueOf(v)
if st.Kind() != reflect.String {
return errors.New("notZZ only validates strings")
return validate.ErrUnsupported
}
if st.String() == "ZZ" {
return errors.New("value cannot be ZZ")
Expand Down Expand Up @@ -127,7 +127,7 @@ To use parameters, it is very similar.
func notSomething(v interface{}, param string) error {
st := reflect.ValueOf(v)
if st.Kind() != reflect.String {
return errors.New("notSomething only validates strings")
return validate.ErrUnsupported
}
if st.String() == param {
return errors.New("value cannot be " + param)
Expand Down Expand Up @@ -155,7 +155,8 @@ And you can delete a validation function by setting it to nil.
validate.SetValidationFunc("notzz", nil)
validate.SetValidationFunc("nonzero", nil)
Using a non-existing validation func in a field tag will panic.
Using a non-existing validation func in a field tag will always return
false and with error validate.ErrUnknownTag.
Finally, package validator also provides a helper function that can be used
to validate simple variables/values.
Expand Down
2 changes: 1 addition & 1 deletion examplevalidate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ func ExampleValidate() {
// Street cannot be empty.
// Invalid due to fields:
// - Age ([less than min])
// - Email ([regexp does not match])
// - Email ([regular expression mismatch])
// - Address.Street ([zero value])
}

Expand Down
25 changes: 18 additions & 7 deletions validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,18 @@ var (
// ErrLen is the error returned when length is not equal to
// param specified
ErrLen = errors.New("invalid length")
// ErrRegexp is the error returned when the value does not
// match the provided regular expression parameter
ErrRegexp = errors.New("regular expression mismatch")
// ErrUnsupported is the error error returned when a validation rule
// is used with an unsupported variable type
ErrUnsupported = errors.New("unsupported type")
// ErrBadParameter is the error returned when an invalid parameter
// is provided to a validation rule (e.g. a string where an int was
// expected (max=foo,len=bar) or missing a parameter when one is required (len=))
ErrBadParameter = errors.New("bad parameter")
// ErrUnknownTag is the error returned when an unknown tag is found
ErrUnknownTag = errors.New("unknown tag")
// ErrInvalid is the error returned when variable is invalid
// (normally a nil pointer)
ErrInvalid = errors.New("invalid value")
Expand Down Expand Up @@ -219,7 +228,11 @@ func (mv *Validator) Valid(val interface{}, tags string) (bool, []error) {

// validateVar validates one single variable
func (mv *Validator) validateVar(v interface{}, tag string) []error {
tags := mv.parseTags(tag)
tags, err := mv.parseTags(tag)
if err != nil {
// unknown tag found, give up.
return []error{err}
}
errs := make([]error, 0, len(tags))
for _, t := range tags {
if err := t.Fn(v, t.Param); err != nil {
Expand All @@ -237,27 +250,25 @@ type tag struct {
}

// parseTags parses all individual tags found within a struct tag.
// These usually are in the
func (mv *Validator) parseTags(t string) []tag {
func (mv *Validator) parseTags(t string) ([]tag, error) {
tl := strings.Split(t, ",")
tags := make([]tag, 0, len(tl))
for _, i := range tl {
tg := tag{}
v := strings.SplitN(i, "=", 2)
tg.Name = strings.Trim(v[0], " ")
if tg.Name == "" {
panic("empty tag")
return []tag{}, ErrUnknownTag
}
if len(v) > 1 {
tg.Param = strings.Trim(v[1], " ")
}
var found bool
if tg.Fn, found = mv.validationFuncs[tg.Name]; !found {
panic("invalid tag " + tg.Name)
return []tag{}, ErrUnknownTag
}

tags = append(tags, tg)

}
return tags
return tags, nil
}
47 changes: 35 additions & 12 deletions validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
package validator_test

import (
. "gopkg.in/check.v1"
"testing"
. "gopkg.in/check.v1"

"gopkg.in/validator.v1"
)
Expand Down Expand Up @@ -178,21 +178,44 @@ func (ms *MySuite) TestValidateStructVar(c *C) {
c.Assert(errs, HasError, validator.ErrUnsupported)
}

/*
func (ms *MySuite) TestValidateVar1(c *C) {
t1 := TestStruct{Number: 123, Text: "Blah"}
func (ms *MySuite) TestUnknownTag(c *C) {
type test struct {
A int `validate:"foo"`
}
t := test{}
valid, errs := validator.Validate(t)
c.Assert(valid, Equals, false)
c.Assert(errs, HasLen, 1)
c.Assert(errs["A"], HasError, validator.ErrUnknownTag)
}

//s := reflect.ValueOf(t).Elem()
s := reflect.ValueOf(t1)
st := s.Type()
for i := 0; i < s.NumField(); i++ {
f := s.Field(i)
fmt.Printf("%d: %s %s %s = %v\n", i,
st.Field(i).Name, f.Type(), st.Field(i).Tag.Get("validator"), f.Interface())
func (ms *MySuite) TestUnsupported(c *C) {
type test struct {
A int `validate:"regexp=a.*b"`
B float64 `validate:"regexp=.*"`
}
t := test{}
valid, errs := validator.Validate(t)
c.Assert(valid, Equals, false)
c.Assert(errs, HasLen, 2)
c.Assert(errs["A"], HasError, validator.ErrUnsupported)
c.Assert(errs["B"], HasError, validator.ErrUnsupported)
}

func (ms *MySuite) TestBadParameter(c *C) {
type test struct {
A string `validate:"min="`
B string `validate:"len=="`
C string `validate:"max=foo"`
}
t := test{}
valid, errs := validator.Validate(t)
c.Assert(valid, Equals, false)
c.Assert(errs, HasLen, 3)
c.Assert(errs["A"], HasError, validator.ErrBadParameter)
c.Assert(errs["B"], HasError, validator.ErrBadParameter)
c.Assert(errs["C"], HasError, validator.ErrBadParameter)
}
*/

type hasErrorChecker struct {
*CheckerInfo
Expand Down

0 comments on commit 422656f

Please sign in to comment.