package validation import ( "encoding/json" "fmt" "reflect" "strconv" "strings" ) // StructValidator validates a struct using individual validation functions type StructValidator struct { errors []error } // NewStructValidator creates a new struct validator func NewStructValidator() *StructValidator { return &StructValidator{ errors: make([]error, 0), } } // Validate validates a struct and returns all validation errors func (sv *StructValidator) Validate(data map[string]interface{}, structType interface{}) []error { sv.errors = make([]error, 0) // Get struct type information val := reflect.ValueOf(structType) if val.Kind() == reflect.Ptr { val = val.Elem() } typ := val.Type() // Build expected fields map expectedFields := make(map[string]struct{}) requiredFields := make(map[string]struct{}) fieldValidations := make(map[string]map[string]string) // Extract field information from struct tags for i := 0; i < typ.NumField(); i++ { field := typ.Field(i) jsonTag := field.Tag.Get("json") validateTag := field.Tag.Get("validate") minTag := field.Tag.Get("min") maxTag := field.Tag.Get("max") if jsonTag != "" && jsonTag != "-" { expectedFields[jsonTag] = struct{}{} // Store validations for this field fieldValidations[jsonTag] = make(map[string]string) if validateTag != "" { fieldValidations[jsonTag]["validate"] = validateTag } if minTag != "" { fieldValidations[jsonTag]["min"] = minTag } if maxTag != "" { fieldValidations[jsonTag]["max"] = maxTag } // Check if field is required if strings.Contains(validateTag, "required") { requiredFields[jsonTag] = struct{}{} } } } // Validate required fields exist for field := range requiredFields { if err := ExistKey(field, data, fmt.Sprintf("Field '%s' is required", field)); err != nil { sv.errors = append(sv.errors, err) } } // Validate each field in the data for key, value := range data { // Check for unexpected fields if _, ok := expectedFields[key]; !ok { err := ErrBadRequest.SetMessage(fmt.Sprintf("Unexpected field '%s'", key)) sv.errors = append(sv.errors, err) continue } // Get field validations validations, exists := fieldValidations[key] if !exists { continue } // Apply validations based on struct tags sv.applyFieldValidations(key, value, data, validations) } return sv.errors } // applyFieldValidations applies all validations for a specific field func (sv *StructValidator) applyFieldValidations(key string, value interface{}, data map[string]interface{}, validations map[string]string) { // Check if field is required if validateTag, ok := validations["validate"]; ok && strings.Contains(validateTag, "required") { if err := NotBlank(key, data, fmt.Sprintf("Field '%s' cannot be blank", key)); err != nil { sv.errors = append(sv.errors, err) } } // Type validations if value != nil { switch value.(type) { case string: if err := IsString(key, data, fmt.Sprintf("Field '%s' must be a string", key)); err != nil { sv.errors = append(sv.errors, err) } case float64: // Check if it's an integer if validateTag, ok := validations["validate"]; ok && strings.Contains(validateTag, "int") { if err := IsInt(key, data, fmt.Sprintf("Field '%s' must be an integer", key)); err != nil { sv.errors = append(sv.errors, err) } } else { if err := IsFloat64(key, data, fmt.Sprintf("Field '%s' must be a number", key)); err != nil { sv.errors = append(sv.errors, err) } } case bool: if err := IsBool(key, data, fmt.Sprintf("Field '%s' must be a boolean", key)); err != nil { sv.errors = append(sv.errors, err) } case []interface{}: // Slice validation - could be extended for specific slice types if validateTag, ok := validations["validate"]; ok && strings.Contains(validateTag, "required") { if err := NotBlank(key, data, fmt.Sprintf("Field '%s' cannot be empty", key)); err != nil { sv.errors = append(sv.errors, err) } } } } // Range validations if minTag, ok := validations["min"]; ok { if min, err := strconv.Atoi(minTag); err == nil { if err := MinRange(key, min, data, fmt.Sprintf("Field '%s' must be at least %d", key, min)); err != nil { sv.errors = append(sv.errors, err) } } } if maxTag, ok := validations["max"]; ok { if max, err := strconv.Atoi(maxTag); err == nil { if err := MaxRange(key, max, data, fmt.Sprintf("Field '%s' must be at most %d", key, max)); err != nil { sv.errors = append(sv.errors, err) } } } } // ValidateStruct is a convenience function that validates a struct directly func ValidateStruct(data map[string]interface{}, structType interface{}) []error { validator := NewStructValidator() return validator.Validate(data, structType) } // ValidateJSON validates JSON data against a struct func ValidateJSON(jsonData []byte, structType interface{}) []error { var data map[string]interface{} if err := json.Unmarshal(jsonData, &data); err != nil { return []error{ErrBadRequest.SetMessage(fmt.Sprintf("Invalid JSON: %v", err))} } return ValidateStruct(data, structType) } // HasErrors returns true if there are validation errors func (sv *StructValidator) HasErrors() bool { return len(sv.errors) > 0 } // GetErrors returns all validation errors func (sv *StructValidator) GetErrors() []error { return sv.errors } // AddError adds a custom error func (sv *StructValidator) AddError(err error) { sv.errors = append(sv.errors, err) }