Compare commits

...

2 Commits

2 changed files with 58 additions and 12 deletions

View File

@@ -175,6 +175,33 @@ type FieldValidationError struct {
Message string Message string
} }
func getFieldDisplayName(field reflect.StructField) string {
jsonTag := field.Tag.Get("json")
if jsonTag != "" && jsonTag != "-" {
if idx := strings.Index(jsonTag, ","); idx != -1 {
jsonTag = jsonTag[:idx]
}
if jsonTag != "" {
return jsonTag
}
}
return camelCaseToWords(field.Name)
}
func camelCaseToWords(s string) string {
if s == "" {
return s
}
var result strings.Builder
for i, r := range s {
if i > 0 && unicode.IsUpper(r) {
result.WriteRune(' ')
}
result.WriteRune(unicode.ToLower(r))
}
return result.String()
}
func ValidateStruct(s interface{}) error { func ValidateStruct(s interface{}) error {
if s == nil { if s == nil {
return nil return nil
@@ -232,9 +259,10 @@ func ValidateStruct(s interface{}) error {
tagName = tag tagName = tag
} }
if err := validateField(field.Name, fieldVal, tagName, param, omitempty); err != nil { displayName := getFieldDisplayName(field)
if err := validateField(displayName, fieldVal, tagName, param, omitempty); err != nil {
errors = append(errors, FieldValidationError{ errors = append(errors, FieldValidationError{
Field: field.Name, Field: displayName,
Tag: tagName, Tag: tagName,
Param: param, Param: param,
Message: err.Message, Message: err.Message,
@@ -293,7 +321,7 @@ func validateField(fieldName string, fieldVal reflect.Value, tagName, param stri
func isEmptyValue(v reflect.Value) bool { func isEmptyValue(v reflect.Value) bool {
switch v.Kind() { switch v.Kind() {
case reflect.String: case reflect.String:
return v.String() == "" return strings.TrimSpace(v.String()) == ""
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return v.Int() == 0 return v.Int() == 0
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
@@ -301,7 +329,7 @@ func isEmptyValue(v reflect.Value) bool {
case reflect.Float32, reflect.Float64: case reflect.Float32, reflect.Float64:
return v.Float() == 0 return v.Float() == 0
case reflect.Bool: case reflect.Bool:
return !v.Bool() return false
case reflect.Pointer, reflect.Interface, reflect.Slice, reflect.Map: case reflect.Pointer, reflect.Interface, reflect.Slice, reflect.Map:
return v.IsNil() return v.IsNil()
default: default:
@@ -317,7 +345,8 @@ func validateMin(fieldName string, v reflect.Value, param string) *ValidationErr
switch v.Kind() { switch v.Kind() {
case reflect.String: case reflect.String:
if len(v.String()) < min { s := strings.TrimSpace(v.String())
if len([]rune(s)) < min {
return &ValidationError{Field: fieldName, Message: fmt.Sprintf("%s must be at least %d characters", fieldName, min)} return &ValidationError{Field: fieldName, Message: fmt.Sprintf("%s must be at least %d characters", fieldName, min)}
} }
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
@@ -344,7 +373,7 @@ func validateMax(fieldName string, v reflect.Value, param string) *ValidationErr
switch v.Kind() { switch v.Kind() {
case reflect.String: case reflect.String:
if len(v.String()) > max { if len([]rune(v.String())) > max {
return &ValidationError{Field: fieldName, Message: fmt.Sprintf("%s must be at most %d characters", fieldName, max)} return &ValidationError{Field: fieldName, Message: fmt.Sprintf("%s must be at most %d characters", fieldName, max)}
} }
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:

View File

@@ -250,12 +250,12 @@ func TestSanitizeString(t *testing.T) {
func TestValidateStruct(t *testing.T) { func TestValidateStruct(t *testing.T) {
type TestStruct struct { type TestStruct struct {
Username string `validate:"required,min=3,max=20"` Username string `json:"username" validate:"required,min=3,max=20"`
Email string `validate:"required,email"` Email string `json:"email" validate:"required,email"`
Age int `validate:"min=18,max=120"` Age int `json:"age" validate:"min=18,max=120"`
URL string `validate:"url"` URL string `json:"url" validate:"url"`
Status string `validate:"oneof=active inactive pending"` Status string `json:"status" validate:"oneof=active inactive pending"`
Optional string `validate:"omitempty,min=1"` Optional string `json:"optional" validate:"omitempty,min=1"`
} }
t.Run("valid struct", func(t *testing.T) { t.Run("valid struct", func(t *testing.T) {
@@ -287,6 +287,9 @@ func TestValidateStruct(t *testing.T) {
if len(structErr.Errors) == 0 { if len(structErr.Errors) == 0 {
t.Error("Expected validation errors, got none") t.Error("Expected validation errors, got none")
} }
if structErr.Errors[0].Message != "username is required" {
t.Errorf("Expected JSON tag name in error, got %q", structErr.Errors[0].Message)
}
} }
}) })
@@ -318,6 +321,20 @@ func TestValidateStruct(t *testing.T) {
} }
}) })
t.Run("whitespace required field", func(t *testing.T) {
s := TestStruct{
Username: " ",
Email: "test@example.com",
Age: 25,
URL: "https://example.com",
Status: "active",
}
err := ValidateStruct(s)
if err == nil {
t.Error("ValidateStruct() expected error, got nil")
}
})
t.Run("invalid max", func(t *testing.T) { t.Run("invalid max", func(t *testing.T) {
s := TestStruct{ s := TestStruct{
Username: strings.Repeat("a", 21), Username: strings.Repeat("a", 21),