Skip to content
0

Validation

介绍

Validation 是 MiladyStack 框架中的一个验证包,提供灵活的请求参数验证机制。该包通过反射机制实现了动态的验证函数注册和调用,支持结构体字段的必需性验证以及自定义验证规则的应用。

主要功能包括:

  • 自动注册和调用命名规则为 Validate{TypeName} 的验证方法
  • 验证结构体中的必需字段是否存在且不为空
  • 基于规则的结构体字段验证
  • 支持选择特定字段进行验证
  • 获取结构体中所有可导出字段的名称

代码演示

基本使用

go
package main

import (
	"context"
	"errors"
	"fmt"

	"github.com/miladystack/miladystack/pkg/validation"
)

// UserRegisterIn 用户注册请求结构体
type UserRegisterIn struct {
	Username string   `json:"username"`
	Password string   `json:"password"`
	Profile  *Profile `json:"profile"`
}

type Profile struct {
	Email string `json:"email"`
}

// CustomValidator 自定义验证器结构体
type CustomValidator struct{}

// ValidateUserRegisterIn 验证用户注册请求
func (v *CustomValidator) ValidateUserRegisterIn(ctx context.Context, req *UserRegisterIn) error {
	// 验证必需字段, 只支持 chan, func, interface, map, pointer, or slice value
	if err := validation.ValidRequired(req, "Profile"); err != nil {
		return err
	}

	// 自定义验证逻辑
	if len(req.Password) < 6 {
		return errors.New("password must be at least 6 characters long")
	}

	return nil
}

func main() {
	// 创建自定义验证器实例
	customValidator := &CustomValidator{}

	// 创建验证器
	validator := validation.NewValidator(customValidator)

	// 验证用户注册请求
	registerReq := &UserRegisterIn{
		Username: "user1",
		Password: "123456",
		Profile: &Profile{
			Email: "[email protected]",
		},
	}

	err := validator.Validate(context.Background(), registerReq)
	if err != nil {
		fmt.Printf("Validation failed: %v\n", err)
	} else {
		fmt.Println("UserRegisterIn validation passed")
	}
}

使用规则验证

go
package main

import (
	"errors"
	"fmt"
	"strings"

	"github.com/miladystack/miladystack/pkg/validation"
)

// Product 产品结构体
type Product struct {
	Name        string  `json:"name"`
	Price       float64 `json:"price"`
	Description string  `json:"description"`
	SKU         string  `json:"sku"`
}

func main() {
	// 定义验证规则
	rules := validation.Rules{
		"Name": func(value any) error {
			name := value.(string)
			if len(name) < 2 {
				return errors.New("product name must be at least 2 characters long")
			}
			return nil
		},
		"Price": func(value any) error {
			price := value.(float64)
			if price <= 0 {
				return errors.New("product price must be greater than zero")
			}
			return nil
		},
		"SKU": func(value any) error {
			sku := value.(string)
			if !strings.HasPrefix(sku, "PROD-") {
				return errors.New("SKU must start with PROD-")
			}
			return nil
		},
	}
	
	// 验证产品
	product := Product{
		Name:        "Laptop",
		Price:       999.99,
		Description: "A powerful laptop",
		SKU:         "PROD-12345",
	}
	
	// 验证所有字段
	err := validation.ValidateAllFields(product, rules)
	if err != nil {
		fmt.Printf("Validation failed: %v\n", err)
	} else {
		fmt.Println("All fields validation passed")
	}
	
	// 只验证特定字段
	err = validation.ValidateSelectedFields(product, rules, "Name", "Price")
	if err != nil {
		fmt.Printf("Validation failed: %v\n", err)
	} else {
		fmt.Println("Selected fields validation passed")
	}
}

自定义验证器

自定义验证器允许用户根据业务需求实现特定的验证逻辑。以下是一个示例,展示了如何创建一个自定义验证器来验证用户注册请求。

go
package main

import (
	"context"
	"fmt"
	"regexp"

	"github.com/jinzhu/gorm"

	genericvalidation "github.com/miladystack/miladystack/pkg/validation"
)

