Browse Source

project: 接口加入参数校验

Yi 1 year ago
parent
commit
ba241127c1
47 changed files with 5448 additions and 46 deletions
  1. 10 0
      backend/operation/system.proto
  2. 50 4
      http/handler/system/user.go
  3. 34 28
      http/middleware/cors.go
  4. 2 1
      http/route/app_api.go
  5. 4 2
      module/backend/interface.go
  6. 5 0
      module/backend/system_service.go
  7. 658 0
      pkg/valid/README.md
  8. 199 0
      pkg/valid/error.go
  9. 185 0
      pkg/valid/error_test.go
  10. 192 0
      pkg/valid/example_test.go
  11. 29 0
      pkg/valid/interface.go
  12. 293 0
      pkg/valid/is/rule.go
  13. 98 0
      pkg/valid/is/rule_test.go
  14. 144 0
      pkg/valid/map.go
  15. 119 0
      pkg/valid/map_test.go
  16. 25 0
      pkg/valid/rule.go
  17. 63 0
      pkg/valid/rule_absent.go
  18. 100 0
      pkg/valid/rule_absent_test.go
  19. 98 0
      pkg/valid/rule_date.go
  20. 87 0
      pkg/valid/rule_date_test.go
  21. 93 0
      pkg/valid/rule_each.go
  22. 74 0
      pkg/valid/rule_each_test.go
  23. 51 0
      pkg/valid/rule_in.go
  24. 53 0
      pkg/valid/rule_in_test.go
  25. 100 0
      pkg/valid/rule_length.go
  26. 103 0
      pkg/valid/rule_length_test.go
  27. 50 0
      pkg/valid/rule_match.go
  28. 51 0
      pkg/valid/rule_match_test.go
  29. 191 0
      pkg/valid/rule_minmax.go
  30. 144 0
      pkg/valid/rule_minmax_test.go
  31. 47 0
      pkg/valid/rule_not_in.go
  32. 51 0
      pkg/valid/rule_not_in_test.go
  33. 40 0
      pkg/valid/rule_not_nil.go
  34. 58 0
      pkg/valid/rule_not_nil_test.go
  35. 68 0
      pkg/valid/rule_required.go
  36. 96 0
      pkg/valid/rule_required_test.go
  37. 79 0
      pkg/valid/rule_strings.go
  38. 139 0
      pkg/valid/rule_strings_test.go
  39. 47 0
      pkg/valid/rule_when.go
  40. 90 0
      pkg/valid/rule_when_test.go
  41. 140 0
      pkg/valid/struct.go
  42. 204 0
      pkg/valid/struct_test.go
  43. 175 0
      pkg/valid/util.go
  44. 296 0
      pkg/valid/util_test.go
  45. 221 0
      pkg/valid/validation.go
  46. 266 0
      pkg/valid/validation_test.go
  47. 126 11
      proto/go/backend/operation/system.pb.go

+ 10 - 0
backend/operation/system.proto

@@ -35,10 +35,20 @@ message SystemToken {
   string token = 1;
 }
 
+// 用户登录
 message UserAuth {
   string user_name = 1;   // 用户名称
   string password = 2;    // 用户密码
   string phone = 3;       // 用户手机号
   int64  role_id  = 4;    // 用户角色id
   string  role_name = 5;  // 用户角色名称
+}
+
+message AddSystemUser {
+  int64 id = 1;           // 用户id
+  string name = 2;        // 用户名称
+  string phone = 3;       // 用户手机号
+  int64 role_id = 4;       // 角色id
+  IsShow.Kind is_show = 5;  // 是否开启
+  string employee_name = 6; // 员工姓名
 }

+ 50 - 4
http/handler/system/user.go

