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 }