func main() {
	// 创建模拟的数据库连接(实际使用中应替换为真实的数据库连接)
	db := (*gorm.DB)(nil)
	
	// 创建验证器实例
	validator := &Validator{
		db: db,
	}
	
	// 创建上下文,包含用户ID
	ctx := context.WithValue(context.Background(), userIDKey{}, "user-123")
	
	// 示例1:使用ValidateListUserRequest验证有效的用户列表请求
	validListReq := &ListUserRequest{
		Offset: 0,
		Limit:  10, // 有效的limit值
	}
	
	err := validator.ValidateListUserRequest(ctx, validListReq)
	if err != nil {
		fmt.Printf("验证失败(有效请求): %v\n", err)
	} else {
		fmt.Println("有效用户列表请求验证通过")
		fmt.Printf("查询参数: offset=%d, limit=%d\n", validListReq.Offset, validListReq.Limit)
	}
	
	// 示例2:使用ValidateListUserRequest验证无效的用户列表请求(limit <= 0)
	invalidListReq := &ListUserRequest{
		Offset: 0,
		Limit:  0, // 无效的limit值
	}
	
	err = validator.ValidateListUserRequest(ctx, invalidListReq)
	if err != nil {
		fmt.Printf("验证失败(无效请求): %v\n", err)
	} else {
		fmt.Println("无效用户列表请求验证通过")
	}
	
	// 示例3:使用其他验证函数
	fmt.Println("\n--- 其他验证函数示例 ---")
	
	// 登录请求验证
	loginReq := &LoginRequest{
		Username: "valid_user",
		Password: "Valid123",
	}
	if err := validator.ValidateLoginRequest(ctx, loginReq); err != nil {
		fmt.Printf("登录请求验证失败: %v\n", err)
	} else {
		fmt.Println("登录请求验证通过")
	}
	
	// 创建用户请求验证
	createReq := &CreateUserRequest{
		Username: "new_user123",
		Password: "NewPass123",
		Email:    "[email protected]",
		Phone:    "13800138000",
	}
	if err := validator.ValidateCreateUserRequest(ctx, createReq); err != nil {
		fmt.Printf("创建用户请求验证失败: %v\n", err)
	} else {
		fmt.Println("创建用户请求验证通过")
	}
	
	fmt.Println("\n所有验证示例执行完毕")
}

// 定义用于上下文的键.
type (
	// userIDKey 定义用户 ID 的上下文键.
	userIDKey struct{}
)

func UserID(ctx context.Context) string {
	userID, _ := ctx.Value(userIDKey{}).(string)
	return userID
}

// Validator 是验证逻辑的实现结构体.
type Validator struct {
	// 有些复杂的验证逻辑,可能需要直接查询数据库
	// 这里只是一个举例,如果验证时,有其他依赖的客户端/服务/资源等,
	// 都可以一并注入进来
	db *gorm.DB
}

