modified: main_dc/docker-compose.yml
modified: main_dc/yalarba/api_es/internal/handler/all_handlers.go new file: main_dc/yalarba/api_es/internal/handler/health.go modified: main_dc/yalarba/api_es/internal/router/router.go new file: main_dc/yalarba/api_es/internal/utils/formatTime.go new file: main_dc/yalarba/api_es/internal/utils/response.go new file: main_dc/yalarba/api_es/internal/utils/utils.go new file: main_dc/yalarba/api_es/internal/utils/validation.go add utils and health check heandlers into routes
This commit is contained in:
@@ -254,7 +254,7 @@ services:
|
|||||||
"--no-verbose",
|
"--no-verbose",
|
||||||
"--tries=1",
|
"--tries=1",
|
||||||
"--spider",
|
"--spider",
|
||||||
"http://localhost:8081/health",
|
"http://localhost:8088/health",
|
||||||
]
|
]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type AllHandler struct {
|
type AllHandler struct {
|
||||||
userHandler *UserHandler
|
userHandler *UserHandler
|
||||||
|
healthHandler *HealthHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAllHandler(db *gorm.DB, cfg *config.Config) *AllHandler {
|
func NewAllHandler(db *gorm.DB, cfg *config.Config) *AllHandler {
|
||||||
@@ -20,14 +21,19 @@ func NewAllHandler(db *gorm.DB, cfg *config.Config) *AllHandler {
|
|||||||
userService := service.NewUserService(userRepo, utils.NewJWTUtil(cfg.JWTSecret))
|
userService := service.NewUserService(userRepo, utils.NewJWTUtil(cfg.JWTSecret))
|
||||||
|
|
||||||
userHandler := NewUserHandler(userService)
|
userHandler := NewUserHandler(userService)
|
||||||
|
healthHandler := NewHealthHandler()
|
||||||
|
|
||||||
return &AllHandler{
|
return &AllHandler{
|
||||||
userHandler: userHandler,
|
userHandler: userHandler,
|
||||||
|
healthHandler: healthHandler,
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func (h *AllHandler) UserHandler() *UserHandler {
|
func (h *AllHandler) UserHandler() *UserHandler {
|
||||||
return h.userHandler
|
return h.userHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *AllHandler) HealthHandler() *HealthHandler {
|
||||||
|
return h.healthHandler
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"api_es/internal/utils"
|
||||||
|
|
||||||
|
)
|
||||||
|
|
||||||
|
type HealthHandler struct{}
|
||||||
|
|
||||||
|
func NewHealthHandler() *HealthHandler {
|
||||||
|
return &HealthHandler{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HealthHandler) HealthCheck(w http.ResponseWriter, r *http.Request) {
|
||||||
|
response := map[string]string{
|
||||||
|
"status": "ok",
|
||||||
|
"message": "Service is healthy",
|
||||||
|
}
|
||||||
|
utils.RespondWithJSON(w, http.StatusOK, response)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HealthHandler) Check(w http.ResponseWriter, r *http.Request) {
|
||||||
|
response := map[string]string{
|
||||||
|
"status": "ok",
|
||||||
|
"message": "API is working",
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.RespondWithJSON(w, http.StatusOK, response)
|
||||||
|
}
|
||||||
@@ -29,6 +29,12 @@ func SetupRouter(db *gorm.DB, config *config.Config) http.Handler {
|
|||||||
|
|
||||||
h := handler.NewAllHandler(db, config)
|
h := handler.NewAllHandler(db, config)
|
||||||
|
|
||||||
|
// Health routes
|
||||||
|
r.Route("/", func(r chi.Router) {
|
||||||
|
r.Get("/health", h.HealthHandler().HealthCheck)
|
||||||
|
r.Get("/check", h.HealthHandler().Check)
|
||||||
|
})
|
||||||
|
|
||||||
r.Route("/auth", func(r chi.Router) {
|
r.Route("/auth", func(r chi.Router) {
|
||||||
r.Post("/register", h.UserHandler().Register)
|
r.Post("/register", h.UserHandler().Register)
|
||||||
r.Post("/login", h.UserHandler().Login)
|
r.Post("/login", h.UserHandler().Login)
|
||||||
@@ -45,7 +51,7 @@ func SetupRouter(db *gorm.DB, config *config.Config) http.Handler {
|
|||||||
r.With(appMiddleware.AdminMiddleware).Get("/{id}", h.UserHandler().GetUser)
|
r.With(appMiddleware.AdminMiddleware).Get("/{id}", h.UserHandler().GetUser)
|
||||||
})
|
})
|
||||||
|
|
||||||
zapLogger.Debug("End setup rounting")
|
zapLogger.Debug("End setup routing")
|
||||||
|
|
||||||
// Логируем все зарегистрированные маршруты
|
// Логируем все зарегистрированные маршруты
|
||||||
routeLogger := logger.NewRouteLogger(baseLogger)
|
routeLogger := logger.NewRouteLogger(baseLogger)
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
// formatPace форматирует темп в строку "MM:SS"
|
||||||
|
func FormatPace(minutes, seconds int) string {
|
||||||
|
if seconds >= 60 {
|
||||||
|
minutes += seconds / 60
|
||||||
|
seconds = seconds % 60
|
||||||
|
}
|
||||||
|
return FormatTwoDigits(minutes) + ":" + FormatTwoDigits(seconds)
|
||||||
|
}
|
||||||
|
|
||||||
|
// formatTwoDigits форматирует число в двузначную строку
|
||||||
|
func FormatTwoDigits(num int) string {
|
||||||
|
if num < 10 {
|
||||||
|
return "0" + string(rune(num+'0'))
|
||||||
|
}
|
||||||
|
return string(rune(num/10+'0')) + string(rune(num%10+'0'))
|
||||||
|
}
|
||||||
|
|
||||||
|
// formatTime форматирует время в строку "MM:SS"
|
||||||
|
func FormatTime(minutes, seconds int) string {
|
||||||
|
if seconds >= 60 {
|
||||||
|
minutes += seconds / 60
|
||||||
|
seconds = seconds % 60
|
||||||
|
}
|
||||||
|
return FormatTwoDigits(minutes) + ":" + FormatTwoDigits(seconds)
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
// pkg/utils/response.go (дополнение)
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RespondWithValidationError отправляет ответ с ошибками валидации
|
||||||
|
func RespondWithValidationError(w http.ResponseWriter, validationError error) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
|
||||||
|
response := map[string]interface{}{
|
||||||
|
"error": "Validation failed",
|
||||||
|
"details": GetValidationErrors(validationError),
|
||||||
|
}
|
||||||
|
|
||||||
|
json.NewEncoder(w).Encode(response)
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RespondWithJSON(w http.ResponseWriter, statusCode int, data interface{}) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(statusCode)
|
||||||
|
json.NewEncoder(w).Encode(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func RespondWithError(w http.ResponseWriter, statusCode int, message string) {
|
||||||
|
RespondWithJSON(w, statusCode, map[string]string{"error": message})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecodeJSONBody декодирует JSON тело запроса
|
||||||
|
func DecodeJSONBody(w http.ResponseWriter, r *http.Request, dst interface{}) error {
|
||||||
|
if r.Header.Get("Content-Type") != "application/json" {
|
||||||
|
return errors.New("Content-Type header is not application/json")
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, 1048576) // 1MB limit
|
||||||
|
|
||||||
|
dec := json.NewDecoder(r.Body)
|
||||||
|
dec.DisallowUnknownFields()
|
||||||
|
|
||||||
|
err := dec.Decode(dst)
|
||||||
|
if err != nil {
|
||||||
|
var syntaxError *json.SyntaxError
|
||||||
|
var unmarshalTypeError *json.UnmarshalTypeError
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case errors.As(err, &syntaxError):
|
||||||
|
return fmt.Errorf("request body contains badly-formed JSON (at position %d)", syntaxError.Offset)
|
||||||
|
|
||||||
|
case errors.Is(err, io.ErrUnexpectedEOF):
|
||||||
|
return errors.New("request body contains badly-formed JSON")
|
||||||
|
|
||||||
|
case errors.As(err, &unmarshalTypeError):
|
||||||
|
return fmt.Errorf("request body contains an invalid value for the %q field (at position %d)", unmarshalTypeError.Field, unmarshalTypeError.Offset)
|
||||||
|
|
||||||
|
case strings.HasPrefix(err.Error(), "json: unknown field "):
|
||||||
|
fieldName := strings.TrimPrefix(err.Error(), "json: unknown field ")
|
||||||
|
return fmt.Errorf("request body contains unknown field %s", fieldName)
|
||||||
|
|
||||||
|
case errors.Is(err, io.EOF):
|
||||||
|
return errors.New("request body must not be empty")
|
||||||
|
|
||||||
|
case err.Error() == "http: request body too large":
|
||||||
|
return errors.New("request body must not be larger than 1MB")
|
||||||
|
|
||||||
|
default:
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = dec.Decode(&struct{}{})
|
||||||
|
if err != io.EOF {
|
||||||
|
return errors.New("request body must only contain a single JSON object")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserIDFromContext извлекает userID из контекста
|
||||||
|
func GetUserIDFromContext(r *http.Request) (uint, bool) {
|
||||||
|
userID, ok := r.Context().Value("userID").(uint)
|
||||||
|
return userID, ok
|
||||||
|
}
|
||||||
@@ -0,0 +1,398 @@
|
|||||||
|
// 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"
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user