Build and publish Docker images / Build and publish images (push) Failing after 25s
351 lines
9.5 KiB
Go
351 lines
9.5 KiB
Go
package validator
|
||
|
||
import (
|
||
"errors"
|
||
"fmt"
|
||
"reflect"
|
||
"strings"
|
||
"sync"
|
||
|
||
"github.com/gin-gonic/gin/binding"
|
||
playground "github.com/go-playground/validator/v10"
|
||
)
|
||
|
||
// Validate holds the validator instance.
|
||
var validate *playground.Validate
|
||
|
||
// ValidationError keeps the original validator error for errors.As while
|
||
// exposing a user-facing message instead of validator's internal English text.
|
||
type ValidationError struct {
|
||
cause error
|
||
}
|
||
|
||
func (e *ValidationError) Error() string {
|
||
return validationMessage(e.cause)
|
||
}
|
||
|
||
func (e *ValidationError) Unwrap() error {
|
||
return e.cause
|
||
}
|
||
|
||
// Init initializes the global validator.
|
||
func Init() {
|
||
validate = newValidator("")
|
||
}
|
||
|
||
// ValidateStruct validates a struct using go-playground/validator tags.
|
||
func ValidateStruct(s any) error {
|
||
if validate == nil {
|
||
Init()
|
||
}
|
||
err := validate.Struct(s)
|
||
return wrapValidationError(err)
|
||
}
|
||
|
||
// Var validates a single variable.
|
||
func Var(field any, tag string) error {
|
||
if validate == nil {
|
||
Init()
|
||
}
|
||
err := validate.Var(field, tag)
|
||
return wrapValidationError(err)
|
||
}
|
||
|
||
// ValidationMessage returns a safe, localized message for a validation error.
|
||
func ValidationMessage(err error) string {
|
||
if err == nil {
|
||
return ""
|
||
}
|
||
if localized, ok := err.(*ValidationError); ok {
|
||
return validationMessage(localized.cause)
|
||
}
|
||
var localized *ValidationError
|
||
if errors.As(err, &localized) {
|
||
return validationMessage(localized.cause)
|
||
}
|
||
return validationMessage(err)
|
||
}
|
||
|
||
// ValidationMessageText removes validator's raw diagnostic format from a
|
||
// message that has already been converted to a string by a caller.
|
||
func ValidationMessageText(message string) string {
|
||
message = strings.TrimSpace(message)
|
||
if message == "" {
|
||
return message
|
||
}
|
||
lower := strings.ToLower(message)
|
||
for _, prefix := range []string{"validation error:", "validation:"} {
|
||
if strings.HasPrefix(lower, prefix) {
|
||
message = strings.TrimSpace(message[len(prefix):])
|
||
lower = strings.ToLower(message)
|
||
break
|
||
}
|
||
}
|
||
if strings.Contains(lower, "field validation for") && strings.Contains(lower, "failed on the '") {
|
||
return "请求参数不符合要求"
|
||
}
|
||
return message
|
||
}
|
||
|
||
// IsValidationError reports whether err came from go-playground validation,
|
||
// including errors wrapped by a service.
|
||
func IsValidationError(err error) bool {
|
||
if err == nil {
|
||
return false
|
||
}
|
||
var localized *ValidationError
|
||
if errors.As(err, &localized) {
|
||
return true
|
||
}
|
||
var fieldErrors playground.ValidationErrors
|
||
if errors.As(err, &fieldErrors) {
|
||
return true
|
||
}
|
||
var invalidValidation *playground.InvalidValidationError
|
||
if errors.As(err, &invalidValidation) {
|
||
return true
|
||
}
|
||
var sliceErrors binding.SliceValidationError
|
||
return errors.As(err, &sliceErrors)
|
||
}
|
||
|
||
type ginStructValidator struct {
|
||
once sync.Once
|
||
validate *playground.Validate
|
||
}
|
||
|
||
var _ binding.StructValidator = (*ginStructValidator)(nil)
|
||
|
||
// init replaces Gin's default validator so every ShouldBind* call gets the
|
||
// same field names and user-facing messages as service-layer validation.
|
||
func init() {
|
||
binding.Validator = &ginStructValidator{}
|
||
}
|
||
|
||
func (v *ginStructValidator) ValidateStruct(obj any) error {
|
||
if obj == nil {
|
||
return nil
|
||
}
|
||
|
||
value := reflect.ValueOf(obj)
|
||
switch value.Kind() {
|
||
case reflect.Ptr:
|
||
if value.IsNil() {
|
||
return nil
|
||
}
|
||
if value.Elem().Kind() != reflect.Struct {
|
||
return v.ValidateStruct(value.Elem().Interface())
|
||
}
|
||
return v.validateStruct(obj)
|
||
case reflect.Struct:
|
||
return v.validateStruct(obj)
|
||
case reflect.Slice, reflect.Array:
|
||
validationErrors := make(binding.SliceValidationError, 0)
|
||
for i := 0; i < value.Len(); i++ {
|
||
if err := v.ValidateStruct(value.Index(i).Interface()); err != nil {
|
||
validationErrors = append(validationErrors, err)
|
||
}
|
||
}
|
||
if len(validationErrors) > 0 {
|
||
return validationErrors
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (v *ginStructValidator) validateStruct(obj any) error {
|
||
v.once.Do(func() {
|
||
v.validate = newValidator("binding")
|
||
})
|
||
return wrapValidationError(v.validate.Struct(obj))
|
||
}
|
||
|
||
func (v *ginStructValidator) Engine() any {
|
||
v.once.Do(func() {
|
||
v.validate = newValidator("binding")
|
||
})
|
||
return v.validate
|
||
}
|
||
|
||
func newValidator(tagName string) *playground.Validate {
|
||
v := playground.New()
|
||
if tagName != "" {
|
||
v.SetTagName(tagName)
|
||
}
|
||
v.RegisterTagNameFunc(func(field reflect.StructField) string {
|
||
for _, tagName := range []string{"json", "form", "uri"} {
|
||
name, _, _ := strings.Cut(field.Tag.Get(tagName), ",")
|
||
if name != "" && name != "-" {
|
||
return name
|
||
}
|
||
}
|
||
return field.Name
|
||
})
|
||
return v
|
||
}
|
||
|
||
func wrapValidationError(err error) error {
|
||
if err == nil {
|
||
return nil
|
||
}
|
||
return &ValidationError{cause: err}
|
||
}
|
||
|
||
func validationMessage(err error) string {
|
||
if err == nil {
|
||
return ""
|
||
}
|
||
var fieldErrors playground.ValidationErrors
|
||
if errors.As(err, &fieldErrors) {
|
||
messages := make([]string, 0, len(fieldErrors))
|
||
seen := make(map[string]struct{}, len(fieldErrors))
|
||
for _, fieldError := range fieldErrors {
|
||
message := formatFieldError(fieldError)
|
||
if _, ok := seen[message]; ok {
|
||
continue
|
||
}
|
||
seen[message] = struct{}{}
|
||
messages = append(messages, message)
|
||
}
|
||
if len(messages) > 0 {
|
||
return strings.Join(messages, ";")
|
||
}
|
||
}
|
||
var sliceErrors binding.SliceValidationError
|
||
if errors.As(err, &sliceErrors) {
|
||
messages := make([]string, 0, len(sliceErrors))
|
||
for _, item := range sliceErrors {
|
||
if item != nil {
|
||
messages = append(messages, ValidationMessage(item))
|
||
}
|
||
}
|
||
if len(messages) > 0 {
|
||
return strings.Join(messages, ";")
|
||
}
|
||
return "请求参数不符合要求"
|
||
}
|
||
var invalidValidation *playground.InvalidValidationError
|
||
if errors.As(err, &invalidValidation) {
|
||
return "请求参数不符合要求"
|
||
}
|
||
return ValidationMessageText(err.Error())
|
||
}
|
||
|
||
func formatFieldError(err playground.FieldError) string {
|
||
field := fieldLabel(err.Field())
|
||
param := err.Param()
|
||
|
||
switch err.Tag() {
|
||
case "required", "required_if", "required_unless", "required_with", "required_with_all", "required_without", "required_without_all":
|
||
return "请填写" + field
|
||
case "email":
|
||
return "请输入有效的" + field
|
||
case "min":
|
||
if err.Kind() == reflect.String {
|
||
return fmt.Sprintf("%s长度不能少于 %s 个字符", field, param)
|
||
}
|
||
return fmt.Sprintf("%s不能小于 %s", field, param)
|
||
case "max":
|
||
if err.Kind() == reflect.String {
|
||
return fmt.Sprintf("%s长度不能超过 %s 个字符", field, param)
|
||
}
|
||
return fmt.Sprintf("%s不能大于 %s", field, param)
|
||
case "len":
|
||
if err.Kind() == reflect.String {
|
||
return fmt.Sprintf("%s长度必须为 %s 个字符", field, param)
|
||
}
|
||
return fmt.Sprintf("%s长度必须为 %s", field, param)
|
||
case "gte":
|
||
return fmt.Sprintf("%s不能小于 %s", field, param)
|
||
case "lte":
|
||
return fmt.Sprintf("%s不能大于 %s", field, param)
|
||
case "gt":
|
||
return fmt.Sprintf("%s必须大于 %s", field, param)
|
||
case "lt":
|
||
return fmt.Sprintf("%s必须小于 %s", field, param)
|
||
case "oneof":
|
||
return field + "取值不正确"
|
||
case "eqfield":
|
||
return field + "与" + fieldLabel(param) + "不一致"
|
||
case "nefield":
|
||
return field + "不能与" + fieldLabel(param) + "相同"
|
||
case "url", "uri", "http_url":
|
||
return "请输入有效的" + field
|
||
case "uuid", "uuid3", "uuid4", "uuid5", "uuid8", "json":
|
||
return field + "格式不正确"
|
||
case "numeric", "number", "integer", "boolean":
|
||
return field + "格式不正确"
|
||
case "excluded", "excluded_if", "excluded_unless", "excluded_with", "excluded_with_all", "excluded_without", "excluded_without_all":
|
||
return field + "不允许填写"
|
||
default:
|
||
return field + "格式不正确"
|
||
}
|
||
}
|
||
|
||
var fieldLabels = map[string]string{
|
||
"account_id": "账户",
|
||
"agent_id": "坐席",
|
||
"assignee_id": "负责人",
|
||
"avatar_url": "头像地址",
|
||
"client_id": "客户端 ID",
|
||
"confirmation_token": "确认令牌",
|
||
"contact_id": "联系人",
|
||
"conversation_id": "会话",
|
||
"conversation_ids": "会话",
|
||
"content": "内容",
|
||
"custom_attributes": "自定义属性",
|
||
"email": "邮箱",
|
||
"event_id": "事件 ID",
|
||
"external_url": "外部地址",
|
||
"file": "文件",
|
||
"inbox_id": "收件箱",
|
||
"identifier": "标识",
|
||
"identifier_hash": "标识校验值",
|
||
"label": "标签",
|
||
"locale": "语言",
|
||
"message": "消息",
|
||
"message_id": "消息",
|
||
"name": "名称",
|
||
"password": "密码",
|
||
"password_confirmation": "确认密码",
|
||
"phone_number": "手机号",
|
||
"priority": "优先级",
|
||
"refresh_token": "刷新令牌",
|
||
"reset_password_token": "重置密码令牌",
|
||
"role": "角色",
|
||
"source_id": "来源 ID",
|
||
"status": "状态",
|
||
"team_id": "团队",
|
||
"typing_status": "输入状态",
|
||
"user_id": "用户",
|
||
"user_ids": "用户",
|
||
"website_token": "网站令牌",
|
||
"widget_token": "组件令牌",
|
||
}
|
||
|
||
func fieldLabel(field string) string {
|
||
field = strings.TrimSpace(field)
|
||
if dot := strings.LastIndex(field, "."); dot >= 0 {
|
||
field = field[dot+1:]
|
||
}
|
||
if bracket := strings.IndexByte(field, '['); bracket >= 0 {
|
||
field = field[:bracket]
|
||
}
|
||
key := strings.ToLower(field)
|
||
if label, ok := fieldLabels[key]; ok {
|
||
return label
|
||
}
|
||
switch {
|
||
case strings.Contains(key, "password"):
|
||
return "密码"
|
||
case strings.Contains(key, "email"):
|
||
return "邮箱"
|
||
case strings.Contains(key, "token"):
|
||
return "令牌"
|
||
case strings.Contains(key, "url"):
|
||
return "地址"
|
||
default:
|
||
return "该字段"
|
||
}
|
||
}
|