veza/veza-backend-api/internal/validators/validator.go
2025-12-03 20:29:37 +01:00

150 lines
4.6 KiB
Go

package validators
import (
"fmt"
"strings"
"github.com/go-playground/validator/v10"
"veza-backend-api/internal/dto"
)
// Validator est un wrapper autour de go-playground/validator
// GO-013: Validation input centralisée avec go-validator
type Validator struct {
validate *validator.Validate
}
// NewValidator crée une nouvelle instance de Validator
func NewValidator() *Validator {
v := validator.New()
// Enregistrer des validations personnalisées
registerCustomValidations(v)
return &Validator{
validate: v,
}
}
// Validate valide une structure et retourne des erreurs formatées
func (v *Validator) Validate(s interface{}) []dto.ValidationError {
var validationErrors []dto.ValidationError
err := v.validate.Struct(s)
if err != nil {
if validationErrs, ok := err.(validator.ValidationErrors); ok {
for _, fieldErr := range validationErrs {
validationErrors = append(validationErrors, dto.ValidationError{
Field: getFieldName(fieldErr),
Message: getErrorMessage(fieldErr),
Value: fmt.Sprintf("%v", fieldErr.Value()),
})
}
}
}
return validationErrors
}
// ValidateVar valide une variable unique
func (v *Validator) ValidateVar(field interface{}, tag string) error {
return v.validate.Var(field, tag)
}
// getFieldName extrait le nom du champ depuis l'erreur de validation
// GO-013: Extrait le tag JSON si disponible via StructNamespace, sinon convertit en camelCase
func getFieldName(fieldErr validator.FieldError) string {
// Utiliser StructNamespace qui donne le chemin complet (ex: "TestStruct.Name")
// et extraire le dernier segment
structNamespace := fieldErr.StructNamespace()
if structNamespace != "" {
parts := strings.Split(structNamespace, ".")
if len(parts) > 0 {
fieldName := parts[len(parts)-1]
// Convertir en camelCase pour JSON (première lettre en minuscule)
if len(fieldName) > 0 {
return strings.ToLower(fieldName[:1]) + fieldName[1:]
}
return fieldName
}
}
// Fallback: utiliser Field() et convertir en camelCase
fieldName := fieldErr.Field()
if len(fieldName) > 0 {
return strings.ToLower(fieldName[:1]) + fieldName[1:]
}
return fieldName
}
// getErrorMessage génère un message d'erreur lisible depuis l'erreur de validation
func getErrorMessage(fieldErr validator.FieldError) string {
fieldName := getFieldName(fieldErr)
switch fieldErr.Tag() {
case "required":
return fmt.Sprintf("%s is required", fieldName)
case "email":
return fmt.Sprintf("%s must be a valid email address", fieldName)
case "min":
return fmt.Sprintf("%s must be at least %s characters", fieldName, fieldErr.Param())
case "max":
return fmt.Sprintf("%s must be at most %s characters", fieldName, fieldErr.Param())
case "oneof":
return fmt.Sprintf("%s must be one of: %s", fieldName, fieldErr.Param())
case "eqfield":
return fmt.Sprintf("%s must equal %s", fieldName, fieldErr.Param())
case "uuid":
return fmt.Sprintf("%s must be a valid UUID", fieldName)
case "url":
return fmt.Sprintf("%s must be a valid URL", fieldName)
case "numeric":
return fmt.Sprintf("%s must be numeric", fieldName)
case "alpha":
return fmt.Sprintf("%s must contain only letters", fieldName)
case "alphanum":
return fmt.Sprintf("%s must contain only letters and numbers", fieldName)
case "gte":
return fmt.Sprintf("%s must be greater than or equal to %s", fieldName, fieldErr.Param())
case "lte":
return fmt.Sprintf("%s must be less than or equal to %s", fieldName, fieldErr.Param())
case "gt":
return fmt.Sprintf("%s must be greater than %s", fieldName, fieldErr.Param())
case "lt":
return fmt.Sprintf("%s must be less than %s", fieldName, fieldErr.Param())
default:
return fmt.Sprintf("%s is invalid", fieldName)
}
}
// registerCustomValidations enregistre des validations personnalisées
func registerCustomValidations(v *validator.Validate) {
// Validation pour username (alphanumeric + underscore, 3-30 chars)
v.RegisterValidation("username", func(fl validator.FieldLevel) bool {
username := fl.Field().String()
if len(username) < 3 || len(username) > 30 {
return false
}
for _, char := range username {
if !((char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') ||
(char >= '0' && char <= '9') || char == '_') {
return false
}
}
return true
})
// Validation pour UUID string
v.RegisterValidation("uuid_string", func(fl validator.FieldLevel) bool {
uuidStr := fl.Field().String()
if uuidStr == "" {
return true // Optionnel
}
// Utiliser le même validator pour éviter la récursion
uuidValidator := validator.New()
err := uuidValidator.Var(uuidStr, "uuid")
return err == nil
})
}