Files
tp/serv_nginx/api_bb/pkg/utils/validation.go
T
valitovgaziz 42ead16848 modified: serv_nginx/api_bb/internal/database/migrate.go
modified:   serv_nginx/api_bb/internal/handlers/handlers.go
	new file:   serv_nginx/api_bb/internal/handlers/user_achievement_handler.go
	modified:   serv_nginx/api_bb/internal/routes/routes.go
	modified:   serv_nginx/api_bb/internal/service/achievement_service.go
	modified:   serv_nginx/api_bb/pkg/utils/validation.go
	modified:   serv_nginx/bbvue/src/views/Home.vue
add achievement's handler, routing, service, migrator gorm and update
repository
2025-10-19 09:17:03 +05:00

399 lines
10 KiB
Go

// pkg/utils/validation.go
package utils
import (
"fmt"
"reflect"
"regexp"
"strconv"
"strings"
"time"
"go.uber.org/zap"
)
// ValidationError представляет ошибку валидации
type ValidationError struct {
Field string `json:"field"`
Message string `json:"message"`
}
func (e ValidationError) Error() string {
return fmt.Sprintf("%s: %s", e.Field, e.Message)
}
// ValidationResult содержит результат валидации
type ValidationResult struct {
IsValid bool
Errors []ValidationError
}
// TagOptions содержит опции из тега validate
type TagOptions struct {
Required bool
Min *float64
Max *float64
MinInt *int64
MaxInt *int64
OneOf []string
Email bool
MaxLength *int
MinLength *int
Custom string
}
// ValidateStruct валидирует структуру на основе тегов validate
func ValidateStruct(s interface{}) error {
val := reflect.ValueOf(s)
if val.Kind() == reflect.Ptr {
val = val.Elem()
}
if val.Kind() != reflect.Struct {
return fmt.Errorf("ValidateStruct expects a struct, got %T", s)
}
var errors []ValidationError
typ := val.Type()
for i := 0; i < val.NumField(); i++ {
field := val.Field(i)
fieldType := typ.Field(i)
// Пропускаем неэкспортируемые поля
if !field.CanInterface() {
continue
}
tag := fieldType.Tag.Get("validate")
if tag == "" {
continue
}
options := parseTagOptions(tag)
fieldName := getFieldName(fieldType)
// Валидация поля
if err := validateField(field, fieldName, options); err != nil {
errors = append(errors, err...)
}
}
if len(errors) > 0 {
return &ValidationResult{
IsValid: false,
Errors: errors,
}
}
return nil
}
// parseTagOptions парсит тег validate и возвращает опции
func parseTagOptions(tag string) TagOptions {
options := TagOptions{}
parts := strings.Split(tag, ",")
for _, part := range parts {
part = strings.TrimSpace(part)
switch {
case part == "required":
options.Required = true
case part == "email":
options.Email = true
case strings.HasPrefix(part, "min="):
if val, err := strconv.ParseFloat(part[4:], 64); err == nil {
options.Min = &val
}
case strings.HasPrefix(part, "max="):
if val, err := strconv.ParseFloat(part[4:], 64); err == nil {
options.Max = &val
}
case strings.HasPrefix(part, "minint="):
if val, err := strconv.ParseInt(part[7:], 10, 64); err == nil {
options.MinInt = &val
}
case strings.HasPrefix(part, "maxint="):
if val, err := strconv.ParseInt(part[7:], 10, 64); err == nil {
options.MaxInt = &val
}
case strings.HasPrefix(part, "oneof="):
options.OneOf = strings.Split(part[6:], " ")
case strings.HasPrefix(part, "maxlen="):
if val, err := strconv.Atoi(part[7:]); err == nil {
options.MaxLength = &val
}
case strings.HasPrefix(part, "minlen="):
if val, err := strconv.Atoi(part[7:]); err == nil {
options.MinLength = &val
}
case strings.HasPrefix(part, "custom="):
options.Custom = part[7:]
}
}
return options
}
// getFieldName возвращает имя поля для сообщений об ошибках
func getFieldName(field reflect.StructField) string {
jsonTag := field.Tag.Get("json")
if jsonTag != "" {
parts := strings.Split(jsonTag, ",")
if parts[0] != "" {
return parts[0]
}
}
return field.Name
}
// validateField валидирует отдельное поле
func validateField(field reflect.Value, fieldName string, options TagOptions) []ValidationError {
var errors []ValidationError
// Проверка required
if options.Required {
if isEmptyValue(field) {
errors = append(errors, ValidationError{
Field: fieldName,
Message: "field is required",
})
return errors // Если поле обязательно и пустое, дальше не проверяем
}
}
// Если поле пустое и не обязательное, дальше не проверяем
if isEmptyValue(field) {
return errors
}
// Валидация в зависимости от типа поля
switch field.Kind() {
case reflect.String:
errors = append(errors, validateString(field.String(), fieldName, options)...)
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
errors = append(errors, validateInt(field.Int(), fieldName, options)...)
case reflect.Float32, reflect.Float64:
errors = append(errors, validateFloat(field.Float(), fieldName, options)...)
case reflect.Struct:
// Для time.Time и других структур
if field.Type().String() == "time.Time" {
errors = append(errors, validateTime(field.Interface().(time.Time), fieldName, options)...)
}
}
return errors
}
// validateString валидирует строковые поля
func validateString(value, fieldName string, options TagOptions) []ValidationError {
var errors []ValidationError
// Проверка email
if options.Email {
if !isValidEmail(value) {
errors = append(errors, ValidationError{
Field: fieldName,
Message: "invalid email format",
})
}
}
// Проверка длины строки
if options.MinLength != nil && len(value) < *options.MinLength {
errors = append(errors, ValidationError{
Field: fieldName,
Message: fmt.Sprintf("minimum length is %d characters", *options.MinLength),
})
}
if options.MaxLength != nil && len(value) > *options.MaxLength {
errors = append(errors, ValidationError{
Field: fieldName,
Message: fmt.Sprintf("maximum length is %d characters", *options.MaxLength),
})
}
// Проверка oneof
if len(options.OneOf) > 0 {
valid := false
for _, allowed := range options.OneOf {
if value == allowed {
valid = true
break
}
}
if !valid {
errors = append(errors, ValidationError{
Field: fieldName,
Message: fmt.Sprintf("must be one of: %s", strings.Join(options.OneOf, ", ")),
})
}
}
return errors
}
// validateInt валидирует целочисленные поля
func validateInt(value int64, fieldName string, options TagOptions) []ValidationError {
var errors []ValidationError
if options.MinInt != nil && value < *options.MinInt {
errors = append(errors, ValidationError{
Field: fieldName,
Message: fmt.Sprintf("minimum value is %d", *options.MinInt),
})
}
if options.MaxInt != nil && value > *options.MaxInt {
errors = append(errors, ValidationError{
Field: fieldName,
Message: fmt.Sprintf("maximum value is %d", *options.MaxInt),
})
}
return errors
}
// validateFloat валидирует поля с плавающей точкой
func validateFloat(value float64, fieldName string, options TagOptions) []ValidationError {
var errors []ValidationError
if options.Min != nil && value < *options.Min {
errors = append(errors, ValidationError{
Field: fieldName,
Message: fmt.Sprintf("minimum value is %.2f", *options.Min),
})
}
if options.Max != nil && value > *options.Max {
errors = append(errors, ValidationError{
Field: fieldName,
Message: fmt.Sprintf("maximum value is %.2f", *options.Max),
})
}
return errors
}
// validateTime валидирует временные поля
func validateTime(value time.Time, fieldName string, options TagOptions) []ValidationError {
var errors []ValidationError
// Проверка, что дата не нулевая
if value.IsZero() && options.Required {
errors = append(errors, ValidationError{
Field: fieldName,
Message: "date is required",
})
}
// Проверка, что дата не в будущем (пример кастомной валидации)
if options.Custom == "not_future" && value.After(time.Now()) {
errors = append(errors, ValidationError{
Field: fieldName,
Message: "date cannot be in the future",
})
}
return errors
}
// isEmptyValue проверяет, является ли значение пустым
func isEmptyValue(v reflect.Value) bool {
switch v.Kind() {
case reflect.String:
return v.String() == ""
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return v.Int() == 0
case reflect.Float32, reflect.Float64:
return v.Float() == 0
case reflect.Bool:
return !v.Bool()
case reflect.Struct:
if v.Type().String() == "time.Time" {
return v.Interface().(time.Time).IsZero()
}
case reflect.Ptr, reflect.Interface:
return v.IsNil()
case reflect.Slice, reflect.Map, reflect.Array:
return v.Len() == 0
}
return false
}
// isValidEmail проверяет валидность email
func isValidEmail(email string) bool {
emailRegex := `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`
matched, _ := regexp.MatchString(emailRegex, email)
return matched
}
// Error возвращает строковое представление ошибок валидации
func (vr *ValidationResult) Error() string {
var errorMessages []string
for _, err := range vr.Errors {
errorMessages = append(errorMessages, err.Error())
}
return strings.Join(errorMessages, "; ")
}
// GetValidationErrors возвращает ошибки валидации в структурированном виде
func GetValidationErrors(err error) []ValidationError {
if vr, ok := err.(*ValidationResult); ok {
return vr.Errors
}
return nil
}
// LogValidationErrors логирует ошибки валидации
func LogValidationErrors(logger *zap.Logger, err error, context string) {
if vr, ok := err.(*ValidationResult); ok {
for _, validationErr := range vr.Errors {
logger.Warn("validation error",
zap.String("context", context),
zap.String("field", validationErr.Field),
zap.String("error", validationErr.Message),
)
}
}
}
// ParseUintFromQuery парсит uint из query параметра
func ParseUintFromQuery(queryParam string, defaultValue uint) (uint, error) {
if queryParam == "" {
return defaultValue, nil
}
value, err := strconv.ParseUint(queryParam, 10, 32)
if err != nil {
return defaultValue, err
}
return uint(value), nil
}
// ParseIntFromQuery парсит int из query параметра
func ParseIntFromQuery(queryParam string, defaultValue int) (int, error) {
if queryParam == "" {
return defaultValue, nil
}
value, err := strconv.Atoi(queryParam)
if err != nil {
return defaultValue, err
}
return value, nil
}
// ParseBoolFromQuery парсит bool из query параметра
func ParseBoolFromQuery(queryParam string, defaultValue bool) bool {
if queryParam == "" {
return defaultValue
}
return strings.ToLower(queryParam) == "true" || queryParam == "1"
}