Files
MyClub/internal/controllers/validation_helper.go
T
Tomas Dvorak 9ccca365b3 dev day #65
2025-10-19 18:09:28 +02:00

172 lines
5.2 KiB
Go

package controllers
import (
"fmt"
"regexp"
"strings"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
)
// ValidationHelper provides validation utilities
type ValidationHelper struct {
validator *validator.Validate
}
// ValidationError represents a validation error
type ValidationError struct {
Field string `json:"field"`
Message string `json:"message"`
Tag string `json:"tag"`
}
// NewValidationHelper creates a new ValidationHelper instance
func NewValidationHelper() *ValidationHelper {
v := validator.New()
// Register custom validators
_ = v.RegisterValidation("slug", validateSlug)
_ = v.RegisterValidation("color", validateColor)
return &ValidationHelper{
validator: v,
}
}
// Validate validates a struct and returns user-friendly errors
func (vh *ValidationHelper) Validate(data interface{}) []ValidationError {
err := vh.validator.Struct(data)
if err == nil {
return nil
}
var validationErrors []ValidationError
for _, err := range err.(validator.ValidationErrors) {
validationErrors = append(validationErrors, ValidationError{
Field: strings.ToLower(err.Field()),
Message: vh.getErrorMessage(err),
Tag: err.Tag(),
})
}
return validationErrors
}
// ValidateAndRespond validates data and sends error response if invalid
// Returns true if valid, false if invalid (and response already sent)
func (vh *ValidationHelper) ValidateAndRespond(c *gin.Context, data interface{}) bool {
errors := vh.Validate(data)
if len(errors) > 0 {
Respond.ValidationError(c, errors)
return false
}
return true
}
// getErrorMessage returns a user-friendly error message for a validation error
func (vh *ValidationHelper) getErrorMessage(err validator.FieldError) string {
field := err.Field()
switch err.Tag() {
case "required":
return fmt.Sprintf("%s is required", field)
case "email":
return fmt.Sprintf("%s must be a valid email address", field)
case "min":
return fmt.Sprintf("%s must be at least %s characters", field, err.Param())
case "max":
return fmt.Sprintf("%s must be at most %s characters", field, err.Param())
case "len":
return fmt.Sprintf("%s must be exactly %s characters", field, err.Param())
case "url":
return fmt.Sprintf("%s must be a valid URL", field)
case "slug":
return fmt.Sprintf("%s must be a valid slug (lowercase, alphanumeric, hyphens)", field)
case "color":
return fmt.Sprintf("%s must be a valid hex color code", field)
case "oneof":
return fmt.Sprintf("%s must be one of: %s", field, err.Param())
case "gte":
return fmt.Sprintf("%s must be greater than or equal to %s", field, err.Param())
case "lte":
return fmt.Sprintf("%s must be less than or equal to %s", field, err.Param())
case "gt":
return fmt.Sprintf("%s must be greater than %s", field, err.Param())
case "lt":
return fmt.Sprintf("%s must be less than %s", field, err.Param())
default:
return fmt.Sprintf("%s is invalid", field)
}
}
// IsValidEmail checks if a string is a valid email
func (vh *ValidationHelper) IsValidEmail(email string) bool {
return vh.validator.Var(email, "required,email") == nil
}
// IsValidURL checks if a string is a valid URL
func (vh *ValidationHelper) IsValidURL(url string) bool {
return vh.validator.Var(url, "required,url") == nil
}
// IsValidSlug checks if a string is a valid slug
func (vh *ValidationHelper) IsValidSlug(slug string) bool {
return vh.validator.Var(slug, "required,slug") == nil
}
// SanitizeString removes leading/trailing whitespace and normalizes spaces
func (vh *ValidationHelper) SanitizeString(s string) string {
// Trim whitespace
s = strings.TrimSpace(s)
// Replace multiple spaces with single space
s = regexp.MustCompile(`\s+`).ReplaceAllString(s, " ")
return s
}
// SanitizeEmail normalizes an email address
func (vh *ValidationHelper) SanitizeEmail(email string) string {
return strings.ToLower(strings.TrimSpace(email))
}
// SanitizeSlug normalizes a slug
func (vh *ValidationHelper) SanitizeSlug(slug string) string {
slug = strings.ToLower(strings.TrimSpace(slug))
// Replace spaces with hyphens
slug = regexp.MustCompile(`\s+`).ReplaceAllString(slug, "-")
// Remove non-alphanumeric characters except hyphens
slug = regexp.MustCompile(`[^a-z0-9-]`).ReplaceAllString(slug, "")
// Replace multiple hyphens with single hyphen
slug = regexp.MustCompile(`-+`).ReplaceAllString(slug, "-")
// Remove leading/trailing hyphens
slug = strings.Trim(slug, "-")
return slug
}
// Custom validator functions
// validateSlug validates that a string is a valid slug
func validateSlug(fl validator.FieldLevel) bool {
slug := fl.Field().String()
if slug == "" {
return true // Let 'required' handle empty values
}
// Valid slug: lowercase alphanumeric and hyphens, no leading/trailing hyphens
matched, _ := regexp.MatchString(`^[a-z0-9]+(-[a-z0-9]+)*$`, slug)
return matched
}
// validateColor validates that a string is a valid hex color code
func validateColor(fl validator.FieldLevel) bool {
color := fl.Field().String()
if color == "" {
return true // Let 'required' handle empty values
}
// Valid color: #RGB, #RRGGBB, #RGBA, #RRGGBBAA
matched, _ := regexp.MatchString(`^#([A-Fa-f0-9]{3}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{4}|[A-Fa-f0-9]{8})$`, color)
return matched
}
// Global ValidationHelper instance for convenience
var Validator = NewValidationHelper()