diff --git a/baked_in.go b/baked_in.go new file mode 100644 index 0000000..1fa65c2 --- /dev/null +++ b/baked_in.go @@ -0,0 +1,203 @@ +package validator + +import ( + "log" + "reflect" + "regexp" + "strconv" +) + +var bakedInValidators = map[string]ValidationFunc{ + "required": required, + "length": length, + "min": min, + "max": max, + "regex": regex, +} + +func required(field interface{}, param string) bool { + + return field != nil && field != reflect.Zero(reflect.TypeOf(field)).Interface() +} + +// length tests whether a variable's length is equal to a given +// value. For strings it tests the number of characters whereas +// for maps and slices it tests the number of items. +func length(field interface{}, param string) bool { + + st := reflect.ValueOf(field) + + switch st.Kind() { + + case reflect.String: + p := asInt(param) + + return int64(len(st.String())) == p + + case reflect.Slice, reflect.Map, reflect.Array: + p := asInt(param) + + return int64(st.Len()) == p + + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + p := asInt(param) + + return st.Int() == p + + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + p := asUint(param) + + return st.Uint() == p + + case reflect.Float32, reflect.Float64: + p := asFloat(param) + + return st.Float() == p + + default: + log.Fatalf("Bad field type for Input Param %s for %s\n", param, field) + return false + } +} + +// min tests whether a variable value is larger or equal to a given +// number. For number types, it's a simple lesser-than test; for +// strings it tests the number of characters whereas for maps +// and slices it tests the number of items. +func min(field interface{}, param string) bool { + + st := reflect.ValueOf(field) + + switch st.Kind() { + + case reflect.String: + p := asInt(param) + + return int64(len(st.String())) >= p + + case reflect.Slice, reflect.Map, reflect.Array: + p := asInt(param) + + return int64(st.Len()) >= p + + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + p := asInt(param) + + return st.Int() >= p + + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + p := asUint(param) + + return st.Uint() >= p + + case reflect.Float32, reflect.Float64: + p := asFloat(param) + + return st.Float() >= p + + default: + log.Fatalf("Bad field type for Input Param %s for %s\n", param, field) + return false + } +} + +// max tests whether a variable value is lesser than a given +// value. For numbers, it's a simple lesser-than test; for +// strings it tests the number of characters whereas for maps +// and slices it tests the number of items. +func max(field interface{}, param string) bool { + + st := reflect.ValueOf(field) + + switch st.Kind() { + + case reflect.String: + p := asInt(param) + + return int64(len(st.String())) <= p + + case reflect.Slice, reflect.Map, reflect.Array: + p := asInt(param) + + return int64(st.Len()) <= p + + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + p := asInt(param) + + return st.Int() <= p + + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + p := asUint(param) + + return st.Uint() <= p + + case reflect.Float32, reflect.Float64: + p := asFloat(param) + + return st.Float() <= p + + default: + log.Fatalf("Bad field type for Input Param %s for %s\n", param, field) + return false + } +} + +// regex is the builtin validation function that checks +// whether the string variable matches a regular expression +func regex(field interface{}, param string) bool { + + s, ok := field.(string) + if !ok { + log.Fatalf("Bad field type %s\n", field) + } + + re, err := regexp.Compile(param) + if err != nil { + log.Fatalf("Bad regex param %s\n", param) + } + + if !re.MatchString(s) { + return false + } + + return true +} + +// asInt retuns the parameter as a int64 +// or panics if it can't convert +func asInt(param string) int64 { + + i, err := strconv.ParseInt(param, 0, 64) + + if err != nil { + log.Fatalf("Bad Input Param %s\n", param) + } + + return i +} + +// asUint returns the parameter as a uint64 +// or panics if it can't convert +func asUint(param string) uint64 { + + i, err := strconv.ParseUint(param, 0, 64) + + if err != nil { + log.Fatalf("Bad Input Param %s\n", param) + } + + return i +} + +// asFloat returns the parameter as a float64 +// or panics if it can't convert +func asFloat(param string) float64 { + + i, err := strconv.ParseFloat(param, 64) + + if err != nil { + log.Fatalf("Bad Input Param %s\n", param) + } + + return i +} diff --git a/validator.go b/validator.go index 1b1d13f..e9f62b4 100644 --- a/validator.go +++ b/validator.go @@ -2,9 +2,14 @@ package validator import ( "errors" - "fmt" "log" "reflect" + "strings" + "unicode" +) + +const ( + omitempty string = "omitempty" ) // FieldValidationError contains a single fields validation error @@ -13,21 +18,17 @@ type FieldValidationError struct { ErrorTag string } -// StructValidationErrors is a slice of errors for struct fields ( Excluding struct fields) +// StructValidationErrors is a struct of errors for struct fields ( Excluding fields of type struct ) // NOTE: if a field within a struct is a struct it's errors will not be contained within the current -// StructValidationErrors but rather a new ArrayValidationErrors is created for each struct +// StructValidationErrors but rather a new StructValidationErrors is created for each struct resulting in +// a neat & tidy 2D flattened list of structs validation errors type StructValidationErrors struct { - Errors []FieldValidationError -} - -// ArrayStructValidationErrors is a struct that contains a 2D flattened list of struct specific StructValidationErrors -type ArrayStructValidationErrors struct { - // Key = Struct Name - Errors map[string][]StructValidationErrors + Struct string + Errors map[string]*FieldValidationError } // ValidationFunc that accepts the value of a field and parameter for use in validation (parameter not always used or needed) -type ValidationFunc func(v interface{}, param string) error +type ValidationFunc func(v interface{}, param string) bool // Validator implements the Validator Struct // NOTE: Fields within are not thread safe and that is on purpose @@ -40,7 +41,7 @@ type Validator struct { validationFuncs map[string]ValidationFunc } -var bakedInValidators = map[string]ValidationFunc{} +// var bakedInValidators = map[string]ValidationFunc{} var internalValidator = NewValidator("validate", bakedInValidators) @@ -89,17 +90,24 @@ func (v *Validator) AddFunction(key string, f ValidationFunc) error { return nil } -func ValidateStruct(s interface{}) ArrayStructValidationErrors { +// ValidateStruct validates a struct and returns a struct containing the errors +func ValidateStruct(s interface{}) map[string]*StructValidationErrors { return internalValidator.ValidateStruct(s) } -func (v *Validator) ValidateStruct(s interface{}) ArrayStructValidationErrors { - - var errorStruct = ArrayStructValidationErrors{} +// ValidateStruct validates a struct and returns a struct containing the errors +func (v *Validator) ValidateStruct(s interface{}) map[string]*StructValidationErrors { + errorArray := map[string]*StructValidationErrors{} structValue := reflect.ValueOf(s) structType := reflect.TypeOf(s) + structName := structType.Name() + + var currentStructError = &StructValidationErrors{ + Struct: structName, + Errors: map[string]*FieldValidationError{}, + } if structValue.Kind() == reflect.Ptr && !structValue.IsNil() { return v.ValidateStruct(structValue.Elem().Interface()) @@ -120,8 +128,135 @@ func (v *Validator) ValidateStruct(s interface{}) ArrayStructValidationErrors { valueField = valueField.Elem() } - fmt.Println(typeField.Name) + tag := typeField.Tag.Get(v.tagName) + + if tag == "-" { + continue + } + + // if no validation and not a struct (which may containt fields for validation) + if tag == "" && valueField.Kind() != reflect.Struct { + continue + } + + switch valueField.Kind() { + + case reflect.Struct: + + if !unicode.IsUpper(rune(typeField.Name[0])) { + continue + } + + if structErrors := v.ValidateStruct(valueField.Interface()); structErrors != nil { + for key, val := range structErrors { + errorArray[key] = val + } + // free up memory map no longer needed + structErrors = nil + } + + default: + + if fieldError := v.validateStructFieldByTag(valueField.Interface(), typeField.Name, tag); fieldError != nil { + currentStructError.Errors[fieldError.Field] = fieldError + // free up memory reference + fieldError = nil + } + } + } + + if len(currentStructError.Errors) > 0 { + errorArray[currentStructError.Struct] = currentStructError + // free up memory + currentStructError = nil + } + + if len(errorArray) == 0 { + return nil + } + + return errorArray +} + +// ValidateFieldWithTag validates the given field by the given tag arguments +func (v *Validator) validateStructFieldByTag(f interface{}, name string, tag string) *FieldValidationError { + + if err := v.validateFieldByNameAndTag(f, name, tag); err != nil { + return &FieldValidationError{ + Field: name, + ErrorTag: err.Error(), + } } - return errorStruct + return nil +} + +// ValidateFieldByTag allows validation of a single field with the internal validator, still using tag style validation to check multiple errors +func ValidateFieldByTag(f interface{}, tag string) error { + + return internalValidator.validateFieldByNameAndTag(f, "", tag) +} + +// ValidateFieldByTag allows validation of a single field, still using tag style validation to check multiple errors +func (v *Validator) ValidateFieldByTag(f interface{}, tag string) error { + + return v.validateFieldByNameAndTag(f, "", tag) +} + +func (v *Validator) validateFieldByNameAndTag(f interface{}, name string, tag string) error { + + // This is a double check if coming from ValidateStruct but need to be here in case function is called directly + if tag == "-" { + return nil + } + + if strings.Contains(tag, omitempty) && !required(f, "") { + return nil + } + + valueField := reflect.ValueOf(f) + + if valueField.Kind() == reflect.Ptr && !valueField.IsNil() { + return v.ValidateFieldByTag(valueField.Elem().Interface(), tag) + } + + switch valueField.Kind() { + + case reflect.Struct, reflect.Invalid: + log.Fatal("Invalid field passed to ValidateFieldWithTag") + } + + valTags := strings.Split(tag, ",") + + for _, valTag := range valTags { + + vals := strings.Split(valTag, "=") + key := strings.Trim(vals[0], " ") + + if len(key) == 0 { + log.Fatalf("Invalid validation tag on field %s", name) + } + + if key == omitempty { + continue + } + + valFunc := v.validationFuncs[key] + if valFunc == nil { + log.Fatalf("Undefined validation function on field %s", name) + } + + param := "" + if len(vals) > 1 { + param = strings.Trim(vals[1], " ") + } + + if err := valFunc(f, param); !err { + + return errors.New(key) + } + + } + + return nil } diff --git a/validator_test.go b/validator_test.go index 9180a03..5a4a344 100644 --- a/validator_test.go +++ b/validator_test.go @@ -8,7 +8,7 @@ import ( ) type UserDetails struct { - Address string `validate:"required"` + Address string `validate:"omitempty,length=6"` } type User struct { @@ -19,13 +19,38 @@ type User struct { func TestValidateStruct(t *testing.T) { u := &User{ - FirstName: "Dean Karn", + FirstName: "", Details: &UserDetails{ - "26 Here Blvd.", + "", }, } errors := validator.ValidateStruct(u) - fmt.Println(errors) + fmt.Println(errors == nil) + + for _, i := range errors { + fmt.Printf("Error Struct:%s\n", i.Struct) + + for _, j := range i.Errors { + + fmt.Printf("Error Field:%s Error Tag:%s\n", j.Field, j.ErrorTag) + } + } + } + +// func TestValidateField(t *testing.T) { +// +// u := &User{ +// FirstName: "Dean Karn", +// Details: &UserDetails{ +// "26 Here Blvd.", +// }, +// } +// +// err := validator.ValidateFieldByTag(u.FirstName, "required") +// +// fmt.Println(err == nil) +// fmt.Println(err) +// }