├── LICENSE.txt ├── README.md ├── callbacks.go ├── validation_test.go └── validations.go /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) The Plant https://theplant.jp 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Validations 2 | 3 | Validations provides a means to [*validate*](https://en.wikipedia.org/wiki/Data_validation) [GORM](https://github.com/jinzhu/gorm) models when creating and updating them. 4 | 5 | ### Register GORM Callbacks 6 | 7 | Validations uses [GORM](https://github.com/jinzhu/gorm) callbacks to handle *validations*, so you will need to register callbacks first: 8 | 9 | ```go 10 | import ( 11 | "github.com/jinzhu/gorm" 12 | "github.com/qor/validations" 13 | ) 14 | 15 | func main() { 16 | db, err := gorm.Open("sqlite3", "demo_db") 17 | 18 | validations.RegisterCallbacks(db) 19 | } 20 | ``` 21 | 22 | ### Usage 23 | 24 | After callbacks have been registered, attempting to create or update any record will trigger the `Validate` method that you have implemented for your model. If your implementation adds or returns an error, the attempt will be aborted. 25 | 26 | ```go 27 | type User struct { 28 | gorm.Model 29 | Age uint 30 | } 31 | 32 | func (user User) Validate(db *gorm.DB) { 33 | if user.Age <= 18 { 34 | db.AddError(errors.New("age need to be 18+")) 35 | } 36 | } 37 | 38 | db.Create(&User{Age: 10}) // won't insert the record into database, as the `Validate` method will return error 39 | 40 | var user User{Age: 20} 41 | db.Create(&user) // user with age 20 will be inserted into database 42 | db.Model(&user).Update("age", 10) // user's age won't be updated, will return error `age need to be 18+` 43 | 44 | // If you have added more than one error, could get all of them with `db.GetErrors()` 45 | func (user User) Validate(db *gorm.DB) { 46 | if user.Age <= 18 { 47 | db.AddError(errors.New("age need to be 18+")) 48 | } 49 | if user.Name == "" { 50 | db.AddError(errors.New("name can't be blank")) 51 | } 52 | } 53 | 54 | db.Create(&User{}).GetErrors() // => []error{"age need to be 18+", "name can't be blank"} 55 | ``` 56 | 57 | ## [Govalidator](https://github.com/asaskevich/govalidator) integration 58 | 59 | Qor [Validations](https://github.com/qor/validations) supports [govalidator](https://github.com/asaskevich/govalidator), so you could add a tag into your struct for some common *validations*, such as *check required*, *numeric*, *length*, etc. 60 | 61 | ``` 62 | type User struct { 63 | gorm.Model 64 | Name string `valid:"required"` 65 | Password string `valid:"length(6|20)"` 66 | SecurePassword string `valid:"numeric"` 67 | Email string `valid:"email"` 68 | } 69 | ``` 70 | 71 | ## Customize errors on form field 72 | 73 | If you want to display errors for each form field in [QOR Admin](http://github.com/qor/admin), you could register your error like this: 74 | 75 | ```go 76 | func (user User) Validate(db *gorm.DB) { 77 | if user.Age <= 18 { 78 | db.AddError(validations.NewError(user, "Age", "age need to be 18+")) 79 | } 80 | } 81 | ``` 82 | 83 | ## Try it out for yourself 84 | 85 | Checkout the [http://demo.getqor.com/admin/products/1](http://demo.getqor.com/admin/products/1) demo, change `Name` to be a blank string and save to see what happens. 86 | 87 | ## License 88 | 89 | Released under the [MIT License](http://opensource.org/licenses/MIT). 90 | -------------------------------------------------------------------------------- /callbacks.go: -------------------------------------------------------------------------------- 1 | package validations 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strings" 7 | 8 | "github.com/asaskevich/govalidator" 9 | "github.com/jinzhu/gorm" 10 | ) 11 | 12 | var skipValidations = "validations:skip_validations" 13 | 14 | func validate(scope *gorm.Scope) { 15 | if _, ok := scope.Get("gorm:update_column"); !ok { 16 | if result, ok := scope.DB().Get(skipValidations); !(ok && result.(bool)) { 17 | if !scope.HasError() { 18 | scope.CallMethod("Validate") 19 | if scope.Value != nil { 20 | resource := scope.IndirectValue().Interface() 21 | _, validatorErrors := govalidator.ValidateStruct(resource) 22 | if validatorErrors != nil { 23 | if errors, ok := validatorErrors.(govalidator.Errors); ok { 24 | for _, err := range flatValidatorErrors(errors) { 25 | scope.DB().AddError(formattedError(err, resource)) 26 | } 27 | } else { 28 | scope.DB().AddError(validatorErrors) 29 | } 30 | } 31 | } 32 | } 33 | } 34 | } 35 | } 36 | 37 | func flatValidatorErrors(validatorErrors govalidator.Errors) []govalidator.Error { 38 | resultErrors := []govalidator.Error{} 39 | for _, validatorError := range validatorErrors.Errors() { 40 | if errors, ok := validatorError.(govalidator.Errors); ok { 41 | for _, e := range errors { 42 | resultErrors = append(resultErrors, e.(govalidator.Error)) 43 | } 44 | } 45 | if e, ok := validatorError.(govalidator.Error); ok { 46 | resultErrors = append(resultErrors, e) 47 | } 48 | } 49 | return resultErrors 50 | } 51 | 52 | func formattedError(err govalidator.Error, resource interface{}) error { 53 | message := err.Error() 54 | attrName := err.Name 55 | if strings.Index(message, "non zero value required") >= 0 { 56 | message = fmt.Sprintf("%v can't be blank", attrName) 57 | } else if strings.Index(message, "as length") >= 0 { 58 | reg, _ := regexp.Compile(`\(([0-9]+)\|([0-9]+)\)`) 59 | submatch := reg.FindSubmatch([]byte(err.Error())) 60 | message = fmt.Sprintf("%v is the wrong length (should be %v~%v characters)", attrName, string(submatch[1]), string(submatch[2])) 61 | } else if strings.Index(message, "as numeric") >= 0 { 62 | message = fmt.Sprintf("%v is not a number", attrName) 63 | } else if strings.Index(message, "as email") >= 0 { 64 | message = fmt.Sprintf("%v is not a valid email address", attrName) 65 | } 66 | return NewError(resource, attrName, message) 67 | 68 | } 69 | 70 | // RegisterCallbacks register callback into GORM DB 71 | func RegisterCallbacks(db *gorm.DB) { 72 | callback := db.Callback() 73 | if callback.Create().Get("validations:validate") == nil { 74 | callback.Create().Before("gorm:before_create").Register("validations:validate", validate) 75 | } 76 | if callback.Update().Get("validations:validate") == nil { 77 | callback.Update().Before("gorm:before_update").Register("validations:validate", validate) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /validation_test.go: -------------------------------------------------------------------------------- 1 | package validations_test 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "regexp" 7 | "testing" 8 | 9 | "github.com/asaskevich/govalidator" 10 | "github.com/jinzhu/gorm" 11 | _ "github.com/mattn/go-sqlite3" 12 | "github.com/qor/qor/test/utils" 13 | "github.com/qor/validations" 14 | ) 15 | 16 | var db *gorm.DB 17 | 18 | type User struct { 19 | gorm.Model 20 | Name string `valid:"required"` 21 | Password string `valid:"length(6|20)"` 22 | SecurePassword string `valid:"numeric"` 23 | Email string `valid:"email,uniqEmail~Email already be token"` 24 | CompanyID int 25 | Company Company 26 | CreditCard CreditCard 27 | Addresses []Address 28 | Languages []Language `gorm:"many2many:user_languages"` 29 | } 30 | 31 | func (user *User) Validate(db *gorm.DB) { 32 | govalidator.CustomTypeTagMap.Set("uniqEmail", govalidator.CustomTypeValidator(func(email interface{}, context interface{}) bool { 33 | var count int 34 | if db.Model(&User{}).Where("email = ?", email).Count(&count); count == 0 || email == "" { 35 | return true 36 | } 37 | return false 38 | })) 39 | if user.Name == "invalid" { 40 | db.AddError(validations.NewError(user, "Name", "invalid user name")) 41 | } 42 | } 43 | 44 | type Company struct { 45 | gorm.Model 46 | Name string 47 | } 48 | 49 | func (company *Company) Validate(db *gorm.DB) { 50 | if company.Name == "invalid" { 51 | db.AddError(errors.New("invalid company name")) 52 | } 53 | } 54 | 55 | type CreditCard struct { 56 | gorm.Model 57 | UserID int 58 | Number string 59 | } 60 | 61 | func (card *CreditCard) Validate(db *gorm.DB) { 62 | if !regexp.MustCompile("^(\\d){13,16}$").MatchString(card.Number) { 63 | db.AddError(validations.NewError(card, "Number", "invalid card number")) 64 | } 65 | } 66 | 67 | type Address struct { 68 | gorm.Model 69 | UserID int 70 | Address string 71 | } 72 | 73 | func (address *Address) Validate(db *gorm.DB) { 74 | if address.Address == "invalid" { 75 | db.AddError(validations.NewError(address, "Address", "invalid address")) 76 | } 77 | } 78 | 79 | type Language struct { 80 | gorm.Model 81 | Code string 82 | } 83 | 84 | func (language *Language) Validate(db *gorm.DB) error { 85 | if language.Code == "invalid" { 86 | return validations.NewError(language, "Code", "invalid language") 87 | } 88 | return nil 89 | } 90 | 91 | func init() { 92 | db = utils.TestDB() 93 | validations.RegisterCallbacks(db) 94 | tables := []interface{}{&User{}, &Company{}, &CreditCard{}, &Address{}, &Language{}} 95 | for _, table := range tables { 96 | if err := db.DropTableIfExists(table).Error; err != nil { 97 | panic(err) 98 | } 99 | db.AutoMigrate(table) 100 | } 101 | } 102 | 103 | func TestGoValidation(t *testing.T) { 104 | user := User{Name: "", Password: "123123", Email: "a@gmail.com"} 105 | 106 | result := db.Save(&user) 107 | if result.Error == nil { 108 | t.Errorf("Should get error when save empty user") 109 | } 110 | 111 | if result.Error.Error() != "Name can't be blank" { 112 | t.Errorf("Error message should be equal `Name can't be blank`") 113 | } 114 | 115 | user = User{Name: "", Password: "123", SecurePassword: "AB123", Email: "aagmail.com"} 116 | result = db.Save(&user) 117 | messages := []string{"Name can't be blank", 118 | "Password is the wrong length (should be 6~20 characters)", 119 | "SecurePassword is not a number", 120 | "Email is not a valid email address"} 121 | for i, err := range result.GetErrors() { 122 | if messages[i] != err.Error() { 123 | t.Errorf(fmt.Sprintf("Error message should be equal `%v`, but it is `%v`", messages[i], err.Error())) 124 | } 125 | } 126 | 127 | user = User{Name: "A", Password: "123123", Email: "a@gmail.com"} 128 | result = db.Save(&user) 129 | user = User{Name: "B", Password: "123123", Email: "a@gmail.com"} 130 | if result := db.Save(&user); result.Error.Error() != "Email already be token" { 131 | t.Errorf("Should get email alredy be token error") 132 | } 133 | } 134 | 135 | func TestSaveInvalidUser(t *testing.T) { 136 | user := User{Name: "invalid"} 137 | 138 | if result := db.Save(&user); result.Error == nil { 139 | t.Errorf("Should get error when save invalid user") 140 | } 141 | } 142 | 143 | func TestSaveInvalidCompany(t *testing.T) { 144 | user := User{ 145 | Name: "valid", 146 | Company: Company{Name: "invalid"}, 147 | } 148 | 149 | if result := db.Save(&user); result.Error == nil { 150 | t.Errorf("Should get error when save invalid company") 151 | } 152 | } 153 | 154 | func TestSaveInvalidCreditCard(t *testing.T) { 155 | user := User{ 156 | Name: "valid", 157 | Company: Company{Name: "valid"}, 158 | CreditCard: CreditCard{Number: "invalid"}, 159 | } 160 | 161 | if result := db.Save(&user); result.Error == nil { 162 | t.Errorf("Should get error when save invalid credit card") 163 | } 164 | } 165 | 166 | func TestSaveInvalidAddresses(t *testing.T) { 167 | user := User{ 168 | Name: "valid", 169 | Company: Company{Name: "valid"}, 170 | CreditCard: CreditCard{Number: "4111111111111111"}, 171 | Addresses: []Address{{Address: "invalid"}}, 172 | } 173 | 174 | if result := db.Save(&user); result.Error == nil { 175 | t.Errorf("Should get error when save invalid addresses") 176 | } 177 | } 178 | 179 | func TestSaveInvalidLanguage(t *testing.T) { 180 | user := User{ 181 | Name: "valid", 182 | Company: Company{Name: "valid"}, 183 | CreditCard: CreditCard{Number: "4111111111111111"}, 184 | Addresses: []Address{{Address: "valid"}}, 185 | Languages: []Language{{Code: "invalid"}}, 186 | } 187 | 188 | if result := db.Save(&user); result.Error == nil { 189 | t.Errorf("Should get error when save invalid language") 190 | } 191 | } 192 | 193 | func TestSaveAllValidData(t *testing.T) { 194 | user := User{ 195 | Name: "valid", 196 | Company: Company{Name: "valid"}, 197 | CreditCard: CreditCard{Number: "4111111111111111"}, 198 | Addresses: []Address{{Address: "valid1"}, {Address: "valid2"}}, 199 | Languages: []Language{{Code: "valid1"}, {Code: "valid2"}}, 200 | } 201 | 202 | if result := db.Save(&user); result.Error != nil { 203 | t.Errorf("Should get no error when save valid data, but got: %v", result.Error) 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /validations.go: -------------------------------------------------------------------------------- 1 | package validations 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/jinzhu/gorm" 7 | ) 8 | 9 | // NewError generate a new error for a model's field 10 | func NewError(resource interface{}, column, err string) error { 11 | return &Error{Resource: resource, Column: column, Message: err} 12 | } 13 | 14 | // Error is a validation error struct that hold model, column and error message 15 | type Error struct { 16 | Resource interface{} 17 | Column string 18 | Message string 19 | } 20 | 21 | // Label is a label including model type, primary key and column name 22 | func (err Error) Label() string { 23 | scope := gorm.Scope{Value: err.Resource} 24 | return fmt.Sprintf("%v_%v_%v", scope.GetModelStruct().ModelType.Name(), scope.PrimaryKeyValue(), err.Column) 25 | } 26 | 27 | // Error show error message 28 | func (err Error) Error() string { 29 | return fmt.Sprintf("%v", err.Message) 30 | } 31 | --------------------------------------------------------------------------------