@@ -4,6 +4,7 @@ import (
 	"kpt-tmr-group/http/middleware"
 	"kpt-tmr-group/pkg/apierr"
 	"kpt-tmr-group/pkg/apiok"
+	"kpt-tmr-group/pkg/valid"
 	operationPb "kpt-tmr-group/proto/go/backend/operation"
 	"net/http"
 
@@ -11,12 +12,25 @@ import (
 )
 
 func Auth(c *gin.Context) {
-	req := &operationPb.UserAuth{}
-	if err := c.BindJSON(req); err != nil {
+	var req operationPb.UserAuth
+	if err := c.BindJSON(&req); err != nil {
 		apierr.AbortBadRequest(c, http.StatusBadRequest, err)
 		return
 	}
-	res, err := middleware.Dependency(c).StoreEventHub.OpsService.Auth(c, req)
+
+	validate := func() error {
+		return valid.ValidateStruct(&req,
+			valid.Field(&req.UserName, valid.Required),
+			valid.Field(&req.Password, valid.Required),
+		)
+	}
+
+	if err := validate(); err != nil {
+		apierr.AbortBadRequest(c, http.StatusBadRequest, err)
+		return
+	}
+
+	res, err := middleware.Dependency(c).StoreEventHub.OpsService.Auth(c, &req)
 	if err != nil {
 		apierr.ClassifiedAbort(c, err)
 		return
@@ -26,11 +40,43 @@ func Auth(c *gin.Context) {
 
 func AddUser(c *gin.Context) {
 
+	req := make([]*operationPb.AddSystemUser, 0)
+	if err := c.BindJSON(&req); err != nil {
+		apierr.AbortBadRequest(c, http.StatusBadRequest, err)
+		return
+	}
+
+	for _, field := range req {
+		validate := func() error {
+			return valid.ValidateStruct(&field,
+				valid.Field(&field.Name, valid.Required),
+				valid.Field(&field.Phone, valid.Required),
+				valid.Field(&field.EmployeeName, valid.Required),
+				valid.Field(&field.RoleId, valid.Required),
+				valid.Field(&field.IsShow, valid.Max(2), valid.Min(0)),
+			)
+		}
+		if err := validate(); err != nil {
+			apierr.AbortBadRequest(c, http.StatusBadRequest, err)
+			return
+		}
+	}
+
+	err := middleware.Dependency(c).StoreEventHub.OpsService.CreateSystemUser(c, req)
+	if err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
 	c.JSON(http.StatusOK, apiok.CommonResponse(apiok.NewApiOk(true)))
 }
 
 func GetUserInfo(c *gin.Context) {
-	res, err := middleware.BackendOperation(c).OpsService.GetUserInfo(c, middleware.GetToken(c))
+	token := middleware.GetToken(c)
+	if err := valid.Validate(token, valid.Required); err != nil {
+		apierr.ClassifiedAbort(c, err)
+		return
+	}
+	res, err := middleware.BackendOperation(c).OpsService.GetUserInfo(c, token)
 	if err != nil {
 		apierr.ClassifiedAbort(c, err)
 		return

+ 34 - 28
http/middleware/cors.go

@@ -1,43 +1,49 @@
 package middleware
 
 import (
-	"time"
+	"kpt-tmr-group/pkg/logger/zaplog"
+	"net/http"
+
+	"go.uber.org/zap"
 
 	"github.com/gin-contrib/cors"
 	"github.com/gin-gonic/gin"
 )
 
-var defaultCORSConfig = cors.Config{
-	AllowAllOrigins: true,
-	AllowMethods:    []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"},
-	AllowHeaders: []string{
-		"Origin",
-		"Accept",
-		"Accept-Language",
-		"Content-Language",
-		"Content-Type",
-		"User-Agent",
-		"Authorization",
-		"x-timezone-offset",
-		"x-user-id",
-		"X-User-Id",
-		"X-Rate-Limit-Token",
-		"x-rate-limit-token",
-		"X-Timezone-Name",
-		"x-timezone-name",
-		"X-Lingochamp-Id",
-		"x-lingochamp-id",
-		"x-user-language",
-	},
-	AllowCredentials: true,
-	MaxAge:           12 * time.Hour,
-}
-
 // CORS enable CORS support
 func CORS(configs ...cors.Config) gin.HandlerFunc {
 	if len(configs) != 0 {
 		return cors.New(configs[0])
 	}
+	return func(c *gin.Context) {
+		method := c.Request.Method
+		origin := c.Request.Header.Get("Origin") //请求头部
+		if origin != "" {
+			//接收客户端发送的origin (重要!)
+			c.Writer.Header().Set("Access-Control-Allow-Origin", origin)
+			//服务器支持的所有跨域请求的方法
+			c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE,UPDATE")
+			//允许跨域设置可以返回其他子段,可以自定义字段
+			c.Header("Access-Control-Allow-Headers", "Authorization, Content-Length, X-CSRF-Token, Token,session")
+			// 允许浏览器(客户端)可以解析的头部 (重要)
+			c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers")
+			//设置缓存时间
+			c.Header("Access-Control-Max-Age", "172800")
+			//允许客户端传递校验信息比如 cookie (重要)
+			c.Header("Access-Control-Allow-Credentials", "true")
+		}
 
-	return cors.New(defaultCORSConfig)
+		//允许类型校验
+		if method == "OPTIONS" {
+			c.JSON(http.StatusOK, "ok!")
+		}
+
+		defer func() {
+			if err := recover(); err != nil {
+				zaplog.Error("cors", zap.Any("recover", err))
+			}
+		}()
+
+		c.Next()
+	}
 }

+ 2 - 1
http/route/app_api.go

@@ -25,6 +25,7 @@ func AppAPI(opts ...func(engine *gin.Engine)) func(s *gin.Engine) {
 		lingoRoute := authRouteGroup(s, "/api/v1/system/")
 		lingoRoute.POST("/user_info", system.GetUserInfo)
 		lingoRoute.POST("/user/add", system.AddUser)
+		lingoRoute.POST("/user/list", system.AddUser)
 
 		lingoRoute.POST("/role/add", system.AddRole)
 
@@ -37,6 +38,6 @@ func AppAPI(opts ...func(engine *gin.Engine)) func(s *gin.Engine) {
 func authRouteGroup(s *gin.Engine, relativePath string) *gin.RouterGroup {
 	group := s.Group(relativePath)
 	// 中间件鉴权
-	group.Use(middleware.RequireAdmin(), middleware.Pagination())
+	group.Use(middleware.RequireAdmin(), middleware.Pagination(), middleware.CORS())
 	return group
 }

+ 4 - 2
module/backend/interface.go

@@ -34,7 +34,7 @@ func NewStore(store StoreEntry) KptService {
 
 type KptService interface {
 	Operation
-	SSOAuth
+	SystemOperation
 }
 
 type Operation interface {
@@ -45,7 +45,9 @@ type Operation interface {
 	SearchPastureList(ctx context.Context, req *operationPb.SearchPastureRequest) (*model.GroupPastureResponse, error)
 }
 
-type SSOAuth interface {
+type SystemOperation interface {
 	Auth(ctx context.Context, auth *operationPb.UserAuth) (*operationPb.SystemToken, error)
 	GetUserInfo(ctx context.Context, token string) (*operationPb.UserAuth, error)
+
+	CreateSystemUser(ctx context.Context, req []*operationPb.AddSystemUser) error
 }

+ 5 - 0
module/backend/sso_service.go → module/backend/system_service.go

@@ -49,3 +49,8 @@ func (s *StoreEntry) GetUserInfo(ctx context.Context, token string) (*operationP
 
 	return systemUser.SystemUserFormat(), nil
 }
+
+func (s *StoreEntry) CreateSystemUser(ctx context.Context, req []*operationPb.AddSystemUser) error {
+
+	return nil
+}

+ 658 - 0
pkg/valid/README.md

@@ -0,0 +1,658 @@
+# valid 校验 library
+
+### Validating a Simple Value
+
+For a simple value, such as a string or an integer, you may use `valid.Validate()` to validate it. For example, 
+
+```go
+package main
+
+import (
+	"fmt"
+
+	"kpt-tmr-group/pkg/valid"
+	"kpt-tmr-group/pkg/valid/is"
+)
+
+func main() {
+	data := "example"
+	err := valid.Validate(data,
+		valid.Required,       // not empty
+		valid.Length(5, 100), // length between 5 and 100
+		is.URL,                    // is a valid URL
+	)
+	fmt.Println(err)
+	// Output:
+	// must be a valid URL
+}
+```
+
+The method `valid.Validate()` will run through the rules in the order that they are listed. If a rule fails
+the validation, the method will return the corresponding error and skip the rest of the rules. The method will
+return nil if the value passes all validation rules.
+
+
+### Validating a Struct
+
+For a struct value, you usually want to check if its fields are valid. For example, in a RESTful application, you
+may unmarshal the request payload into a struct and then validate the struct fields. If one or multiple fields
+are invalid, you may want to get an error describing which fields are invalid. You can use `valid.ValidateStruct()`
+to achieve this purpose. A single struct can have rules for multiple fields, and a field can be associated with multiple 
+rules. For example,
+
+```go
+type Address struct {
+	Street string
+	City   string
+	State  string
+	Zip    string
+}
+
+func (a Address) Validate() error {
+	return valid.ValidateStruct(&a,
+		// Street cannot be empty, and the length must between 5 and 50
+		valid.Field(&a.Street, valid.Required, valid.Length(5, 50)),
+		// City cannot be empty, and the length must between 5 and 50
+		valid.Field(&a.City, valid.Required, valid.Length(5, 50)),
+		// State cannot be empty, and must be a string consisting of two letters in upper case
+		valid.Field(&a.State, valid.Required, valid.Match(regexp.MustCompile("^[A-Z]{2}$"))),
+		// State cannot be empty, and must be a string consisting of five digits
+		valid.Field(&a.Zip, valid.Required, valid.Match(regexp.MustCompile("^[0-9]{5}$"))),
+	)
+}
+
+a := Address{
+    Street: "123",
+    City:   "Unknown",
+    State:  "Virginia",
+    Zip:    "12345",
+}
+
+err := a.Validate()
+fmt.Println(err)
+// Output:
+// Street: the length must be between 5 and 50; State: must be in a valid format.
+```
+
+Note that when calling `valid.ValidateStruct` to validate a struct, you should pass to the method a pointer 
+to the struct instead of the struct itself. Similarly, when calling `valid.Field` to specify the rules
+for a struct field, you should use a pointer to the struct field. 
+
+When the struct validation is performed, the fields are validated in the order they are specified in `ValidateStruct`. 
+And when each field is validated, its rules are also evaluated in the order they are associated with the field.
+If a rule fails, an error is recorded for that field, and the validation will continue with the next field.
+
+
+### Validating a Map
+
+Sometimes you might need to work with dynamic data stored in maps rather than a typed model. You can use `valid.Map()`
+in this situation. A single map can have rules for multiple keys, and a key can be associated with multiple 
+rules. For example,
+
+```go
+c := map[string]interface{}{
+	"Name":  "Qiang Xue",
+	"Email": "q",
+	"Address": map[string]interface{}{
+		"Street": "123",
+		"City":   "Unknown",
+		"State":  "Virginia",
+		"Zip":    "12345",
+	},
+}
+
+err := valid.Validate(c,
+	valid.Map(
+		// Name cannot be empty, and the length must be between 5 and 20.
+		valid.Key("Name", valid.Required, valid.Length(5, 20)),
+		// Email cannot be empty and should be in a valid email format.
+		valid.Key("Email", valid.Required, is.Email),
+		// Validate Address using its own validation rules
+		valid.Key("Address", valid.Map(
+			// Street cannot be empty, and the length must between 5 and 50
+			valid.Key("Street", valid.Required, valid.Length(5, 50)),
+			// City cannot be empty, and the length must between 5 and 50
+			valid.Key("City", valid.Required, valid.Length(5, 50)),
+			// State cannot be empty, and must be a string consisting of two letters in upper case
+			valid.Key("State", valid.Required, valid.Match(regexp.MustCompile("^[A-Z]{2}$"))),
+			// State cannot be empty, and must be a string consisting of five digits
+			valid.Key("Zip", valid.Required, valid.Match(regexp.MustCompile("^[0-9]{5}$"))),
+		)),
+	),
+)
+fmt.Println(err)
+// Output:
+// Address: (State: must be in a valid format; Street: the length must be between 5 and 50.); Email: must be a valid email address.
+```
+
+When the map validation is performed, the keys are validated in the order they are specified in `Map`. 
+And when each key is validated, its rules are also evaluated in the order they are associated with the key.
+If a rule fails, an error is recorded for that key, and the validation will continue with the next key.
+
+
+### Validation Errors
+
+The `valid.ValidateStruct` method returns validation errors found in struct fields in terms of `valid.Errors` 
+which is a map of fields and their corresponding errors. Nil is returned if validation passes.
+
+By default, `valid.Errors` uses the struct tags named `json` to determine what names should be used to 
+represent the invalid fields. The type also implements the `json.Marshaler` interface so that it can be marshaled 
+into a proper JSON object. For example,
+
+```go
+type Address struct {
+	Street string `json:"street"`
+	City   string `json:"city"`
+	State  string `json:"state"`
+	Zip    string `json:"zip"`
+}
+
+// ...perform validation here...
+
+err := a.Validate()
+b, _ := json.Marshal(err)
+fmt.Println(string(b))
+// Output:
+// {"street":"the length must be between 5 and 50","state":"must be in a valid format"}
+```
+
+You may modify `valid.ErrorTag` to use a different struct tag name.
+
+If you do not like the magic that `ValidateStruct` determines error keys based on struct field names or corresponding
+tag values, you may use the following alternative approach:
+
+```go
+c := Customer{
+	Name:  "Qiang Xue",
+	Email: "q",
+	Address: Address{
+		State:  "Virginia",
+	},
+}
+
+err := valid.Errors{
+	"name": valid.Validate(c.Name, valid.Required, valid.Length(5, 20)),
+	"email": valid.Validate(c.Name, valid.Required, is.Email),
+	"zip": valid.Validate(c.Address.Zip, valid.Required, valid.Match(regexp.MustCompile("^[0-9]{5}$"))),
+}.Filter()
+fmt.Println(err)
+// Output:
+// email: must be a valid email address; zip: cannot be blank.
+```
+
+In the above example, we build a `valid.Errors` by a list of names and the corresponding validation results. 
+At the end we call `Errors.Filter()` to remove from `Errors` all nils which correspond to those successful validation 
+results. The method will return nil if `Errors` is empty.
+
+The above approach is very flexible as it allows you to freely build up your validation error structure. You can use
+it to validate both struct and non-struct values. Compared to using `ValidateStruct` to validate a struct, 
+it has the drawback that you have to redundantly specify the error keys while `ValidateStruct` can automatically 
+find them out.
+
+
+### Internal Errors
+
+Internal errors are different from validation errors in that internal errors are caused by malfunctioning code (e.g.
+a validator making a remote call to validate some data when the remote service is down) rather
+than the data being validated. When an internal error happens during data validation, you may allow the user to resubmit
+the same data to perform validation again, hoping the program resumes functioning. On the other hand, if data validation
+fails due to data error, the user should generally not resubmit the same data again.
+
+To differentiate internal errors from validation errors, when an internal error occurs in a validator, wrap it
+into `valid.InternalError` by calling `valid.NewInternalError()`. The user of the validator can then check
+if a returned error is an internal error or not. For example,
+
+```go
+if err := a.Validate(); err != nil {
+	if e, ok := err.(valid.InternalError); ok {
+		// an internal error happened
+		fmt.Println(e.InternalError())
+	}
+}
+```
+
+
+## Validatable Types
+
+A type is validatable if it implements the `valid.Validatable` interface. 
+
+When `valid.Validate` is used to validate a validatable value, if it does not find any error with the 
+given validation rules, it will further call the value's `Validate()` method. 
+
+Similarly, when `valid.ValidateStruct` is validating a struct field whose type is validatable, it will call 
+the field's `Validate` method after it passes the listed rules.
+
+> Note: When implementing `valid.Validatable`, do not call `valid.Validate()` to validate the value in its
+> original type because this will cause infinite loops. For example, if you define a new type `MyString` as `string`
+> and implement `valid.Validatable` for `MyString`, within the `Validate()` function you should cast the value 
+> to `string` first before calling `valid.Validate()` to validate it.
+
+In the following example, the `Address` field of `Customer` is validatable because `Address` implements 
+`valid.Validatable`. Therefore, when validating a `Customer` struct with `valid.ValidateStruct`,
+validation will "dive" into the `Address` field.
+
+```go
+type Customer struct {
+	Name    string
+	Gender  string
+	Email   string
+	Address Address
+}
+
+func (c Customer) Validate() error {
+	return valid.ValidateStruct(&c,
+		// Name cannot be empty, and the length must be between 5 and 20.
+		valid.Field(&c.Name, valid.Required, valid.Length(5, 20)),
+		// Gender is optional, and should be either "Female" or "Male".
+		valid.Field(&c.Gender, valid.In("Female", "Male")),
+		// Email cannot be empty and should be in a valid email format.
+		valid.Field(&c.Email, valid.Required, is.Email),
+		// Validate Address using its own validation rules
+		valid.Field(&c.Address),
+	)
+}
+
+c := Customer{
+	Name:  "Qiang Xue",
+	Email: "q",
+	Address: Address{
+		Street: "123 Main Street",
+		City:   "Unknown",
+		State:  "Virginia",
+		Zip:    "12345",
+	},
+}
+
+err := c.Validate()
+fmt.Println(err)
+// Output:
+// Address: (State: must be in a valid format.); Email: must be a valid email address.
+```
+
+Sometimes, you may want to skip the invocation of a type's `Validate` method. To do so, simply associate
+a `valid.Skip` rule with the value being validated.
+
+### Maps/Slices/Arrays of Validatables
+
+When validating an iterable (map, slice, or array), whose element type implements the `valid.Validatable` interface,
+the `valid.Validate` method will call the `Validate` method of every non-nil element.
+The validation errors of the elements will be returned as `valid.Errors` which maps the keys of the
+invalid elements to their corresponding validation errors. For example,
+
+```go
+addresses := []Address{
+	Address{State: "MD", Zip: "12345"},
+	Address{Street: "123 Main St", City: "Vienna", State: "VA", Zip: "12345"},
+	Address{City: "Unknown", State: "NC", Zip: "123"},
+}
+err := valid.Validate(addresses)
+fmt.Println(err)
+// Output:
+// 0: (City: cannot be blank; Street: cannot be blank.); 2: (Street: cannot be blank; Zip: must be in a valid format.).
+```
+
+When using `valid.ValidateStruct` to validate a struct, the above validation procedure also applies to those struct 
+fields which are map/slices/arrays of validatables. 
+
+#### Each
+
+The `Each` validation rule allows you to apply a set of rules to each element of an array, slice, or map.
+
+```go
+type Customer struct {
+    Name      string
+    Emails    []string
+}
+
+func (c Customer) Validate() error {
+    return valid.ValidateStruct(&c,
+        // Name cannot be empty, and the length must be between 5 and 20.
+		valid.Field(&c.Name, valid.Required, valid.Length(5, 20)),
+		// Emails are optional, but if given must be valid.
+		valid.Field(&c.Emails, valid.Each(is.Email)),
+    )
+}
+
+c := Customer{
+    Name:   "Qiang Xue",
+    Emails: []Email{
+        "valid@example.com",
+        "invalid",
+    },
+}
+
+err := c.Validate()
+fmt.Println(err)
+// Output:
+// Emails: (1: must be a valid email address.).
+```
+
+### Pointers
+
+When a value being validated is a pointer, most validation rules will validate the actual value pointed to by the pointer.
+If the pointer is nil, these rules will skip the valid.
+
+An exception is the `valid.Required` and `valid.NotNil` rules. When a pointer is nil, they
+will report a validation error.
+
+
+### Types Implementing `sql.Valuer`
+
+If a data type implements the `sql.Valuer` interface (e.g. `sql.NullString`), the built-in validation rules will handle
+it properly. In particular, when a rule is validating such data, it will call the `Value()` method and validate
+the returned value instead.
+
+
+### Required vs. Not Nil
+
+When validating input values, there are two different scenarios about checking if input values are provided or not.
+
+In the first scenario, an input value is considered missing if it is not entered or it is entered as a zero value
+(e.g. an empty string, a zero integer). You can use the `valid.Required` rule in this case.
+
+In the second scenario, an input value is considered missing only if it is not entered. A pointer field is usually
+used in this case so that you can detect if a value is entered or not by checking if the pointer is nil or not.
+You can use the `valid.NotNil` rule to ensure a value is entered (even if it is a zero value).
+
+
+### Embedded Structs
+
+The `valid.ValidateStruct` method will properly validate a struct that contains embedded structs. In particular,
+the fields of an embedded struct are treated as if they belong directly to the containing struct. For example,
+
+```go
+type Employee struct {
+	Name string
+}
+
+type Manager struct {
+	Employee
+	Level int
+}
+
+m := Manager{}
+err := valid.ValidateStruct(&m,
+	valid.Field(&m.Name, valid.Required),
+	valid.Field(&m.Level, valid.Required),
+)
+fmt.Println(err)
+// Output:
+// Level: cannot be blank; Name: cannot be blank.
+```
+
+In the above code, we use `&m.Name` to specify the validation of the `Name` field of the embedded struct `Employee`.
+And the validation error uses `Name` as the key for the error associated with the `Name` field as if `Name` a field
+directly belonging to `Manager`.
+
+If `Employee` implements the `valid.Validatable` interface, we can also use the following code to validate
+`Manager`, which generates the same validation result:
+
+```go
+func (e Employee) Validate() error {
+	return valid.ValidateStruct(&e,
+		valid.Field(&e.Name, valid.Required),
+	)
+}
+
+err := valid.ValidateStruct(&m,
+	valid.Field(&m.Employee),
+	valid.Field(&m.Level, valid.Required),
+)
+fmt.Println(err)
+// Output:
+// Level: cannot be blank; Name: cannot be blank.
+```
+
+
+### Conditional Validation
+
+Sometimes, we may want to validate a value only when certain condition is met. For example, we want to ensure the 
+`unit` struct field is not empty only when the `quantity` field is not empty; or we may want to ensure either `email`
+or `phone` is provided. The so-called conditional validation can be achieved with the help of `valid.When`.
+The following code implements the aforementioned examples:
+
+```go
+result := valid.ValidateStruct(&a,
+    valid.Field(&a.Unit, valid.When(a.Quantity != "", valid.Required).Else(valid.Nil)),
+    valid.Field(&a.Phone, valid.When(a.Email == "", valid.Required.Error('Either phone or Email is required.')),
+    valid.Field(&a.Email, valid.When(a.Phone == "", valid.Required.Error('Either phone or Email is required.')),
+)
+```
+
+Note that `valid.When` and `valid.When.Else` can take a list of validation rules. These rules will be executed only when the condition is true (When) or false (Else).
+
+The above code can also be simplified using the shortcut `valid.Required.When`:
+
+```go
+result := valid.ValidateStruct(&a,
+    valid.Field(&a.Unit, valid.Required.When(a.Quantity != ""), valid.Nil.When(a.Quantity == "")),
+    valid.Field(&a.Phone, valid.Required.When(a.Email == "").Error('Either phone or Email is required.')),
+    valid.Field(&a.Email, valid.Required.When(a.Phone == "").Error('Either phone or Email is required.')),
+)
+```
+
+### Customizing Error Messages
+
+All built-in validation rules allow you to customize their error messages. To do so, simply call the `Error()` method
+of the rules. For example,
+
+```go
+data := "2123"
+err := valid.Validate(data,
+	valid.Required.Error("is required"),
+	valid.Match(regexp.MustCompile("^[0-9]{5}$")).Error("must be a string with five digits"),
+)
+fmt.Println(err)
+// Output:
+// must be a string with five digits
+```
+
+You can also customize the pre-defined error(s) of a built-in rule such that the customization applies to *every*
+instance of the rule. For example, the `Required` rule uses the pre-defined error `ErrRequired`. You can customize it
+during the application initialization:
+```go
+valid.ErrRequired = valid.ErrRequired.SetMessage("the value is required") 
+```
+
+### Error Code and Message Translation
+
+The errors returned by the validation rules implement the `Error` interface which contains the `Code()` method 
+to provide the error code information. While the message of a validation error is often customized, the code is immutable.
+You can use error code to programmatically check a validation error or look for the translation of the corresponding message.
+
+If you are developing your own validation rules, you can use `valid.NewError()` to create a validation error which
+implements the aforementioned `Error` interface.
+
+## Creating Custom Rules
+
+Creating a custom rule is as simple as implementing the `valid.Rule` interface. The interface contains a single
+method as shown below, which should validate the value and return the validation error, if any:
+
+```go
+// Validate validates a value and returns an error if validation fails.
+Validate(value interface{}) error
+```
+
+If you already have a function with the same signature as shown above, you can call `valid.By()` to turn
+it into a validation rule. For example,
+
+```go
+func checkAbc(value interface{}) error {
+	s, _ := value.(string)
+	if s != "abc" {
+		return errors.New("must be abc")
+	}
+	return nil
+}
+
+err := valid.Validate("xyz", valid.By(checkAbc))
+fmt.Println(err)
+// Output: must be abc
+```
+
+If your validation function takes additional parameters, you can use the following closure trick:
+
+```go
+func stringEquals(str string) valid.RuleFunc {
+	return func(value interface{}) error {
+		s, _ := value.(string)
+        if s != str {
+            return errors.New("unexpected string")
+        }
+        return nil
+    }
+}
+
+err := valid.Validate("xyz", valid.By(stringEquals("abc")))
+fmt.Println(err)
+// Output: unexpected string
+```
+
+
+### Rule Groups
+
+When a combination of several rules are used in multiple places, you may use the following trick to create a 
+rule group so that your code is more maintainable.
+
+```go
+var NameRule = []valid.Rule{
+	valid.Required,
+	valid.Length(5, 20),
+}
+
+type User struct {
+	FirstName string
+	LastName  string
+}
+
+func (u User) Validate() error {
+	return valid.ValidateStruct(&u,
+		valid.Field(&u.FirstName, NameRule...),
+		valid.Field(&u.LastName, NameRule...),
+	)
+}
+```
+
+In the above example, we create a rule group `NameRule` which consists of two validation rules. We then use this rule
+group to validate both `FirstName` and `LastName`.
+
+
+## Context-aware Validation
+
+While most validation rules are self-contained, some rules may depend dynamically on a context. A rule may implement the
+`valid.RuleWithContext` interface to support the so-called context-aware valid.
+ 
+To validate an arbitrary value with a context, call `valid.ValidateWithContext()`. The `context.Conext` parameter 
+will be passed along to those rules that implement `valid.RuleWithContext`.
+
+To validate the fields of a struct with a context, call `valid.ValidateStructWithContext()`. 
+
+You can define a context-aware rule from scratch by implementing both `valid.Rule` and `valid.RuleWithContext`. 
+You can also use `valid.WithContext()` to turn a function into a context-aware rule. For example,
+
+
+```go
+rule := valid.WithContext(func(ctx context.Context, value interface{}) error {
+	if ctx.Value("secret") == value.(string) {
+	    return nil
+	}
+	return errors.New("value incorrect")
+})
+value := "xyz"
+ctx := context.WithValue(context.Background(), "secret", "example")
+err := valid.ValidateWithContext(ctx, value, rule)
+fmt.Println(err)
+// Output: value incorrect
+```
+
+When performing context-aware validation, if a rule does not implement `valid.RuleWithContext`, its
+`valid.Rule` will be used instead.
+
+
+## Built-in Validation Rules
+
+The following rules are provided in the `validation` package:
+
+* `In(...interface{})`: checks if a value can be found in the given list of values.
+* `NotIn(...interface{})`: checks if a value is NOT among the given list of values.
+* `Length(min, max int)`: checks if the length of a value is within the specified range.
+  This rule should only be used for validating strings, slices, maps, and arrays.
+* `RuneLength(min, max int)`: checks if the length of a string is within the specified range.
+  This rule is similar as `Length` except that when the value being validated is a string, it checks
+  its rune length instead of byte length.
+* `Min(min interface{})` and `Max(max interface{})`: checks if a value is within the specified range.
+  These two rules should only be used for validating int, uint, float and time.Time types.
+* `Match(*regexp.Regexp)`: checks if a value matches the specified regular expression.
+  This rule should only be used for strings and byte slices.
+* `Date(layout string)`: checks if a string value is a date whose format is specified by the layout.
+  By calling `Min()` and/or `Max()`, you can check additionally if the date is within the specified range.
+* `Required`: checks if a value is not empty (neither nil nor zero).
+* `NotNil`: checks if a pointer value is not nil. Non-pointer values are considered valid.
+* `NilOrNotEmpty`: checks if a value is a nil pointer or a non-empty value. This differs from `Required` in that it treats a nil pointer as valid.
+* `Nil`: checks if a value is a nil pointer.
+* `Empty`: checks if a value is empty. nil pointers are considered valid.
+* `Skip`: this is a special rule used to indicate that all rules following it should be skipped (including the nested ones).
+* `MultipleOf`: checks if the value is a multiple of the specified range.
+* `Each(rules ...Rule)`: checks the elements within an iterable (map/slice/array) with other rules.
+* `When(condition, rules ...Rule)`: validates with the specified rules only when the condition is true.
+* `Else(rules ...Rule)`: must be used with `When(condition, rules ...Rule)`, validates with the specified rules only when the condition is false.
+
+The `is` sub-package provides a list of commonly used string validation rules that can be used to check if the format
+of a value satisfies certain requirements. Note that these rules only handle strings and byte slices and if a string
+ or byte slice is empty, it is considered valid. You may use a `Required` rule to ensure a value is not empty.
+Below is the whole list of the rules provided by the `is` package:
+
+* `Email`: validates if a string is an email or not. It also checks if the MX record exists for the email domain.
+* `EmailFormat`: validates if a string is an email or not. It does NOT check the existence of the MX record.
+* `URL`: validates if a string is a valid URL
+* `RequestURL`: validates if a string is a valid request URL
+* `RequestURI`: validates if a string is a valid request URI
+* `Alpha`: validates if a string contains English letters only (a-zA-Z)
+* `Digit`: validates if a string contains digits only (0-9)
+* `Alphanumeric`: validates if a string contains English letters and digits only (a-zA-Z0-9)
+* `UTFLetter`: validates if a string contains unicode letters only
+* `UTFDigit`: validates if a string contains unicode decimal digits only
+* `UTFLetterNumeric`: validates if a string contains unicode letters and numbers only
+* `UTFNumeric`: validates if a string contains unicode number characters (category N) only
+* `LowerCase`: validates if a string contains lower case unicode letters only
+* `UpperCase`: validates if a string contains upper case unicode letters only
+* `Hexadecimal`: validates if a string is a valid hexadecimal number
+* `HexColor`: validates if a string is a valid hexadecimal color code
+* `RGBColor`: validates if a string is a valid RGB color in the form of rgb(R, G, B)
+* `Int`: validates if a string is a valid integer number
+* `Float`: validates if a string is a floating point number
+* `UUIDv3`: validates if a string is a valid version 3 UUID
+* `UUIDv4`: validates if a string is a valid version 4 UUID
+* `UUIDv5`: validates if a string is a valid version 5 UUID
+* `UUID`: validates if a string is a valid UUID
+* `CreditCard`: validates if a string is a valid credit card number
+* `ISBN10`: validates if a string is an ISBN version 10
+* `ISBN13`: validates if a string is an ISBN version 13
+* `ISBN`: validates if a string is an ISBN (either version 10 or 13)
+* `JSON`: validates if a string is in valid JSON format
+* `ASCII`: validates if a string contains ASCII characters only
+* `PrintableASCII`: validates if a string contains printable ASCII characters only
+* `Multibyte`: validates if a string contains multibyte characters
+* `FullWidth`: validates if a string contains full-width characters
+* `HalfWidth`: validates if a string contains half-width characters
+* `VariableWidth`: validates if a string contains both full-width and half-width characters
+* `Base64`: validates if a string is encoded in Base64
+* `DataURI`: validates if a string is a valid base64-encoded data URI
+* `E164`: validates if a string is a valid E164 phone number (+19251232233)
+* `CountryCode2`: validates if a string is a valid ISO3166 Alpha 2 country code
+* `CountryCode3`: validates if a string is a valid ISO3166 Alpha 3 country code
+* `DialString`: validates if a string is a valid dial string that can be passed to Dial()
+* `MAC`: validates if a string is a MAC address
+* `IP`: validates if a string is a valid IP address (either version 4 or 6)
+* `IPv4`: validates if a string is a valid version 4 IP address
+* `IPv6`: validates if a string is a valid version 6 IP address
+* `Subdomain`: validates if a string is valid subdomain
+* `Domain`: validates if a string is valid domain
+* `DNSName`: validates if a string is valid DNS name
+* `Host`: validates if a string is a valid IP (both v4 and v6) or a valid DNS name
+* `Port`: validates if a string is a valid port number
+* `MongoID`: validates if a string is a valid Mongo ID
+* `Latitude`: validates if a string is a valid latitude
+* `Longitude`: validates if a string is a valid longitude
+* `SSN`: validates if a string is a social security number (SSN)
+* `Semver`: validates if a string is a valid semantic version

+ 199 - 0
pkg/valid/error.go

@@ -0,0 +1,199 @@
+package valid
+
+import (
+	"bytes"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"sort"
+	"strings"
+	"text/template"
+)
+
+var (
+	// ErrStructPointer is the error that a struct being validated is not specified as a pointer.
+	ErrStructPointer = errors.New("only a pointer to a struct can be validated")
+)
+
+// ErrorTag is the struct tag name used to customize the error field name for a struct field.
+var ErrorTag = "json"
+
+// Error interface represents an validation error
+type Error interface {
+	Error() string
+	Code() string
+	Message() string
+	SetMessage(string) Error
+	Params() map[string]interface{}
+	SetParams(map[string]interface{}) Error
+}
+
+// InternalError represents an error that should NOT be treated as a validation error.
+type InternalError interface {
+	error
+	InternalError() error
+}
+
+type internalError struct {
+	error
+}
+
+// NewInternalError wraps a given error into an InternalError.
+func NewInternalError(err error) InternalError {
+	return internalError{error: err}
+}
+
+// InternalError returns the actual error that it wraps around.
+func (e internalError) InternalError() error {
+	return e.error
+}
+
+// Errors represents the validation errors that are indexed by struct field names, map or slice keys.
+// values are Error or Errors (for map, slice and array error value is Errors).
+type Errors map[string]error
+
+// Error returns the error string of Errors.
+func (es Errors) Error() string {
+	if len(es) == 0 {
+		return ""
+	}
+
+	keys := make([]string, len(es))
+	i := 0
+	for key := range es {
+		keys[i] = key
+		i++
+	}
+	sort.Strings(keys)
+
+	var s strings.Builder
+	for i, key := range keys {
+		if i > 0 {
+			s.WriteString("; ")
+		}
+		if errs, ok := es[key].(Errors); ok {
+			_, _ = fmt.Fprintf(&s, "%v: (%v)", key, errs)
+		} else {
+			_, _ = fmt.Fprintf(&s, "%v: %v", key, es[key].Error())
+		}
+	}
+	s.WriteString(".")
+	return s.String()
+}
+
+// MarshalJSON converts the Errors into a valid JSON.
+func (es Errors) MarshalJSON() ([]byte, error) {
+	errs := map[string]interface{}{}
+	for key, err := range es {
+		if ms, ok := err.(json.Marshaler); ok {
+			errs[key] = ms
+		} else {
+			errs[key] = err.Error()
+		}
+	}
+	return json.Marshal(errs)
+}
+
+// Filter removes all nils from Errors and returns back the updated Errors as an error.
+// If the length of Errors becomes 0, it will return nil.
+func (es Errors) Filter() error {
+	for key, value := range es {
+		if value == nil {
+			delete(es, key)
+		}
+	}
+	if len(es) == 0 {
+		return nil
+	}
+	return es
+}
+
+// NewError create new validation error.
+func NewError(code, message string) Error {
+	return ErrorObject{
+		code:    code,
+		message: message,
+	}
+}
+
+// Assert that our ErrorObject implements the Error interface.
+var _ Error = ErrorObject{}
+
+// ErrorObject is the default validation error
+// that implements the Error interface.
+type ErrorObject struct {
+	code    string
+	message string
+	params  map[string]interface{}
+}
+
+// SetCode set the error's translation code.
+func (e ErrorObject) SetCode(code string) Error {
+	e.code = code
+	return e
+}
+
+// Code get the error's translation code.
+func (e ErrorObject) Code() string {
+	return e.code
+}
+
+// SetParams set the error's params.
+func (e ErrorObject) SetParams(params map[string]interface{}) Error {
+	e.params = params
+	return e
+}
+
+// AddParam add parameter to the error's parameters.
+func (e ErrorObject) AddParam(name string, value interface{}) Error {
+	if e.params == nil {
+		e.params = make(map[string]interface{})
+	}
+
+	e.params[name] = value
+	return e
+}
+
+// Params returns the error's params.
+func (e ErrorObject) Params() map[string]interface{} {
+	return e.params
+}
+
+// SetMessage set the error's message.
+func (e ErrorObject) SetMessage(message string) Error {
+	e.message = message
+	return e
+}
+
+// Message return the error's message.
+func (e ErrorObject) Message() string {
+	return e.message
+}
+
+// Error returns the error message.
+func (e ErrorObject) Error() string {
+	if len(e.params) == 0 {
+		return e.message
+	}
+
+	res := bytes.Buffer{}
+	_ = template.Must(template.New("err").Parse(e.message)).Execute(&res, e.params)
+
+	return res.String()
+}
+
+// ErrFieldPointer is the error that a field is not specified as a pointer.
+type ErrFieldPointer int
+
+// Error returns the error string of ErrFieldPointer.
+func (e ErrFieldPointer) Error() string {
+	return fmt.Sprintf("field #%v must be specified as a pointer", int(e))
+}
+
+// ErrFieldNotFound is the error that a field cannot be found in the struct.
+type ErrFieldNotFound int
+
+// Error returns the error string of ErrFieldNotFound.
+func (e ErrFieldNotFound) Error() string {
+	return fmt.Sprintf("field #%v cannot be found in the struct", int(e))
+}

+ 185 - 0
pkg/valid/error_test.go

@@ -0,0 +1,185 @@
+package valid
+
+import (
+	"errors"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestNewInternalError(t *testing.T) {
+	err := NewInternalError(errors.New("abc"))
+	if assert.NotNil(t, err.InternalError()) {
+		assert.Equal(t, "abc", err.InternalError().Error())
+	}
+}
+
+func TestErrors_Error(t *testing.T) {
+	errs := Errors{
+		"B": errors.New("B1"),
+		"C": errors.New("C1"),
+		"A": errors.New("A1"),
+	}
+	assert.Equal(t, "A: A1; B: B1; C: C1.", errs.Error())
+
+	errs = Errors{
+		"B": errors.New("B1"),
+	}
+	assert.Equal(t, "B: B1.", errs.Error())
+
+	errs = Errors{}
+	assert.Equal(t, "", errs.Error())
+}
+
+func TestErrors_MarshalMessage(t *testing.T) {
+	errs := Errors{
+		"A": errors.New("A1"),
+		"B": Errors{
+			"2": errors.New("B1"),
+		},
+	}
+	errsJSON, err := errs.MarshalJSON()
+	assert.Nil(t, err)
+	assert.Equal(t, "{\"A\":\"A1\",\"B\":{\"2\":\"B1\"}}", string(errsJSON))
+}
+
+func TestErrors_Filter(t *testing.T) {
+	errs := Errors{
+		"B": errors.New("B1"),
+		"C": nil,
+		"A": errors.New("A1"),
+	}
+	err := errs.Filter()
+	assert.Equal(t, 2, len(errs))
+	if assert.NotNil(t, err) {
+		assert.Equal(t, "A: A1; B: B1.", err.Error())
+	}
+
+	errs = Errors{}
+	assert.Nil(t, errs.Filter())
+
+	errs = Errors{
+		"B": nil,
+		"C": nil,
+	}
+
+	assert.Nil(t, errs.Filter())
+}
+
+func TestErrorObject_SetCode(t *testing.T) {
+	err := NewError("A", "msg").(ErrorObject)
+
+	assert.Equal(t, err.code, "A")
+	assert.Equal(t, err.Code(), "A")
+
+	err = err.SetCode("B").(ErrorObject)
+	assert.Equal(t, "B", err.code)
+}
+
+func TestErrorObject_Code(t *testing.T) {
+	err := NewError("A", "msg").(ErrorObject)
+
+	assert.Equal(t, err.Code(), "A")
+}
+
+func TestErrorObject_SetMessage(t *testing.T) {
+	err := NewError("code", "A").(ErrorObject)
+
+	assert.Equal(t, err.message, "A")
+	assert.Equal(t, err.Message(), "A")
+
+	err = err.SetMessage("abc").(ErrorObject)
+	assert.Equal(t, err.message, "abc")
+	assert.Equal(t, err.Message(), "abc")
+}
+
+func TestErrorObject_Message(t *testing.T) {
+	err := NewError("code", "A").(ErrorObject)
+
+	assert.Equal(t, err.message, "A")
+	assert.Equal(t, err.Message(), "A")
+}
+
+func TestErrorObject_Params(t *testing.T) {
+	p := map[string]interface{}{"A": "val1", "AA": "val2"}
+
+	err := NewError("code", "A").(ErrorObject)
+	err = err.SetParams(p).(ErrorObject)
+	err = err.SetMessage("B").(ErrorObject)
+
+	assert.Equal(t, err.params, p)
+	assert.Equal(t, err.Params(), p)
+}
+
+func TestErrorObject_AddParam(t *testing.T) {
+	t.Run("case1", func(t *testing.T) {
+		p := map[string]interface{}{"A": "val1", "B": "val2"}
+
+		err := NewError("code", "A").(ErrorObject)
+		err = err.SetParams(p).(ErrorObject)
+		err = err.AddParam("C", "val3").(ErrorObject)
+
+		p["C"] = "val3"
+
+		assert.Equal(t, err.params, p)
+		assert.Equal(t, err.Params(), p)
+	})
+
+	t.Run("case2", func(t *testing.T) {
+		p := map[string]interface{}{"key": "val"}
+		err := NewError("code", "A").(ErrorObject)
+		err = err.AddParam("key", "val").(ErrorObject)
+
+		assert.Equal(t, err.params, p)
+		assert.Equal(t, err.Params(), p)
+	})
+}
+
+func TestError_Code(t *testing.T) {
+	err := NewError("A", "msg")
+
+	assert.Equal(t, err.Code(), "A")
+}
+
+func TestError_SetMessage(t *testing.T) {
+	err := NewError("code", "A")
+
+	assert.Equal(t, err.Message(), "A")
+
+	err = err.SetMessage("abc")
+	assert.Equal(t, err.Message(), "abc")
+}
+
+func TestError_Message(t *testing.T) {
+	err := NewError("code", "A")
+
+	assert.Equal(t, err.Message(), "A")
+}
+
+func TestError_Params(t *testing.T) {
+	p := map[string]interface{}{"A": "val1", "AA": "val2"}
+
+	err := NewError("code", "A")
+	err = err.SetParams(p)
+	err = err.SetMessage("B")
+
+	assert.Equal(t, err.Params(), p)
+}
+
+func TestValidationError(t *testing.T) {
+	params := map[string]interface{}{
+		"A": "B",
+	}
+
+	err := NewError("code", "msg")
+	err = err.SetParams(params)
+
+	assert.Equal(t, err.Code(), "code")
+	assert.Equal(t, err.Message(), "msg")
+	assert.Equal(t, err.Params(), params)
+
+	params = map[string]interface{}{"min": 1}
+	err = err.SetParams(params)
+
+	assert.Equal(t, err.Params(), params)
+}

+ 192 - 0
pkg/valid/example_test.go

@@ -0,0 +1,192 @@
+package valid_test
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"regexp"
+
+	"kpt-tmr-group/pkg/valid"
+	"kpt-tmr-group/pkg/valid/is"
+)
+
+type Address struct {
+	Street string
+	City   string
+	State  string
+	Zip    string
+}
+
+type Customer struct {
+	Name    string
+	Gender  string
+	Email   string
+	Address Address
+}
+
+func (a Address) Validate() error {
+	return valid.ValidateStruct(&a,
+		// Street cannot be empty, and the length must between 5 and 50
+		valid.Field(&a.Street, valid.Required, valid.Length(5, 50)),
+		// City cannot be empty, and the length must between 5 and 50
+		valid.Field(&a.City, valid.Required, valid.Length(5, 50)),
+		// State cannot be empty, and must be a string consisting of two letters in upper case
+		valid.Field(&a.State, valid.Required, valid.Match(regexp.MustCompile("^[A-Z]{2}$"))),
+		// State cannot be empty, and must be a string consisting of five digits
+		valid.Field(&a.Zip, valid.Required, valid.Match(regexp.MustCompile("^[0-9]{5}$"))),
+	)
+}
+
+func (c Customer) Validate() error {
+	return valid.ValidateStruct(&c,
+		// Name cannot be empty, and the length must be between 5 and 20.
+		valid.Field(&c.Name, valid.Required, valid.Length(5, 20)),
+		// Gender is optional, and should be either "Female" or "Male".
+		valid.Field(&c.Gender, valid.In("Female", "Male")),
+		// Email cannot be empty and should be in a valid email format.
+		valid.Field(&c.Email, valid.Required, is.Email),
+		// Validate Address using its own validation rules
+		valid.Field(&c.Address),
+	)
+}
+
+func Example() {
+	c := Customer{
+		Name:  "Qiang Xue",
+		Email: "q",
+		Address: Address{
+			Street: "123 Main Street",
+			City:   "Unknown",
+			State:  "Virginia",
+			Zip:    "12345",
+		},
+	}
+
+	err := c.Validate()
+	fmt.Println(err)
+	// Output:
+	// Address: (State: must be in a valid format.); Email: must be a valid email address.
+}
+
+func Example_second() {
+	data := "example"
+	err := valid.Validate(data,
+		valid.Required,       // not empty
+		valid.Length(5, 100), // length between 5 and 100
+		is.URL,               // is a valid URL
+	)
+	fmt.Println(err)
+	// Output:
+	// must be a valid URL
+}
+
+func Example_third() {
+	addresses := []Address{
+		{State: "MD", Zip: "12345"},
+		{Street: "123 Main St", City: "Vienna", State: "VA", Zip: "12345"},
+		{City: "Unknown", State: "NC", Zip: "123"},
+	}
+	err := valid.Validate(addresses)
+	fmt.Println(err)
+	// Output:
+	// 0: (City: cannot be blank; Street: cannot be blank.); 2: (Street: cannot be blank; Zip: must be in a valid format.).
+}
+
+func Example_four() {
+	c := Customer{
+		Name:  "Qiang Xue",
+		Email: "q",
+		Address: Address{
+			State: "Virginia",
+		},
+	}
+
+	err := valid.Errors{
+		"name":  valid.Validate(c.Name, valid.Required, valid.Length(5, 20)),
+		"email": valid.Validate(c.Name, valid.Required, is.Email),
+		"zip":   valid.Validate(c.Address.Zip, valid.Required, valid.Match(regexp.MustCompile("^[0-9]{5}$"))),
+	}.Filter()
+	fmt.Println(err)
+	// Output:
+	// email: must be a valid email address; zip: cannot be blank.
+}
+
+func Example_five() {
+	type Employee struct {
+		Name string
+	}
+
+	type Manager struct {
+		Employee
+		Level int
+	}
+
+	m := Manager{}
+	err := valid.ValidateStruct(&m,
+		valid.Field(&m.Name, valid.Required),
+		valid.Field(&m.Level, valid.Required),
+	)
+	fmt.Println(err)
+	// Output:
+	// Level: cannot be blank; Name: cannot be blank.
+}
+
+type contextKey int
+
+func Example_six() {
+	key := contextKey(1)
+	rule := valid.WithContext(func(ctx context.Context, value interface{}) error {
+		s, _ := value.(string)
+		if ctx.Value(key) == s {
+			return nil
+		}
+		return errors.New("unexpected value")
+	})
+	ctx := context.WithValue(context.Background(), key, "good sample")
+
+	err1 := valid.ValidateWithContext(ctx, "bad sample", rule)
+	fmt.Println(err1)
+
+	err2 := valid.ValidateWithContext(ctx, "good sample", rule)
+	fmt.Println(err2)
+
+	// Output:
+	// unexpected value
+	// <nil>
+}
+
+func Example_seven() {
+	c := map[string]interface{}{
+		"Name":  "Qiang Xue",
+		"Email": "q",
+		"Address": map[string]interface{}{
+			"Street": "123",
+			"City":   "Unknown",
+			"State":  "Virginia",
+			"Zip":    "12345",
+		},
+	}
+
+	err := valid.Validate(c,
+		valid.Map(
+			// Name cannot be empty, and the length must be between 5 and 20.
+			valid.Key("Name", valid.Required, valid.Length(5, 20)),
+			// Email cannot be empty and should be in a valid email format.
+			valid.Key("Email", valid.Required, is.Email),
+			// Validate Address using its own validation rules
+			valid.Key("Address", valid.Map(
+				// Street cannot be empty, and the length must between 5 and 50
+				valid.Key("Street", valid.Required, valid.Length(5, 50)),
+				// City cannot be empty, and the length must between 5 and 50
+				valid.Key("City", valid.Required, valid.Length(5, 50)),
+				// State cannot be empty, and must be a string consisting of two letters in upper case
+				valid.Key("State", valid.Required, valid.Match(regexp.MustCompile("^[A-Z]{2}$"))),
+				// State cannot be empty, and must be a string consisting of five digits
+				valid.Key("Zip", valid.Required, valid.Match(regexp.MustCompile("^[0-9]{5}$"))),
+			)),
+		),
+	)
+	fmt.Println(err)
+	// Output:
+	// Address: (State: must be in a valid format; Street: the length must be between 5 and 50.); Email: must be a valid email address.
+}

+ 29 - 0
pkg/valid/interface.go

@@ -0,0 +1,29 @@
+package valid
+
+import (
+	"context"
+)
+
+// Validatable is the interface indicating the type implementing it supports data validation.
+type Validatable interface {
+	// Validate validates the data and returns an error if validation fails.
+	Validate() error
+}
+
+// ValidatableWithContext is the interface indicating the type implementing it supports context-aware data validation.
+type ValidatableWithContext interface {
+	// ValidateWithContext validates the data with the given context and returns an error if validation fails.
+	ValidateWithContext(ctx context.Context) error
+}
+
+// Rule represents a validation rule.
+type Rule interface {
+	// Validate validates a value and returns a value if validation fails.
+	Validate(value interface{}) error
+}
+
+// RuleWithContext represents a context-aware validation rule.
+type RuleWithContext interface {
+	// ValidateWithContext validates a value and returns a value if validation fails.
+	ValidateWithContext(ctx context.Context, value interface{}) error
+}

+ 293 - 0
pkg/valid/is/rule.go

@@ -0,0 +1,293 @@
+package is
+
+import (
+	"regexp"
+	"unicode"
+
+	"git.llsapp.com/zhenghe/pkg/valid"
+	"github.com/asaskevich/govalidator"
+	"github.com/nyaruka/phonenumbers"
+)
+
+var (
+	// ErrEmail is the error that returns in case of an invalid email.
+	ErrEmail = valid.NewError("validation_is_email", "must be a valid email address")
+	// ErrURL is the error that returns in case of an invalid URL.
+	ErrURL = valid.NewError("validation_is_url", "must be a valid URL")
+	// ErrRequestURL is the error that returns in case of an invalid request URL.
+	ErrRequestURL = valid.NewError("validation_is_request_url", "must be a valid request URL")
+	// ErrRequestURI is the error that returns in case of an invalid request URI.
+	ErrRequestURI = valid.NewError("validation_request_is_request_uri", "must be a valid request URI")
+	// ErrAlpha is the error that returns in case of an invalid alpha value.
+	ErrAlpha = valid.NewError("validation_is_alpha", "must contain English letters only")
+	// ErrDigit is the error that returns in case of an invalid digit value.
+	ErrDigit = valid.NewError("validation_is_digit", "must contain digits only")
+	// ErrAlphanumeric is the error that returns in case of an invalid alphanumeric value.
+	ErrAlphanumeric = valid.NewError("validation_is_alphanumeric", "must contain English letters and digits only")
+	// ErrUTFLetter is the error that returns in case of an invalid utf letter value.
+	ErrUTFLetter = valid.NewError("validation_is_utf_letter", "must contain unicode letter characters only")
+	// ErrUTFDigit is the error that returns in case of an invalid utf digit value.
+	ErrUTFDigit = valid.NewError("validation_is_utf_digit", "must contain unicode decimal digits only")
+	// ErrUTFLetterNumeric is the error that returns in case of an invalid utf numeric or letter value.
+	ErrUTFLetterNumeric = valid.NewError("validation_is utf_letter_numeric", "must contain unicode letters and numbers only")
+	// ErrUTFNumeric is the error that returns in case of an invalid utf numeric value.
+	ErrUTFNumeric = valid.NewError("validation_is_utf_numeric", "must contain unicode number characters only")
+	// ErrLowerCase is the error that returns in case of an invalid lower case value.
+	ErrLowerCase = valid.NewError("validation_is_lower_case", "must be in lower case")
+	// ErrUpperCase is the error that returns in case of an invalid upper case value.
+	ErrUpperCase = valid.NewError("validation_is_upper_case", "must be in upper case")
+	// ErrHexadecimal is the error that returns in case of an invalid hexadecimal number.
+	ErrHexadecimal = valid.NewError("validation_is_hexadecimal", "must be a valid hexadecimal number")
+	// ErrHexColor is the error that returns in case of an invalid hexadecimal color code.
+	ErrHexColor = valid.NewError("validation_is_hex_color", "must be a valid hexadecimal color code")
+	// ErrRGBColor is the error that returns in case of an invalid RGB color code.
+	ErrRGBColor = valid.NewError("validation_is_rgb_color", "must be a valid RGB color code")
+	// ErrInt is the error that returns in case of an invalid integer value.
+	ErrInt = valid.NewError("validation_is_int", "must be an integer number")
+	// ErrFloat is the error that returns in case of an invalid float value.
+	ErrFloat = valid.NewError("validation_is_float", "must be a floating point number")
+	// ErrUUIDv3 is the error that returns in case of an invalid UUIDv3 value.
+	ErrUUIDv3 = valid.NewError("validation_is_uuid_v3", "must be a valid UUID v3")
+	// ErrUUIDv4 is the error that returns in case of an invalid UUIDv4 value.
+	ErrUUIDv4 = valid.NewError("validation_is_uuid_v4", "must be a valid UUID v4")
+	// ErrUUIDv5 is the error that returns in case of an invalid UUIDv5 value.
+	ErrUUIDv5 = valid.NewError("validation_is_uuid_v5", "must be a valid UUID v5")
+	// ErrUUID is the error that returns in case of an invalid UUID value.
+	ErrUUID = valid.NewError("validation_is_uuid", "must be a valid UUID")
+	// ErrCreditCard is the error that returns in case of an invalid credit card number.
+	ErrCreditCard = valid.NewError("validation_is_credit_card", "must be a valid credit card number")
+	// ErrISBN10 is the error that returns in case of an invalid ISBN-10 value.
+	ErrISBN10 = valid.NewError("validation_is_isbn_10", "must be a valid ISBN-10")
+	// ErrISBN13 is the error that returns in case of an invalid ISBN-13 value.
+	ErrISBN13 = valid.NewError("validation_is_isbn_13", "must be a valid ISBN-13")
+	// ErrISBN is the error that returns in case of an invalid ISBN value.
+	ErrISBN = valid.NewError("validation_is_isbn", "must be a valid ISBN")
+	// ErrJSON is the error that returns in case of an invalid JSON.
+	ErrJSON = valid.NewError("validation_is_json", "must be in valid JSON format")
+	// ErrASCII is the error that returns in case of an invalid ASCII.
+	ErrASCII = valid.NewError("validation_is_ascii", "must contain ASCII characters only")
+	// ErrPrintableASCII is the error that returns in case of an invalid printable ASCII value.
+	ErrPrintableASCII = valid.NewError("validation_is_printable_ascii", "must contain printable ASCII characters only")
+	// ErrMultibyte is the error that returns in case of an invalid multibyte value.
+	ErrMultibyte = valid.NewError("validation_is_multibyte", "must contain multibyte characters")
+	// ErrFullWidth is the error that returns in case of an invalid full-width value.
+	ErrFullWidth = valid.NewError("validation_is_full_width", "must contain full-width characters")
+	// ErrHalfWidth is the error that returns in case of an invalid half-width value.
+	ErrHalfWidth = valid.NewError("validation_is_half_width", "must contain half-width characters")
+	// ErrVariableWidth is the error that returns in case of an invalid variable width value.
+	ErrVariableWidth = valid.NewError("validation_is_variable_width", "must contain both full-width and half-width characters")
+	// ErrBase64 is the error that returns in case of an invalid base54 value.
+	ErrBase64 = valid.NewError("validation_is_base64", "must be encoded in Base64")
+	// ErrDataURI is the error that returns in case of an invalid data URI.
+	ErrDataURI = valid.NewError("validation_is_data_uri", "must be a Base64-encoded data URI")
+	// ErrE164 is the error that returns in case of an invalid e165.
+	ErrE164 = valid.NewError("validation_is_e164_number", "must be a valid E164 number")
+	// ErrCountryCode2 is the error that returns in case of an invalid two-letter country code.
+	ErrCountryCode2 = valid.NewError("validation_is_country_code_2_letter", "must be a valid two-letter country code")
+	// ErrCountryCode3 is the error that returns in case of an invalid three-letter country code.
+	ErrCountryCode3 = valid.NewError("validation_is_country_code_3_letter", "must be a valid three-letter country code")
+	// ErrCurrencyCode is the error that returns in case of an invalid currency code.
+	ErrCurrencyCode = valid.NewError("validation_is_currency_code", "must be valid ISO 4217 currency code")
+	// ErrDialString is the error that returns in case of an invalid string.
+	ErrDialString = valid.NewError("validation_is_dial_string", "must be a valid dial string")
+	// ErrMac is the error that returns in case of an invalid mac address.
+	ErrMac = valid.NewError("validation_is_mac_address", "must be a valid MAC address")
+	// ErrIP is the error that returns in case of an invalid IP.
+	ErrIP = valid.NewError("validation_is_ip", "must be a valid IP address")
+	// ErrIPv4 is the error that returns in case of an invalid IPv4.
+	ErrIPv4 = valid.NewError("validation_is_ipv4", "must be a valid IPv4 address")
+	// ErrIPv6 is the error that returns in case of an invalid IPv6.
+	ErrIPv6 = valid.NewError("validation_is_ipv6", "must be a valid IPv6 address")
+	// ErrSubdomain is the error that returns in case of an invalid subdomain.
+	ErrSubdomain = valid.NewError("validation_is_sub_domain", "must be a valid subdomain")
+	// ErrDomain is the error that returns in case of an invalid domain.
+	ErrDomain = valid.NewError("validation_is_domain", "must be a valid domain")
+	// ErrDNSName is the error that returns in case of an invalid DNS name.
+	ErrDNSName = valid.NewError("validation_is_dns_name", "must be a valid DNS name")
+	// ErrHost is the error that returns in case of an invalid host.
+	ErrHost = valid.NewError("validation_is_host", "must be a valid IP address or DNS name")
+	// ErrPort is the error that returns in case of an invalid port.
+	ErrPort = valid.NewError("validation_is_port", "must be a valid port number")
+	// ErrMongoID is the error that returns in case of an invalid MongoID.
+	ErrMongoID = valid.NewError("validation_is_mongo_id", "must be a valid hex-encoded MongoDB ObjectId")
+	// ErrLatitude is the error that returns in case of an invalid latitude.
+	ErrLatitude = valid.NewError("validation_is_latitude", "must be a valid latitude")
+	// ErrLongitude is the error that returns in case of an invalid longitude.
+	ErrLongitude = valid.NewError("validation_is_longitude", "must be a valid longitude")
+	// ErrSSN is the error that returns in case of an invalid SSN.
+	ErrSSN = valid.NewError("validation_is_ssn", "must be a valid social security number")
+	// ErrSemver is the error that returns in case of an invalid semver.
+	ErrSemver = valid.NewError("validation_is_semver", "must be a valid semantic version")
+)
+
+var (
+	// Email validates if a string is an email or not. It also checks if the MX record exists for the email domain.
+	Email = valid.NewStringRuleWithError(govalidator.IsExistingEmail, ErrEmail)
+	// EmailFormat validates if a string is an email or not. Note that it does NOT check if the MX record exists or not.
+	EmailFormat = valid.NewStringRuleWithError(govalidator.IsEmail, ErrEmail)
+	// EmailUser validates if a string is an user email or not.
+	EmailUser = valid.NewStringRuleWithError(isUserEmail, ErrEmail)
+	// URL validates if a string is a valid URL
+	URL = valid.NewStringRuleWithError(govalidator.IsURL, ErrURL)
+	// RequestURL validates if a string is a valid request URL
+	RequestURL = valid.NewStringRuleWithError(govalidator.IsRequestURL, ErrRequestURL)
+	// RequestURI validates if a string is a valid request URI
+	RequestURI = valid.NewStringRuleWithError(govalidator.IsRequestURI, ErrRequestURI)
+	// Alpha validates if a string contains English letters only (a-zA-Z)
+	Alpha = valid.NewStringRuleWithError(govalidator.IsAlpha, ErrAlpha)
+	// Digit validates if a string contains digits only (0-9)
+	Digit = valid.NewStringRuleWithError(isDigit, ErrDigit)
+	// Alphanumeric validates if a string contains English letters and digits only (a-zA-Z0-9)
+	Alphanumeric = valid.NewStringRuleWithError(govalidator.IsAlphanumeric, ErrAlphanumeric)
+	// UTFLetter validates if a string contains unicode letters only
+	UTFLetter = valid.NewStringRuleWithError(govalidator.IsUTFLetter, ErrUTFLetter)
+	// UTFDigit validates if a string contains unicode decimal digits only
+	UTFDigit = valid.NewStringRuleWithError(govalidator.IsUTFDigit, ErrUTFDigit)
+	// UTFLetterNumeric validates if a string contains unicode letters and numbers only
+	UTFLetterNumeric = valid.NewStringRuleWithError(govalidator.IsUTFLetterNumeric, ErrUTFLetterNumeric)
+	// UTFNumeric validates if a string contains unicode number characters (category N) only
+	UTFNumeric = valid.NewStringRuleWithError(isUTFNumeric, ErrUTFNumeric)
+	// LowerCase validates if a string contains lower case unicode letters only
+	LowerCase = valid.NewStringRuleWithError(govalidator.IsLowerCase, ErrLowerCase)
+	// UpperCase validates if a string contains upper case unicode letters only
+	UpperCase = valid.NewStringRuleWithError(govalidator.IsUpperCase, ErrUpperCase)
+	// Hexadecimal validates if a string is a valid hexadecimal number
+	Hexadecimal = valid.NewStringRuleWithError(govalidator.IsHexadecimal, ErrHexadecimal)
+	// HexColor validates if a string is a valid hexadecimal color code
+	HexColor = valid.NewStringRuleWithError(govalidator.IsHexcolor, ErrHexColor)
+	// RGBColor validates if a string is a valid RGB color in the form of rgb(R, G, B)
+	RGBColor = valid.NewStringRuleWithError(govalidator.IsRGBcolor, ErrRGBColor)
+	// Int validates if a string is a valid integer number
+	Int = valid.NewStringRuleWithError(govalidator.IsInt, ErrInt)
+	// Float validates if a string is a floating point number
+	Float = valid.NewStringRuleWithError(govalidator.IsFloat, ErrFloat)
+	// UUIDv3 validates if a string is a valid version 3 UUID
+	UUIDv3 = valid.NewStringRuleWithError(govalidator.IsUUIDv3, ErrUUIDv3)
+	// UUIDv4 validates if a string is a valid version 4 UUID
+	UUIDv4 = valid.NewStringRuleWithError(govalidator.IsUUIDv4, ErrUUIDv4)
+	// UUIDv5 validates if a string is a valid version 5 UUID
+	UUIDv5 = valid.NewStringRuleWithError(govalidator.IsUUIDv5, ErrUUIDv5)
+	// UUID validates if a string is a valid UUID
+	UUID = valid.NewStringRuleWithError(govalidator.IsUUID, ErrUUID)
+	// CreditCard validates if a string is a valid credit card number
+	CreditCard = valid.NewStringRuleWithError(govalidator.IsCreditCard, ErrCreditCard)
+	// ISBN10 validates if a string is an ISBN version 10
+	ISBN10 = valid.NewStringRuleWithError(govalidator.IsISBN10, ErrISBN10)
+	// ISBN13 validates if a string is an ISBN version 13
+	ISBN13 = valid.NewStringRuleWithError(govalidator.IsISBN13, ErrISBN13)
+	// ISBN validates if a string is an ISBN (either version 10 or 13)
+	ISBN = valid.NewStringRuleWithError(isISBN, ErrISBN)
+	// JSON validates if a string is in valid JSON format
+	JSON = valid.NewStringRuleWithError(govalidator.IsJSON, ErrJSON)
+	// ASCII validates if a string contains ASCII characters only
+	ASCII = valid.NewStringRuleWithError(govalidator.IsASCII, ErrASCII)
+	// PrintableASCII validates if a string contains printable ASCII characters only
+	PrintableASCII = valid.NewStringRuleWithError(govalidator.IsPrintableASCII, ErrPrintableASCII)
+	// Multibyte validates if a string contains multibyte characters
+	Multibyte = valid.NewStringRuleWithError(govalidator.IsMultibyte, ErrMultibyte)
+	// FullWidth validates if a string contains full-width characters
+	FullWidth = valid.NewStringRuleWithError(govalidator.IsFullWidth, ErrFullWidth)
+	// HalfWidth validates if a string contains half-width characters
+	HalfWidth = valid.NewStringRuleWithError(govalidator.IsHalfWidth, ErrHalfWidth)
+	// VariableWidth validates if a string contains both full-width and half-width characters
+	VariableWidth = valid.NewStringRuleWithError(govalidator.IsVariableWidth, ErrVariableWidth)
+	// Base64 validates if a string is encoded in Base64
+	Base64 = valid.NewStringRuleWithError(govalidator.IsBase64, ErrBase64)
+	// DataURI validates if a string is a valid base64-encoded data URI
+	DataURI = valid.NewStringRuleWithError(govalidator.IsDataURI, ErrDataURI)
+	// E164 validates if a string is a valid ISO3166 Alpha 2 country code
+	E164 = valid.NewStringRuleWithError(isE164Number, ErrE164)
+	// CountryCode2 validates if a string is a valid ISO3166 Alpha 2 country code
+	CountryCode2 = valid.NewStringRuleWithError(govalidator.IsISO3166Alpha2, ErrCountryCode2)
+	// CountryCode3 validates if a string is a valid ISO3166 Alpha 3 country code
+	CountryCode3 = valid.NewStringRuleWithError(govalidator.IsISO3166Alpha3, ErrCountryCode3)
+	// CurrencyCode validates if a string is a valid IsISO4217 currency code.
+	CurrencyCode = valid.NewStringRuleWithError(govalidator.IsISO4217, ErrCurrencyCode)
+	// DialString validates if a string is a valid dial string that can be passed to Dial()
+	DialString = valid.NewStringRuleWithError(govalidator.IsDialString, ErrDialString)
+	// MAC validates if a string is a MAC address
+	MAC = valid.NewStringRuleWithError(govalidator.IsMAC, ErrMac)
+	// IP validates if a string is a valid IP address (either version 4 or 6)
+	IP = valid.NewStringRuleWithError(govalidator.IsIP, ErrIP)
+	// IPv4 validates if a string is a valid version 4 IP address
+	IPv4 = valid.NewStringRuleWithError(govalidator.IsIPv4, ErrIPv4)
+	// IPv6 validates if a string is a valid version 6 IP address
+	IPv6 = valid.NewStringRuleWithError(govalidator.IsIPv6, ErrIPv6)
+	// Subdomain validates if a string is valid subdomain
+	Subdomain = valid.NewStringRuleWithError(isSubdomain, ErrSubdomain)
+	// Domain validates if a string is valid domain
+	Domain = valid.NewStringRuleWithError(isDomain, ErrDomain)
+	// DNSName validates if a string is valid DNS name
+	DNSName = valid.NewStringRuleWithError(govalidator.IsDNSName, ErrDNSName)
+	// Host validates if a string is a valid IP (both v4 and v6) or a valid DNS name
+	Host = valid.NewStringRuleWithError(govalidator.IsHost, ErrHost)
+	// Port validates if a string is a valid port number
+	Port = valid.NewStringRuleWithError(govalidator.IsPort, ErrPort)
+	// MongoID validates if a string is a valid Mongo ID
+	MongoID = valid.NewStringRuleWithError(govalidator.IsMongoID, ErrMongoID)
+	// Latitude validates if a string is a valid latitude
+	Latitude = valid.NewStringRuleWithError(govalidator.IsLatitude, ErrLatitude)
+	// Longitude validates if a string is a valid longitude
+	Longitude = valid.NewStringRuleWithError(govalidator.IsLongitude, ErrLongitude)
+	// SSN validates if a string is a social security number (SSN)
+	SSN = valid.NewStringRuleWithError(govalidator.IsSSN, ErrSSN)
+	// Semver validates if a string is a valid semantic version
+	Semver = valid.NewStringRuleWithError(govalidator.IsSemver, ErrSemver)
+)
+
+var (
+	reDigit = regexp.MustCompile("^[0-9]+$")
+	// Subdomain regex source: https://stackoverflow.com/a/7933253
+	reSubdomain = regexp.MustCompile(`^[A-Za-z0-9](?:[A-Za-z0-9\-]{0,61}[A-Za-z0-9])?$`)
+	// Domain regex source: https://stackoverflow.com/a/7933253
+	// Slightly modified: Removed 255 max length validation since Go regex does not
+	// support lookarounds. More info: https://stackoverflow.com/a/38935027
+	reDomain = regexp.MustCompile(`^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-z0-9])?\.)+(?:[a-zA-Z]{1,63}| xn--[a-z0-9]{1,59})$`)
+	// UserEmail regex 来源自 Russell 的代码,这里作为用户邮箱的校验逻辑
+	reUserEmail = regexp.MustCompile(`\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*`)
+)
+
+func isISBN(value string) bool {
+	return govalidator.IsISBN(value, 10) || govalidator.IsISBN(value, 13)
+}
+
+func isDigit(value string) bool {
+	return reDigit.MatchString(value)
+}
+
+func isE164Number(value string) bool {
+	phoneNumber, err := phonenumbers.Parse(value, "CN")
+	if err != nil {
+		return false
+	}
+	if !phonenumbers.IsValidNumber(phoneNumber) {
+		return false
+	}
+
+	return true
+}
+
+func isSubdomain(value string) bool {
+	return reSubdomain.MatchString(value)
+}
+
+func isDomain(value string) bool {
+	if len(value) > 255 {
+		return false
+	}
+
+	return reDomain.MatchString(value)
+}
+
+func isUTFNumeric(value string) bool {
+	for _, c := range value {
+		if unicode.IsNumber(c) == false {
+			return false
+		}
+	}
+	return true
+}
+
+func isUserEmail(value string) bool {
+	return reUserEmail.MatchString(value)
+}

+ 98 - 0
pkg/valid/is/rule_test.go

@@ -0,0 +1,98 @@
+package is
+
+import (
+	"strings"
+	"testing"
+
+	"git.llsapp.com/zhenghe/pkg/valid"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestAll(t *testing.T) {
+	tests := []struct {
+		tag            string
+		rule           valid.Rule
+		valid, invalid string
+		err            string
+	}{
+		{"Email", Email, "test@example.com", "example.com", "must be a valid email address"},
+		{"EmailFormat", EmailFormat, "test@example.com", "example.com", "must be a valid email address"},
+		{"EmailUser", EmailUser, "test@example.com", "example.com", "must be a valid email address"},
+		{"URL", URL, "http://example.com", "examplecom", "must be a valid URL"},
+		{"RequestURL", RequestURL, "http://example.com", "examplecom", "must be a valid request URL"},
+		{"RequestURI", RequestURI, "http://example.com", "examplecom", "must be a valid request URI"},
+		{"Alpha", Alpha, "abcd", "ab12", "must contain English letters only"},
+		{"Digit", Digit, "123", "12ab", "must contain digits only"},
+		{"Alphanumeric", Alphanumeric, "abc123", "abc.123", "must contain English letters and digits only"},
+		{"UTFLetter", UTFLetter, "abc", "123", "must contain unicode letter characters only"},
+		{"UTFDigit", UTFDigit, "123", "abc", "must contain unicode decimal digits only"},
+		{"UTFNumeric", UTFNumeric, "123", "abc.123", "must contain unicode number characters only"},
+		{"UTFLetterNumeric", UTFLetterNumeric, "abc123", "abc.123", "must contain unicode letters and numbers only"},
+		{"LowerCase", LowerCase, "abc", "Abc", "must be in lower case"},
+		{"UpperCase", UpperCase, "ABC", "ABc", "must be in upper case"},
+		{"IP", IP, "74.125.19.99", "74.125.19.999", "must be a valid IP address"},
+		{"IPv4", IPv4, "74.125.19.99", "2001:4860:0:2001::68", "must be a valid IPv4 address"},
+		{"IPv6", IPv6, "2001:4860:0:2001::68", "74.125.19.99", "must be a valid IPv6 address"},
+		{"MAC", MAC, "0123.4567.89ab", "74.125.19.99", "must be a valid MAC address"},
+		{"Subdomain", Subdomain, "example-subdomain", "example.com", "must be a valid subdomain"},
+		{"Domain", Domain, "example-domain.com", "localhost", "must be a valid domain"},
+		{"Domain", Domain, "example-domain.com", strings.Repeat("a", 256), "must be a valid domain"},
+		{"DNSName", DNSName, "example.com", "abc%", "must be a valid DNS name"},
+		{"Host", Host, "example.com", "abc%", "must be a valid IP address or DNS name"},
+		{"Port", Port, "123", "99999", "must be a valid port number"},
+		{"Latitude", Latitude, "23.123", "100", "must be a valid latitude"},
+		{"Longitude", Longitude, "123.123", "abc", "must be a valid longitude"},
+		{"SSN", SSN, "100-00-1000", "100-0001000", "must be a valid social security number"},
+		{"Semver", Semver, "1.0.0", "1.0.0.0", "must be a valid semantic version"},
+		{"ISBN", ISBN, "1-61729-085-8", "1-61729-085-81", "must be a valid ISBN"},
+		{"ISBN10", ISBN10, "1-61729-085-8", "1-61729-085-81", "must be a valid ISBN-10"},
+		{"ISBN13", ISBN13, "978-4-87311-368-5", "978-4-87311-368-a", "must be a valid ISBN-13"},
+		{"UUID", UUID, "a987fbc9-4bed-3078-cf07-9141ba07c9f1", "a987fbc9-4bed-3078-cf07-9141ba07c9f3a", "must be a valid UUID"},
+		{"UUIDv3", UUIDv3, "b987fbc9-4bed-3078-cf07-9141ba07c9f3", "b987fbc9-4bed-4078-cf07-9141ba07c9f3", "must be a valid UUID v3"},
+		{"UUIDv4", UUIDv4, "57b73598-8764-4ad0-a76a-679bb6640eb1", "b987fbc9-4bed-3078-cf07-9141ba07c9f3", "must be a valid UUID v4"},
+		{"UUIDv5", UUIDv5, "987fbc97-4bed-5078-af07-9141ba07c9f3", "b987fbc9-4bed-3078-cf07-9141ba07c9f3", "must be a valid UUID v5"},
+		{"MongoID", MongoID, "507f1f77bcf86cd799439011", "507f1f77bcf86cd79943901", "must be a valid hex-encoded MongoDB ObjectId"},
+		{"CreditCard", CreditCard, "375556917985515", "375556917985516", "must be a valid credit card number"},
+		{"JSON", JSON, "[1, 2]", "[1, 2,]", "must be in valid JSON format"},
+		{"ASCII", ASCII, "abc", "aabc", "must contain ASCII characters only"},
+		{"PrintableASCII", PrintableASCII, "abc", "aabc", "must contain printable ASCII characters only"},
+		{"E164", E164, "+8618964826225", "+0001", "must be a valid E164 number"},
+		{"CountryCode2", CountryCode2, "US", "XY", "must be a valid two-letter country code"},
+		{"CountryCode3", CountryCode3, "USA", "XYZ", "must be a valid three-letter country code"},
+		{"CurrencyCode", CurrencyCode, "USD", "USS", "must be valid ISO 4217 currency code"},
+		{"DialString", DialString, "localhost.local:1", "localhost.loc:100000", "must be a valid dial string"},
+		{"DataURI", DataURI, "data:image/png;base64,TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdC4=", "image/gif;base64,U3VzcGVuZGlzc2UgbGVjdHVzIGxlbw==", "must be a Base64-encoded data URI"},
+		{"Base64", Base64, "TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdC4=", "image", "must be encoded in Base64"},
+		{"Multibyte", Multibyte, "abc", "abc", "must contain multibyte characters"},
+		{"FullWidth", FullWidth, "3ー0", "abc", "must contain full-width characters"},
+		{"HalfWidth", HalfWidth, "abc123い", "0011", "must contain half-width characters"},
+		{"VariableWidth", VariableWidth, "3ー0123", "abc", "must contain both full-width and half-width characters"},
+		{"Hexadecimal", Hexadecimal, "FEF", "FTF", "must be a valid hexadecimal number"},
+		{"HexColor", HexColor, "F00", "FTF", "must be a valid hexadecimal color code"},
+		{"RGBColor", RGBColor, "rgb(100, 200, 1)", "abc", "must be a valid RGB color code"},
+		{"Int", Int, "100", "1.1", "must be an integer number"},
+		{"Float", Float, "1.1", "a.1", "must be a floating point number"},
+		{"VariableWidth", VariableWidth, "", "", ""},
+	}
+
+	for _, test := range tests {
+		err := test.rule.Validate("")
+		assert.Nil(t, err, test.tag)
+		err = test.rule.Validate(test.valid)
+		assert.Nil(t, err, test.tag)
+		err = test.rule.Validate(&test.valid)
+		assert.Nil(t, err, test.tag)
+		err = test.rule.Validate(test.invalid)
+		assertError(t, test.err, err, test.tag)
+		err = test.rule.Validate(&test.invalid)
+		assertError(t, test.err, err, test.tag)
+	}
+}
+
+func assertError(t *testing.T, expected string, err error, tag string) {
+	if expected == "" {
+		assert.Nil(t, err, tag)
+	} else if assert.NotNil(t, err, tag) {
+		assert.Equal(t, expected, err.Error(), tag)
+	}
+}

+ 144 - 0
pkg/valid/map.go

@@ -0,0 +1,144 @@
+package valid
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"reflect"
+)
+
+var (
+	// ErrNotMap is the error that the value being validated is not a map.
+	ErrNotMap = errors.New("only a map can be validated")
+
+	// ErrKeyWrongType is the error returned in case of an incorrect key type.
+	ErrKeyWrongType = NewError("validation_key_wrong_type", "key not the correct type")
+
+	// ErrKeyMissing is the error returned in case of a missing key.
+	ErrKeyMissing = NewError("validation_key_missing", "required key is missing")
+
+	// ErrKeyUnexpected is the error returned in case of an unexpected key.
+	ErrKeyUnexpected = NewError("validation_key_unexpected", "key not expected")
+)
+
+type (
+	// MapRule represents a rule set associated with a map.
+	MapRule struct {
+		keys           []*KeyRules
+		allowExtraKeys bool
+	}
+
+	// KeyRules represents a rule set associated with a map key.
+	KeyRules struct {
+		key      interface{}
+		optional bool
+		rules    []Rule
+	}
+)
+
+// Map returns a validation rule that checks the keys and values of a map.
+// This rule should only be used for validating maps, or a validation error will be reported.
+// Use Key() to specify map keys that need to be validated. Each Key() call specifies a single key which can
+// be associated with multiple rules.
+// For example,
+//    validation.Map(
+//        validation.Key("Name", validation.Required),
+//        validation.Key("Value", validation.Required, validation.Length(5, 10)),
+//    )
+//
+// A nil value is considered valid. Use the Required rule to make sure a map value is present.
+func Map(keys ...*KeyRules) MapRule {
+	return MapRule{keys: keys}
+}
+
+// AllowExtraKeys configures the rule to ignore extra keys.
+func (r MapRule) AllowExtraKeys() MapRule {
+	r.allowExtraKeys = true
+	return r
+}
+
+// Validate checks if the given value is valid or not.
+func (r MapRule) Validate(m interface{}) error {
+	return r.ValidateWithContext(nil, m)
+}
+
+// ValidateWithContext checks if the given value is valid or not.
+func (r MapRule) ValidateWithContext(ctx context.Context, m interface{}) error {
+	value := reflect.ValueOf(m)
+	if value.Kind() == reflect.Ptr {
+		value = value.Elem()
+	}
+	if value.Kind() != reflect.Map {
+		// must be a map
+		return NewInternalError(ErrNotMap)
+	}
+	if value.IsNil() {
+		// treat a nil map as valid
+		return nil
+	}
+
+	errs := Errors{}
+	kt := value.Type().Key()
+
+	var extraKeys map[interface{}]bool
+	if !r.allowExtraKeys {
+		extraKeys = make(map[interface{}]bool, value.Len())
+		for _, k := range value.MapKeys() {
+			extraKeys[k.Interface()] = true
+		}
+	}
+
+	for _, kr := range r.keys {
+		var err error
+		if kv := reflect.ValueOf(kr.key); !kt.AssignableTo(kv.Type()) {
+			err = ErrKeyWrongType
+		} else if vv := value.MapIndex(kv); !vv.IsValid() {
+			if !kr.optional {
+				err = ErrKeyMissing
+			}
+		} else if ctx == nil {
+			err = Validate(vv.Interface(), kr.rules...)
+		} else {
+			err = ValidateWithContext(ctx, vv.Interface(), kr.rules...)
+		}
+		if err != nil {
+			if ie, ok := err.(InternalError); ok && ie.InternalError() != nil {
+				return err
+			}
+			errs[getErrorKeyName(kr.key)] = err
+		}
+		if !r.allowExtraKeys {
+			delete(extraKeys, kr.key)
+		}
+	}
+
+	if !r.allowExtraKeys {
+		for key := range extraKeys {
+			errs[getErrorKeyName(key)] = ErrKeyUnexpected
+		}
+	}
+
+	if len(errs) > 0 {
+		return errs
+	}
+	return nil
+}
+
+// Key specifies a map key and the corresponding validation rules.
+func Key(key interface{}, rules ...Rule) *KeyRules {
+	return &KeyRules{
+		key:   key,
+		rules: rules,
+	}
+}
+
+// Optional configures the rule to ignore the key if missing.
+func (r *KeyRules) Optional() *KeyRules {
+	r.optional = true
+	return r
+}
+
+// getErrorKeyName returns the name that should be used to represent the validation error of a map key.
+func getErrorKeyName(key interface{}) string {
+	return fmt.Sprintf("%v", key)
+}

+ 119 - 0
pkg/valid/map_test.go

@@ -0,0 +1,119 @@
+package valid
+
+import (
+	"context"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestMap(t *testing.T) {
+	var m0 map[string]interface{}
+	m1 := map[string]interface{}{"A": "abc", "B": "xyz", "c": "abc", "D": (*string)(nil), "F": (*String123)(nil), "H": []string{"abc", "abc"}, "I": map[string]string{"foo": "abc"}}
+	m2 := map[string]interface{}{"E": String123("xyz"), "F": (*String123)(nil)}
+	m3 := map[string]interface{}{"M3": Model3{}}
+	m4 := map[string]interface{}{"M3": Model3{A: "abc"}}
+	m5 := map[string]interface{}{"A": "internal", "B": ""}
+	m6 := map[int]string{11: "abc", 22: "xyz"}
+	tests := []struct {
+		tag   string
+		model interface{}
+		rules []*KeyRules
+		err   string
+	}{
+		// empty rules
+		{"t1.1", m1, []*KeyRules{}, ""},
+		{"t1.2", m1, []*KeyRules{Key("A"), Key("B")}, ""},
+		// normal rules
+		{"t2.1", m1, []*KeyRules{Key("A", &validateAbc{}), Key("B", &validateXyz{})}, ""},
+		{"t2.2", m1, []*KeyRules{Key("A", &validateXyz{}), Key("B", &validateAbc{})}, "A: error xyz; B: error abc."},
+		{"t2.3", m1, []*KeyRules{Key("A", &validateXyz{}), Key("c", &validateXyz{})}, "A: error xyz; c: error xyz."},
+		{"t2.4", m1, []*KeyRules{Key("D", Length(0, 5))}, ""},
+		{"t2.5", m1, []*KeyRules{Key("F", Length(0, 5))}, ""},
+		{"t2.6", m1, []*KeyRules{Key("H", Each(&validateAbc{})), Key("I", Each(&validateAbc{}))}, ""},
+		{"t2.7", m1, []*KeyRules{Key("H", Each(&validateXyz{})), Key("I", Each(&validateXyz{}))}, "H: (0: error xyz; 1: error xyz.); I: (foo: error xyz.)."},
+		{"t2.8", m1, []*KeyRules{Key("I", Map(Key("foo", &validateAbc{})))}, ""},
+		{"t2.9", m1, []*KeyRules{Key("I", Map(Key("foo", &validateXyz{})))}, "I: (foo: error xyz.)."},
+		// non-map value
+		{"t3.1", &m1, []*KeyRules{}, ""},
+		{"t3.2", nil, []*KeyRules{}, ErrNotMap.Error()},
+		{"t3.3", m0, []*KeyRules{}, ""},
+		{"t3.4", &m0, []*KeyRules{}, ""},
+		{"t3.5", 123, []*KeyRules{}, ErrNotMap.Error()},
+		// invalid key spec
+		{"t4.1", m1, []*KeyRules{Key(123)}, "123: key not the correct type."},
+		{"t4.2", m1, []*KeyRules{Key("X")}, "X: required key is missing."},
+		{"t4.3", m1, []*KeyRules{Key("X").Optional()}, ""},
+		// non-string keys
+		{"t5.1", m6, []*KeyRules{Key(11, &validateAbc{}), Key(22, &validateXyz{})}, ""},
+		{"t5.2", m6, []*KeyRules{Key(11, &validateXyz{}), Key(22, &validateAbc{})}, "11: error xyz; 22: error abc."},
+		// validatable value
+		{"t6.1", m2, []*KeyRules{Key("E")}, "E: error 123."},
+		{"t6.2", m2, []*KeyRules{Key("E", Skip)}, ""},
+		{"t6.3", m2, []*KeyRules{Key("E", Skip.When(true))}, ""},
+		{"t6.4", m2, []*KeyRules{Key("E", Skip.When(false))}, "E: error 123."},
+		// Required, NotNil
+		{"t7.1", m2, []*KeyRules{Key("F", Required)}, "F: cannot be blank."},
+		{"t7.2", m2, []*KeyRules{Key("F", NotNil)}, "F: is required."},
+		{"t7.3", m2, []*KeyRules{Key("F", Skip, Required)}, ""},
+		{"t7.4", m2, []*KeyRules{Key("F", Skip, NotNil)}, ""},
+		{"t7.5", m2, []*KeyRules{Key("F", Skip.When(true), Required)}, ""},
+		{"t7.6", m2, []*KeyRules{Key("F", Skip.When(true), NotNil)}, ""},
+		{"t7.7", m2, []*KeyRules{Key("F", Skip.When(false), Required)}, "F: cannot be blank."},
+		{"t7.8", m2, []*KeyRules{Key("F", Skip.When(false), NotNil)}, "F: is required."},
+		// validatable structs
+		{"t8.1", m3, []*KeyRules{Key("M3", Skip)}, ""},
+		{"t8.2", m3, []*KeyRules{Key("M3")}, "M3: (A: error abc.)."},
+		{"t8.3", m4, []*KeyRules{Key("M3")}, ""},
+		// internal error
+		{"t9.1", m5, []*KeyRules{Key("A", &validateAbc{}), Key("B", Required), Key("A", &validateInternalError{})}, "error internal"},
+	}
+	for _, test := range tests {
+		err1 := Validate(test.model, Map(test.rules...).AllowExtraKeys())
+		err2 := ValidateWithContext(context.Background(), test.model, Map(test.rules...).AllowExtraKeys())
+		assertError(t, test.err, err1, test.tag)
+		assertError(t, test.err, err2, test.tag)
+	}
+
+	a := map[string]interface{}{"Name": "name", "Value": "demo", "Extra": true}
+	err := Validate(a, Map(
+		Key("Name", Required),
+		Key("Value", Required, Length(5, 10)),
+	))
+	assert.EqualError(t, err, "Extra: key not expected; Value: the length must be between 5 and 10.")
+}
+
+func TestMapWithContext(t *testing.T) {
+	m1 := map[string]interface{}{"A": "abc", "B": "xyz", "c": "abc", "g": "xyz"}
+	m2 := map[string]interface{}{"A": "internal", "B": ""}
+	tests := []struct {
+		tag   string
+		model interface{}
+		rules []*KeyRules
+		err   string
+	}{
+		// normal rules
+		{"t1.1", m1, []*KeyRules{Key("A", &validateContextAbc{}), Key("B", &validateContextXyz{})}, ""},
+		{"t1.2", m1, []*KeyRules{Key("A", &validateContextXyz{}), Key("B", &validateContextAbc{})}, "A: error xyz; B: error abc."},
+		{"t1.3", m1, []*KeyRules{Key("A", &validateContextXyz{}), Key("c", &validateContextXyz{})}, "A: error xyz; c: error xyz."},
+		{"t1.4", m1, []*KeyRules{Key("g", &validateContextAbc{})}, "g: error abc."},
+		// skip rule
+		{"t2.1", m1, []*KeyRules{Key("g", Skip, &validateContextAbc{})}, ""},
+		{"t2.2", m1, []*KeyRules{Key("g", &validateContextAbc{}, Skip)}, "g: error abc."},
+		// internal error
+		{"t3.1", m2, []*KeyRules{Key("A", &validateContextAbc{}), Key("B", Required), Key("A", &validateInternalError{})}, "error internal"},
+	}
+	for _, test := range tests {
+		err := ValidateWithContext(context.Background(), test.model, Map(test.rules...).AllowExtraKeys())
+		assertError(t, test.err, err, test.tag)
+	}
+
+	a := map[string]interface{}{"Name": "name", "Value": "demo", "Extra": true}
+	err := ValidateWithContext(context.Background(), a, Map(
+		Key("Name", Required),
+		Key("Value", Required, Length(5, 10)),
+	))
+	if assert.NotNil(t, err) {
+		assert.Equal(t, "Extra: key not expected; Value: the length must be between 5 and 10.", err.Error())
+	}
+}

+ 25 - 0
pkg/valid/rule.go

@@ -0,0 +1,25 @@
+package valid
+
+var (
+	// ErrNilOrNotEmpty is the error that returns when a value is not nil and is empty.
+	ErrNilOrNotEmpty = NewError("validation_nil_or_not_empty_required", "cannot be blank")
+)
+
+var (
+	// Skip is a special validation rule that indicates all rules following it should be skipped.
+	Skip = skipRule{skip: true}
+)
+
+type skipRule struct {
+	skip bool
+}
+
+func (r skipRule) Validate(interface{}) error {
+	return nil
+}
+
+// When determines if all rules following it should be skipped.
+func (r skipRule) When(condition bool) skipRule {
+	r.skip = condition
+	return r
+}

+ 63 - 0
pkg/valid/rule_absent.go

@@ -0,0 +1,63 @@
+package valid
+
+var (
+	// ErrNil is the error that returns when a value is not nil.
+	ErrNil = NewError("validation_nil", "must be blank")
+	// ErrEmpty is the error that returns when a not nil value is not empty.
+	ErrEmpty = NewError("validation_empty", "must be blank")
+)
+
+// Nil is a validation rule that checks if a value is nil.
+// It is the opposite of NotNil rule
+var Nil = absentRule{condition: true, skipNil: false}
+
+// Empty checks if a not nil value is empty.
+var Empty = absentRule{condition: true, skipNil: true}
+
+type absentRule struct {
+	condition bool
+	err       Error
+	skipNil   bool
+}
+
+// Validate checks if the given value is valid or not.
+func (r absentRule) Validate(value interface{}) error {
+	if r.condition {
+		value, isNil := Indirect(value)
+		if !r.skipNil && !isNil || r.skipNil && !isNil && !IsEmpty(value) {
+			if r.err != nil {
+				return r.err
+			}
+			if r.skipNil {
+				return ErrEmpty
+			}
+			return ErrNil
+		}
+	}
+	return nil
+}
+
+// When sets the condition that determines if the validation should be performed.
+func (r absentRule) When(condition bool) absentRule {
+	r.condition = condition
+	return r
+}
+
+// Error sets the error message for the rule.
+func (r absentRule) Error(message string) absentRule {
+	if r.err == nil {
+		if r.skipNil {
+			r.err = ErrEmpty
+		} else {
+			r.err = ErrNil
+		}
+	}
+	r.err = r.err.SetMessage(message)
+	return r
+}
+
+// ErrorObject sets the error struct for the rule.
+func (r absentRule) ErrorObject(err Error) absentRule {
+	r.err = err
+	return r
+}

+ 100 - 0
pkg/valid/rule_absent_test.go

@@ -0,0 +1,100 @@
+package valid
+
+import (
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestNil(t *testing.T) {
+	s1 := "123"
+	s2 := ""
+	var time1 time.Time
+	tests := []struct {
+		tag   string
+		value interface{}
+		err   string
+	}{
+		{"t1", 123, "must be blank"},
+		{"t2", "", "must be blank"},
+		{"t3", &s1, "must be blank"},
+		{"t4", &s2, "must be blank"},
+		{"t5", nil, ""},
+		{"t6", time1, "must be blank"},
+	}
+
+	for _, test := range tests {
+		r := Nil
+		err := r.Validate(test.value)
+		assertError(t, test.err, err, test.tag)
+	}
+}
+
+func TestEmpty(t *testing.T) {
+	s1 := "123"
+	s2 := ""
+	time1 := time.Now()
+	var time2 time.Time
+	tests := []struct {
+		tag   string
+		value interface{}
+		err   string
+	}{
+		{"t1", 123, "must be blank"},
+		{"t2", "", ""},
+		{"t3", &s1, "must be blank"},
+		{"t4", &s2, ""},
+		{"t5", nil, ""},
+		{"t6", time1, "must be blank"},
+		{"t7", time2, ""},
+	}
+
+	for _, test := range tests {
+		r := Empty
+		err := r.Validate(test.value)
+		assertError(t, test.err, err, test.tag)
+	}
+}
+
+func TestAbsentRule_When(t *testing.T) {
+	r := Nil.When(false)
+	err := Validate(42, r)
+	assert.Nil(t, err)
+
+	r = Nil.When(true)
+	err = Validate(42, r)
+	assert.Equal(t, ErrNil, err)
+}
+
+func Test_absentRule_Error(t *testing.T) {
+	r := Nil
+	assert.Equal(t, "must be blank", r.Validate("42").Error())
+	assert.False(t, r.skipNil)
+	r2 := r.Error("123")
+	assert.Equal(t, "must be blank", r.Validate("42").Error())
+	assert.False(t, r.skipNil)
+	assert.Equal(t, "123", r2.err.Message())
+	assert.False(t, r2.skipNil)
+
+	r = Empty
+	assert.Equal(t, "must be blank", r.Validate("42").Error())
+	assert.True(t, r.skipNil)
+	r2 = r.Error("123")
+	assert.Equal(t, "must be blank", r.Validate("42").Error())
+	assert.True(t, r.skipNil)
+	assert.Equal(t, "123", r2.err.Message())
+	assert.True(t, r2.skipNil)
+}
+
+func TestAbsentRule_Error(t *testing.T) {
+	r := Nil
+
+	err := NewError("code", "abc")
+	r = r.ErrorObject(err)
+
+	assert.Equal(t, err, r.err)
+	assert.Equal(t, err.Code(), r.err.Code())
+	assert.Equal(t, err.Message(), r.err.Message())
+	assert.NotEqual(t, err, Nil.err)
+}

+ 98 - 0
pkg/valid/rule_date.go

@@ -0,0 +1,98 @@
+package valid
+
+import (
+	"time"
+)
+
+var (
+	// ErrDateInvalid is the error that returns in case of an invalid date.
+	ErrDateInvalid = NewError("validation_date_invalid", "must be a valid date")
+	// ErrDateOutOfRange is the error that returns in case of an invalid date.
+	ErrDateOutOfRange = NewError("validation_date_out_of_range", "the date is out of range")
+)
+
+// DateRule is a validation rule that validates date/time string values.
+type DateRule struct {
+	layout        string
+	min, max      time.Time
+	err, rangeErr Error
+}
+
+// Date returns a validation rule that checks if a string value is in a format that can be parsed into a date.
+// The format of the date should be specified as the layout parameter which accepts the same value as that for time.Parse.
+// For example,
+//    validation.Date(time.ANSIC)
+//    validation.Date("02 Jan 06 15:04 MST")
+//    validation.Date("2006-01-02")
+//
+// By calling Min() and/or Max(), you can let the Date rule to check if a parsed date value is within
+// the specified date range.
+//
+// An empty value is considered valid. Use the Required rule to make sure a value is not empty.
+func Date(layout string) DateRule {
+	return DateRule{
+		layout:   layout,
+		err:      ErrDateInvalid,
+		rangeErr: ErrDateOutOfRange,
+	}
+}
+
+// Error sets the error message that is used when the value being validated is not a valid date.
+func (r DateRule) Error(message string) DateRule {
+	r.err = r.err.SetMessage(message)
+	return r
+}
+
+// ErrorObject sets the error struct that is used when the value being validated is not a valid date..
+func (r DateRule) ErrorObject(err Error) DateRule {
+	r.err = err
+	return r
+}
+
+// RangeError sets the error message that is used when the value being validated is out of the specified Min/Max date range.
+func (r DateRule) RangeError(message string) DateRule {
+	r.rangeErr = r.rangeErr.SetMessage(message)
+	return r
+}
+
+// RangeErrorObject sets the error struct that is used when the value being validated is out of the specified Min/Max date range.
+func (r DateRule) RangeErrorObject(err Error) DateRule {
+	r.rangeErr = err
+	return r
+}
+
+// Min sets the minimum date range. A zero value means skipping the minimum range validation.
+func (r DateRule) Min(min time.Time) DateRule {
+	r.min = min
+	return r
+}
+
+// Max sets the maximum date range. A zero value means skipping the maximum range validation.
+func (r DateRule) Max(max time.Time) DateRule {
+	r.max = max
+	return r
+}
+
+// Validate checks if the given value is a valid date.
+func (r DateRule) Validate(value interface{}) error {
+	value, isNil := Indirect(value)
+	if isNil || IsEmpty(value) {
+		return nil
+	}
+
+	str, err := EnsureString(value)
+	if err != nil {
+		return err
+	}
+
+	date, err := time.Parse(r.layout, str)
+	if err != nil {
+		return r.err
+	}
+
+	if !r.min.IsZero() && r.min.After(date) || !r.max.IsZero() && date.After(r.max) {
+		return r.rangeErr
+	}
+
+	return nil
+}

+ 87 - 0
pkg/valid/rule_date_test.go

@@ -0,0 +1,87 @@
+package valid
+
+import (
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestDate(t *testing.T) {
+	tests := []struct {
+		tag    string
+		layout string
+		value  interface{}
+		err    string
+	}{
+		{"t1", time.ANSIC, "", ""},
+		{"t2", time.ANSIC, "Wed Feb  4 21:00:57 2009", ""},
+		{"t3", time.ANSIC, "Wed Feb  29 21:00:57 2009", "must be a valid date"},
+		{"t4", "2006-01-02", "2009-11-12", ""},
+		{"t5", "2006-01-02", "2009-11-12 21:00:57", "must be a valid date"},
+		{"t6", "2006-01-02", "2009-1-12", "must be a valid date"},
+		{"t7", "2006-01-02", "2009-01-12", ""},
+		{"t8", "2006-01-02", "2009-01-32", "must be a valid date"},
+		{"t9", "2006-01-02", 1, "must be either a string or byte slice"},
+	}
+
+	for _, test := range tests {
+		r := Date(test.layout)
+		err := r.Validate(test.value)
+		assertError(t, test.err, err, test.tag)
+	}
+}
+
+func TestDateRule_Error(t *testing.T) {
+	r := Date(time.RFC3339)
+	assert.Equal(t, "must be a valid date", r.Validate("0001-01-02T15:04:05Z07:00").Error())
+	r2 := r.Min(time.Date(2000, 1, 1, 1, 1, 1, 0, time.UTC))
+	assert.Equal(t, "the date is out of range", r2.Validate("1999-01-02T15:04:05Z").Error())
+	r = r.Error("123")
+	r = r.RangeError("456")
+	assert.Equal(t, "123", r.err.Message())
+	assert.Equal(t, "456", r.rangeErr.Message())
+}
+
+func TestDateRule_ErrorObject(t *testing.T) {
+	r := Date(time.RFC3339)
+	assert.Equal(t, "must be a valid date", r.Validate("0001-01-02T15:04:05Z07:00").Error())
+
+	r = r.ErrorObject(NewError("code", "abc"))
+
+	assert.Equal(t, "code", r.err.Code())
+	assert.Equal(t, "abc", r.Validate("0001-01-02T15:04:05Z07:00").Error())
+
+	r2 := r.Min(time.Date(2000, 1, 1, 1, 1, 1, 0, time.UTC))
+	assert.Equal(t, "the date is out of range", r2.Validate("1999-01-02T15:04:05Z").Error())
+
+	r = r.ErrorObject(NewError("C", "def"))
+	r = r.RangeErrorObject(NewError("D", "123"))
+
+	assert.Equal(t, "C", r.err.Code())
+	assert.Equal(t, "def", r.err.Message())
+	assert.Equal(t, "D", r.rangeErr.Code())
+	assert.Equal(t, "123", r.rangeErr.Message())
+}
+
+func TestDateRule_MinMax(t *testing.T) {
+	r := Date(time.ANSIC)
+	assert.True(t, r.min.IsZero())
+	assert.True(t, r.max.IsZero())
+	r = r.Min(time.Now())
+	assert.False(t, r.min.IsZero())
+	assert.True(t, r.max.IsZero())
+	r = r.Max(time.Now())
+	assert.False(t, r.max.IsZero())
+
+	r2 := Date("2006-01-02").Min(time.Date(2000, 12, 1, 0, 0, 0, 0, time.UTC)).Max(time.Date(2020, 2, 1, 0, 0, 0, 0, time.UTC))
+	assert.Nil(t, r2.Validate("2010-01-02"))
+	err := r2.Validate("1999-01-02")
+	if assert.NotNil(t, err) {
+		assert.Equal(t, "the date is out of range", err.Error())
+	}
+	err = r2.Validate("2021-01-02")
+	if assert.NotNil(t, err) {
+		assert.Equal(t, "the date is out of range", err.Error())
+	}
+}

+ 93 - 0
pkg/valid/rule_each.go

@@ -0,0 +1,93 @@
+package valid
+
+import (
+	"context"
+	"errors"
+	"reflect"
+	"strconv"
+)
+
+// Each returns a validation rule that loops through an iterable (map, slice or array)
+// and validates each value inside with the provided rules.
+// An empty iterable is considered valid. Use the Required rule to make sure the iterable is not empty.
+func Each(rules ...Rule) EachRule {
+	return EachRule{
+		rules: rules,
+	}
+}
+
+// EachRule is a validation rule that validates elements in a map/slice/array using the specified list of rules.
+type EachRule struct {
+	rules []Rule
+}
+
+// Validate loops through the given iterable and calls the Ozzo Validate() method for each value.
+func (r EachRule) Validate(value interface{}) error {
+	return r.ValidateWithContext(nil, value)
+}
+
+// ValidateWithContext loops through the given iterable and calls the Ozzo ValidateWithContext() method for each value.
+func (r EachRule) ValidateWithContext(ctx context.Context, value interface{}) error {
+	errs := Errors{}
+
+	v := reflect.ValueOf(value)
+	switch v.Kind() {
+	case reflect.Map:
+		for _, k := range v.MapKeys() {
+			val := r.getInterface(v.MapIndex(k))
+			var err error
+			if ctx == nil {
+				err = Validate(val, r.rules...)
+			} else {
+				err = ValidateWithContext(ctx, val, r.rules...)
+			}
+			if err != nil {
+				errs[r.getString(k)] = err
+			}
+		}
+	case reflect.Slice, reflect.Array:
+		for i := 0; i < v.Len(); i++ {
+			val := r.getInterface(v.Index(i))
+			var err error
+			if ctx == nil {
+				err = Validate(val, r.rules...)
+			} else {
+				err = ValidateWithContext(ctx, val, r.rules...)
+			}
+			if err != nil {
+				errs[strconv.Itoa(i)] = err
+			}
+		}
+	default:
+		return errors.New("must be an iterable (map, slice or array)")
+	}
+
+	if len(errs) > 0 {
+		return errs
+	}
+	return nil
+}
+
+func (r EachRule) getInterface(value reflect.Value) interface{} {
+	switch value.Kind() {
+	case reflect.Ptr, reflect.Interface:
+		if value.IsNil() {
+			return nil
+		}
+		return value.Elem().Interface()
+	default:
+		return value.Interface()
+	}
+}
+
+func (r EachRule) getString(value reflect.Value) string {
+	switch value.Kind() {
+	case reflect.Ptr, reflect.Interface:
+		if value.IsNil() {
+			return ""
+		}
+		return value.Elem().String()
+	default:
+		return value.String()
+	}
+}

+ 74 - 0
pkg/valid/rule_each_test.go

@@ -0,0 +1,74 @@
+package valid
+
+import (
+	"context"
+	"errors"
+	"strings"
+	"testing"
+)
+
+func TestEach(t *testing.T) {
+	var a *int
+	var f = func(v string) string { return v }
+	var c0 chan int
+	c1 := make(chan int)
+
+	tests := []struct {
+		tag   string
+		value interface{}
+		err   string
+	}{
+		{"t1", nil, "must be an iterable (map, slice or array)"},
+		{"t2", map[string]string{}, ""},
+		{"t3", map[string]string{"key1": "value1", "key2": "value2"}, ""},
+		{"t4", map[string]string{"key1": "", "key2": "value2", "key3": ""}, "key1: cannot be blank; key3: cannot be blank."},
+		{"t5", map[string]map[string]string{"key1": {"key1.1": "value1"}, "key2": {"key2.1": "value1"}}, ""},
+		{"t6", map[string]map[string]string{"": nil}, ": cannot be blank."},
+		{"t7", map[interface{}]interface{}{}, ""},
+		{"t8", map[interface{}]interface{}{"key1": struct{ foo string }{"foo"}}, ""},
+		{"t9", map[interface{}]interface{}{nil: "", "": "", "key1": nil}, ": cannot be blank; key1: cannot be blank."},
+		{"t10", []string{"value1", "value2", "value3"}, ""},
+		{"t11", []string{"", "value2", ""}, "0: cannot be blank; 2: cannot be blank."},
+		{"t12", []interface{}{struct{ foo string }{"foo"}}, ""},
+		{"t13", []interface{}{nil, a}, "0: cannot be blank; 1: cannot be blank."},
+		{"t14", []interface{}{c0, c1, f}, "0: cannot be blank."},
+	}
+
+	for _, test := range tests {
+		r := Each(Required)
+		err := r.Validate(test.value)
+		assertError(t, test.err, err, test.tag)
+	}
+}
+
+func TestEachWithContext(t *testing.T) {
+	rule := Each(WithContext(func(ctx context.Context, value interface{}) error {
+		if !strings.Contains(value.(string), ctx.Value("contains").(string)) {
+			return errors.New("unexpected value")
+		}
+		return nil
+	}))
+	ctx1 := context.WithValue(context.Background(), "contains", "abc")
+	ctx2 := context.WithValue(context.Background(), "contains", "xyz")
+
+	tests := []struct {
+		tag   string
+		value interface{}
+		ctx   context.Context
+		err   string
+	}{
+		{"t1.1", map[string]string{"key": "abc"}, ctx1, ""},
+		{"t1.2", map[string]string{"key": "abc"}, ctx2, "key: unexpected value."},
+		{"t1.3", map[string]string{"key": "xyz"}, ctx1, "key: unexpected value."},
+		{"t1.4", map[string]string{"key": "xyz"}, ctx2, ""},
+		{"t1.5", []string{"abc"}, ctx1, ""},
+		{"t1.6", []string{"abc"}, ctx2, "0: unexpected value."},
+		{"t1.7", []string{"xyz"}, ctx1, "0: unexpected value."},
+		{"t1.8", []string{"xyz"}, ctx2, ""},
+	}
+
+	for _, test := range tests {
+		err := ValidateWithContext(test.ctx, test.value, rule)
+		assertError(t, test.err, err, test.tag)
+	}
+}

+ 51 - 0
pkg/valid/rule_in.go

@@ -0,0 +1,51 @@
+package valid
+
+import "reflect"
+
+// ErrInInvalid is the error that returns in case of an invalid value for "in" rule.
+var ErrInInvalid = NewError("validation_in_invalid", "must be a valid value")
+
+// In returns a validation rule that checks if a value can be found in the given list of values.
+// reflect.DeepEqual() will be used to determine if two values are equal.
+// For more details please refer to https://golang.org/pkg/reflect/#DeepEqual
+// An empty value is considered valid. Use the Required rule to make sure a value is not empty.
+func In(values ...interface{}) InRule {
+	return InRule{
+		elements: values,
+		err:      ErrInInvalid,
+	}
+}
+
+// InRule is a validation rule that validates if a value can be found in the given list of values.
+type InRule struct {
+	elements []interface{}
+	err      Error
+}
+
+// Validate checks if the given value is valid or not.
+func (r InRule) Validate(value interface{}) error {
+	value, isNil := Indirect(value)
+	if isNil || IsEmpty(value) {
+		return nil
+	}
+
+	for _, e := range r.elements {
+		if reflect.DeepEqual(e, value) {
+			return nil
+		}
+	}
+
+	return r.err
+}
+
+// Error sets the error message for the rule.
+func (r InRule) Error(message string) InRule {
+	r.err = r.err.SetMessage(message)
+	return r
+}
+
+// ErrorObject sets the error struct for the rule.
+func (r InRule) ErrorObject(err Error) InRule {
+	r.err = err
+	return r
+}

+ 53 - 0
pkg/valid/rule_in_test.go

@@ -0,0 +1,53 @@
+package valid
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestIn(t *testing.T) {
+	var v = 1
+	var v2 *int
+	tests := []struct {
+		tag    string
+		values []interface{}
+		value  interface{}
+		err    string
+	}{
+		{"t0", []interface{}{1, 2}, 0, ""},
+		{"t1", []interface{}{1, 2}, 1, ""},
+		{"t2", []interface{}{1, 2}, 2, ""},
+		{"t3", []interface{}{1, 2}, 3, "must be a valid value"},
+		{"t4", []interface{}{}, 3, "must be a valid value"},
+		{"t5", []interface{}{1, 2}, "1", "must be a valid value"},
+		{"t6", []interface{}{1, 2}, &v, ""},
+		{"t7", []interface{}{1, 2}, v2, ""},
+		{"t8", []interface{}{[]byte{1}, 1, 2}, []byte{1}, ""},
+	}
+
+	for _, test := range tests {
+		r := In(test.values...)
+		err := r.Validate(test.value)
+		assertError(t, test.err, err, test.tag)
+	}
+}
+
+func Test_InRule_Error(t *testing.T) {
+	r := In(1, 2, 3)
+	val := 4
+	assert.Equal(t, "must be a valid value", r.Validate(&val).Error())
+	r = r.Error("123")
+	assert.Equal(t, "123", r.err.Message())
+}
+
+func TestInRule_ErrorObject(t *testing.T) {
+	r := In(1, 2, 3)
+
+	err := NewError("code", "abc")
+	r = r.ErrorObject(err)
+
+	assert.Equal(t, err, r.err)
+	assert.Equal(t, err.Code(), r.err.Code())
+	assert.Equal(t, err.Message(), r.err.Message())
+}

+ 100 - 0
pkg/valid/rule_length.go

@@ -0,0 +1,100 @@
+package valid
+
+import (
+	"unicode/utf8"
+)
+
+var (
+	// ErrLengthTooLong is the error that returns in case of too long length.
+	ErrLengthTooLong = NewError("validation_length_too_long", "the length must be no more than {{.max}}")
+	// ErrLengthTooShort is the error that returns in case of too short length.
+	ErrLengthTooShort = NewError("validation_length_too_short", "the length must be no less than {{.min}}")
+	// ErrLengthInvalid is the error that returns in case of an invalid length.
+	ErrLengthInvalid = NewError("validation_length_invalid", "the length must be exactly {{.min}}")
+	// ErrLengthOutOfRange is the error that returns in case of out of range length.
+	ErrLengthOutOfRange = NewError("validation_length_out_of_range", "the length must be between {{.min}} and {{.max}}")
+	// ErrLengthEmptyRequired is the error that returns in case of non-empty value.
+	ErrLengthEmptyRequired = NewError("validation_length_empty_required", "the value must be empty")
+)
+
+// Length returns a validation rule that checks if a value's length is within the specified range.
+// If max is 0, it means there is no upper bound for the length.
+// This rule should only be used for validating strings, slices, maps, and arrays.
+// An empty value is considered valid. Use the Required rule to make sure a value is not empty.
+func Length(min, max int) LengthRule {
+	return LengthRule{min: min, max: max, err: buildLengthRuleError(min, max)}
+}
+
+// RuneLength returns a validation rule that checks if a string's rune length is within the specified range.
+// If max is 0, it means there is no upper bound for the length.
+// This rule should only be used for validating strings, slices, maps, and arrays.
+// An empty value is considered valid. Use the Required rule to make sure a value is not empty.
+// If the value being validated is not a string, the rule works the same as Length.
+func RuneLength(min, max int) LengthRule {
+	r := Length(min, max)
+	r.rune = true
+
+	return r
+}
+
+// LengthRule is a validation rule that checks if a value's length is within the specified range.
+type LengthRule struct {
+	err Error
+
+	min, max int
+	rune     bool
+}
+
+// Validate checks if the given value is valid or not.
+func (r LengthRule) Validate(value interface{}) error {
+	value, isNil := Indirect(value)
+	if isNil || IsEmpty(value) {
+		return nil
+	}
+
+	var (
+		l   int
+		err error
+	)
+	if s, ok := value.(string); ok && r.rune {
+		l = utf8.RuneCountInString(s)
+	} else if l, err = LengthOfValue(value); err != nil {
+		return err
+	}
+
+	if r.min > 0 && l < r.min || r.max > 0 && l > r.max || r.min == 0 && r.max == 0 && l > 0 {
+		return r.err
+	}
+
+	return nil
+}
+
+// Error sets the error message for the rule.
+func (r LengthRule) Error(message string) LengthRule {
+	r.err = r.err.SetMessage(message)
+	return r
+}
+
+// ErrorObject sets the error struct for the rule.
+func (r LengthRule) ErrorObject(err Error) LengthRule {
+	r.err = err
+	return r
+}
+
+func buildLengthRuleError(min, max int) (err Error) {
+	if min == 0 && max > 0 {
+		err = ErrLengthTooLong
+	} else if min > 0 && max == 0 {
+		err = ErrLengthTooShort
+	} else if min > 0 && max > 0 {
+		if min == max {
+			err = ErrLengthInvalid
+		} else {
+			err = ErrLengthOutOfRange
+		}
+	} else {
+		err = ErrLengthEmptyRequired
+	}
+
+	return err.SetParams(map[string]interface{}{"min": min, "max": max})
+}

+ 103 - 0
pkg/valid/rule_length_test.go

@@ -0,0 +1,103 @@
+package valid
+
+import (
+	"database/sql"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestLength(t *testing.T) {
+	var v *string
+	tests := []struct {
+		tag      string
+		min, max int
+		value    interface{}
+		err      string
+	}{
+		{"t1", 2, 4, "abc", ""},
+		{"t2", 2, 4, "", ""},
+		{"t3", 2, 4, "abcdf", "the length must be between 2 and 4"},
+		{"t4", 0, 4, "ab", ""},
+		{"t5", 0, 4, "abcde", "the length must be no more than 4"},
+		{"t6", 2, 0, "ab", ""},
+		{"t7", 2, 0, "a", "the length must be no less than 2"},
+		{"t8", 2, 0, v, ""},
+		{"t9", 2, 0, 123, "cannot get the length of int"},
+		{"t10", 2, 4, sql.NullString{String: "abc", Valid: true}, ""},
+		{"t11", 2, 4, sql.NullString{String: "", Valid: true}, ""},
+		{"t12", 2, 4, &sql.NullString{String: "abc", Valid: true}, ""},
+		{"t13", 2, 2, "abcdf", "the length must be exactly 2"},
+		{"t14", 2, 2, "ab", ""},
+		{"t15", 0, 0, "", ""},
+		{"t16", 0, 0, "ab", "the value must be empty"},
+	}
+
+	for _, test := range tests {
+		r := Length(test.min, test.max)
+		err := r.Validate(test.value)
+		assertError(t, test.err, err, test.tag)
+	}
+}
+
+func TestRuneLength(t *testing.T) {
+	var v *string
+	tests := []struct {
+		tag      string
+		min, max int
+		value    interface{}
+		err      string
+	}{
+		{"t1", 2, 4, "abc", ""},
+		{"t1.1", 2, 3, "💥💥", ""},
+		{"t1.2", 2, 3, "💥💥💥", ""},
+		{"t1.3", 2, 3, "💥", "the length must be between 2 and 3"},
+		{"t1.4", 2, 3, "💥💥💥💥", "the length must be between 2 and 3"},
+		{"t2", 2, 4, "", ""},
+		{"t3", 2, 4, "abcdf", "the length must be between 2 and 4"},
+		{"t4", 0, 4, "ab", ""},
+		{"t5", 0, 4, "abcde", "the length must be no more than 4"},
+		{"t6", 2, 0, "ab", ""},
+		{"t7", 2, 0, "a", "the length must be no less than 2"},
+		{"t8", 2, 0, v, ""},
+		{"t9", 2, 0, 123, "cannot get the length of int"},
+		{"t10", 2, 4, sql.NullString{String: "abc", Valid: true}, ""},
+		{"t11", 2, 4, sql.NullString{String: "", Valid: true}, ""},
+		{"t12", 2, 4, &sql.NullString{String: "abc", Valid: true}, ""},
+		{"t13", 2, 3, &sql.NullString{String: "💥💥", Valid: true}, ""},
+		{"t14", 2, 3, &sql.NullString{String: "💥", Valid: true}, "the length must be between 2 and 3"},
+	}
+
+	for _, test := range tests {
+		r := RuneLength(test.min, test.max)
+		err := r.Validate(test.value)
+		assertError(t, test.err, err, test.tag)
+	}
+}
+
+func Test_LengthRule_Error(t *testing.T) {
+	r := Length(10, 20)
+	assert.Equal(t, "the length must be between 10 and 20", r.Validate("abc").Error())
+
+	r = Length(0, 20)
+	assert.Equal(t, "the length must be no more than 20", r.Validate(make([]string, 21)).Error())
+
+	r = Length(10, 0)
+	assert.Equal(t, "the length must be no less than 10", r.Validate([9]string{}).Error())
+
+	r = Length(0, 0)
+	assert.Equal(t, "validation_length_empty_required", r.err.Code())
+
+	r = r.Error("123")
+	assert.Equal(t, "123", r.err.Message())
+}
+
+func TestLengthRule_ErrorObject(t *testing.T) {
+	r := Length(10, 20)
+	err := NewError("code", "abc")
+	r = r.ErrorObject(err)
+
+	assert.Equal(t, err, r.err)
+	assert.Equal(t, err.Code(), r.err.Code())
+	assert.Equal(t, err.Message(), r.err.Message())
+}

+ 50 - 0
pkg/valid/rule_match.go

@@ -0,0 +1,50 @@
+package valid
+
+import "regexp"
+
+// ErrMatchInvalid is the error that returns in case of invalid format.
+var ErrMatchInvalid = NewError("validation_match_invalid", "must be in a valid format")
+
+// Match returns a validation rule that checks if a value matches the specified regular expression.
+// This rule should only be used for validating strings and byte slices, or a validation error will be reported.
+// An empty value is considered valid. Use the Required rule to make sure a value is not empty.
+func Match(re *regexp.Regexp) MatchRule {
+	return MatchRule{
+		re:  re,
+		err: ErrMatchInvalid,
+	}
+}
+
+// MatchRule is a validation rule that checks if a value matches the specified regular expression.
+type MatchRule struct {
+	re  *regexp.Regexp
+	err Error
+}
+
+// Validate checks if the given value is valid or not.
+func (r MatchRule) Validate(value interface{}) error {
+	value, isNil := Indirect(value)
+	if isNil {
+		return nil
+	}
+
+	isString, str, isBytes, bs := StringOrBytes(value)
+	if isString && (str == "" || r.re.MatchString(str)) {
+		return nil
+	} else if isBytes && (len(bs) == 0 || r.re.Match(bs)) {
+		return nil
+	}
+	return r.err
+}
+
+// Error sets the error message for the rule.
+func (r MatchRule) Error(message string) MatchRule {
+	r.err = r.err.SetMessage(message)
+	return r
+}
+
+// ErrorObject sets the error struct for the rule.
+func (r MatchRule) ErrorObject(err Error) MatchRule {
+	r.err = err
+	return r
+}

+ 51 - 0
pkg/valid/rule_match_test.go

@@ -0,0 +1,51 @@
+package valid
+
+import (
+	"regexp"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestMatch(t *testing.T) {
+	var v2 *string
+	tests := []struct {
+		tag   string
+		re    string
+		value interface{}
+		err   string
+	}{
+		{"t1", "[a-z]+", "abc", ""},
+		{"t2", "[a-z]+", "", ""},
+		{"t3", "[a-z]+", v2, ""},
+		{"t4", "[a-z]+", "123", "must be in a valid format"},
+		{"t5", "[a-z]+", []byte("abc"), ""},
+		{"t6", "[a-z]+", []byte("123"), "must be in a valid format"},
+		{"t7", "[a-z]+", []byte(""), ""},
+		{"t8", "[a-z]+", nil, ""},
+	}
+
+	for _, test := range tests {
+		r := Match(regexp.MustCompile(test.re))
+		err := r.Validate(test.value)
+		assertError(t, test.err, err, test.tag)
+	}
+}
+
+func Test_MatchRule_Error(t *testing.T) {
+	r := Match(regexp.MustCompile("[a-z]+"))
+	assert.Equal(t, "must be in a valid format", r.Validate("13").Error())
+	r = r.Error("123")
+	assert.Equal(t, "123", r.err.Message())
+}
+
+func TestMatchRule_ErrorObject(t *testing.T) {
+	r := Match(regexp.MustCompile("[a-z]+"))
+
+	err := NewError("code", "abc")
+	r = r.ErrorObject(err)
+
+	assert.Equal(t, err, r.err)
+	assert.Equal(t, err.Code(), r.err.Code())
+	assert.Equal(t, err.Message(), r.err.Message())
+}

+ 191 - 0
pkg/valid/rule_minmax.go

@@ -0,0 +1,191 @@
+package valid
+
+import (
+	"fmt"
+	"reflect"
+	"time"
+)
+
+var (
+	// ErrMinGreaterEqualThanRequired is the error that returns when a value is less than a specified threshold.
+	ErrMinGreaterEqualThanRequired = NewError("validation_min_greater_equal_than_required", "must be no less than {{.threshold}}")
+	// ErrMaxLessEqualThanRequired is the error that returns when a value is greater than a specified threshold.
+	ErrMaxLessEqualThanRequired = NewError("validation_max_less_equal_than_required", "must be no greater than {{.threshold}}")
+	// ErrMinGreaterThanRequired is the error that returns when a value is less than or equal to a specified threshold.
+	ErrMinGreaterThanRequired = NewError("validation_min_greater_than_required", "must be greater than {{.threshold}}")
+	// ErrMaxLessThanRequired is the error that returns when a value is greater than or equal to a specified threshold.
+	ErrMaxLessThanRequired = NewError("validation_max_less_than_required", "must be less than {{.threshold}}")
+)
+
+// ThresholdRule is a validation rule that checks if a value satisfies the specified threshold requirement.
+type ThresholdRule struct {
+	threshold interface{}
+	operator  int
+	err       Error
+}
+
+const (
+	greaterThan = iota
+	greaterEqualThan
+	lessThan
+	lessEqualThan
+)
+
+// Min returns a validation rule that checks if a value is greater or equal than the specified value.
+// By calling Exclusive, the rule will check if the value is strictly greater than the specified value.
+// Note that the value being checked and the threshold value must be of the same type.
+// Only int, uint, float and time.Time types are supported.
+// An empty value is considered valid. Please use the Required rule to make sure a value is not empty.
+func Min(min interface{}) ThresholdRule {
+	return ThresholdRule{
+		threshold: min,
+		operator:  greaterEqualThan,
+		err:       ErrMinGreaterEqualThanRequired,
+	}
+
+}
+
+// Max returns a validation rule that checks if a value is less or equal than the specified value.
+// By calling Exclusive, the rule will check if the value is strictly less than the specified value.
+// Note that the value being checked and the threshold value must be of the same type.
+// Only int, uint, float and time.Time types are supported.
+// An empty value is considered valid. Please use the Required rule to make sure a value is not empty.
+func Max(max interface{}) ThresholdRule {
+	return ThresholdRule{
+		threshold: max,
+		operator:  lessEqualThan,
+		err:       ErrMaxLessEqualThanRequired,
+	}
+}
+
+// Exclusive sets the comparison to exclude the boundary value.
+func (r ThresholdRule) Exclusive() ThresholdRule {
+	if r.operator == greaterEqualThan {
+		r.operator = greaterThan
+		r.err = ErrMinGreaterThanRequired
+	} else if r.operator == lessEqualThan {
+		r.operator = lessThan
+		r.err = ErrMaxLessThanRequired
+	}
+	return r
+}
+
+// Validate checks if the given value is valid or not.
+func (r ThresholdRule) Validate(value interface{}) error {
+	value, isNil := Indirect(value)
+	if isNil || IsEmpty(value) {
+		return nil
+	}
+
+	rv := reflect.ValueOf(r.threshold)
+	switch rv.Kind() {
+	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+		v, err := ToInt(value)
+		if err != nil {
+			return err
+		}
+		if r.compareInt(rv.Int(), v) {
+			return nil
+		}
+
+	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
+		v, err := ToUint(value)
+		if err != nil {
+			return err
+		}
+		if r.compareUint(rv.Uint(), v) {
+			return nil
+		}
+
+	case reflect.Float32, reflect.Float64:
+		v, err := ToFloat(value)
+		if err != nil {
+			return err
+		}
+		if r.compareFloat(rv.Float(), v) {
+			return nil
+		}
+
+	case reflect.Struct:
+		t, ok := r.threshold.(time.Time)
+		if !ok {
+			return fmt.Errorf("type not supported: %v", rv.Type())
+		}
+		v, ok := value.(time.Time)
+		if !ok {
+			return fmt.Errorf("cannot convert %v to time.Time", reflect.TypeOf(value))
+		}
+		if v.IsZero() || r.compareTime(t, v) {
+			return nil
+		}
+
+	default:
+		return fmt.Errorf("type not supported: %v", rv.Type())
+	}
+
+	return r.err.SetParams(map[string]interface{}{"threshold": r.threshold})
+}
+
+// Error sets the error message for the rule.
+func (r ThresholdRule) Error(message string) ThresholdRule {
+	r.err = r.err.SetMessage(message)
+	return r
+}
+
+// ErrorObject sets the error struct for the rule.
+func (r ThresholdRule) ErrorObject(err Error) ThresholdRule {
+	r.err = err
+	return r
+}
+
+func (r ThresholdRule) compareInt(threshold, value int64) bool {
+	switch r.operator {
+	case greaterThan:
+		return value > threshold
+	case greaterEqualThan:
+		return value >= threshold
+	case lessThan:
+		return value < threshold
+	default:
+		return value <= threshold
+	}
+}
+
+func (r ThresholdRule) compareUint(threshold, value uint64) bool {
+	switch r.operator {
+	case greaterThan:
+		return value > threshold
+	case greaterEqualThan:
+		return value >= threshold
+	case lessThan:
+		return value < threshold
+	default:
+		return value <= threshold
+	}
+}
+
+func (r ThresholdRule) compareFloat(threshold, value float64) bool {
+	switch r.operator {
+	case greaterThan:
+		return value > threshold
+	case greaterEqualThan:
+		return value >= threshold
+	case lessThan:
+		return value < threshold
+	default:
+		return value <= threshold
+	}
+}
+
+func (r ThresholdRule) compareTime(threshold, value time.Time) bool {
+	switch r.operator {
+	case greaterThan:
+		return value.After(threshold)
+	case greaterEqualThan:
+		return value.After(threshold) || value.Equal(threshold)
+	case lessThan:
+		return value.Before(threshold)
+	default:
+		return value.Before(threshold) || value.Equal(threshold)
+	}
+}

+ 144 - 0
pkg/valid/rule_minmax_test.go

@@ -0,0 +1,144 @@
+package valid
+
+import (
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestMin(t *testing.T) {
+	date0 := time.Time{}
+	date20000101 := time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)
+	date20001201 := time.Date(2000, 12, 1, 0, 0, 0, 0, time.UTC)
+	date20000601 := time.Date(2000, 6, 1, 0, 0, 0, 0, time.UTC)
+
+	tests := []struct {
+		tag       string
+		threshold interface{}
+		exclusive bool
+		value     interface{}
+		err       string
+	}{
+		// int cases
+		{"t1.1", 1, false, 1, ""},
+		{"t1.2", 1, false, 2, ""},
+		{"t1.3", 1, false, -1, "must be no less than 1"},
+		{"t1.4", 1, false, 0, ""},
+		{"t1.5", 1, true, 1, "must be greater than 1"},
+		{"t1.6", 1, false, "1", "cannot convert string to int64"},
+		{"t1.7", "1", false, 1, "type not supported: string"},
+		// uint cases
+		{"t2.1", uint(2), false, uint(2), ""},
+		{"t2.2", uint(2), false, uint(3), ""},
+		{"t2.3", uint(2), false, uint(1), "must be no less than 2"},
+		{"t2.4", uint(2), false, uint(0), ""},
+		{"t2.5", uint(2), true, uint(2), "must be greater than 2"},
+		{"t2.6", uint(2), false, "1", "cannot convert string to uint64"},
+		// float cases
+		{"t3.1", float64(2), false, float64(2), ""},
+		{"t3.2", float64(2), false, float64(3), ""},
+		{"t3.3", float64(2), false, float64(1), "must be no less than 2"},
+		{"t3.4", float64(2), false, float64(0), ""},
+		{"t3.5", float64(2), true, float64(2), "must be greater than 2"},
+		{"t3.6", float64(2), false, "1", "cannot convert string to float64"},
+		// Time cases
+		{"t4.1", date20000601, false, date20000601, ""},
+		{"t4.2", date20000601, false, date20001201, ""},
+		{"t4.3", date20000601, false, date20000101, "must be no less than 2000-06-01 00:00:00 +0000 UTC"},
+		{"t4.4", date20000601, false, date0, ""},
+		{"t4.5", date20000601, true, date20000601, "must be greater than 2000-06-01 00:00:00 +0000 UTC"},
+		{"t4.6", date20000601, true, 1, "cannot convert int to time.Time"},
+		{"t4.7", struct{}{}, false, 1, "type not supported: struct {}"},
+		{"t4.8", date0, false, date20000601, ""},
+	}
+
+	for _, test := range tests {
+		r := Min(test.threshold)
+		if test.exclusive {
+			r = r.Exclusive()
+		}
+		err := r.Validate(test.value)
+		assertError(t, test.err, err, test.tag)
+	}
+}
+
+func TestMinError(t *testing.T) {
+	r := Min(10)
+	assert.Equal(t, "must be no less than 10", r.Validate(9).Error())
+
+	r = r.Error("123")
+	assert.Equal(t, "123", r.err.Message())
+}
+
+func TestMax(t *testing.T) {
+	date0 := time.Time{}
+	date20000101 := time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)
+	date20001201 := time.Date(2000, 12, 1, 0, 0, 0, 0, time.UTC)
+	date20000601 := time.Date(2000, 6, 1, 0, 0, 0, 0, time.UTC)
+
+	tests := []struct {
+		tag       string
+		threshold interface{}
+		exclusive bool
+		value     interface{}
+		err       string
+	}{
+		// int cases
+		{"t1.1", 2, false, 2, ""},
+		{"t1.2", 2, false, 1, ""},
+		{"t1.3", 2, false, 3, "must be no greater than 2"},
+		{"t1.4", 2, false, 0, ""},
+		{"t1.5", 2, true, 2, "must be less than 2"},
+		{"t1.6", 2, false, "1", "cannot convert string to int64"},
+		{"t1.7", "1", false, 1, "type not supported: string"},
+		// uint cases
+		{"t2.1", uint(2), false, uint(2), ""},
+		{"t2.2", uint(2), false, uint(1), ""},
+		{"t2.3", uint(2), false, uint(3), "must be no greater than 2"},
+		{"t2.4", uint(2), false, uint(0), ""},
+		{"t2.5", uint(2), true, uint(2), "must be less than 2"},
+		{"t2.6", uint(2), false, "1", "cannot convert string to uint64"},
+		// float cases
+		{"t3.1", float64(2), false, float64(2), ""},
+		{"t3.2", float64(2), false, float64(1), ""},
+		{"t3.3", float64(2), false, float64(3), "must be no greater than 2"},
+		{"t3.4", float64(2), false, float64(0), ""},
+		{"t3.5", float64(2), true, float64(2), "must be less than 2"},
+		{"t3.6", float64(2), false, "1", "cannot convert string to float64"},
+		// Time cases
+		{"t4.1", date20000601, false, date20000601, ""},
+		{"t4.2", date20000601, false, date20000101, ""},
+		{"t4.3", date20000601, false, date20001201, "must be no greater than 2000-06-01 00:00:00 +0000 UTC"},
+		{"t4.4", date20000601, false, date0, ""},
+		{"t4.5", date20000601, true, date20000601, "must be less than 2000-06-01 00:00:00 +0000 UTC"},
+		{"t4.6", date20000601, true, 1, "cannot convert int to time.Time"},
+	}
+
+	for _, test := range tests {
+		r := Max(test.threshold)
+		if test.exclusive {
+			r = r.Exclusive()
+		}
+		err := r.Validate(test.value)
+		assertError(t, test.err, err, test.tag)
+	}
+}
+
+func TestMaxError(t *testing.T) {
+	r := Max(10)
+	assert.Equal(t, "must be no greater than 10", r.Validate(11).Error())
+
+	r = r.Error("123")
+	assert.Equal(t, "123", r.err.Message())
+}
+
+func TestThresholdRule_ErrorObject(t *testing.T) {
+	r := Max(10)
+	err := NewError("code", "abc")
+	r = r.ErrorObject(err)
+
+	assert.Equal(t, err, r.err)
+	assert.Equal(t, err.Code(), r.err.Code())
+	assert.Equal(t, err.Message(), r.err.Message())
+}

+ 47 - 0
pkg/valid/rule_not_in.go

@@ -0,0 +1,47 @@
+package valid
+
+// ErrNotInInvalid is the error that returns when a value is in a list.
+var ErrNotInInvalid = NewError("validation_not_in_invalid", "must not be in list")
+
+// NotIn returns a validation rule that checks if a value is absent from the given list of values.
+// Note that the value being checked and the possible range of values must be of the same type.
+// An empty value is considered valid. Use the Required rule to make sure a value is not empty.
+func NotIn(values ...interface{}) NotInRule {
+	return NotInRule{
+		elements: values,
+		err:      ErrNotInInvalid,
+	}
+}
+
+// NotInRule is a validation rule that checks if a value is absent from the given list of values.
+type NotInRule struct {
+	elements []interface{}
+	err      Error
+}
+
+// Validate checks if the given value is valid or not.
+func (r NotInRule) Validate(value interface{}) error {
+	value, isNil := Indirect(value)
+	if isNil || IsEmpty(value) {
+		return nil
+	}
+
+	for _, e := range r.elements {
+		if e == value {
+			return r.err
+		}
+	}
+	return nil
+}
+
+// Error sets the error message for the rule.
+func (r NotInRule) Error(message string) NotInRule {
+	r.err = r.err.SetMessage(message)
+	return r
+}
+
+// ErrorObject sets the error struct for the rule.
+func (r NotInRule) ErrorObject(err Error) NotInRule {
+	r.err = err
+	return r
+}

+ 51 - 0
pkg/valid/rule_not_in_test.go

@@ -0,0 +1,51 @@
+package valid
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestNotIn(t *testing.T) {
+	v := 1
+	var v2 *int
+	var tests = []struct {
+		tag    string
+		values []interface{}
+		value  interface{}
+		err    string
+	}{
+		{"t0", []interface{}{1, 2}, 0, ""},
+		{"t1", []interface{}{1, 2}, 1, "must not be in list"},
+		{"t2", []interface{}{1, 2}, 2, "must not be in list"},
+		{"t3", []interface{}{1, 2}, 3, ""},
+		{"t4", []interface{}{}, 3, ""},
+		{"t5", []interface{}{1, 2}, "1", ""},
+		{"t6", []interface{}{1, 2}, &v, "must not be in list"},
+		{"t7", []interface{}{1, 2}, v2, ""},
+	}
+
+	for _, test := range tests {
+		r := NotIn(test.values...)
+		err := r.Validate(test.value)
+		assertError(t, test.err, err, test.tag)
+	}
+}
+
+func Test_NotInRule_Error(t *testing.T) {
+	r := NotIn(1, 2, 3)
+	assert.Equal(t, "must not be in list", r.Validate(1).Error())
+	r = r.Error("123")
+	assert.Equal(t, "123", r.err.Message())
+}
+
+func TestNotInRule_ErrorObject(t *testing.T) {
+	r := NotIn(1, 2, 3)
+
+	err := NewError("code", "abc")
+	r = r.ErrorObject(err)
+
+	assert.Equal(t, err, r.err)
+	assert.Equal(t, err.Code(), r.err.Code())
+	assert.Equal(t, err.Message(), r.err.Message())
+}

+ 40 - 0
pkg/valid/rule_not_nil.go

@@ -0,0 +1,40 @@
+package valid
+
+// ErrNotNilRequired is the error that returns when a value is Nil.
+var ErrNotNilRequired = NewError("validation_not_nil_required", "is required")
+
+// NotNil is a validation rule that checks if a value is not nil.
+// NotNil only handles types including interface, pointer, slice, and map.
+// All other types are considered valid.
+var NotNil = notNilRule{}
+
+type notNilRule struct {
+	err Error
+}
+
+// Validate checks if the given value is valid or not.
+func (r notNilRule) Validate(value interface{}) error {
+	_, isNil := Indirect(value)
+	if isNil {
+		if r.err != nil {
+			return r.err
+		}
+		return ErrNotNilRequired
+	}
+	return nil
+}
+
+// Error sets the error message for the rule.
+func (r notNilRule) Error(message string) notNilRule {
+	if r.err == nil {
+		r.err = ErrNotNilRequired
+	}
+	r.err = r.err.SetMessage(message)
+	return r
+}
+
+// ErrorObject sets the error struct for the rule.
+func (r notNilRule) ErrorObject(err Error) notNilRule {
+	r.err = err
+	return r
+}

+ 58 - 0
pkg/valid/rule_not_nil_test.go

@@ -0,0 +1,58 @@
+package valid
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+type MyInterface interface {
+	Hello()
+}
+
+func TestNotNil(t *testing.T) {
+	var v1 []int
+	var v2 map[string]int
+	var v3 *int
+	var v4 interface{}
+	var v5 MyInterface
+	tests := []struct {
+		tag   string
+		value interface{}
+		err   string
+	}{
+		{"t1", v1, "is required"},
+		{"t2", v2, "is required"},
+		{"t3", v3, "is required"},
+		{"t4", v4, "is required"},
+		{"t5", v5, "is required"},
+		{"t6", "", ""},
+		{"t7", 0, ""},
+	}
+
+	for _, test := range tests {
+		r := NotNil
+		err := r.Validate(test.value)
+		assertError(t, test.err, err, test.tag)
+	}
+}
+
+func Test_notNilRule_Error(t *testing.T) {
+	r := NotNil
+	assert.Equal(t, "is required", r.Validate(nil).Error())
+	r2 := r.Error("123")
+	assert.Equal(t, "is required", r.Validate(nil).Error())
+	assert.Equal(t, "123", r2.err.Message())
+}
+
+func TestNotNilRule_ErrorObject(t *testing.T) {
+	r := NotNil
+
+	err := NewError("code", "abc")
+	r = r.ErrorObject(err)
+
+	assert.Equal(t, err, r.err)
+	assert.Equal(t, err.Code(), r.err.Code())
+	assert.Equal(t, err.Message(), r.err.Message())
+	assert.NotEqual(t, err, NotNil.err)
+}

+ 68 - 0
pkg/valid/rule_required.go

@@ -0,0 +1,68 @@
+package valid
+
+var (
+	// Required is a validation rule that checks if a value is not empty.
+	// A value is considered not empty if
+	// - integer, float: not zero
+	// - bool: true
+	// - string, array, slice, map: len() > 0
+	// - interface, pointer: not nil and the referenced value is not empty
+	// - any other types
+	Required = RequiredRule{skipNil: false, condition: true}
+
+	// NilOrNotEmpty checks if a value is a nil pointer or a value that is not empty.
+	// NilOrNotEmpty differs from Required in that it treats a nil pointer as valid.
+	NilOrNotEmpty = RequiredRule{skipNil: true, condition: true}
+
+	// ErrRequired is the error that returns when a value is required.
+	ErrRequired = NewError("validation_required", "cannot be blank")
+)
+
+// RequiredRule is a rule that checks if a value is not empty.
+type RequiredRule struct {
+	condition bool
+	skipNil   bool
+	err       Error
+}
+
+// Validate checks if the given value is valid or not.
+func (r RequiredRule) Validate(value interface{}) error {
+	if r.condition {
+		value, isNil := Indirect(value)
+		if r.skipNil && !isNil && IsEmpty(value) || !r.skipNil && (isNil || IsEmpty(value)) {
+			if r.err != nil {
+				return r.err
+			}
+			if r.skipNil {
+				return ErrNilOrNotEmpty
+			}
+			return ErrRequired
+		}
+	}
+	return nil
+}
+
+// When sets the condition that determines if the validation should be performed.
+func (r RequiredRule) When(condition bool) RequiredRule {
+	r.condition = condition
+	return r
+}
+
+// Error sets the error message for the rule.
+func (r RequiredRule) Error(message string) RequiredRule {
+	if r.err == nil {
+		if r.skipNil {
+			r.err = ErrNilOrNotEmpty
+		} else {
+			r.err = ErrRequired
+		}
+	}
+	r.err = r.err.SetMessage(message)
+	return r
+}
+
+// ErrorObject sets the error struct for the rule.
+func (r RequiredRule) ErrorObject(err Error) RequiredRule {
+	r.err = err
+	return r
+}

+ 96 - 0
pkg/valid/rule_required_test.go

@@ -0,0 +1,96 @@
+package valid
+
+import (
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestRequired(t *testing.T) {
+	s1 := "123"
+	s2 := ""
+	var time1 time.Time
+	tests := []struct {
+		tag   string
+		value interface{}
+		err   string
+	}{
+		{"t1", 123, ""},
+		{"t2", "", "cannot be blank"},
+		{"t3", &s1, ""},
+		{"t4", &s2, "cannot be blank"},
+		{"t5", nil, "cannot be blank"},
+		{"t6", time1, "cannot be blank"},
+	}
+
+	for _, test := range tests {
+		r := Required
+		err := r.Validate(test.value)
+		assertError(t, test.err, err, test.tag)
+	}
+}
+
+func TestRequiredRule_When(t *testing.T) {
+	r := Required.When(false)
+	err := Validate(nil, r)
+	assert.Nil(t, err)
+
+	r = Required.When(true)
+	err = Validate(nil, r)
+	assert.Equal(t, ErrRequired, err)
+}
+
+func TestNilOrNotEmpty(t *testing.T) {
+	s1 := "123"
+	s2 := ""
+	tests := []struct {
+		tag   string
+		value interface{}
+		err   string
+	}{
+		{"t1", 123, ""},
+		{"t2", "", "cannot be blank"},
+		{"t3", &s1, ""},
+		{"t4", &s2, "cannot be blank"},
+		{"t5", nil, ""},
+	}
+
+	for _, test := range tests {
+		r := NilOrNotEmpty
+		err := r.Validate(test.value)
+		assertError(t, test.err, err, test.tag)
+	}
+}
+
+func Test_requiredRule_Error(t *testing.T) {
+	r := Required
+	assert.Equal(t, "cannot be blank", r.Validate(nil).Error())
+	assert.False(t, r.skipNil)
+	r2 := r.Error("123")
+	assert.Equal(t, "cannot be blank", r.Validate(nil).Error())
+	assert.False(t, r.skipNil)
+	assert.Equal(t, "123", r2.err.Message())
+	assert.False(t, r2.skipNil)
+
+	r = NilOrNotEmpty
+	assert.Equal(t, "cannot be blank", r.Validate("").Error())
+	assert.True(t, r.skipNil)
+	r2 = r.Error("123")
+	assert.Equal(t, "cannot be blank", r.Validate("").Error())
+	assert.True(t, r.skipNil)
+	assert.Equal(t, "123", r2.err.Message())
+	assert.True(t, r2.skipNil)
+}
+
+func TestRequiredRule_Error(t *testing.T) {
+	r := Required
+
+	err := NewError("code", "abc")
+	r = r.ErrorObject(err)
+
+	assert.Equal(t, err, r.err)
+	assert.Equal(t, err.Code(), r.err.Code())
+	assert.Equal(t, err.Message(), r.err.Message())
+	assert.NotEqual(t, err, Required.err)
+}

+ 79 - 0
pkg/valid/rule_strings.go

@@ -0,0 +1,79 @@
+package valid
+
+var (
+	// ErrBlankString is the error that returns when a value is an empty string/only whitespaces.
+	ErrBlankString = NewError("validation_required", "cannot be blank")
+)
+
+type stringValidator func(string) bool
+
+// StringRule is a rule that checks a string variable using a specified stringValidator.
+type StringRule struct {
+	validate  stringValidator
+	err       Error
+	denyBlank bool
+}
+
+// NewStringRule creates a new validation rule using a function that takes a string value and returns a bool.
+// The rule returned will use the function to check if a given string or byte slice is valid or not.
+// An empty value is considered to be valid. Please use the Required rule to make sure a value is not empty.
+func NewStringRule(allowBlank bool, validator stringValidator, message string) StringRule {
+	return StringRule{
+		validate:  validator,
+		err:       NewError("", message),
+		denyBlank: !allowBlank,
+	}
+}
+
+// NewStringRuleWithError creates a new validation rule using a function that takes a string value and returns a bool.
+// The rule returned will use the function to check if a given string or byte slice is valid or not.
+// An empty value is considered to be valid. Please use the Required rule to make sure a value is not empty.
+func NewStringRuleWithError(validator stringValidator, err Error) StringRule {
+	return StringRule{
+		validate: validator,
+		err:      err,
+	}
+}
+
+// Error sets the error message for the rule.
+func (r StringRule) Error(message string) StringRule {
+	r.err = r.err.SetMessage(message)
+	return r
+}
+
+// ErrorObject sets the error struct for the rule.
+func (r StringRule) ErrorObject(err Error) StringRule {
+	r.err = err
+	return r
+}
+
+// Validate checks if the given value is valid or not.
+func (r StringRule) Validate(value interface{}) error {
+	value, isNil := Indirect(value)
+	if isNil {
+		return nil
+	}
+
+	str, err := EnsureString(value)
+	if err != nil {
+		return err
+	}
+	if r.denyBlank && IsEmpty(value) {
+		return nil
+	}
+	if !r.denyBlank {
+		if IsEmpty(value) {
+			return nil
+		}
+	} else {
+		if IsBlankString(str) {
+			return ErrBlankString
+		}
+	}
+
+	if r.validate(str) {
+		return nil
+	}
+
+	return r.err
+}

+ 139 - 0
pkg/valid/rule_strings_test.go

@@ -0,0 +1,139 @@
+package valid
+
+import (
+	"database/sql"
+	"reflect"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func validateMe(s string) bool {
+	return s == "me"
+}
+
+func TestNewStringRule(t *testing.T) {
+	v := NewStringRule(true, validateMe, "abc")
+
+	assert.NotNil(t, v.validate)
+	assert.Equal(t, "", v.err.Code())
+	assert.Equal(t, "abc", v.err.Message())
+}
+
+func TestNewStringRuleWithError(t *testing.T) {
+	err := NewError("C", "abc")
+	v := NewStringRuleWithError(validateMe, err)
+
+	assert.NotNil(t, v.validate)
+	assert.Equal(t, err, v.err)
+	assert.Equal(t, "C", v.err.Code())
+	assert.Equal(t, "abc", v.err.Message())
+}
+
+func TestStringRule_Error(t *testing.T) {
+	err := NewError("code", "abc")
+	v := NewStringRuleWithError(validateMe, err).Error("abc")
+	assert.Equal(t, "code", v.err.Code())
+	assert.Equal(t, "abc", v.err.Message())
+
+	v2 := v.Error("correct")
+	assert.Equal(t, "code", v.err.Code())
+	assert.Equal(t, "correct", v2.err.Message())
+	assert.Equal(t, "abc", v.err.Message())
+}
+
+func TestStringValidator_Validate(t *testing.T) {
+	t.Run("allow blank", func(t *testing.T) {
+		v := NewStringRule(true, validateMe, "wrong_rule").Error("wrong")
+
+		value := "me"
+
+		err := v.Validate(value)
+		assert.Nil(t, err)
+
+		err = v.Validate(&value)
+		assert.Nil(t, err)
+
+		value = ""
+
+		err = v.Validate(value)
+		assert.Nil(t, err)
+
+		err = v.Validate(&value)
+		assert.Nil(t, err)
+
+		nullValue := sql.NullString{String: "me", Valid: true}
+		err = v.Validate(nullValue)
+		assert.Nil(t, err)
+
+		nullValue = sql.NullString{String: "", Valid: true}
+		err = v.Validate(nullValue)
+		assert.Nil(t, err)
+
+		var s *string
+		err = v.Validate(s)
+		assert.Nil(t, err)
+
+		err = v.Validate("not me")
+		if assert.NotNil(t, err) {
+			assert.Equal(t, "wrong", err.Error())
+		}
+
+		err = v.Validate(100)
+		if assert.NotNil(t, err) {
+			assert.NotEqual(t, "wrong", err.Error())
+		}
+
+		v2 := v.Error("Wrong!")
+		err = v2.Validate("not me")
+		if assert.NotNil(t, err) {
+			assert.Equal(t, "Wrong!", err.Error())
+		}
+	})
+
+	t.Run("deny blank", func(t *testing.T) {
+		v := NewStringRule(false, validateMe, "abc")
+
+		err := v.Validate(" ")
+		if assert.NotNil(t, err) {
+			assert.Equal(t, "cannot be blank", err.Error())
+		}
+	})
+}
+
+func TestGetErrorFieldName(t *testing.T) {
+	type A struct {
+		T0 string
+		T1 string `json:"t1"`
+		T2 string `json:"t2,omitempty"`
+		T3 string `json:",omitempty"`
+		T4 string `json:"t4,x1,omitempty"`
+	}
+	tests := []struct {
+		tag   string
+		field string
+		name  string
+	}{
+		{"t1", "T0", "T0"},
+		{"t2", "T1", "t1"},
+		{"t3", "T2", "t2"},
+		{"t4", "T3", "T3"},
+		{"t5", "T4", "t4"},
+	}
+	a := reflect.TypeOf(A{})
+	for _, test := range tests {
+		field, _ := a.FieldByName(test.field)
+		assert.Equal(t, test.name, getErrorFieldName(&field), test.tag)
+	}
+}
+
+func TestStringRule_ErrorObject(t *testing.T) {
+	r := NewStringRule(true, validateMe, "wrong_rule")
+
+	err := NewError("code", "abc")
+	r = r.ErrorObject(err)
+
+	assert.Equal(t, err, r.err)
+	assert.Equal(t, "code", r.err.Code())
+	assert.Equal(t, "abc", r.err.Message())
+}

+ 47 - 0
pkg/valid/rule_when.go

@@ -0,0 +1,47 @@
+package valid
+
+import "context"
+
+// When returns a validation rule that executes the given list of rules when the condition is true.
+func When(condition bool, rules ...Rule) WhenRule {
+	return WhenRule{
+		condition: condition,
+		rules:     rules,
+		elseRules: []Rule{},
+	}
+}
+
+// WhenRule is a validation rule that executes the given list of rules when the condition is true.
+type WhenRule struct {
+	condition bool
+	rules     []Rule
+	elseRules []Rule
+}
+
+// Validate checks if the condition is true and if so, it validates the value using the specified rules.
+func (r WhenRule) Validate(value interface{}) error {
+	return r.ValidateWithContext(nil, value)
+}
+
+// ValidateWithContext checks if the condition is true and if so, it validates the value using the specified rules.
+func (r WhenRule) ValidateWithContext(ctx context.Context, value interface{}) error {
+	if r.condition {
+		if ctx == nil {
+			return Validate(value, r.rules...)
+		} else {
+			return ValidateWithContext(ctx, value, r.rules...)
+		}
+	}
+
+	if ctx == nil {
+		return Validate(value, r.elseRules...)
+	} else {
+		return ValidateWithContext(ctx, value, r.elseRules...)
+	}
+}
+
+// Else returns a validation rule that executes the given list of rules when the condition is false.
+func (r WhenRule) Else(rules ...Rule) WhenRule {
+	r.elseRules = rules
+	return r
+}

+ 90 - 0
pkg/valid/rule_when_test.go

@@ -0,0 +1,90 @@
+package valid
+
+import (
+	"context"
+	"errors"
+	"strings"
+	"testing"
+)
+
+func abcValidation(val string) bool {
+	return val == "abc"
+}
+
+func TestWhen(t *testing.T) {
+	abcRule := NewStringRule(true, abcValidation, "wrong_abc")
+	validateMeRule := NewStringRule(true, validateMe, "wrong_me")
+
+	tests := []struct {
+		tag       string
+		condition bool
+		value     interface{}
+		rules     []Rule
+		elseRules []Rule
+		err       string
+	}{
+		// True condition
+		{"t1.1", true, nil, []Rule{}, []Rule{}, ""},
+		{"t1.2", true, "", []Rule{}, []Rule{}, ""},
+		{"t1.3", true, "", []Rule{abcRule}, []Rule{}, ""},
+		{"t1.4", true, 12, []Rule{Required}, []Rule{}, ""},
+		{"t1.5", true, nil, []Rule{Required}, []Rule{}, "cannot be blank"},
+		{"t1.6", true, "123", []Rule{abcRule}, []Rule{}, "wrong_abc"},
+		{"t1.7", true, "abc", []Rule{abcRule}, []Rule{}, ""},
+		{"t1.8", true, "abc", []Rule{abcRule, abcRule}, []Rule{}, ""},
+		{"t1.9", true, "abc", []Rule{abcRule, validateMeRule}, []Rule{}, "wrong_me"},
+		{"t1.10", true, "me", []Rule{abcRule, validateMeRule}, []Rule{}, "wrong_abc"},
+		{"t1.11", true, "me", []Rule{}, []Rule{abcRule}, ""},
+
+		// False condition
+		{"t2.1", false, "", []Rule{}, []Rule{}, ""},
+		{"t2.2", false, "", []Rule{abcRule}, []Rule{}, ""},
+		{"t2.3", false, "abc", []Rule{abcRule}, []Rule{}, ""},
+		{"t2.4", false, "abc", []Rule{abcRule, abcRule}, []Rule{}, ""},
+		{"t2.5", false, "abc", []Rule{abcRule, validateMeRule}, []Rule{}, ""},
+		{"t2.6", false, "me", []Rule{abcRule, validateMeRule}, []Rule{}, ""},
+		{"t2.7", false, "", []Rule{abcRule, validateMeRule}, []Rule{}, ""},
+		{"t2.8", false, "me", []Rule{}, []Rule{abcRule, validateMeRule}, "wrong_abc"},
+	}
+
+	for _, test := range tests {
+		err := Validate(test.value, When(test.condition, test.rules...).Else(test.elseRules...))
+		assertError(t, test.err, err, test.tag)
+	}
+}
+
+func TestWhenWithContext(t *testing.T) {
+	rule := WithContext(func(ctx context.Context, value interface{}) error {
+		if !strings.Contains(value.(string), ctx.Value("contains").(string)) {
+			return errors.New("unexpected value")
+		}
+		return nil
+	})
+	ctx1 := context.WithValue(context.Background(), "contains", "abc")
+	ctx2 := context.WithValue(context.Background(), "contains", "xyz")
+
+	tests := []struct {
+		tag       string
+		condition bool
+		value     interface{}
+		ctx       context.Context
+		err       string
+	}{
+		// True condition
+		{"t1.1", true, "abc", ctx1, ""},
+		{"t1.2", true, "abc", ctx2, "unexpected value"},
+		{"t1.3", true, "xyz", ctx1, "unexpected value"},
+		{"t1.4", true, "xyz", ctx2, ""},
+
+		// False condition
+		{"t2.1", false, "abc", ctx1, ""},
+		{"t2.2", false, "abc", ctx2, "unexpected value"},
+		{"t2.3", false, "xyz", ctx1, "unexpected value"},
+		{"t2.4", false, "xyz", ctx2, ""},
+	}
+
+	for _, test := range tests {
+		err := ValidateWithContext(test.ctx, test.value, When(test.condition, rule).Else(rule))
+		assertError(t, test.err, err, test.tag)
+	}
+}

+ 140 - 0
pkg/valid/struct.go

@@ -0,0 +1,140 @@
+package valid
+
+import (
+	"context"
+	"reflect"
+	"strings"
+)
+
+// ValidateStruct validates a struct by checking the specified struct fields against the corresponding validation rules.
+// Note that the struct being validated must be specified as a pointer to it. If the pointer is nil, it is considered valid.
+// Use Field() to specify struct fields that need to be validated. Each Field() call specifies a single field which
+// should be specified as a pointer to the field. A field can be associated with multiple rules.
+// For example,
+//
+//    value := struct {
+//        Name  string
+//        Value string
+//    }{"name", "demo"}
+//    err := validation.ValidateStruct(&value,
+//        validation.Field(&a.Name, validation.Required),
+//        validation.Field(&a.Value, validation.Required, validation.Length(5, 10)),
+//    )
+//    fmt.Println(err)
+//    // Value: the length must be between 5 and 10.
+//
+// An error will be returned if validation fails.
+func ValidateStruct(structPtr interface{}, fields ...*FieldRules) error {
+	return ValidateStructWithContext(nil, structPtr, fields...)
+}
+
+// ValidateStructWithContext validates a struct with the given context.
+// The only difference between ValidateStructWithContext and ValidateStruct is that the former will
+// validate struct fields with the provided context.
+// Please refer to ValidateStruct for the detailed instructions on how to use this function.
+func ValidateStructWithContext(ctx context.Context, structPtr interface{}, fields ...*FieldRules) error {
+	value := reflect.ValueOf(structPtr)
+	if value.Kind() != reflect.Ptr || !value.IsNil() && value.Elem().Kind() != reflect.Struct {
+		// must be a pointer to a struct
+		return NewInternalError(ErrStructPointer)
+	}
+	if value.IsNil() {
+		// treat a nil struct pointer as valid
+		return nil
+	}
+	value = value.Elem()
+
+	errs := Errors{}
+
+	for i, fr := range fields {
+		fv := reflect.ValueOf(fr.fieldPtr)
+		if fv.Kind() != reflect.Ptr {
+			return NewInternalError(ErrFieldPointer(i))
+		}
+		ft := findStructField(value, fv)
+		if ft == nil {
+			return NewInternalError(ErrFieldNotFound(i))
+		}
+		var err error
+		if ctx == nil {
+			err = Validate(fv.Elem().Interface(), fr.rules...)
+		} else {
+			err = ValidateWithContext(ctx, fv.Elem().Interface(), fr.rules...)
+		}
+		if err != nil {
+			if ie, ok := err.(InternalError); ok && ie.InternalError() != nil {
+				return err
+			}
+			if ft.Anonymous {
+				// merge errors from anonymous struct field
+				if es, ok := err.(Errors); ok {
+					for name, value := range es {
+						errs[name] = value
+					}
+					continue
+				}
+			}
+			errs[getErrorFieldName(ft)] = err
+		}
+	}
+
+	if len(errs) > 0 {
+		return errs
+	}
+	return nil
+}
+
+// FieldRules represents a rule set associated with a struct field.
+type FieldRules struct {
+	fieldPtr interface{}
+	rules    []Rule
+}
+
+// Field specifies a struct field and the corresponding validation rules.
+// The struct field must be specified as a pointer to it.
+func Field(fieldPtr interface{}, rules ...Rule) *FieldRules {
+	return &FieldRules{
+		fieldPtr: fieldPtr,
+		rules:    rules,
+	}
+}
+
+// findStructField looks for a field in the given struct.
+// The field being looked for should be a pointer to the actual struct field.
+// If found, the field info will be returned. Otherwise, nil will be returned.
+func findStructField(structValue reflect.Value, fieldValue reflect.Value) *reflect.StructField {
+	ptr := fieldValue.Pointer()
+	for i := structValue.NumField() - 1; i >= 0; i-- {
+		sf := structValue.Type().Field(i)
+		if ptr == structValue.Field(i).UnsafeAddr() {
+			// do additional type comparison because it's possible that the address of
+			// an embedded struct is the same as the first field of the embedded struct
+			if sf.Type == fieldValue.Elem().Type() {
+				return &sf
+			}
+		}
+		if sf.Anonymous {
+			// delve into anonymous struct to look for the field
+			fi := structValue.Field(i)
+			if sf.Type.Kind() == reflect.Ptr {
+				fi = fi.Elem()
+			}
+			if fi.Kind() == reflect.Struct {
+				if f := findStructField(fi, fieldValue); f != nil {
+					return f
+				}
+			}
+		}
+	}
+	return nil
+}
+
+// getErrorFieldName returns the name that should be used to represent the validation error of a struct field.
+func getErrorFieldName(f *reflect.StructField) string {
+	if tag := f.Tag.Get(ErrorTag); tag != "" && tag != "-" {
+		if cps := strings.SplitN(tag, ",", 2); cps[0] != "" {
+			return cps[0]
+		}
+	}
+	return f.Name
+}

+ 204 - 0
pkg/valid/struct_test.go

@@ -0,0 +1,204 @@
+package valid
+
+import (
+	"context"
+	"reflect"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+type Struct1 struct {
+	Field1 int
+	Field2 *int
+	Field3 []int
+	Field4 [4]int
+	field5 int
+	Struct2
+	S1               *Struct2
+	S2               Struct2
+	JSONField        int `json:"some_json_field"`
+	JSONIgnoredField int `json:"-"`
+}
+
+type Struct2 struct {
+	Field21 string
+	Field22 string
+}
+
+type Struct3 struct {
+	*Struct2
+	S1 string
+}
+
+func TestFindStructField(t *testing.T) {
+	var s1 Struct1
+	v1 := reflect.ValueOf(&s1).Elem()
+	assert.NotNil(t, findStructField(v1, reflect.ValueOf(&s1.Field1)))
+	assert.Nil(t, findStructField(v1, reflect.ValueOf(s1.Field2)))
+	assert.NotNil(t, findStructField(v1, reflect.ValueOf(&s1.Field2)))
+	assert.Nil(t, findStructField(v1, reflect.ValueOf(s1.Field3)))
+	assert.NotNil(t, findStructField(v1, reflect.ValueOf(&s1.Field3)))
+	assert.NotNil(t, findStructField(v1, reflect.ValueOf(&s1.Field4)))
+	assert.NotNil(t, findStructField(v1, reflect.ValueOf(&s1.field5)))
+	assert.NotNil(t, findStructField(v1, reflect.ValueOf(&s1.Struct2)))
+	assert.Nil(t, findStructField(v1, reflect.ValueOf(s1.S1)))
+	assert.NotNil(t, findStructField(v1, reflect.ValueOf(&s1.S1)))
+	assert.NotNil(t, findStructField(v1, reflect.ValueOf(&s1.Field21)))
+	assert.NotNil(t, findStructField(v1, reflect.ValueOf(&s1.Field22)))
+	assert.NotNil(t, findStructField(v1, reflect.ValueOf(&s1.Struct2.Field22)))
+	s2 := reflect.ValueOf(&s1.Struct2).Elem()
+	assert.NotNil(t, findStructField(s2, reflect.ValueOf(&s1.Field21)))
+	assert.NotNil(t, findStructField(s2, reflect.ValueOf(&s1.Struct2.Field21)))
+	assert.NotNil(t, findStructField(s2, reflect.ValueOf(&s1.Struct2.Field22)))
+	s3 := Struct3{
+		Struct2: &Struct2{},
+	}
+	v3 := reflect.ValueOf(&s3).Elem()
+	assert.NotNil(t, findStructField(v3, reflect.ValueOf(&s3.Struct2)))
+	assert.NotNil(t, findStructField(v3, reflect.ValueOf(&s3.Field21)))
+}
+
+func TestValidateStruct(t *testing.T) {
+	var m0 *Model1
+	m1 := Model1{A: "abc", B: "xyz", c: "abc", G: "xyz", H: []string{"abc", "abc"}, I: map[string]string{"foo": "abc"}}
+	m2 := Model1{E: String123("xyz")}
+	m3 := Model2{}
+	m4 := Model2{M3: Model3{A: "abc"}, Model3: Model3{A: "abc"}}
+	m5 := Model2{Model3: Model3{A: "internal"}}
+	tests := []struct {
+		tag   string
+		model interface{}
+		rules []*FieldRules
+		err   string
+	}{
+		// empty rules
+		{"t1.1", &m1, []*FieldRules{}, ""},
+		{"t1.2", &m1, []*FieldRules{Field(&m1.A), Field(&m1.B)}, ""},
+		// normal rules
+		{"t2.1", &m1, []*FieldRules{Field(&m1.A, &validateAbc{}), Field(&m1.B, &validateXyz{})}, ""},
+		{"t2.2", &m1, []*FieldRules{Field(&m1.A, &validateXyz{}), Field(&m1.B, &validateAbc{})}, "A: error xyz; B: error abc."},
+		{"t2.3", &m1, []*FieldRules{Field(&m1.A, &validateXyz{}), Field(&m1.c, &validateXyz{})}, "A: error xyz; c: error xyz."},
+		{"t2.4", &m1, []*FieldRules{Field(&m1.D, Length(0, 5))}, ""},
+		{"t2.5", &m1, []*FieldRules{Field(&m1.F, Length(0, 5))}, ""},
+		{"t2.6", &m1, []*FieldRules{Field(&m1.H, Each(&validateAbc{})), Field(&m1.I, Each(&validateAbc{}))}, ""},
+		{"t2.7", &m1, []*FieldRules{Field(&m1.H, Each(&validateXyz{})), Field(&m1.I, Each(&validateXyz{}))}, "H: (0: error xyz; 1: error xyz.); I: (foo: error xyz.)."},
+		// non-struct pointer
+		{"t3.1", m1, []*FieldRules{}, ErrStructPointer.Error()},
+		{"t3.2", nil, []*FieldRules{}, ErrStructPointer.Error()},
+		{"t3.3", m0, []*FieldRules{}, ""},
+		{"t3.4", &m0, []*FieldRules{}, ErrStructPointer.Error()},
+		// invalid field spec
+		{"t4.1", &m1, []*FieldRules{Field(m1)}, ErrFieldPointer(0).Error()},
+		{"t4.2", &m1, []*FieldRules{Field(&m1)}, ErrFieldNotFound(0).Error()},
+		// struct tag
+		{"t5.1", &m1, []*FieldRules{Field(&m1.G, &validateAbc{})}, "g: error abc."},
+		// validatable field
+		{"t6.1", &m2, []*FieldRules{Field(&m2.E)}, "E: error 123."},
+		{"t6.2", &m2, []*FieldRules{Field(&m2.E, Skip)}, ""},
+		{"t6.3", &m2, []*FieldRules{Field(&m2.E, Skip.When(true))}, ""},
+		{"t6.4", &m2, []*FieldRules{Field(&m2.E, Skip.When(false))}, "E: error 123."},
+		// Required, NotNil
+		{"t7.1", &m2, []*FieldRules{Field(&m2.F, Required)}, "F: cannot be blank."},
+		{"t7.2", &m2, []*FieldRules{Field(&m2.F, NotNil)}, "F: is required."},
+		{"t7.3", &m2, []*FieldRules{Field(&m2.F, Skip, Required)}, ""},
+		{"t7.4", &m2, []*FieldRules{Field(&m2.F, Skip, NotNil)}, ""},
+		{"t7.5", &m2, []*FieldRules{Field(&m2.F, Skip.When(true), Required)}, ""},
+		{"t7.6", &m2, []*FieldRules{Field(&m2.F, Skip.When(true), NotNil)}, ""},
+		{"t7.7", &m2, []*FieldRules{Field(&m2.F, Skip.When(false), Required)}, "F: cannot be blank."},
+		{"t7.8", &m2, []*FieldRules{Field(&m2.F, Skip.When(false), NotNil)}, "F: is required."},
+		// embedded structs
+		{"t8.1", &m3, []*FieldRules{Field(&m3.M3, Skip)}, ""},
+		{"t8.2", &m3, []*FieldRules{Field(&m3.M3)}, "M3: (A: error abc.)."},
+		{"t8.3", &m3, []*FieldRules{Field(&m3.Model3, Skip)}, ""},
+		{"t8.4", &m3, []*FieldRules{Field(&m3.Model3)}, "A: error abc."},
+		{"t8.5", &m4, []*FieldRules{Field(&m4.M3)}, ""},
+		{"t8.6", &m4, []*FieldRules{Field(&m4.Model3)}, ""},
+		{"t8.7", &m3, []*FieldRules{Field(&m3.A, Required), Field(&m3.B, Required)}, "A: cannot be blank; B: cannot be blank."},
+		{"t8.8", &m3, []*FieldRules{Field(&m4.A, Required)}, "field #0 cannot be found in the struct"},
+		// internal error
+		{"t9.1", &m5, []*FieldRules{Field(&m5.A, &validateAbc{}), Field(&m5.B, Required), Field(&m5.A, &validateInternalError{})}, "error internal"},
+	}
+	for _, test := range tests {
+		err1 := ValidateStruct(test.model, test.rules...)
+		err2 := ValidateStructWithContext(context.Background(), test.model, test.rules...)
+		assertError(t, test.err, err1, test.tag)
+		assertError(t, test.err, err2, test.tag)
+	}
+
+	// embedded struct
+	err := Validate(&m3)
+	assert.EqualError(t, err, "A: error abc.")
+
+	a := struct {
+		Name  string
+		Value string
+	}{"name", "demo"}
+	err = ValidateStruct(&a,
+		Field(&a.Name, Required),
+		Field(&a.Value, Required, Length(5, 10)),
+	)
+	assert.EqualError(t, err, "Value: the length must be between 5 and 10.")
+}
+
+func TestValidateStructWithContext(t *testing.T) {
+	m1 := Model1{A: "abc", B: "xyz", c: "abc", G: "xyz"}
+	m2 := Model2{Model3: Model3{A: "internal"}}
+	m3 := Model5{}
+	tests := []struct {
+		tag   string
+		model interface{}
+		rules []*FieldRules
+		err   string
+	}{
+		// normal rules
+		{"t1.1", &m1, []*FieldRules{Field(&m1.A, &validateContextAbc{}), Field(&m1.B, &validateContextXyz{})}, ""},
+		{"t1.2", &m1, []*FieldRules{Field(&m1.A, &validateContextXyz{}), Field(&m1.B, &validateContextAbc{})}, "A: error xyz; B: error abc."},
+		{"t1.3", &m1, []*FieldRules{Field(&m1.A, &validateContextXyz{}), Field(&m1.c, &validateContextXyz{})}, "A: error xyz; c: error xyz."},
+		{"t1.4", &m1, []*FieldRules{Field(&m1.G, &validateContextAbc{})}, "g: error abc."},
+		// skip rule
+		{"t2.1", &m1, []*FieldRules{Field(&m1.G, Skip, &validateContextAbc{})}, ""},
+		{"t2.2", &m1, []*FieldRules{Field(&m1.G, &validateContextAbc{}, Skip)}, "g: error abc."},
+		// internal error
+		{"t3.1", &m2, []*FieldRules{Field(&m2.A, &validateContextAbc{}), Field(&m2.B, Required), Field(&m2.A, &validateInternalError{})}, "error internal"},
+	}
+	for _, test := range tests {
+		err := ValidateStructWithContext(context.Background(), test.model, test.rules...)
+		assertError(t, test.err, err, test.tag)
+	}
+
+	//embedded struct
+	err := ValidateWithContext(context.Background(), &m3)
+	if assert.NotNil(t, err) {
+		assert.Equal(t, "A: error abc.", err.Error())
+	}
+
+	a := struct {
+		Name  string
+		Value string
+	}{"name", "demo"}
+	err = ValidateStructWithContext(context.Background(), &a,
+		Field(&a.Name, Required),
+		Field(&a.Value, Required, Length(5, 10)),
+	)
+	if assert.NotNil(t, err) {
+		assert.Equal(t, "Value: the length must be between 5 and 10.", err.Error())
+	}
+}
+
+func Test_getErrorFieldName(t *testing.T) {
+	var s1 Struct1
+	v1 := reflect.ValueOf(&s1).Elem()
+
+	sf1 := findStructField(v1, reflect.ValueOf(&s1.Field1))
+	assert.NotNil(t, sf1)
+	assert.Equal(t, "Field1", getErrorFieldName(sf1))
+
+	jsonField := findStructField(v1, reflect.ValueOf(&s1.JSONField))
+	assert.NotNil(t, jsonField)
+	assert.Equal(t, "some_json_field", getErrorFieldName(jsonField))
+
+	jsonIgnoredField := findStructField(v1, reflect.ValueOf(&s1.JSONIgnoredField))
+	assert.NotNil(t, jsonIgnoredField)
+	assert.Equal(t, "JSONIgnoredField", getErrorFieldName(jsonIgnoredField))
+}

+ 175 - 0
pkg/valid/util.go

@@ -0,0 +1,175 @@
+package valid
+
+import (
+	"database/sql/driver"
+	"errors"
+	"fmt"
+	"reflect"
+	"strings"
+	"time"
+)
+
+var (
+	bytesType  = reflect.TypeOf([]byte(nil))
+	valuerType = reflect.TypeOf((*driver.Valuer)(nil)).Elem()
+
+	// ErrEnsureString is the error that returns when a value is not a string or byte slice.
+	ErrEnsureString = errors.New("must be either a string or byte slice")
+)
+
+// EnsureString ensures the given value is a string.
+// If the value is a byte slice, it will be typecast into a string.
+// An error is returned otherwise.
+func EnsureString(value interface{}) (string, error) {
+	v := reflect.ValueOf(value)
+	if v.Kind() == reflect.String {
+		return v.String(), nil
+	}
+	if v.Type() == bytesType {
+		return string(v.Interface().([]byte)), nil
+	}
+	return "", ErrEnsureString
+}
+
+// IsBlankString check the given value is a empty values or whitespace only values
+func IsBlankString(value string) bool {
+	if value == "" {
+		return true
+	}
+	if strings.TrimSpace(value) == "" {
+		return true
+	}
+
+	return false
+}
+
+// StringOrBytes typecasts a value into a string or byte slice.
+// Boolean flags are returned to indicate if the typecasting succeeds or not.
+func StringOrBytes(value interface{}) (isString bool, str string, isBytes bool, bs []byte) {
+	v := reflect.ValueOf(value)
+	if v.Kind() == reflect.String {
+		str = v.String()
+		isString = true
+	} else if v.Kind() == reflect.Slice && v.Type() == bytesType {
+		bs = v.Interface().([]byte)
+		isBytes = true
+	}
+	return
+}
+
+// LengthOfValue returns the length of a value that is a string, slice, map, or array.
+// An error is returned for all other types.
+func LengthOfValue(value interface{}) (int, error) {
+	v := reflect.ValueOf(value)
+	switch v.Kind() {
+	case reflect.String, reflect.Slice, reflect.Map, reflect.Array:
+		return v.Len(), nil
+	}
+	return 0, fmt.Errorf("cannot get the length of %v", v.Kind())
+}
+
+// ToInt converts the given value to an int64.
+// An error is returned for all incompatible types.
+func ToInt(value interface{}) (int64, error) {
+	v := reflect.ValueOf(value)
+	switch v.Kind() {
+	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+		return v.Int(), nil
+	}
+	return 0, fmt.Errorf("cannot convert %v to int64", v.Kind())
+}
+
+// ToUint converts the given value to an uint64.
+// An error is returned for all incompatible types.
+func ToUint(value interface{}) (uint64, error) {
+	v := reflect.ValueOf(value)
+	switch v.Kind() {
+	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
+		return v.Uint(), nil
+	}
+	return 0, fmt.Errorf("cannot convert %v to uint64", v.Kind())
+}
+
+// ToFloat converts the given value to a float64.
+// An error is returned for all incompatible types.
+func ToFloat(value interface{}) (float64, error) {
+	v := reflect.ValueOf(value)
+	switch v.Kind() {
+	case reflect.Float32, reflect.Float64:
+		return v.Float(), nil
+	}
+	return 0, fmt.Errorf("cannot convert %v to float64", v.Kind())
+}
+
+// IsEmpty checks if a value is empty or not.
+// A value is considered empty if
+// - integer, float: zero
+// - bool: false
+// - string, array: len() == 0
+// - slice, map: nil or len() == 0
+// - interface, pointer: nil or the referenced value is empty
+func IsEmpty(value interface{}) bool {
+	v := reflect.ValueOf(value)
+	switch v.Kind() {
+	case reflect.String, reflect.Array, reflect.Map, reflect.Slice:
+		return v.Len() == 0
+	case reflect.Bool:
+		return !v.Bool()
+	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+		return v.Int() == 0
+	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
+		return v.Uint() == 0
+	case reflect.Float32, reflect.Float64:
+		return v.Float() == 0
+	case reflect.Invalid:
+		return true
+	case reflect.Interface, reflect.Ptr:
+		if v.IsNil() {
+			return true
+		}
+		return IsEmpty(v.Elem().Interface())
+	case reflect.Struct:
+		v, ok := value.(time.Time)
+		if ok && v.IsZero() {
+			return true
+		}
+	}
+
+	return false
+}
+
+// Indirect returns the value that the given interface or pointer references to.
+// If the value implements driver.Valuer, it will deal with the value returned by
+// the Value() method instead. A boolean value is also returned to indicate if
+// the value is nil or not (only applicable to interface, pointer, map, and slice).
+// If the value is neither an interface nor a pointer, it will be returned back.
+func Indirect(value interface{}) (interface{}, bool) {
+	rv := reflect.ValueOf(value)
+	kind := rv.Kind()
+	switch kind {
+	case reflect.Invalid:
+		return nil, true
+	case reflect.Ptr, reflect.Interface:
+		if rv.IsNil() {
+			return nil, true
+		}
+		return Indirect(rv.Elem().Interface())
+	case reflect.Slice, reflect.Map, reflect.Func, reflect.Chan:
+		if rv.IsNil() {
+			return nil, true
+		}
+	}
+
+	if rv.Type().Implements(valuerType) {
+		return indirectValuer(value.(driver.Valuer))
+	}
+
+	return value, false
+}
+
+func indirectValuer(valuer driver.Valuer) (interface{}, bool) {
+	if value, err := valuer.Value(); value != nil && err == nil {
+		return Indirect(value)
+	}
+	return nil, true
+}

+ 296 - 0
pkg/valid/util_test.go

@@ -0,0 +1,296 @@
+package valid
+
+import (
+	"database/sql"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestEnsureString(t *testing.T) {
+	str := "abc"
+	bytes := []byte("abc")
+
+	tests := []struct {
+		tag      string
+		value    interface{}
+		expected string
+		hasError bool
+	}{
+		{"t1", "abc", "abc", false},
+		{"t2", &str, "", true},
+		{"t3", bytes, "abc", false},
+		{"t4", &bytes, "", true},
+		{"t5", 100, "", true},
+	}
+	for _, test := range tests {
+		s, err := EnsureString(test.value)
+		if test.hasError {
+			assert.NotNil(t, err, test.tag)
+		} else {
+			assert.Nil(t, err, test.tag)
+			assert.Equal(t, test.expected, s, test.tag)
+		}
+	}
+}
+
+type MyString string
+
+func TestStringOrBytes(t *testing.T) {
+	str := "abc"
+	bytes := []byte("abc")
+	var str2 string
+	var bytes2 []byte
+	var str3 MyString = "abc"
+	var str4 *string
+
+	tests := []struct {
+		tag      string
+		value    interface{}
+		str      string
+		bs       []byte
+		isString bool
+		isBytes  bool
+	}{
+		{"t1", str, "abc", nil, true, false},
+		{"t2", &str, "", nil, false, false},
+		{"t3", bytes, "", []byte("abc"), false, true},
+		{"t4", &bytes, "", nil, false, false},
+		{"t5", 100, "", nil, false, false},
+		{"t6", str2, "", nil, true, false},
+		{"t7", &str2, "", nil, false, false},
+		{"t8", bytes2, "", nil, false, true},
+		{"t9", &bytes2, "", nil, false, false},
+		{"t10", str3, "abc", nil, true, false},
+		{"t11", &str3, "", nil, false, false},
+		{"t12", str4, "", nil, false, false},
+	}
+	for _, test := range tests {
+		isString, str, isBytes, bs := StringOrBytes(test.value)
+		assert.Equal(t, test.str, str, test.tag)
+		assert.Equal(t, test.bs, bs, test.tag)
+		assert.Equal(t, test.isString, isString, test.tag)
+		assert.Equal(t, test.isBytes, isBytes, test.tag)
+	}
+}
+
+func TestLengthOfValue(t *testing.T) {
+	var a [3]int
+
+	tests := []struct {
+		tag    string
+		value  interface{}
+		length int
+		err    string
+	}{
+		{"t1", "abc", 3, ""},
+		{"t2", []int{1, 2}, 2, ""},
+		{"t3", map[string]int{"A": 1, "B": 2}, 2, ""},
+		{"t4", a, 3, ""},
+		{"t5", &a, 0, "cannot get the length of ptr"},
+		{"t6", 123, 0, "cannot get the length of int"},
+	}
+
+	for _, test := range tests {
+		l, err := LengthOfValue(test.value)
+		assert.Equal(t, test.length, l, test.tag)
+		assertError(t, test.err, err, test.tag)
+	}
+}
+
+func TestToInt(t *testing.T) {
+	var a int
+
+	tests := []struct {
+		tag    string
+		value  interface{}
+		result int64
+		err    string
+	}{
+		{"t1", 1, 1, ""},
+		{"t2", int8(1), 1, ""},
+		{"t3", int16(1), 1, ""},
+		{"t4", int32(1), 1, ""},
+		{"t5", int64(1), 1, ""},
+		{"t6", &a, 0, "cannot convert ptr to int64"},
+		{"t7", uint(1), 0, "cannot convert uint to int64"},
+		{"t8", float64(1), 0, "cannot convert float64 to int64"},
+		{"t9", "abc", 0, "cannot convert string to int64"},
+		{"t10", []int{1, 2}, 0, "cannot convert slice to int64"},
+		{"t11", map[string]int{"A": 1}, 0, "cannot convert map to int64"},
+	}
+
+	for _, test := range tests {
+		l, err := ToInt(test.value)
+		assert.Equal(t, test.result, l, test.tag)
+		assertError(t, test.err, err, test.tag)
+	}
+}
+
+func TestToUint(t *testing.T) {
+	var a int
+	var b uint
+
+	tests := []struct {
+		tag    string
+		value  interface{}
+		result uint64
+		err    string
+	}{
+		{"t1", uint(1), 1, ""},
+		{"t2", uint8(1), 1, ""},
+		{"t3", uint16(1), 1, ""},
+		{"t4", uint32(1), 1, ""},
+		{"t5", uint64(1), 1, ""},
+		{"t6", 1, 0, "cannot convert int to uint64"},
+		{"t7", &a, 0, "cannot convert ptr to uint64"},
+		{"t8", &b, 0, "cannot convert ptr to uint64"},
+		{"t9", float64(1), 0, "cannot convert float64 to uint64"},
+		{"t10", "abc", 0, "cannot convert string to uint64"},
+		{"t11", []int{1, 2}, 0, "cannot convert slice to uint64"},
+		{"t12", map[string]int{"A": 1}, 0, "cannot convert map to uint64"},
+	}
+
+	for _, test := range tests {
+		l, err := ToUint(test.value)
+		assert.Equal(t, test.result, l, test.tag)
+		assertError(t, test.err, err, test.tag)
+	}
+}
+
+func TestToFloat(t *testing.T) {
+	var a int
+	var b uint
+
+	tests := []struct {
+		tag    string
+		value  interface{}
+		result float64
+		err    string
+	}{
+		{"t1", float32(1), 1, ""},
+		{"t2", float64(1), 1, ""},
+		{"t3", 1, 0, "cannot convert int to float64"},
+		{"t4", uint(1), 0, "cannot convert uint to float64"},
+		{"t5", &a, 0, "cannot convert ptr to float64"},
+		{"t6", &b, 0, "cannot convert ptr to float64"},
+		{"t7", "abc", 0, "cannot convert string to float64"},
+		{"t8", []int{1, 2}, 0, "cannot convert slice to float64"},
+		{"t9", map[string]int{"A": 1}, 0, "cannot convert map to float64"},
+	}
+
+	for _, test := range tests {
+		l, err := ToFloat(test.value)
+		assert.Equal(t, test.result, l, test.tag)
+		assertError(t, test.err, err, test.tag)
+	}
+}
+
+func TestIsEmpty(t *testing.T) {
+	var s1 string
+	var s2 = "a"
+	var s3 *string
+	s4 := struct{}{}
+	time1 := time.Now()
+	var time2 time.Time
+	tests := []struct {
+		tag   string
+		value interface{}
+		empty bool
+	}{
+		// nil
+		{"t0", nil, true},
+		// string
+		{"t1.1", "", true},
+		{"t1.2", "1", false},
+		{"t1.3", MyString(""), true},
+		{"t1.4", MyString("1"), false},
+		// slice
+		{"t2.1", []byte(""), true},
+		{"t2.2", []byte("1"), false},
+		// map
+		{"t3.1", map[string]int{}, true},
+		{"t3.2", map[string]int{"a": 1}, false},
+		// bool
+		{"t4.1", false, true},
+		{"t4.2", true, false},
+		// int
+		{"t5.1", 0, true},
+		{"t5.2", int8(0), true},
+		{"t5.3", int16(0), true},
+		{"t5.4", int32(0), true},
+		{"t5.5", int64(0), true},
+		{"t5.6", 1, false},
+		{"t5.7", int8(1), false},
+		{"t5.8", int16(1), false},
+		{"t5.9", int32(1), false},
+		{"t5.10", int64(1), false},
+		// uint
+		{"t6.1", uint(0), true},
+		{"t6.2", uint8(0), true},
+		{"t6.3", uint16(0), true},
+		{"t6.4", uint32(0), true},
+		{"t6.5", uint64(0), true},
+		{"t6.6", uint(1), false},
+		{"t6.7", uint8(1), false},
+		{"t6.8", uint16(1), false},
+		{"t6.9", uint32(1), false},
+		{"t6.10", uint64(1), false},
+		// float
+		{"t7.1", float32(0), true},
+		{"t7.2", float64(0), true},
+		{"t7.3", float32(1), false},
+		{"t7.4", float64(1), false},
+		// interface, ptr
+		{"t8.1", &s1, true},
+		{"t8.2", &s2, false},
+		{"t8.3", s3, true},
+		// struct
+		{"t9.1", s4, false},
+		{"t9.2", &s4, false},
+		// time.Time
+		{"t10.1", time1, false},
+		{"t10.2", &time1, false},
+		{"t10.3", time2, true},
+		{"t10.4", &time2, true},
+	}
+
+	for _, test := range tests {
+		empty := IsEmpty(test.value)
+		assert.Equal(t, test.empty, empty, test.tag)
+	}
+}
+
+func TestIndirect(t *testing.T) {
+	var a = 100
+	var b *int
+	var c *sql.NullInt64
+
+	tests := []struct {
+		tag    string
+		value  interface{}
+		result interface{}
+		isNil  bool
+	}{
+		{"t1", 100, 100, false},
+		{"t2", &a, 100, false},
+		{"t3", b, nil, true},
+		{"t4", nil, nil, true},
+		{"t5", sql.NullInt64{Int64: 0, Valid: false}, nil, true},
+		{"t6", sql.NullInt64{Int64: 1, Valid: false}, nil, true},
+		{"t7", &sql.NullInt64{Int64: 0, Valid: false}, nil, true},
+		{"t8", &sql.NullInt64{Int64: 1, Valid: false}, nil, true},
+		{"t9", sql.NullInt64{Int64: 0, Valid: true}, int64(0), false},
+		{"t10", sql.NullInt64{Int64: 1, Valid: true}, int64(1), false},
+		{"t11", &sql.NullInt64{Int64: 0, Valid: true}, int64(0), false},
+		{"t12", &sql.NullInt64{Int64: 1, Valid: true}, int64(1), false},
+		{"t13", c, nil, true},
+	}
+
+	for _, test := range tests {
+		result, isNil := Indirect(test.value)
+		assert.Equal(t, test.result, result, test.tag)
+		assert.Equal(t, test.isNil, isNil, test.tag)
+	}
+}

+ 221 - 0
pkg/valid/validation.go

@@ -0,0 +1,221 @@
+package valid
+
+import (
+	"context"
+	"fmt"
+	"reflect"
+	"strconv"
+)
+
+var (
+	validatableType            = reflect.TypeOf((*Validatable)(nil)).Elem()
+	validatableWithContextType = reflect.TypeOf((*ValidatableWithContext)(nil)).Elem()
+)
+
+// Validate validates the given value and returns the validation error, if any.
+//
+// Validate performs validation using the following steps:
+// 1. For each rule, call its `Validate()` to validate the value. Return if any error is found.
+// 2. If the value being validated implements `Validatable`, call the value's `Validate()`.
+//    Return with the validation result.
+// 3. If the value being validated is a map/slice/array, and the element type implements `Validatable`,
+//    for each element call the element value's `Validate()`. Return with the validation result.
+func Validate(value interface{}, rules ...Rule) error {
+	for _, rule := range rules {
+		if s, ok := rule.(skipRule); ok && s.skip {
+			return nil
+		}
+		if err := rule.Validate(value); err != nil {
+			return err
+		}
+	}
+
+	rv := reflect.ValueOf(value)
+	if (rv.Kind() == reflect.Ptr || rv.Kind() == reflect.Interface) && rv.IsNil() {
+		return nil
+	}
+
+	if v, ok := value.(Validatable); ok {
+		return v.Validate()
+	}
+
+	switch rv.Kind() {
+	case reflect.Map:
+		if rv.Type().Elem().Implements(validatableType) {
+			return validateMap(rv)
+		}
+	case reflect.Slice, reflect.Array:
+		if rv.Type().Elem().Implements(validatableType) {
+			return validateSlice(rv)
+		}
+	case reflect.Ptr, reflect.Interface:
+		return Validate(rv.Elem().Interface())
+	}
+
+	return nil
+}
+
+// ValidateWithContext validates the given value with the given context and returns the validation error, if any.
+//
+// ValidateWithContext performs validation using the following steps:
+// 1. For each rule, call its `ValidateWithContext()` to validate the value if the rule implements `RuleWithContext`.
+//    Otherwise call `Validate()` of the rule. Return if any error is found.
+// 2. If the value being validated implements `ValidatableWithContext`, call the value's `ValidateWithContext()`
+//    and return with the validation result.
+// 3. If the value being validated implements `Validatable`, call the value's `Validate()`
+//    and return with the validation result.
+// 4. If the value being validated is a map/slice/array, and the element type implements `ValidatableWithContext`,
+//    for each element call the element value's `ValidateWithContext()`. Return with the validation result.
+// 5. If the value being validated is a map/slice/array, and the element type implements `Validatable`,
+//    for each element call the element value's `Validate()`. Return with the validation result.
+func ValidateWithContext(ctx context.Context, value interface{}, rules ...Rule) error {
+	for _, rule := range rules {
+		if s, ok := rule.(skipRule); ok && s.skip {
+			return nil
+		}
+		if rc, ok := rule.(RuleWithContext); ok {
+			if err := rc.ValidateWithContext(ctx, value); err != nil {
+				return err
+			}
+		} else if err := rule.Validate(value); err != nil {
+			return err
+		}
+	}
+
+	rv := reflect.ValueOf(value)
+	if (rv.Kind() == reflect.Ptr || rv.Kind() == reflect.Interface) && rv.IsNil() {
+		return nil
+	}
+
+	if v, ok := value.(ValidatableWithContext); ok {
+		return v.ValidateWithContext(ctx)
+	}
+
+	if v, ok := value.(Validatable); ok {
+		return v.Validate()
+	}
+
+	switch rv.Kind() {
+	case reflect.Map:
+		if rv.Type().Elem().Implements(validatableWithContextType) {
+			return validateMapWithContext(ctx, rv)
+		}
+		if rv.Type().Elem().Implements(validatableType) {
+			return validateMap(rv)
+		}
+	case reflect.Slice, reflect.Array:
+		if rv.Type().Elem().Implements(validatableWithContextType) {
+			return validateSliceWithContext(ctx, rv)
+		}
+		if rv.Type().Elem().Implements(validatableType) {
+			return validateSlice(rv)
+		}
+	case reflect.Ptr, reflect.Interface:
+		return ValidateWithContext(ctx, rv.Elem().Interface())
+	}
+
+	return nil
+}
+
+// By wraps a RuleFunc into a Rule.
+func By(f RuleFunc) Rule {
+	return &inlineRule{f: f}
+}
+
+// RuleFunc represents a validator function.
+// You may wrap it as a Rule by calling By().
+type RuleFunc func(value interface{}) error
+
+// WithContext wraps a RuleWithContextFunc into a context-aware Rule.
+func WithContext(f RuleWithContextFunc) Rule {
+	return &inlineRule{fc: f}
+}
+
+// RuleWithContextFunc represents a validator function that is context-aware.
+// You may wrap it as a Rule by calling WithContext().
+type RuleWithContextFunc func(ctx context.Context, value interface{}) error
+
+// validateMap validates a map of validatable elements
+func validateMap(rv reflect.Value) error {
+	errs := Errors{}
+	for _, key := range rv.MapKeys() {
+		if mv := rv.MapIndex(key).Interface(); mv != nil {
+			if err := mv.(Validatable).Validate(); err != nil {
+				errs[fmt.Sprintf("%v", key.Interface())] = err
+			}
+		}
+	}
+	if len(errs) > 0 {
+		return errs
+	}
+	return nil
+}
+
+// validateMapWithContext validates a map of validatable elements with the given context.
+func validateMapWithContext(ctx context.Context, rv reflect.Value) error {
+	errs := Errors{}
+	for _, key := range rv.MapKeys() {
+		if mv := rv.MapIndex(key).Interface(); mv != nil {
+			if err := mv.(ValidatableWithContext).ValidateWithContext(ctx); err != nil {
+				errs[fmt.Sprintf("%v", key.Interface())] = err
+			}
+		}
+	}
+	if len(errs) > 0 {
+		return errs
+	}
+	return nil
+}
+
+// validateSlice validates a slice/array of validatable elements
+func validateSlice(rv reflect.Value) error {
+	errs := Errors{}
+	l := rv.Len()
+	for i := 0; i < l; i++ {
+		if ev := rv.Index(i).Interface(); ev != nil {
+			if err := ev.(Validatable).Validate(); err != nil {
+				errs[strconv.Itoa(i)] = err
+			}
+		}
+	}
+	if len(errs) > 0 {
+		return errs
+	}
+	return nil
+}
+
+// validateSliceWithContext validates a slice/array of validatable elements with the given context.
+func validateSliceWithContext(ctx context.Context, rv reflect.Value) error {
+	errs := Errors{}
+	l := rv.Len()
+	for i := 0; i < l; i++ {
+		if ev := rv.Index(i).Interface(); ev != nil {
+			if err := ev.(ValidatableWithContext).ValidateWithContext(ctx); err != nil {
+				errs[strconv.Itoa(i)] = err
+			}
+		}
+	}
+	if len(errs) > 0 {
+		return errs
+	}
+	return nil
+}
+
+type inlineRule struct {
+	f  RuleFunc
+	fc RuleWithContextFunc
+}
+
+func (r *inlineRule) Validate(value interface{}) error {
+	if r.f == nil {
+		return r.fc(context.Background(), value)
+	}
+	return r.f(value)
+}
+
+func (r *inlineRule) ValidateWithContext(ctx context.Context, value interface{}) error {
+	if r.fc == nil {
+		return r.f(value)
+	}
+	return r.fc(ctx, value)
+}

+ 266 - 0
pkg/valid/validation_test.go

@@ -0,0 +1,266 @@
+package valid
+
+import (
+	"context"
+	"errors"
+	"strings"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestValidate(t *testing.T) {
+	slice := []String123{String123("abc"), String123("123"), String123("xyz")}
+	ctxSlice := []Model4{{A: "abc"}, {A: "def"}}
+	mp := map[string]String123{"c": String123("abc"), "b": String123("123"), "a": String123("xyz")}
+	mpCtx := map[string]StringValidateContext{"c": StringValidateContext("abc"), "b": StringValidateContext("123"), "a": StringValidateContext("xyz")}
+	var (
+		ptr     *string
+		noCtx   StringValidate        = "abc"
+		withCtx StringValidateContext = "xyz"
+	)
+	tests := []struct {
+		tag            string
+		value          interface{}
+		err            string
+		errWithContext string
+	}{
+		{"t1", 123, "", ""},
+		{"t2", String123("123"), "", ""},
+		{"t3", String123("abc"), "error 123", "error 123"},
+		{"t4", []String123{}, "", ""},
+		{"t4.1", []StringValidateContext{}, "", ""},
+		{"t4.2", map[string]StringValidateContext{}, "", ""},
+		{"t5", slice, "0: error 123; 2: error 123.", "0: error 123; 2: error 123."},
+		{"t6", &slice, "0: error 123; 2: error 123.", "0: error 123; 2: error 123."},
+		{"t7", ctxSlice, "", "1: (A: error abc.)."},
+		{"t8", mp, "a: error 123; c: error 123.", "a: error 123; c: error 123."},
+		{"t8.1", mpCtx, "a: must be abc; b: must be abc.", "a: must be abc with context; b: must be abc with context."},
+		{"t9", &mp, "a: error 123; c: error 123.", "a: error 123; c: error 123."},
+		{"t10", map[string]String123{}, "", ""},
+		{"t11", ptr, "", ""},
+		{"t12", noCtx, "called validate", "called validate"},
+		{"t13", withCtx, "must be abc", "must be abc with context"},
+	}
+	for _, test := range tests {
+		err := Validate(test.value)
+		assertError(t, test.err, err, test.tag)
+		// rules that are not context-aware should still be applied in context-aware validation
+		err = ValidateWithContext(context.Background(), test.value)
+		assertError(t, test.errWithContext, err, test.tag)
+	}
+
+	// with rules
+	err := Validate("123", &validateAbc{}, &validateXyz{})
+	assert.EqualError(t, err, "error abc")
+	err = Validate("abc", &validateAbc{}, &validateXyz{})
+	assert.EqualError(t, err, "error xyz")
+	err = Validate("abcxyz", &validateAbc{}, &validateXyz{})
+	assert.NoError(t, err)
+
+	err = Validate("123", &validateAbc{}, Skip, &validateXyz{})
+	assert.EqualError(t, err, "error abc")
+	err = Validate("abc", &validateAbc{}, Skip, &validateXyz{})
+	assert.NoError(t, err)
+
+	err = Validate("123", &validateAbc{}, Skip.When(true), &validateXyz{})
+	assert.EqualError(t, err, "error abc")
+	err = Validate("abc", &validateAbc{}, Skip.When(true), &validateXyz{})
+	assert.NoError(t, err)
+
+	err = Validate("123", &validateAbc{}, Skip.When(false), &validateXyz{})
+	assert.EqualError(t, err, "error abc")
+	err = Validate("abc", &validateAbc{}, Skip.When(false), &validateXyz{})
+	assert.EqualError(t, err, "error xyz")
+}
+
+func stringEqual(str string) RuleFunc {
+	return func(value interface{}) error {
+		s, _ := value.(string)
+		if s != str {
+			return errors.New("unexpected string")
+		}
+		return nil
+	}
+}
+
+func TestBy(t *testing.T) {
+	abcRule := By(func(value interface{}) error {
+		s, _ := value.(string)
+		if s != "abc" {
+			return errors.New("must be abc")
+		}
+		return nil
+	})
+	assert.Nil(t, Validate("abc", abcRule))
+	err := Validate("xyz", abcRule)
+	if assert.NotNil(t, err) {
+		assert.Equal(t, "must be abc", err.Error())
+	}
+
+	xyzRule := By(stringEqual("xyz"))
+	assert.Nil(t, Validate("xyz", xyzRule))
+	assert.NotNil(t, Validate("abc", xyzRule))
+	assert.Nil(t, ValidateWithContext(context.Background(), "xyz", xyzRule))
+	assert.NotNil(t, ValidateWithContext(context.Background(), "abc", xyzRule))
+}
+
+type key int
+
+func TestByWithContext(t *testing.T) {
+	k := key(1)
+	abcRule := WithContext(func(ctx context.Context, value interface{}) error {
+		if ctx.Value(k) != value.(string) {
+			return errors.New("must be abc")
+		}
+		return nil
+	})
+	ctx := context.WithValue(context.Background(), k, "abc")
+	assert.Nil(t, ValidateWithContext(ctx, "abc", abcRule))
+	err := ValidateWithContext(ctx, "xyz", abcRule)
+	if assert.NotNil(t, err) {
+		assert.Equal(t, "must be abc", err.Error())
+	}
+
+	assert.NotNil(t, Validate("abc", abcRule))
+}
+
+func Test_skipRule_Validate(t *testing.T) {
+	assert.Nil(t, Skip.Validate(100))
+}
+
+func assertError(t *testing.T, expected string, err error, tag string) {
+	if expected == "" {
+		assert.NoError(t, err, tag)
+	} else {
+		assert.EqualError(t, err, expected, tag)
+	}
+}
+
+type validateAbc struct{}
+
+func (v *validateAbc) Validate(obj interface{}) error {
+	if !strings.Contains(obj.(string), "abc") {
+		return errors.New("error abc")
+	}
+	return nil
+}
+
+type validateContextAbc struct{}
+
+func (v *validateContextAbc) Validate(obj interface{}) error {
+	return v.ValidateWithContext(context.Background(), obj)
+}
+
+func (v *validateContextAbc) ValidateWithContext(_ context.Context, obj interface{}) error {
+	if !strings.Contains(obj.(string), "abc") {
+		return errors.New("error abc")
+	}
+	return nil
+}
+
+type validateXyz struct{}
+
+func (v *validateXyz) Validate(obj interface{}) error {
+	if !strings.Contains(obj.(string), "xyz") {
+		return errors.New("error xyz")
+	}
+	return nil
+}
+
+type validateContextXyz struct{}
+
+func (v *validateContextXyz) Validate(obj interface{}) error {
+	return v.ValidateWithContext(context.Background(), obj)
+}
+
+func (v *validateContextXyz) ValidateWithContext(_ context.Context, obj interface{}) error {
+	if !strings.Contains(obj.(string), "xyz") {
+		return errors.New("error xyz")
+	}
+	return nil
+}
+
+type validateInternalError struct{}
+
+func (v *validateInternalError) Validate(obj interface{}) error {
+	if strings.Contains(obj.(string), "internal") {
+		return NewInternalError(errors.New("error internal"))
+	}
+	return nil
+}
+
+type Model1 struct {
+	A string
+	B string
+	c string
+	D *string
+	E String123
+	F *String123
+	G string `json:"g"`
+	H []string
+	I map[string]string
+}
+
+type String123 string
+
+func (s String123) Validate() error {
+	if !strings.Contains(string(s), "123") {
+		return errors.New("error 123")
+	}
+	return nil
+}
+
+type Model2 struct {
+	Model3
+	M3 Model3
+	B  string
+}
+
+type Model3 struct {
+	A string
+}
+
+func (m Model3) Validate() error {
+	return ValidateStruct(&m,
+		Field(&m.A, &validateAbc{}),
+	)
+}
+
+type Model4 struct {
+	A string
+}
+
+func (m Model4) ValidateWithContext(ctx context.Context) error {
+	return ValidateStructWithContext(ctx, &m,
+		Field(&m.A, &validateContextAbc{}),
+	)
+}
+
+type Model5 struct {
+	Model4
+	M4 Model4
+	B  string
+}
+
+type StringValidate string
+
+func (s StringValidate) Validate() error {
+	return errors.New("called validate")
+}
+
+type StringValidateContext string
+
+func (s StringValidateContext) Validate() error {
+	if string(s) != "abc" {
+		return errors.New("must be abc")
+	}
+	return nil
+}
+
+func (s StringValidateContext) ValidateWithContext(context.Context) error {
+	if string(s) != "abc" {
+		return errors.New("must be abc with context")
+	}
+	return nil
+}

+ 126 - 11
proto/go/backend/operation/system.pb.go

@@ -321,6 +321,7 @@ func (x *SystemToken) GetToken() string {
 	return ""
 }
 
+// 用户登录
 type UserAuth struct {
 	state         protoimpl.MessageState
 	sizeCache     protoimpl.SizeCache
@@ -400,6 +401,93 @@ func (x *UserAuth) GetRoleName() string {
 	return ""
 }
 
+type AddSystemUser struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	Id           int64       `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`                                                          // 用户id
+	Name         string      `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`                                                       // 用户名称
+	Phone        string      `protobuf:"bytes,3,opt,name=phone,proto3" json:"phone,omitempty"`                                                     // 用户手机号
+	RoleId       int64       `protobuf:"varint,4,opt,name=role_id,json=roleId,proto3" json:"role_id,omitempty"`                                    // 角色id
+	IsShow       IsShow_Kind `protobuf:"varint,5,opt,name=is_show,json=isShow,proto3,enum=backend.operation.IsShow_Kind" json:"is_show,omitempty"` // 是否开启
+	EmployeeName string      `protobuf:"bytes,6,opt,name=employee_name,json=employeeName,proto3" json:"employee_name,omitempty"`                   // 员工姓名
+}
+
+func (x *AddSystemUser) Reset() {
+	*x = AddSystemUser{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_backend_operation_system_proto_msgTypes[6]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *AddSystemUser) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*AddSystemUser) ProtoMessage() {}
+
+func (x *AddSystemUser) ProtoReflect() protoreflect.Message {
+	mi := &file_backend_operation_system_proto_msgTypes[6]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use AddSystemUser.ProtoReflect.Descriptor instead.
+func (*AddSystemUser) Descriptor() ([]byte, []int) {
+	return file_backend_operation_system_proto_rawDescGZIP(), []int{6}
+}
+
+func (x *AddSystemUser) GetId() int64 {
+	if x != nil {
+		return x.Id
+	}
+	return 0
+}
+
+func (x *AddSystemUser) GetName() string {
+	if x != nil {
+		return x.Name
+	}
+	return ""
+}
+
+func (x *AddSystemUser) GetPhone() string {
+	if x != nil {
+		return x.Phone
+	}
+	return ""
+}
+
+func (x *AddSystemUser) GetRoleId() int64 {
+	if x != nil {
+		return x.RoleId
+	}
+	return 0
+}
+
+func (x *AddSystemUser) GetIsShow() IsShow_Kind {
+	if x != nil {
+		return x.IsShow
+	}
+	return IsShow_INVALID
+}
+
+func (x *AddSystemUser) GetEmployeeName() string {
+	if x != nil {
+		return x.EmployeeName
+	}
+	return ""
+}
+
 var File_backend_operation_system_proto protoreflect.FileDescriptor
 
 var file_backend_operation_system_proto_rawDesc = []byte{
@@ -444,8 +532,20 @@ var file_backend_operation_system_proto_rawDesc = []byte{
 	0x0a, 0x07, 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52,
 	0x06, 0x72, 0x6f, 0x6c, 0x65, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x72, 0x6f, 0x6c, 0x65, 0x5f,
 	0x6e, 0x61, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x6f, 0x6c, 0x65,
-	0x4e, 0x61, 0x6d, 0x65, 0x42, 0x0f, 0x5a, 0x0d, 0x2e, 0x3b, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74,
-	0x69, 0x6f, 0x6e, 0x50, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+	0x4e, 0x61, 0x6d, 0x65, 0x22, 0xc0, 0x01, 0x0a, 0x0d, 0x41, 0x64, 0x64, 0x53, 0x79, 0x73, 0x74,
+	0x65, 0x6d, 0x55, 0x73, 0x65, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01,
+	0x28, 0x03, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02,
+	0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x70, 0x68,
+	0x6f, 0x6e, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x70, 0x68, 0x6f, 0x6e, 0x65,
+	0x12, 0x17, 0x0a, 0x07, 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28,
+	0x03, 0x52, 0x06, 0x72, 0x6f, 0x6c, 0x65, 0x49, 0x64, 0x12, 0x37, 0x0a, 0x07, 0x69, 0x73, 0x5f,
+	0x73, 0x68, 0x6f, 0x77, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1e, 0x2e, 0x62, 0x61, 0x63,
+	0x6b, 0x65, 0x6e, 0x64, 0x2e, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x49,
+	0x73, 0x53, 0x68, 0x6f, 0x77, 0x2e, 0x4b, 0x69, 0x6e, 0x64, 0x52, 0x06, 0x69, 0x73, 0x53, 0x68,
+	0x6f, 0x77, 0x12, 0x23, 0x0a, 0x0d, 0x65, 0x6d, 0x70, 0x6c, 0x6f, 0x79, 0x65, 0x65, 0x5f, 0x6e,
+	0x61, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x65, 0x6d, 0x70, 0x6c, 0x6f,
+	0x79, 0x65, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x42, 0x0f, 0x5a, 0x0d, 0x2e, 0x3b, 0x6f, 0x70, 0x65,
+	0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
 }
 
 var (
@@ -460,7 +560,7 @@ func file_backend_operation_system_proto_rawDescGZIP() []byte {
 	return file_backend_operation_system_proto_rawDescData
 }
 
-var file_backend_operation_system_proto_msgTypes = make([]protoimpl.MessageInfo, 6)
+var file_backend_operation_system_proto_msgTypes = make([]protoimpl.MessageInfo, 7)
 var file_backend_operation_system_proto_goTypes = []interface{}{
 	(*AddRoleRequest)(nil),     // 0: backend.operation.AddRoleRequest
 	(*SearchRoleRequest)(nil),  // 1: backend.operation.SearchRoleRequest
@@ -468,16 +568,19 @@ var file_backend_operation_system_proto_goTypes = []interface{}{
 	(*AddMenuRequest)(nil),     // 3: backend.operation.AddMenuRequest
 	(*SystemToken)(nil),        // 4: backend.operation.SystemToken
 	(*UserAuth)(nil),           // 5: backend.operation.UserAuth
-	(*IsShow)(nil),             // 6: backend.operation.IsShow
+	(*AddSystemUser)(nil),      // 6: backend.operation.AddSystemUser
+	(*IsShow)(nil),             // 7: backend.operation.IsShow
+	(IsShow_Kind)(0),           // 8: backend.operation.IsShow.Kind
 }
 var file_backend_operation_system_proto_depIdxs = []int32{
-	6, // 0: backend.operation.AddRoleRequest.is_show:type_name -> backend.operation.IsShow
+	7, // 0: backend.operation.AddRoleRequest.is_show:type_name -> backend.operation.IsShow
 	0, // 1: backend.operation.SearchRoleResponse.list:type_name -> backend.operation.AddRoleRequest
-	2, // [2:2] is the sub-list for method output_type
-	2, // [2:2] is the sub-list for method input_type
-	2, // [2:2] is the sub-list for extension type_name
-	2, // [2:2] is the sub-list for extension extendee
-	0, // [0:2] is the sub-list for field type_name
+	8, // 2: backend.operation.AddSystemUser.is_show:type_name -> backend.operation.IsShow.Kind
+	3, // [3:3] is the sub-list for method output_type
+	3, // [3:3] is the sub-list for method input_type
+	3, // [3:3] is the sub-list for extension type_name
+	3, // [3:3] is the sub-list for extension extendee
+	0, // [0:3] is the sub-list for field type_name
 }
 
 func init() { file_backend_operation_system_proto_init() }
@@ -559,6 +662,18 @@ func file_backend_operation_system_proto_init() {
 				return nil
 			}
 		}
+		file_backend_operation_system_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*AddSystemUser); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
 	}
 	type x struct{}
 	out := protoimpl.TypeBuilder{
@@ -566,7 +681,7 @@ func file_backend_operation_system_proto_init() {
 			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
 			RawDescriptor: file_backend_operation_system_proto_rawDesc,
 			NumEnums:      0,
-			NumMessages:   6,
+			NumMessages:   7,
 			NumExtensions: 0,
 			NumServices:   0,
 		},