diff --git a/validation/README.md b/validation/README.md new file mode 100644 index 00000000..9d252799 --- /dev/null +++ b/validation/README.md @@ -0,0 +1,99 @@ +validation +============== + +validation is a form validation for a data validation and error collecting using Go. + +## Installation and tests + +Install: + + go get github.com/astaxie/beego/validation + +Test: + + go test github.com/astaxie/beego/validation + +## Example + +Direct Use: + + import ( + "github.com/astaxie/beego/validation" + "log" + ) + + type User struct { + Name string + Age int + } + + func main() { + u := User{"man", 40} + valid := validation.Validation{} + valid.Required(u.Name, "name") + valid.MaxSize(u.Name, 15, "nameMax") + valid.Range(u.Age, 0, 140, "age") + if valid.HasErrors { + // validation does not pass + // print invalid message + for _, err := range valid.Errors { + log.Println(err.Key, err.Message) + } + } + // or use like this + if v := valid.Max(u.Age, 140); !v.Ok { + log.Println(v.Error.Key, v.Error.Message) + } + } + +Struct Tag Use: + + import ( + "github.com/astaxie/beego/validation" + ) + + // validation function follow with "valid" tag + // functions divide with ";" + // parameters in parentheses "()" and divide with "," + type user struct { + Id int + Name string `valid:"Required"` + Age int `valid:"Required;Range(1, 140)"` + } + + func main() { + valid := Validation{} + u := user{Name: "test", Age: 40} + b, err := valid.Valid(u) + if err != nil { + // handle error + } + if !b { + // validation does not pass + // blabla... + } + } + +Struct Tag Functions: + + Required + Min(min int) + Max(max int) + Range(min, max int) + MinSize(min int) + MaxSize(max int) + Length(length int) + Alpha + Numeric + AlphaNumeric + Match(regexp string) // does not support yet + NoMatch(regexp string) // does not support yet + AlphaDash + Email + IP + Base64 + + +## LICENSE + +BSD License http://creativecommons.org/licenses/BSD/ diff --git a/validation/util.go b/validation/util.go new file mode 100644 index 00000000..cbdb1fa3 --- /dev/null +++ b/validation/util.go @@ -0,0 +1,198 @@ +package validation + +import ( + "fmt" + "reflect" + "regexp" + "strconv" + "strings" +) + +const ( + VALIDTAG = "valid" +) + +var ( + // key: function name + // value: the number of parameters + funcs = make(Funcs) + + // doesn't belong to validation functions + unFuncs = map[string]bool{ + "Clear": true, + "HasErrors": true, + "ErrorMap": true, + "Error": true, + "apply": true, + "Check": true, + "Valid": true, + } +) + +func init() { + v := &Validation{} + t := reflect.TypeOf(v) + for i := 0; i < t.NumMethod(); i++ { + m := t.Method(i) + if !unFuncs[m.Name] { + funcs[m.Name] = m.Func + } + } +} + +type ValidFunc struct { + Name string + Params []interface{} +} + +type Funcs map[string]reflect.Value + +func (f Funcs) Call(name string, params ...interface{}) (result []reflect.Value, err error) { + defer func() { + if r := recover(); r != nil { + err = r.(error) + } + }() + if _, ok := f[name]; !ok { + err = fmt.Errorf("%s does not exist", name) + return + } + if len(params) != f[name].Type().NumIn() { + err = fmt.Errorf("The number of params is not adapted") + return + } + in := make([]reflect.Value, len(params)) + for k, param := range params { + in[k] = reflect.ValueOf(param) + } + result = f[name].Call(in) + return +} + +func isStruct(t reflect.Type) bool { + return t.Kind() == reflect.Struct +} + +func isStructPtr(t reflect.Type) bool { + return t.Kind() == reflect.Ptr && t.Elem().Kind() == reflect.Struct +} + +func getValidFuncs(f reflect.StructField) (vfs []ValidFunc, err error) { + tag := f.Tag.Get(VALIDTAG) + if len(tag) == 0 { + return + } + fs := strings.Split(tag, ";") + for _, vfunc := range fs { + var vf ValidFunc + vf, err = parseFunc(vfunc) + if err != nil { + return + } + vfs = append(vfs, vf) + } + return +} + +func parseFunc(vfunc string) (v ValidFunc, err error) { + defer func() { + if r := recover(); r != nil { + err = r.(error) + } + }() + + vfunc = strings.TrimSpace(vfunc) + start := strings.Index(vfunc, "(") + var num int + + // doesn't need parameter valid function + if start == -1 { + if num, err = numIn(vfunc); err != nil { + return + } + if num != 0 { + err = fmt.Errorf("%s require %d parameters", vfunc, num) + return + } + v = ValidFunc{vfunc, []interface{}{vfunc}} + return + } + + end := strings.Index(vfunc, ")") + if end == -1 { + err = fmt.Errorf("invalid valid function") + return + } + + name := strings.TrimSpace(vfunc[:start]) + if num, err = numIn(name); err != nil { + return + } + + params := strings.Split(vfunc[start+1:end], ",") + // the num of param must be equal + if num != len(params) { + err = fmt.Errorf("%s require %d parameters", name, num) + return + } + + tParams, err := trim(name, params) + if err != nil { + return + } + v = ValidFunc{name, tParams} + return +} + +func numIn(name string) (num int, err error) { + fn, ok := funcs[name] + if !ok { + err = fmt.Errorf("doesn't exsits %s valid function", name) + return + } + // sub *Validation obj and key + num = fn.Type().NumIn() - 3 + return +} + +func trim(name string, s []string) (ts []interface{}, err error) { + ts = make([]interface{}, len(s), len(s)+1) + fn, ok := funcs[name] + if !ok { + err = fmt.Errorf("doesn't exsits %s valid function", name) + return + } + for i := 0; i < len(s); i++ { + var param interface{} + // skip *Validation and obj params + if param, err = magic(fn.Type().In(i+2), strings.TrimSpace(s[i])); err != nil { + return + } + ts[i] = param + } + ts = append(ts, name) + return +} + +// modify the parameters's type to adapt the function input parameters' type +func magic(t reflect.Type, s string) (i interface{}, err error) { + switch t.Kind() { + case reflect.Int: + i, err = strconv.Atoi(s) + case reflect.String: + i = s + case reflect.Ptr: + if t.Elem().String() != "regexp.Regexp" { + err = fmt.Errorf("does not support %s", t.Elem().String()) + return + } + i, err = regexp.Compile(s) + default: + err = fmt.Errorf("does not support %s", t.Kind().String()) + } + return +} + +func mergeParam(v *Validation, obj interface{}, params []interface{}) []interface{} { + return append([]interface{}{v, obj}, params...) +} diff --git a/validation/util_test.go b/validation/util_test.go new file mode 100644 index 00000000..e181871f --- /dev/null +++ b/validation/util_test.go @@ -0,0 +1,77 @@ +package validation + +import ( + "reflect" + "testing" +) + +type user struct { + Id int + Tag string `valid:"Maxx(aa)"` + Name string `valid:"Required"` + Age int `valid:"Required;Range(1, 140)"` +} + +func TestGetValidFuncs(t *testing.T) { + u := user{Name: "test", Age: 1} + tf := reflect.TypeOf(u) + var vfs []ValidFunc + var err error + + f, _ := tf.FieldByName("Id") + if vfs, err = getValidFuncs(f); err != nil { + t.Fatal(err) + } + if len(vfs) != 0 { + t.Fatal("should get none ValidFunc") + } + + f, _ = tf.FieldByName("Tag") + if vfs, err = getValidFuncs(f); err.Error() != "doesn't exsits Maxx valid function" { + t.Fatal(err) + } + + f, _ = tf.FieldByName("Name") + if vfs, err = getValidFuncs(f); err != nil { + t.Fatal(err) + } + if len(vfs) != 1 { + t.Fatal("should get 1 ValidFunc") + } + if vfs[0].Name != "Required" && len(vfs[0].Params) != 0 { + t.Error("Required funcs should be got") + } + + f, _ = tf.FieldByName("Age") + if vfs, err = getValidFuncs(f); err != nil { + t.Fatal(err) + } + if len(vfs) != 2 { + t.Fatal("should get 2 ValidFunc") + } + if vfs[0].Name != "Required" && len(vfs[0].Params) != 0 { + t.Error("Required funcs should be got") + } + if vfs[1].Name != "Range" && len(vfs[1].Params) != 2 { + t.Error("Range funcs should be got") + } +} + +func TestCall(t *testing.T) { + u := user{Name: "test", Age: 180} + tf := reflect.TypeOf(u) + var vfs []ValidFunc + var err error + f, _ := tf.FieldByName("Age") + if vfs, err = getValidFuncs(f); err != nil { + t.Fatal(err) + } + valid := &Validation{} + vfs[1].Params = append([]interface{}{valid, u.Age}, vfs[1].Params...) + if _, err = funcs.Call(vfs[1].Name, vfs[1].Params...); err != nil { + t.Fatal(err) + } + if len(valid.Errors) != 1 { + t.Error("age out of range should be has an error") + } +} diff --git a/validation/validation.go b/validation/validation.go index fa2c7bc3..46251716 100644 --- a/validation/validation.go +++ b/validation/validation.go @@ -2,6 +2,7 @@ package validation import ( "fmt" + "reflect" "regexp" ) @@ -175,3 +176,32 @@ func (v *Validation) Check(obj interface{}, checks ...Validator) *ValidationResu } return result } + +// the obj parameter must be a struct or a struct pointer +func (v *Validation) Valid(obj interface{}) (b bool, err error) { + objT := reflect.TypeOf(obj) + objV := reflect.ValueOf(obj) + switch { + case isStruct(objT): + case isStructPtr(objT): + objT = objT.Elem() + objV = objV.Elem() + default: + err = fmt.Errorf("%v must be a struct or a struct pointer", obj) + return + } + + for i := 0; i < objT.NumField(); i++ { + var vfs []ValidFunc + if vfs, err = getValidFuncs(objT.Field(i)); err != nil { + return + } + for _, vf := range vfs { + if _, err = funcs.Call(vf.Name, + mergeParam(v, objV.Field(i).Interface(), vf.Params)...); err != nil { + return + } + } + } + return !v.HasErrors(), nil +} diff --git a/validation/validation_test.go b/validation/validation_test.go index 9f814367..d9f35922 100644 --- a/validation/validation_test.go +++ b/validation/validation_test.go @@ -31,7 +31,7 @@ func TestRequired(t *testing.T) { t.Error("empty slice should be false") } if !valid.Required([]interface{}{"ok"}, "slice").Ok { - t.Error("slice should be equal true") + t.Error("slice should be true") } } @@ -217,3 +217,30 @@ func TestBase64(t *testing.T) { t.Error("\"c3VjaHVhbmdqaUBnbWFpbC5jb20=\" are a valid base64 characters should be true") } } + +func TestValid(t *testing.T) { + type user struct { + Id int + Name string `valid:"Required"` + Age int `valid:"Required;Range(1, 140)"` + } + valid := Validation{} + + u := user{Name: "test", Age: 40} + b, err := valid.Valid(u) + if err != nil { + t.Fatal(err) + } + if !b { + t.Error("validation should be passed") + } + + uptr := &user{Name: "test", Age: 180} + b, err = valid.Valid(uptr) + if err != nil { + t.Fatal(err) + } + if b { + t.Error("validation should not be passed") + } +} diff --git a/validation/validators.go b/validation/validators.go index 41ed02ed..e8533749 100644 --- a/validation/validators.go +++ b/validation/validators.go @@ -63,7 +63,7 @@ func (m Min) IsSatisfied(obj interface{}) bool { } func (m Min) DefaultMessage() string { - return fmt.Sprintln("Minimum is", m.Min) + return fmt.Sprint("Minimum is ", m.Min) } func (m Min) GetKey() string { @@ -84,7 +84,7 @@ func (m Max) IsSatisfied(obj interface{}) bool { } func (m Max) DefaultMessage() string { - return fmt.Sprintln("Maximum is", m.Max) + return fmt.Sprint("Maximum is ", m.Max) } func (m Max) GetKey() string { @@ -103,7 +103,7 @@ func (r Range) IsSatisfied(obj interface{}) bool { } func (r Range) DefaultMessage() string { - return fmt.Sprintln("Range is", r.Min.Min, "to", r.Max.Max) + return fmt.Sprint("Range is ", r.Min.Min, " to ", r.Max.Max) } func (r Range) GetKey() string { @@ -128,7 +128,7 @@ func (m MinSize) IsSatisfied(obj interface{}) bool { } func (m MinSize) DefaultMessage() string { - return fmt.Sprintln("Minimum size is", m.Min) + return fmt.Sprint("Minimum size is ", m.Min) } func (m MinSize) GetKey() string { @@ -153,7 +153,7 @@ func (m MaxSize) IsSatisfied(obj interface{}) bool { } func (m MaxSize) DefaultMessage() string { - return fmt.Sprintln("Maximum size is", m.Max) + return fmt.Sprint("Maximum size is ", m.Max) } func (m MaxSize) GetKey() string { @@ -178,7 +178,7 @@ func (l Length) IsSatisfied(obj interface{}) bool { } func (l Length) DefaultMessage() string { - return fmt.Sprintln("Required length is", l.N) + return fmt.Sprint("Required length is ", l.N) } func (l Length) GetKey() string { @@ -202,7 +202,7 @@ func (a Alpha) IsSatisfied(obj interface{}) bool { } func (a Alpha) DefaultMessage() string { - return fmt.Sprintln("Must be valid alpha characters") + return fmt.Sprint("Must be valid alpha characters") } func (a Alpha) GetKey() string { @@ -226,7 +226,7 @@ func (n Numeric) IsSatisfied(obj interface{}) bool { } func (n Numeric) DefaultMessage() string { - return fmt.Sprintln("Must be valid numeric characters") + return fmt.Sprint("Must be valid numeric characters") } func (n Numeric) GetKey() string { @@ -250,7 +250,7 @@ func (a AlphaNumeric) IsSatisfied(obj interface{}) bool { } func (a AlphaNumeric) DefaultMessage() string { - return fmt.Sprintln("Must be valid alpha or numeric characters") + return fmt.Sprint("Must be valid alpha or numeric characters") } func (a AlphaNumeric) GetKey() string { @@ -269,7 +269,7 @@ func (m Match) IsSatisfied(obj interface{}) bool { } func (m Match) DefaultMessage() string { - return fmt.Sprintln("Must match", m.Regexp) + return fmt.Sprint("Must match ", m.Regexp) } func (m Match) GetKey() string { @@ -287,7 +287,7 @@ func (n NoMatch) IsSatisfied(obj interface{}) bool { } func (n NoMatch) DefaultMessage() string { - return fmt.Sprintln("Must not match", n.Regexp) + return fmt.Sprint("Must not match ", n.Regexp) } func (n NoMatch) GetKey() string { @@ -302,7 +302,7 @@ type AlphaDash struct { } func (a AlphaDash) DefaultMessage() string { - return fmt.Sprintln("Must be valid alpha or numeric or dash(-_) characters") + return fmt.Sprint("Must be valid alpha or numeric or dash(-_) characters") } func (a AlphaDash) GetKey() string { @@ -317,7 +317,7 @@ type Email struct { } func (e Email) DefaultMessage() string { - return fmt.Sprintln("Must be a valid email address") + return fmt.Sprint("Must be a valid email address") } func (e Email) GetKey() string { @@ -332,7 +332,7 @@ type IP struct { } func (i IP) DefaultMessage() string { - return fmt.Sprintln("Must be a valid ip address") + return fmt.Sprint("Must be a valid ip address") } func (i IP) GetKey() string { @@ -347,7 +347,7 @@ type Base64 struct { } func (b Base64) DefaultMessage() string { - return fmt.Sprintln("Must be valid base64 characters") + return fmt.Sprint("Must be valid base64 characters") } func (b Base64) GetKey() string {