spark-server/internal/pkg/validate/validate.go
2026-09-22 17:07:26 +08:00

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
}