diff --git a/validation/validation.go b/validation/validation.go new file mode 100644 index 00000000..fa2c7bc3 --- /dev/null +++ b/validation/validation.go @@ -0,0 +1,177 @@ +package validation + +import ( + "fmt" + "regexp" +) + +type ValidationError struct { + Message, Key string +} + +// Returns the Message. +func (e *ValidationError) String() string { + if e == nil { + return "" + } + return e.Message +} + +// A Validation context manages data validation and error messages. +type Validation struct { + Errors []*ValidationError +} + +func (v *Validation) Clear() { + v.Errors = []*ValidationError{} +} + +func (v *Validation) HasErrors() bool { + return len(v.Errors) > 0 +} + +// Return the errors mapped by key. +// If there are multiple validation errors associated with a single key, the +// first one "wins". (Typically the first validation will be the more basic). +func (v *Validation) ErrorMap() map[string]*ValidationError { + m := map[string]*ValidationError{} + for _, e := range v.Errors { + if _, ok := m[e.Key]; !ok { + m[e.Key] = e + } + } + return m +} + +// Add an error to the validation context. +func (v *Validation) Error(message string, args ...interface{}) *ValidationResult { + result := (&ValidationResult{ + Ok: false, + Error: &ValidationError{}, + }).Message(message, args...) + v.Errors = append(v.Errors, result.Error) + return result +} + +// A ValidationResult is returned from every validation method. +// It provides an indication of success, and a pointer to the Error (if any). +type ValidationResult struct { + Error *ValidationError + Ok bool +} + +func (r *ValidationResult) Key(key string) *ValidationResult { + if r.Error != nil { + r.Error.Key = key + } + return r +} + +func (r *ValidationResult) Message(message string, args ...interface{}) *ValidationResult { + if r.Error != nil { + if len(args) == 0 { + r.Error.Message = message + } else { + r.Error.Message = fmt.Sprintf(message, args...) + } + } + return r +} + +// Test that the argument is non-nil and non-empty (if string or list) +func (v *Validation) Required(obj interface{}, key string) *ValidationResult { + return v.apply(Required{key}, obj) +} + +func (v *Validation) Min(n int, min int, key string) *ValidationResult { + return v.apply(Min{min, key}, n) +} + +func (v *Validation) Max(n int, max int, key string) *ValidationResult { + return v.apply(Max{max, key}, n) +} + +func (v *Validation) Range(n, min, max int, key string) *ValidationResult { + return v.apply(Range{Min{Min: min}, Max{Max: max}, key}, n) +} + +func (v *Validation) MinSize(obj interface{}, min int, key string) *ValidationResult { + return v.apply(MinSize{min, key}, obj) +} + +func (v *Validation) MaxSize(obj interface{}, max int, key string) *ValidationResult { + return v.apply(MaxSize{max, key}, obj) +} + +func (v *Validation) Length(obj interface{}, n int, key string) *ValidationResult { + return v.apply(Length{n, key}, obj) +} + +func (v *Validation) Alpha(obj interface{}, key string) *ValidationResult { + return v.apply(Alpha{key}, obj) +} + +func (v *Validation) Numeric(obj interface{}, key string) *ValidationResult { + return v.apply(Numeric{key}, obj) +} + +func (v *Validation) AlphaNumeric(obj interface{}, key string) *ValidationResult { + return v.apply(AlphaNumeric{key}, obj) +} + +func (v *Validation) Match(str string, regex *regexp.Regexp, key string) *ValidationResult { + return v.apply(Match{regex, key}, str) +} + +func (v *Validation) NoMatch(str string, regex *regexp.Regexp, key string) *ValidationResult { + return v.apply(NoMatch{Match{Regexp: regex}, key}, str) +} + +func (v *Validation) AlphaDash(str string, key string) *ValidationResult { + return v.apply(AlphaDash{NoMatch{Match: Match{Regexp: alphaDashPattern}}, key}, str) +} + +func (v *Validation) Email(str string, key string) *ValidationResult { + return v.apply(Email{Match{Regexp: emailPattern}, key}, str) +} + +func (v *Validation) IP(str string, key string) *ValidationResult { + return v.apply(IP{Match{Regexp: ipPattern}, key}, str) +} + +func (v *Validation) Base64(str string, key string) *ValidationResult { + return v.apply(Base64{Match{Regexp: base64Pattern}, key}, str) +} + +func (v *Validation) apply(chk Validator, obj interface{}) *ValidationResult { + if chk.IsSatisfied(obj) { + return &ValidationResult{Ok: true} + } + + // Add the error to the validation context. + err := &ValidationError{ + Message: chk.DefaultMessage(), + Key: chk.GetKey(), + } + v.Errors = append(v.Errors, err) + + // Also return it in the result. + return &ValidationResult{ + Ok: false, + Error: err, + } +} + +// Apply a group of validators to a field, in order, and return the +// ValidationResult from the first one that fails, or the last one that +// succeeds. +func (v *Validation) Check(obj interface{}, checks ...Validator) *ValidationResult { + var result *ValidationResult + for _, check := range checks { + result = v.apply(check, obj) + if !result.Ok { + return result + } + } + return result +} diff --git a/validation/validation_test.go b/validation/validation_test.go new file mode 100644 index 00000000..9f814367 --- /dev/null +++ b/validation/validation_test.go @@ -0,0 +1,219 @@ +package validation + +import ( + "regexp" + "testing" + "time" +) + +func TestRequired(t *testing.T) { + valid := Validation{} + + if valid.Required(nil, "nil").Ok { + t.Error("nil object should be false") + } + if valid.Required("", "string").Ok { + t.Error("\"'\" string should be false") + } + if !valid.Required("astaxie", "string").Ok { + t.Error("string should be true") + } + if valid.Required(0, "zero").Ok { + t.Error("Integer should not be equal 0") + } + if !valid.Required(1, "int").Ok { + t.Error("Integer except 0 should be true") + } + if !valid.Required(time.Now(), "time").Ok { + t.Error("time should be true") + } + if valid.Required([]string{}, "emptySlice").Ok { + t.Error("empty slice should be false") + } + if !valid.Required([]interface{}{"ok"}, "slice").Ok { + t.Error("slice should be equal true") + } +} + +func TestMin(t *testing.T) { + valid := Validation{} + + if valid.Min(-1, 0, "min0").Ok { + t.Error("-1 is less than the minimum value of 0 should be false") + } + if !valid.Min(1, 0, "min0").Ok { + t.Error("1 is greater or equal than the minimum value of 0 should be true") + } +} + +func TestMax(t *testing.T) { + valid := Validation{} + + if valid.Max(1, 0, "max0").Ok { + t.Error("1 is greater than the minimum value of 0 should be false") + } + if !valid.Max(-1, 0, "max0").Ok { + t.Error("-1 is less or equal than the maximum value of 0 should be true") + } +} + +func TestRange(t *testing.T) { + valid := Validation{} + + if valid.Range(-1, 0, 1, "range0_1").Ok { + t.Error("-1 is bettween 0 and 1 should be false") + } + if !valid.Range(1, 0, 1, "range0_1").Ok { + t.Error("1 is bettween 0 and 1 should be true") + } +} + +func TestMinSize(t *testing.T) { + valid := Validation{} + + if valid.MinSize("", 1, "minSize1").Ok { + t.Error("the length of \"\" is less than the minimum value of 1 should be false") + } + if !valid.MinSize("ok", 1, "minSize1").Ok { + t.Error("the length of \"ok\" is greater or equal than the minimum value of 1 should be true") + } + if valid.MinSize([]string{}, 1, "minSize1").Ok { + t.Error("the length of empty slice is less than the minimum value of 1 should be false") + } + if !valid.MinSize([]interface{}{"ok"}, 1, "minSize1").Ok { + t.Error("the length of [\"ok\"] is greater or equal than the minimum value of 1 should be true") + } +} + +func TestMaxSize(t *testing.T) { + valid := Validation{} + + if valid.MaxSize("ok", 1, "maxSize1").Ok { + t.Error("the length of \"ok\" is greater than the maximum value of 1 should be false") + } + if !valid.MaxSize("", 1, "maxSize1").Ok { + t.Error("the length of \"\" is less or equal than the maximum value of 1 should be true") + } + if valid.MaxSize([]interface{}{"ok", false}, 1, "maxSize1").Ok { + t.Error("the length of [\"ok\", false] is greater than the maximum value of 1 should be false") + } + if !valid.MaxSize([]string{}, 1, "maxSize1").Ok { + t.Error("the length of empty slice is less or equal than the maximum value of 1 should be true") + } +} + +func TestLength(t *testing.T) { + valid := Validation{} + + if valid.Length("", 1, "length1").Ok { + t.Error("the length of \"\" must equal 1 should be false") + } + if !valid.Length("1", 1, "length1").Ok { + t.Error("the length of \"1\" must equal 1 should be true") + } + if valid.Length([]string{}, 1, "length1").Ok { + t.Error("the length of empty slice must equal 1 should be false") + } + if !valid.Length([]interface{}{"ok"}, 1, "length1").Ok { + t.Error("the length of [\"ok\"] must equal 1 should be true") + } +} + +func TestAlpha(t *testing.T) { + valid := Validation{} + + if valid.Alpha("a,1-@ $", "alpha").Ok { + t.Error("\"a,1-@ $\" are valid alpha characters should be false") + } + if !valid.Alpha("abCD", "alpha").Ok { + t.Error("\"abCD\" are valid alpha characters should be true") + } +} + +func TestNumeric(t *testing.T) { + valid := Validation{} + + if valid.Numeric("a,1-@ $", "numeric").Ok { + t.Error("\"a,1-@ $\" are valid numeric characters should be false") + } + if !valid.Numeric("1234", "numeric").Ok { + t.Error("\"1234\" are valid numeric characters should be true") + } +} + +func TestAlphaNumeric(t *testing.T) { + valid := Validation{} + + if valid.AlphaNumeric("a,1-@ $", "alphaNumeric").Ok { + t.Error("\"a,1-@ $\" are valid alpha or numeric characters should be false") + } + if !valid.AlphaNumeric("1234aB", "alphaNumeric").Ok { + t.Error("\"1234aB\" are valid alpha or numeric characters should be true") + } +} + +func TestMatch(t *testing.T) { + valid := Validation{} + + if valid.Match("suchuangji@gmail", regexp.MustCompile("^\\w+@\\w+\\.\\w+$"), "match").Ok { + t.Error("\"suchuangji@gmail\" match \"^\\w+@\\w+\\.\\w+$\" should be false") + } + if !valid.Match("suchuangji@gmail.com", regexp.MustCompile("^\\w+@\\w+\\.\\w+$"), "match").Ok { + t.Error("\"suchuangji@gmail\" match \"^\\w+@\\w+\\.\\w+$\" should be true") + } +} + +func TestNoMatch(t *testing.T) { + valid := Validation{} + + if valid.NoMatch("123@gmail", regexp.MustCompile("[^\\w\\d]"), "nomatch").Ok { + t.Error("\"123@gmail\" not match \"[^\\w\\d]\" should be false") + } + if !valid.NoMatch("123gmail", regexp.MustCompile("[^\\w\\d]"), "match").Ok { + t.Error("\"123@gmail\" not match \"[^\\w\\d@]\" should be true") + } +} + +func TestAlphaDash(t *testing.T) { + valid := Validation{} + + if valid.AlphaDash("a,1-@ $", "alphaDash").Ok { + t.Error("\"a,1-@ $\" are valid alpha or numeric or dash(-_) characters should be false") + } + if !valid.AlphaDash("1234aB-_", "alphaDash").Ok { + t.Error("\"1234aB\" are valid alpha or numeric or dash(-_) characters should be true") + } +} + +func TestEmail(t *testing.T) { + valid := Validation{} + + if valid.Email("not@a email", "email").Ok { + t.Error("\"not@a email\" is a valid email address should be false") + } + if !valid.Email("suchuangji@gmail.com", "email").Ok { + t.Error("\"suchuangji@gmail.com\" is a valid email address should be true") + } +} + +func TestIP(t *testing.T) { + valid := Validation{} + + if valid.IP("11.255.255.256", "IP").Ok { + t.Error("\"11.255.255.256\" is a valid ip address should be false") + } + if !valid.IP("01.11.11.11", "IP").Ok { + t.Error("\"suchuangji@gmail.com\" is a valid ip address should be true") + } +} + +func TestBase64(t *testing.T) { + valid := Validation{} + + if valid.Base64("suchuangji@gmail.com", "base64").Ok { + t.Error("\"suchuangji@gmail.com\" are a valid base64 characters should be false") + } + if !valid.Base64("c3VjaHVhbmdqaUBnbWFpbC5jb20=", "base64").Ok { + t.Error("\"c3VjaHVhbmdqaUBnbWFpbC5jb20=\" are a valid base64 characters should be true") + } +} diff --git a/validation/validators.go b/validation/validators.go new file mode 100644 index 00000000..41ed02ed --- /dev/null +++ b/validation/validators.go @@ -0,0 +1,355 @@ +package validation + +import ( + "fmt" + "reflect" + "regexp" + "time" +) + +type Validator interface { + IsSatisfied(interface{}) bool + DefaultMessage() string + GetKey() string +} + +type Required struct { + Key string +} + +func (r Required) IsSatisfied(obj interface{}) bool { + if obj == nil { + return false + } + + if str, ok := obj.(string); ok { + return len(str) > 0 + } + if b, ok := obj.(bool); ok { + return b + } + if i, ok := obj.(int); ok { + return i != 0 + } + if t, ok := obj.(time.Time); ok { + return !t.IsZero() + } + v := reflect.ValueOf(obj) + if v.Kind() == reflect.Slice { + return v.Len() > 0 + } + return true +} + +func (r Required) DefaultMessage() string { + return "Required" +} + +func (r Required) GetKey() string { + return r.Key +} + +type Min struct { + Min int + Key string +} + +func (m Min) IsSatisfied(obj interface{}) bool { + num, ok := obj.(int) + if ok { + return num >= m.Min + } + return false +} + +func (m Min) DefaultMessage() string { + return fmt.Sprintln("Minimum is", m.Min) +} + +func (m Min) GetKey() string { + return m.Key +} + +type Max struct { + Max int + Key string +} + +func (m Max) IsSatisfied(obj interface{}) bool { + num, ok := obj.(int) + if ok { + return num <= m.Max + } + return false +} + +func (m Max) DefaultMessage() string { + return fmt.Sprintln("Maximum is", m.Max) +} + +func (m Max) GetKey() string { + return m.Key +} + +// Requires an integer to be within Min, Max inclusive. +type Range struct { + Min + Max + Key string +} + +func (r Range) IsSatisfied(obj interface{}) bool { + return r.Min.IsSatisfied(obj) && r.Max.IsSatisfied(obj) +} + +func (r Range) DefaultMessage() string { + return fmt.Sprintln("Range is", r.Min.Min, "to", r.Max.Max) +} + +func (r Range) GetKey() string { + return r.Key +} + +// Requires an array or string to be at least a given length. +type MinSize struct { + Min int + Key string +} + +func (m MinSize) IsSatisfied(obj interface{}) bool { + if str, ok := obj.(string); ok { + return len(str) >= m.Min + } + v := reflect.ValueOf(obj) + if v.Kind() == reflect.Slice { + return v.Len() >= m.Min + } + return false +} + +func (m MinSize) DefaultMessage() string { + return fmt.Sprintln("Minimum size is", m.Min) +} + +func (m MinSize) GetKey() string { + return m.Key +} + +// Requires an array or string to be at most a given length. +type MaxSize struct { + Max int + Key string +} + +func (m MaxSize) IsSatisfied(obj interface{}) bool { + if str, ok := obj.(string); ok { + return len(str) <= m.Max + } + v := reflect.ValueOf(obj) + if v.Kind() == reflect.Slice { + return v.Len() <= m.Max + } + return false +} + +func (m MaxSize) DefaultMessage() string { + return fmt.Sprintln("Maximum size is", m.Max) +} + +func (m MaxSize) GetKey() string { + return m.Key +} + +// Requires an array or string to be exactly a given length. +type Length struct { + N int + Key string +} + +func (l Length) IsSatisfied(obj interface{}) bool { + if str, ok := obj.(string); ok { + return len(str) == l.N + } + v := reflect.ValueOf(obj) + if v.Kind() == reflect.Slice { + return v.Len() == l.N + } + return false +} + +func (l Length) DefaultMessage() string { + return fmt.Sprintln("Required length is", l.N) +} + +func (l Length) GetKey() string { + return l.Key +} + +type Alpha struct { + Key string +} + +func (a Alpha) IsSatisfied(obj interface{}) bool { + if str, ok := obj.(string); ok { + for _, v := range str { + if ('Z' < v || v < 'A') && ('z' < v || v < 'a') { + return false + } + } + return true + } + return false +} + +func (a Alpha) DefaultMessage() string { + return fmt.Sprintln("Must be valid alpha characters") +} + +func (a Alpha) GetKey() string { + return a.Key +} + +type Numeric struct { + Key string +} + +func (n Numeric) IsSatisfied(obj interface{}) bool { + if str, ok := obj.(string); ok { + for _, v := range str { + if '9' < v || v < '0' { + return false + } + } + return true + } + return false +} + +func (n Numeric) DefaultMessage() string { + return fmt.Sprintln("Must be valid numeric characters") +} + +func (n Numeric) GetKey() string { + return n.Key +} + +type AlphaNumeric struct { + Key string +} + +func (a AlphaNumeric) IsSatisfied(obj interface{}) bool { + if str, ok := obj.(string); ok { + for _, v := range str { + if ('Z' < v || v < 'A') && ('z' < v || v < 'a') && ('9' < v || v < '0') { + return false + } + } + return true + } + return false +} + +func (a AlphaNumeric) DefaultMessage() string { + return fmt.Sprintln("Must be valid alpha or numeric characters") +} + +func (a AlphaNumeric) GetKey() string { + return a.Key +} + +// Requires a string to match a given regex. +type Match struct { + Regexp *regexp.Regexp + Key string +} + +func (m Match) IsSatisfied(obj interface{}) bool { + str := obj.(string) + return m.Regexp.MatchString(str) +} + +func (m Match) DefaultMessage() string { + return fmt.Sprintln("Must match", m.Regexp) +} + +func (m Match) GetKey() string { + return m.Key +} + +// Requires a string to not match a given regex. +type NoMatch struct { + Match + Key string +} + +func (n NoMatch) IsSatisfied(obj interface{}) bool { + return !n.Match.IsSatisfied(obj) +} + +func (n NoMatch) DefaultMessage() string { + return fmt.Sprintln("Must not match", n.Regexp) +} + +func (n NoMatch) GetKey() string { + return n.Key +} + +var alphaDashPattern = regexp.MustCompile("[^\\d\\w-_]") + +type AlphaDash struct { + NoMatch + Key string +} + +func (a AlphaDash) DefaultMessage() string { + return fmt.Sprintln("Must be valid alpha or numeric or dash(-_) characters") +} + +func (a AlphaDash) GetKey() string { + return a.Key +} + +var emailPattern = regexp.MustCompile("[\\w!#$%&'*+/=?^_`{|}~-]+(?:\\.[\\w!#$%&'*+/=?^_`{|}~-]+)*@(?:[\\w](?:[\\w-]*[\\w])?\\.)+[a-zA-Z0-9](?:[\\w-]*[\\w])?") + +type Email struct { + Match + Key string +} + +func (e Email) DefaultMessage() string { + return fmt.Sprintln("Must be a valid email address") +} + +func (e Email) GetKey() string { + return e.Key +} + +var ipPattern = regexp.MustCompile("^((2[0-4]\\d|25[0-5]|[01]?\\d\\d?)\\.){3}(2[0-4]\\d|25[0-5]|[01]?\\d\\d?)$") + +type IP struct { + Match + Key string +} + +func (i IP) DefaultMessage() string { + return fmt.Sprintln("Must be a valid ip address") +} + +func (i IP) GetKey() string { + return i.Key +} + +var base64Pattern = regexp.MustCompile("^(?:[A-Za-z0-99+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$") + +type Base64 struct { + Match + Key string +} + +func (b Base64) DefaultMessage() string { + return fmt.Sprintln("Must be valid base64 characters") +} + +func (b Base64) GetKey() string { + return b.Key +}