// 使用预编译的全局正则表达式,避免重复创建和编译.
var (
	lengthRegex = regexp.MustCompile(`^.{3,20}$`)                                        // 长度在 3 到 20 个字符之间
	validRegex  = regexp.MustCompile(`^[A-Za-z0-9_]+$`)                                  // 仅包含字母、数字和下划线
	letterRegex = regexp.MustCompile(`[A-Za-z]`)                                         // 至少包含一个字母
	numberRegex = regexp.MustCompile(`\d`)                                               // 至少包含一个数字
	emailRegex  = regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`) // 邮箱格式
	phoneRegex  = regexp.MustCompile(`^1[3-9]\d{9}$`)                                    // 中国手机号
)

// isValidUsername 校验用户名是否合法.
func isValidUsername(username string) bool {
	// 校验长度
	if !lengthRegex.MatchString(username) {
		return false
	}
	// 校验字符合法性
	if !validRegex.MatchString(username) {
		return false
	}
	return true
}

// isValidPassword 判断密码是否符合复杂度要求.
func isValidPassword(password string) error {
	switch {
	// 检查新密码是否为空
	case password == "":
		return fmt.Errorf("password cannot be empty")
	// 检查新密码的长度要求
	case len(password) < 6:
		return fmt.Errorf("password must be at least 6 characters long")
	// 使用正则表达式检查是否至少包含一个字母
	case !letterRegex.MatchString(password):
		return fmt.Errorf("password must contain at least one letter")
	// 使用正则表达式检查是否至少包含一个数字
	case !numberRegex.MatchString(password):
		return fmt.Errorf("password must contain at least one number")
	}
	return nil
}

// isValidEmail 判断电子邮件是否合法.
func isValidEmail(email string) error {
	// 检查电子邮件地址格式
	if email == "" {
		return fmt.Errorf("email cannot be empty")
	}

	// 使用正则表达式校验电子邮件格式
	if !emailRegex.MatchString(email) {
		return fmt.Errorf("invalid email format")
	}

	return nil
}

// isValidPhone 判断手机号码是否合法.
func isValidPhone(phone string) error {
	// 检查手机号码格式
	if phone == "" {
		return fmt.Errorf("phone cannot be empty")
	}

	// 使用正则表达式校验手机号码格式(假设是中国手机号,11 位数字)
	if !phoneRegex.MatchString(phone) {
		return fmt.Errorf("invalid phone format")
	}

	return nil
}

// LoginRequest 表示登录请求
type LoginRequest struct {
	// username 表示用户名称
	Username string `json:"username,omitempty"`
	// password 表示用户密码
	Password string `json:"password,omitempty"`
}

// ChangePasswordRequest 表示修改密码请求
type ChangePasswordRequest struct {
	// userID 表示用户 ID
	UserID string `json:"userID,omitempty"`
	// oldPassword 表示当前密码
	OldPassword string `json:"oldPassword,omitempty"`
	// newPassword 表示准备修改的新密码
	NewPassword string `json:"newPassword,omitempty"`
}

func (x *ChangePasswordRequest) GetUserID() string {
	if x != nil {
		return x.UserID
	}
	return ""
}

// CreateUserRequest 表示创建用户请求
type CreateUserRequest struct {
	// username 表示用户名称
	Username string `json:"username,omitempty"`
	// password 表示用户密码
	Password string `json:"password,omitempty"`
	// nickname 表示用户昵称
	Nickname *string `json:"nickname,omitempty"`
	// email 表示用户电子邮箱
	Email string `json:"email,omitempty"`
	// phone 表示用户手机号
	Phone string `json:"phone,omitempty"`
}

// UpdateUserRequest 表示更新用户请求
type UpdateUserRequest struct {
	// userID 表示用户 ID
	UserID string `json:"userID,omitempty"`
	// username 表示可选的用户名称
	Username *string `json:"username,omitempty"`
	// nickname 表示可选的用户昵称
	Nickname *string `json:"nickname,omitempty"`
	// email 表示可选的用户电子邮箱
	Email *string `json:"email,omitempty"`
	// phone 表示可选的用户手机号
	Phone *string `json:"phone,omitempty"`
}

func (x *UpdateUserRequest) GetUserID() string {
	if x != nil {
		return x.UserID
	}
	return ""
}

// DeleteUserRequest 表示删除用户请求
type DeleteUserRequest struct {
	// userID 表示用户 ID
	// @gotags: uri:"userID"
	UserID string `json:"userID,omitempty" uri:"userID"`
}

// GetUserRequest 表示获取用户请求
type GetUserRequest struct {
	// userID 表示用户 ID
	// @gotags: uri:"userID"
	UserID string `json:"userID,omitempty" uri:"userID"`
}

func (x *GetUserRequest) GetUserID() string {
	if x != nil {
		return x.UserID
	}
	return ""
}

// ListUserRequest 表示用户列表请求
type ListUserRequest struct {
	// offset 表示偏移量
	// @gotags: form:"offset"
	Offset int64 `json:"offset,omitempty" form:"offset"`
	// limit 表示每页数量
	// @gotags: form:"limit"
	Limit int64 `json:"limit,omitempty" form:"limit"`
}

func (v *Validator) ValidateUserRules() genericvalidation.Rules {
	// 通用的密码校验函数
	validatePassword := func() genericvalidation.ValidatorFunc {
		return func(value any) error {
			return isValidPassword(value.(string))
		}
	}

	// 定义各字段的校验逻辑,通过一个 map 实现模块化和简化
	return genericvalidation.Rules{
		"Password":    validatePassword(),
		"OldPassword": validatePassword(),
		"NewPassword": validatePassword(),
		"UserID": func(value any) error {
			if value.(string) == "" {
				return fmt.Errorf("userID cannot be empty")
			}
			return nil
		},
		"Username": func(value any) error {
			if !isValidUsername(value.(string)) {
				return fmt.Errorf("Invalid username: Username must consist of letters, digits, and underscores only, and its length must be between 3 and 20 characters.")
			}
			return nil
		},
		"Nickname": func(value any) error {
			if len(value.(string)) >= 30 {
				return fmt.Errorf("nickname must be less than 30 characters")
			}
			return nil
		},
		"Email": func(value any) error {
			return isValidEmail(value.(string))
		},
		"Phone": func(value any) error {
			return isValidPhone(value.(string))
		},
		"Limit": func(value any) error {
			if value.(int64) <= 0 {
				return fmt.Errorf("limit must be greater than 0")
			}
			return nil
		},
		"Offset": func(value any) error {
			return nil
		},
	}
}

// ValidateLoginRequest 校验修改密码请求.
func (v *Validator) ValidateLoginRequest(ctx context.Context, rq *LoginRequest) error {
	return genericvalidation.ValidateAllFields(rq, v.ValidateUserRules())
}

// ValidateChangePasswordRequest 校验 ChangePasswordRequest 结构体的有效性.
func (v *Validator) ValidateChangePasswordRequest(ctx context.Context, rq *ChangePasswordRequest) error {
	if rq.GetUserID() != UserID(ctx) {
		return fmt.Errorf("The logged-in user `%s` does not match request user `%s`", UserID(ctx), rq.GetUserID())
	}
	return genericvalidation.ValidateAllFields(rq, v.ValidateUserRules())
}

// ValidateCreateUserRequest 校验 CreateUserRequest 结构体的有效性.
func (v *Validator) ValidateCreateUserRequest(ctx context.Context, rq *CreateUserRequest) error {
	return genericvalidation.ValidateAllFields(rq, v.ValidateUserRules())
}

// ValidateUpdateUserRequest 校验更新用户请求.
func (v *Validator) ValidateUpdateUserRequest(ctx context.Context, rq *UpdateUserRequest) error {
	if rq.GetUserID() != UserID(ctx) {
		return fmt.Errorf("The logged-in user `%s` does not match request user `%s`", UserID(ctx), rq.GetUserID())
	}
	return genericvalidation.ValidateSelectedFields(rq, v.ValidateUserRules(), "UserID")
}

// ValidateDeleteUserRequest 校验 DeleteUserRequest 结构体的有效性.
func (v *Validator) ValidateDeleteUserRequest(ctx context.Context, rq *DeleteUserRequest) error {
	return genericvalidation.ValidateAllFields(rq, v.ValidateUserRules())
}

// ValidateGetUserRequest 校验 GetUserRequest 结构体的有效性.
func (v *Validator) ValidateGetUserRequest(ctx context.Context, rq *GetUserRequest) error {
	if rq.GetUserID() != UserID(ctx) {
		return fmt.Errorf("The logged-in user `%s` does not match request user `%s`", UserID(ctx), rq.GetUserID())
	}
	return genericvalidation.ValidateAllFields(rq, v.ValidateUserRules())
}

// ValidateListUserRequest 校验 ListUserRequest 结构体的有效性.
func (v *Validator) ValidateListUserRequest(ctx context.Context, rq *ListUserRequest) error {
	return genericvalidation.ValidateAllFields(rq, v.ValidateUserRules())
}

方法

NewValidator(customValidator any) *Validator

创建并初始化一个自定义验证器。

参数:

  • customValidator any - 包含验证方法的结构体实例

返回值:

  • *Validator - 初始化后的验证器实例

(v *Validator) Validate(ctx context.Context, request any) error

使用适当的验证方法验证请求。

参数:

  • ctx context.Context - 上下文
  • request any - 要验证的请求对象指针

返回值:

  • error - 如果验证失败,返回错误;否则返回nil

ValidRequired(obj any, requiredFields ...string) error

验证结构体中的必需字段是否存在且不为空,只支持 chan, func, interface, map, pointer, or slice value。

参数:

  • obj any - 结构体或结构体指针
  • requiredFields ...string - 必需字段名称列表

返回值:

  • error - 如果任何必需字段为空或不存在,返回错误;否则返回nil

ValidateAllFields(obj any, rules Rules) error

验证结构体的所有字段是否符合规则。

参数:

  • obj any - 结构体或结构体指针
  • rules Rules - 验证规则映射

返回值:

  • error - 如果任何字段验证失败,返回错误;否则返回nil

ValidateSelectedFields(obj any, rules Rules, fields ...string) error

验证结构体中选定的字段是否符合规则。

参数:

  • obj any - 结构体或结构体指针
  • rules Rules - 验证规则映射
  • fields ...string - 要验证的字段名称列表

返回值:

  • error - 如果任何选定字段验证失败,返回错误;否则返回nil

GetExportedFieldNames(obj any) []string

返回传入结构体中所有可导出的字段名字。

参数:

  • obj any - 结构体或结构体指针

返回值:

  • []string - 可导出字段名称列表

功能特性

  1. 自动方法注册:通过反射自动注册符合命名规则的验证方法
  2. 类型安全验证:基于请求类型调用对应的验证方法
  3. 必需字段验证:便捷地验证结构体必需字段
  4. 灵活的规则验证:支持基于函数的自定义验证规则
  5. 选择性验证:可以只验证结构体中的特定字段
  6. 反射机制:利用Go语言的反射机制实现动态验证
  7. 错误处理:提供清晰的错误信息,便于调试和用户反馈
  8. 与上下文集成:验证方法支持上下文参数,可以传递额外信息或实现超时控制
最近更新