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 }