initial commit
This commit is contained in:
613
pkg/validation/generic_validator.go
Normal file
613
pkg/validation/generic_validator.go
Normal file
@@ -0,0 +1,613 @@
|
||||
package validation
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"net/mail"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// ErrorResponse represents the final error response format
|
||||
type ErrorResponse struct {
|
||||
Errors map[string]string `json:"errors"`
|
||||
}
|
||||
|
||||
// ErrorMessage represents error message constants
|
||||
type ErrorMessage string
|
||||
|
||||
const (
|
||||
MissingFieldError ErrorMessage = "This field is missing."
|
||||
NotExpectedField ErrorMessage = "There is unexpected field."
|
||||
StringFieldError ErrorMessage = "This field must be a string."
|
||||
BoolFieldError ErrorMessage = "This field must be a boolean."
|
||||
NotBlankError ErrorMessage = "This field cannot be blank."
|
||||
IntFieldError ErrorMessage = "This field must be an integer."
|
||||
FloatFieldError ErrorMessage = "این مقدار باید از نوع عدد باشد."
|
||||
MaxRangeError ErrorMessage = "این مقدار باید کوچکتر و یا مساوی %v باشد."
|
||||
MinRangeError ErrorMessage = "این مقدار باید بزرگتر و یا مساوی %v باشد."
|
||||
AtLeastOneOfError ErrorMessage = "At least one of the following fields must be present: '%s'."
|
||||
SendingInformationError ErrorMessage = "{\"status\": false, \"error\": {\"code\": 500, \"message\": \"Error sending information\"}}"
|
||||
BadRequest ErrorMessage = "Bad Request"
|
||||
ArrayFieldError ErrorMessage = "This field must be an array."
|
||||
EmailFieldError ErrorMessage = "This field must be a valid email address."
|
||||
PatternFieldError ErrorMessage = "This field must contain '%s'."
|
||||
UUIDFieldError ErrorMessage = "This field must be a valid UUID."
|
||||
URLFieldError ErrorMessage = "This field must be a valid URL."
|
||||
)
|
||||
|
||||
type ValidationTypes string
|
||||
|
||||
const (
|
||||
ValidationTypeString ValidationTypes = "string"
|
||||
ValidationTypeInt ValidationTypes = "int"
|
||||
ValidationTypeFloat ValidationTypes = "float"
|
||||
ValidationTypeBool ValidationTypes = "bool"
|
||||
ValidationTypeEmail ValidationTypes = "email"
|
||||
ValidationTypeArray ValidationTypes = "array"
|
||||
ValidationTypeEmpty ValidationTypes = ""
|
||||
ValidationTypeUUID ValidationTypes = "uuid"
|
||||
ValidationTypeURL ValidationTypes = "url"
|
||||
)
|
||||
|
||||
// GenericValidator provides generic validation functions
|
||||
type GenericValidator struct {
|
||||
errors map[string]string
|
||||
}
|
||||
|
||||
// NewGenericValidator creates a new generic validator
|
||||
func NewGenericValidator() *GenericValidator {
|
||||
return &GenericValidator{
|
||||
errors: make(map[string]string),
|
||||
}
|
||||
}
|
||||
|
||||
// Rule defines a validation rule
|
||||
type Rule struct {
|
||||
Field string
|
||||
Path string
|
||||
Type ValidationTypes
|
||||
Required bool
|
||||
Min *float64
|
||||
Max *float64
|
||||
MinLength *int
|
||||
MaxLength *int
|
||||
Pattern *string
|
||||
Custom func(value interface{}) error
|
||||
Nested Schema // For nested object validation
|
||||
ArrayOf Schema // For array of objects validation
|
||||
|
||||
// Custom error messages
|
||||
RequiredMessage string
|
||||
TypeMessage string
|
||||
MinMessage string
|
||||
MaxMessage string
|
||||
MinLengthMessage string
|
||||
MaxLengthMessage string
|
||||
PatternMessage string
|
||||
}
|
||||
|
||||
// Schema ValidationSchema defines validation rules for a structure
|
||||
type Schema map[string]Rule
|
||||
|
||||
// Validate validates data against a schema
|
||||
func (gv *GenericValidator) Validate(data map[string]interface{}, schema Schema) {
|
||||
gv.errors = make(map[string]string)
|
||||
|
||||
for field, rule := range schema {
|
||||
value, exists := data[field]
|
||||
path := rule.Path
|
||||
if path == "" {
|
||||
path = fmt.Sprintf("[%s]", field)
|
||||
}
|
||||
|
||||
// Check if field is required
|
||||
if rule.Required {
|
||||
if !exists {
|
||||
message := rule.RequiredMessage
|
||||
if message == "" {
|
||||
message = string(MissingFieldError)
|
||||
}
|
||||
gv.addError(path, message)
|
||||
continue
|
||||
}
|
||||
if value == nil {
|
||||
message := rule.RequiredMessage
|
||||
if message == "" {
|
||||
message = string(NotBlankError)
|
||||
}
|
||||
gv.addError(path, message)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Skip validation if field doesn't exist and is not required
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
|
||||
// Type validation
|
||||
if rule.Type != ValidationTypeEmpty {
|
||||
if err := gv.validateType(value, rule.Type, path, rule.TypeMessage); err != nil {
|
||||
gv.addError(path, err.Error())
|
||||
continue // Skip further validations if type is incorrect
|
||||
}
|
||||
}
|
||||
|
||||
// Range validation for numbers
|
||||
if rule.Min != nil || rule.Max != nil {
|
||||
if err := gv.validateRange(value, rule.Min, rule.Max, path, rule.MinMessage, rule.MaxMessage); err != nil {
|
||||
gv.addError(path, err.Error())
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Length validation for strings and arrays
|
||||
if rule.MinLength != nil || rule.MaxLength != nil {
|
||||
if err := gv.validateLength(value, rule.MinLength, rule.MaxLength, path); err != nil {
|
||||
gv.addError(path, err.Error())
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Pattern validation for strings
|
||||
if rule.Pattern != nil {
|
||||
if err := gv.validatePattern(value, *rule.Pattern, path); err != nil {
|
||||
gv.addError(path, err.Error())
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Custom validation
|
||||
if rule.Custom != nil {
|
||||
if err := rule.Custom(value); err != nil {
|
||||
gv.addError(path, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// Nested object validation
|
||||
if rule.Nested != nil {
|
||||
if nestedMap, ok := value.(map[string]interface{}); ok {
|
||||
gv.validateNestedMap(nestedMap, rule.Nested, path)
|
||||
}
|
||||
}
|
||||
|
||||
// Array of objects validation
|
||||
if rule.ArrayOf != nil {
|
||||
if array, ok := value.([]interface{}); ok {
|
||||
for i, item := range array {
|
||||
if itemMap, ok := item.(map[string]interface{}); ok {
|
||||
itemPath := fmt.Sprintf("%s[%d]", path, i)
|
||||
gv.validateNestedMap(itemMap, rule.ArrayOf, itemPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateNested validates nested structures
|
||||
func (gv *GenericValidator) ValidateNested(data interface{}, schema Schema, basePath string) {
|
||||
switch v := data.(type) {
|
||||
case map[string]interface{}:
|
||||
gv.validateNestedMap(v, schema, basePath)
|
||||
case []interface{}:
|
||||
gv.validateNestedSlice(v, schema, basePath)
|
||||
}
|
||||
}
|
||||
|
||||
// validateNestedMap validates nested map structures
|
||||
func (gv *GenericValidator) validateNestedMap(data map[string]interface{}, schema Schema, basePath string) {
|
||||
for field, rule := range schema {
|
||||
value, exists := data[field]
|
||||
path := rule.Path
|
||||
if path == "" {
|
||||
path = fmt.Sprintf("%s[%s]", basePath, field)
|
||||
}
|
||||
|
||||
// Check if field is required
|
||||
if rule.Required {
|
||||
if !exists {
|
||||
message := rule.RequiredMessage
|
||||
if message == "" {
|
||||
message = string(MissingFieldError)
|
||||
}
|
||||
gv.addError(path, message)
|
||||
continue
|
||||
}
|
||||
if value == nil {
|
||||
message := rule.RequiredMessage
|
||||
if message == "" {
|
||||
message = string(NotBlankError)
|
||||
}
|
||||
gv.addError(path, message)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Skip validation if field doesn't exist and is not required
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
|
||||
// Type validation
|
||||
if rule.Type != ValidationTypeEmpty {
|
||||
if err := gv.validateType(value, rule.Type, path, rule.TypeMessage); err != nil {
|
||||
gv.addError(path, err.Error())
|
||||
continue // Skip further validations if type is incorrect
|
||||
}
|
||||
}
|
||||
|
||||
// Range validation for numbers
|
||||
if rule.Min != nil || rule.Max != nil {
|
||||
if err := gv.validateRange(value, rule.Min, rule.Max, path, rule.MinMessage, rule.MaxMessage); err != nil {
|
||||
gv.addError(path, err.Error())
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Length validation for strings and arrays
|
||||
if rule.MinLength != nil || rule.MaxLength != nil {
|
||||
if err := gv.validateLength(value, rule.MinLength, rule.MaxLength, path); err != nil {
|
||||
gv.addError(path, err.Error())
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Pattern validation for strings
|
||||
if rule.Pattern != nil {
|
||||
if err := gv.validatePattern(value, *rule.Pattern, path); err != nil {
|
||||
gv.addError(path, err.Error())
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Custom validation
|
||||
if rule.Custom != nil {
|
||||
if err := rule.Custom(value); err != nil {
|
||||
gv.addError(path, err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// validateNestedSlice validates nested slice structures
|
||||
func (gv *GenericValidator) validateNestedSlice(data []interface{}, schema Schema, basePath string) {
|
||||
for i, item := range data {
|
||||
if itemMap, ok := item.(map[string]interface{}); ok {
|
||||
itemPath := fmt.Sprintf("%s[%d]", basePath, i)
|
||||
gv.validateNestedMap(itemMap, schema, itemPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (gv *GenericValidator) validateString(value any, customErrMsg string) error {
|
||||
if reflect.TypeOf(value).Kind() != reflect.String {
|
||||
if customErrMsg != "" {
|
||||
return fmt.Errorf("%s", customErrMsg)
|
||||
}
|
||||
return fmt.Errorf(string(StringFieldError))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateType validates the type of value
|
||||
func (gv *GenericValidator) validateType(value interface{}, expectedType ValidationTypes, path string, customErrMsg string) error {
|
||||
switch expectedType {
|
||||
case ValidationTypeString:
|
||||
if err := gv.validateString(value, customErrMsg); err != nil {
|
||||
return err
|
||||
}
|
||||
case ValidationTypeInt:
|
||||
if val, ok := value.(float64); ok {
|
||||
if val != float64(int(val)) || val > float64(math.MaxUint32) {
|
||||
if customErrMsg != "" {
|
||||
return fmt.Errorf("%s", customErrMsg)
|
||||
}
|
||||
return fmt.Errorf(string(IntFieldError))
|
||||
}
|
||||
} else {
|
||||
if customErrMsg != "" {
|
||||
return fmt.Errorf("%s", customErrMsg)
|
||||
}
|
||||
return fmt.Errorf(string(IntFieldError))
|
||||
}
|
||||
case ValidationTypeFloat:
|
||||
if _, ok := value.(float64); !ok {
|
||||
if customErrMsg != "" {
|
||||
return fmt.Errorf("%s", customErrMsg)
|
||||
}
|
||||
return fmt.Errorf(string(FloatFieldError))
|
||||
}
|
||||
case ValidationTypeBool:
|
||||
if reflect.TypeOf(value).Kind() != reflect.Bool {
|
||||
if customErrMsg != "" {
|
||||
return fmt.Errorf("%s", customErrMsg)
|
||||
}
|
||||
return fmt.Errorf(string(BoolFieldError))
|
||||
}
|
||||
case ValidationTypeArray:
|
||||
if reflect.TypeOf(value).Kind() != reflect.Slice {
|
||||
if customErrMsg != "" {
|
||||
return fmt.Errorf("%s", customErrMsg)
|
||||
}
|
||||
return fmt.Errorf(string(ArrayFieldError))
|
||||
}
|
||||
case ValidationTypeEmail:
|
||||
if err := gv.validateString(value, customErrMsg); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := mail.ParseAddress(value.(string)); err != nil {
|
||||
if customErrMsg != "" {
|
||||
return fmt.Errorf("%s", customErrMsg)
|
||||
}
|
||||
return fmt.Errorf(string(EmailFieldError))
|
||||
}
|
||||
case ValidationTypeUUID:
|
||||
if err := gv.validateString(value, customErrMsg); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := uuid.Parse(value.(string)); err != nil {
|
||||
if customErrMsg != "" {
|
||||
return fmt.Errorf("%s", customErrMsg)
|
||||
}
|
||||
return fmt.Errorf(string(UUIDFieldError))
|
||||
}
|
||||
case ValidationTypeURL:
|
||||
if err := gv.validateString(value, customErrMsg); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := url.Parse(value.(string)); err != nil {
|
||||
if customErrMsg != "" {
|
||||
return fmt.Errorf("%s", customErrMsg)
|
||||
}
|
||||
return fmt.Errorf(string(URLFieldError))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateRange validates numeric range
|
||||
func (gv *GenericValidator) validateRange(value interface{}, min, max *float64, path string, minMessage, maxMessage string) error {
|
||||
var num float64
|
||||
|
||||
switch v := value.(type) {
|
||||
case float64:
|
||||
num = v
|
||||
case int:
|
||||
num = float64(v)
|
||||
case string:
|
||||
if parsed, err := strconv.ParseFloat(v, 64); err == nil {
|
||||
num = parsed
|
||||
} else {
|
||||
return fmt.Errorf(string(FloatFieldError))
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf(string(FloatFieldError))
|
||||
}
|
||||
|
||||
if min != nil && num < *min {
|
||||
if minMessage != "" {
|
||||
return fmt.Errorf("%s", minMessage)
|
||||
}
|
||||
return fmt.Errorf(string(MinRangeError), *min)
|
||||
}
|
||||
|
||||
if max != nil && num > *max {
|
||||
if maxMessage != "" {
|
||||
return fmt.Errorf("%s", maxMessage)
|
||||
}
|
||||
return fmt.Errorf(string(MaxRangeError), *max)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateLength validates string or array length
|
||||
func (gv *GenericValidator) validateLength(value interface{}, minLength, maxLength *int, path string) error {
|
||||
var length int
|
||||
var isArray bool
|
||||
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
length = len(v)
|
||||
isArray = false
|
||||
case []interface{}:
|
||||
length = len(v)
|
||||
isArray = true
|
||||
default:
|
||||
return fmt.Errorf(string(StringFieldError))
|
||||
}
|
||||
|
||||
if minLength != nil && length < *minLength {
|
||||
if isArray {
|
||||
return fmt.Errorf(string(MinRangeError), *minLength)
|
||||
}
|
||||
return fmt.Errorf(string(MinRangeError), *minLength)
|
||||
}
|
||||
|
||||
if maxLength != nil && length > *maxLength {
|
||||
if isArray {
|
||||
return fmt.Errorf(string(MaxRangeError), *maxLength)
|
||||
}
|
||||
return fmt.Errorf(string(MaxRangeError), *maxLength)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validatePattern validates string pattern (simple implementation)
|
||||
func (gv *GenericValidator) validatePattern(value interface{}, pattern string, path string) error {
|
||||
if str, ok := value.(string); ok {
|
||||
// Simple pattern validation - can be extended with regex
|
||||
if !strings.Contains(str, pattern) {
|
||||
return fmt.Errorf(string(PatternFieldError), pattern)
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf(string(StringFieldError))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// addError adds an error to the validator
|
||||
func (gv *GenericValidator) addError(path, message string) {
|
||||
gv.errors[path] = message
|
||||
}
|
||||
|
||||
// AddError adds a custom error
|
||||
func (gv *GenericValidator) AddError(path, message string) {
|
||||
gv.errors[path] = message
|
||||
}
|
||||
|
||||
// GetErrors returns all validation errors
|
||||
func (gv *GenericValidator) GetErrors() map[string]string {
|
||||
return gv.errors
|
||||
}
|
||||
|
||||
// HasErrors returns true if there are validation errors
|
||||
func (gv *GenericValidator) HasErrors() bool {
|
||||
return len(gv.errors) > 0
|
||||
}
|
||||
|
||||
// ToJSON returns the errors in JSON format
|
||||
func (gv *GenericValidator) ToJSON() ([]byte, error) {
|
||||
response := ErrorResponse{
|
||||
Errors: gv.errors,
|
||||
}
|
||||
return json.Marshal(response)
|
||||
}
|
||||
|
||||
// Convenience functions for common validations
|
||||
|
||||
// ValidateRequired validates that a field exists and is not empty
|
||||
func (gv *GenericValidator) ValidateRequired(data map[string]interface{}, field, path string) {
|
||||
if path == "" {
|
||||
path = fmt.Sprintf("[%s]", field)
|
||||
}
|
||||
|
||||
value, exists := data[field]
|
||||
if !exists {
|
||||
gv.addError(path, string(MissingFieldError))
|
||||
return
|
||||
}
|
||||
|
||||
if value == nil {
|
||||
gv.addError(path, string(NotBlankError))
|
||||
return
|
||||
}
|
||||
|
||||
// Check for empty string
|
||||
if str, ok := value.(string); ok && str == "" {
|
||||
gv.addError(path, string(NotBlankError))
|
||||
return
|
||||
}
|
||||
|
||||
// Check for empty array
|
||||
if arr, ok := value.([]interface{}); ok && len(arr) == 0 {
|
||||
gv.addError(path, string(NotBlankError))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// ValidatePrice validates that a price is a positive number
|
||||
func (gv *GenericValidator) ValidatePrice(data map[string]interface{}, field, path string) {
|
||||
if path == "" {
|
||||
path = fmt.Sprintf("[%s]", field)
|
||||
}
|
||||
|
||||
value, exists := data[field]
|
||||
if !exists {
|
||||
return
|
||||
}
|
||||
|
||||
var num float64
|
||||
switch v := value.(type) {
|
||||
case float64:
|
||||
num = v
|
||||
case int:
|
||||
num = float64(v)
|
||||
case string:
|
||||
if parsed, err := strconv.ParseFloat(v, 64); err == nil {
|
||||
num = parsed
|
||||
} else {
|
||||
gv.addError(path, string(FloatFieldError))
|
||||
return
|
||||
}
|
||||
default:
|
||||
gv.addError(path, string(FloatFieldError))
|
||||
return
|
||||
}
|
||||
|
||||
if num < 1 {
|
||||
gv.addError(path, fmt.Sprintf(string(MinRangeError), 1))
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateQuantity validates that a quantity is a positive integer
|
||||
func (gv *GenericValidator) ValidateQuantity(data map[string]interface{}, field, path string) {
|
||||
if path == "" {
|
||||
path = fmt.Sprintf("[%s]", field)
|
||||
}
|
||||
|
||||
value, exists := data[field]
|
||||
if !exists {
|
||||
return
|
||||
}
|
||||
|
||||
var num float64
|
||||
switch v := value.(type) {
|
||||
case float64:
|
||||
num = v
|
||||
case int:
|
||||
num = float64(v)
|
||||
case string:
|
||||
if parsed, err := strconv.ParseFloat(v, 64); err == nil {
|
||||
num = parsed
|
||||
} else {
|
||||
gv.addError(path, string(FloatFieldError))
|
||||
return
|
||||
}
|
||||
default:
|
||||
gv.addError(path, string(FloatFieldError))
|
||||
return
|
||||
}
|
||||
|
||||
if num < 0 || num != float64(int(num)) {
|
||||
gv.addError(path, string(IntFieldError))
|
||||
}
|
||||
}
|
||||
|
||||
// Global convenience functions
|
||||
|
||||
// ValidateData validates data against a schema
|
||||
func ValidateData(data map[string]interface{}, schema Schema) *GenericValidator {
|
||||
validator := NewGenericValidator()
|
||||
validator.Validate(data, schema)
|
||||
return validator
|
||||
}
|
||||
|
||||
// ValidateJSONData validates JSON data against a schema
|
||||
func ValidateJSONData(jsonData []byte, schema Schema) (*GenericValidator, error) {
|
||||
var data map[string]interface{}
|
||||
if err := json.Unmarshal(jsonData, &data); err != nil {
|
||||
return nil, fmt.Errorf("Invalid JSON: %v", err)
|
||||
}
|
||||
|
||||
validator := NewGenericValidator()
|
||||
validator.Validate(data, schema)
|
||||
return validator, nil
|
||||
}
|
||||
|
||||
func Float64Ptr(f float64) *float64 {
|
||||
return &f
|
||||
}
|
||||
|
||||
func IntPtr(i int) *int {
|
||||
return &i
|
||||
}
|
||||
Reference in New Issue
Block a user