614 lines
16 KiB
Go
614 lines
16 KiB
Go
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
|
|
}
|