initial commit

This commit is contained in:
m.zare
2026-04-10 18:25:21 +03:30
commit 77ca6c34a3
263 changed files with 34470 additions and 0 deletions

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