111 lines
2.7 KiB
Go
111 lines
2.7 KiB
Go
package validate
|
|
|
|
import (
|
|
"errors"
|
|
"reflect"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/go-playground/validator/v10"
|
|
|
|
"spark-server/internal/pkg/errs"
|
|
"spark-server/internal/pkg/i18n"
|
|
)
|
|
|
|
var (
|
|
once sync.Once
|
|
v *validator.Validate
|
|
)
|
|
|
|
func engine() *validator.Validate {
|
|
once.Do(func() {
|
|
v = validator.New(validator.WithRequiredStructEnabled())
|
|
v.RegisterTagNameFunc(func(f reflect.StructField) string {
|
|
if l := f.Tag.Get("label"); l != "" {
|
|
return l
|
|
}
|
|
name, _, _ := strings.Cut(f.Tag.Get("json"), ",")
|
|
if name != "" && name != "-" {
|
|
return name
|
|
}
|
|
return f.Name
|
|
})
|
|
})
|
|
return v
|
|
}
|
|
|
|
// Struct validates v in the default language (en). Prefer StructLang with the request language.
|
|
func Struct(val any) error {
|
|
return StructLang(i18n.Default, val)
|
|
}
|
|
|
|
// StructLang validates v. `label` / `errmsg` are i18n keys (literal text still works as fallback).
|
|
// errmsg example: `errmsg:"required:validate.login.phone_required"`
|
|
func StructLang(lang i18n.Lang, val any) error {
|
|
if val == nil {
|
|
return errs.ErrInvalidArgument.WithMsg(i18n.T(lang, i18n.KeyValidateFailed, nil))
|
|
}
|
|
err := engine().Struct(val)
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
var ves validator.ValidationErrors
|
|
if !errors.As(err, &ves) {
|
|
return errs.ErrInvalidArgument.WithMsg(i18n.T(lang, i18n.KeyValidateFailed, nil))
|
|
}
|
|
msgs := make([]string, 0, len(ves))
|
|
for _, fe := range ves {
|
|
msgs = append(msgs, fieldMessage(lang, val, fe))
|
|
}
|
|
return errs.ErrInvalidArgument.WithMsg(strings.Join(msgs, "; "))
|
|
}
|
|
|
|
func fieldMessage(lang i18n.Lang, root any, fe validator.FieldError) string {
|
|
label := i18n.T(lang, fe.Field(), nil)
|
|
if custom := errmsgFor(root, fe.StructField(), fe.Tag()); custom != "" {
|
|
return i18n.T(lang, custom, map[string]string{"label": label, "param": fe.Param()})
|
|
}
|
|
return defaultMessage(lang, label, fe)
|
|
}
|
|
|
|
func errmsgFor(root any, structField, tag string) string {
|
|
sf, ok := findField(reflect.TypeOf(root), structField)
|
|
if !ok {
|
|
return ""
|
|
}
|
|
raw := sf.Tag.Get("errmsg")
|
|
if raw == "" {
|
|
return ""
|
|
}
|
|
for _, part := range strings.Split(raw, ";") {
|
|
k, v, found := strings.Cut(strings.TrimSpace(part), ":")
|
|
if found && k == tag {
|
|
return v
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func findField(t reflect.Type, name string) (reflect.StructField, bool) {
|
|
for t.Kind() == reflect.Pointer {
|
|
t = t.Elem()
|
|
}
|
|
if t.Kind() != reflect.Struct {
|
|
return reflect.StructField{}, false
|
|
}
|
|
sf, ok := t.FieldByName(name)
|
|
return sf, ok
|
|
}
|
|
|
|
func defaultMessage(lang i18n.Lang, label string, fe validator.FieldError) string {
|
|
key := "validate." + fe.Tag()
|
|
if lookupEmpty(lang, key) {
|
|
key = i18n.KeyValidateDefault
|
|
}
|
|
return i18n.T(lang, key, map[string]string{"label": label, "param": fe.Param()})
|
|
}
|
|
|
|
func lookupEmpty(lang i18n.Lang, key string) bool {
|
|
return i18n.T(lang, key, nil) == key
|
|
}
|