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 }) }