create and moove into new directories for BegushiyBashkir and

yalarbacreate and moove into new directories for BegushiyBashkir and
yalarbacreate and moove into new directories for BegushiyBashkir and
yalarbacreate and moove into new directories for BegushiyBashkir and
yalarbacreate and moove into new directories for BegushiyBashkir and
yalarbacreate and moove into new directories for BegushiyBashkir and
yalarbacreate and moove into new directories for BegushiyBashkir and
yalarbacreate and moove into new directories for BegushiyBashkir and
yalarbacreate and moove into new directories for BegushiyBashkir and
yalarba
This commit is contained in:
2025-10-24 05:22:44 +05:00
parent 358c14428f
commit 15357fd3c0
211 changed files with 3 additions and 3 deletions
+27
View File
@@ -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)
}
+20
View File
@@ -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)
}
+75
View File
@@ -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
}
+398
View File
@@ -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"
}