diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e685b07 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Ahmed Khalil YOUSFI + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/validator/is.go b/validator/is.go new file mode 100644 index 0000000..78955f7 --- /dev/null +++ b/validator/is.go @@ -0,0 +1,361 @@ +package validator + +import ( + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "net" + "net/mail" + "net/url" + "reflect" + "regexp" + "strconv" + "strings" + "time" +) + +// IsNotEmpty checks if a value is not empty. +func IsNotEmpty(value interface{}) error { + if value == nil { + return errors.New("value is nil") + } + + v := reflect.ValueOf(value) + + switch v.Kind() { + case reflect.String, reflect.Slice, reflect.Map, reflect.Array: + if v.Len() == 0 { + return errors.New("value is empty") + } + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + if v.Int() == 0 { + return errors.New("value is zero") + } + case reflect.Float32, reflect.Float64: + if v.Float() == 0 { + return errors.New("value is zero") + } + case reflect.Bool: + if !v.Bool() { + return errors.New("value is false") + } + default: + // For unsupported types, assume the value is not empty + return nil + } + + return nil +} + +// IsAlphanumeric checks if a string contains only alphanumeric characters. +func IsAlphanumeric(value interface{}) error { + str, ok := value.(string) + if !ok { + return errors.New("value is not a string") + } + for _, char := range str { + if !((char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') || (char >= '0' && char <= '9')) { + return errors.New("value contains invalid characters") + } + } + return nil +} + +// IsEmail checks if a string is a valid email address. +func IsEmail(value interface{}) error { + // Check if the input is a string + str, ok := value.(string) + if !ok { + return errors.New("value is not a string") + } + + // Use net/mail to validate the email + _, err := mail.ParseAddress(str) + if err != nil { + return errors.New("value is not a valid email address") + } + + return nil +} + +// IsIn checks if a value is in a predefined list of allowed values. +func IsIn(allowedValues ...interface{}) ValidatorFunc { + return func(value interface{}) error { + for _, allowed := range allowedValues { + if value == allowed { + return nil + } + } + return fmt.Errorf("value must be one of %v", allowedValues) + } +} + +// IsString checks if a value is a string. +func IsString(value interface{}) error { + if reflect.TypeOf(value).Kind() != reflect.String { + return errors.New("value must be a string") + } + return nil +} + +// IsNumber checks if a value is a number (int or float). +func IsNumber(value interface{}) error { + v := reflect.ValueOf(value) + switch v.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Float32, reflect.Float64: + return nil + default: + return errors.New("value must be a number") + } +} + +// IsInt checks if a value is an integer. +func IsInt(value interface{}) error { + v := reflect.ValueOf(value) + switch v.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return nil + default: + return errors.New("value must be an integer") + } +} + +// IsFloat checks if a value is a float. +func IsFloat(value interface{}) error { + v := reflect.ValueOf(value) + switch v.Kind() { + case reflect.Float32, reflect.Float64: + return nil + default: + return errors.New("value must be a float") + } +} + +// IsBool checks if a value is a boolean. +func IsBool(value interface{}) error { + if reflect.TypeOf(value).Kind() != reflect.Bool { + return errors.New("value must be a boolean") + } + return nil +} + +// IsSlice checks if a value is a slice. +func IsSlice(value interface{}) error { + if reflect.TypeOf(value).Kind() != reflect.Slice { + return errors.New("value must be a slice") + } + return nil +} + +// IsMap checks if a value is a map. +func IsMap(value interface{}) error { + if reflect.TypeOf(value).Kind() != reflect.Map { + return errors.New("value must be a map") + } + return nil +} + +// IsURL checks if a string is a valid URL. +func IsURL(value interface{}) error { + str, ok := value.(string) + if !ok { + return errors.New("value must be a string") + } + _, err := url.ParseRequestURI(str) + if err != nil { + return errors.New("value is not a valid URL") + } + return nil +} + +// IsUUID checks if a string is a valid UUID. +func IsUUID(value interface{}) error { + str, ok := value.(string) + if !ok { + return errors.New("value must be a string") + } + uuidRegex := `^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$` + matched, err := regexp.MatchString(uuidRegex, str) + if err != nil || !matched { + return errors.New("value is not a valid UUID") + } + return nil +} + +// IsDate checks if a string is a valid date in the format YYYY-MM-DD. +func IsDate(value interface{}) error { + str, ok := value.(string) + if !ok { + return errors.New("value must be a string") + } + _, err := time.Parse("2006-01-02", str) + if err != nil { + return errors.New("value is not a valid date (expected format: YYYY-MM-DD)") + } + return nil +} + +// IsTime checks if a string is a valid time in the format HH:MM:SS. +func IsTime(value interface{}) error { + str, ok := value.(string) + if !ok { + return errors.New("value must be a string") + } + _, err := time.Parse("15:04:05", str) + if err != nil { + return errors.New("value is not a valid time (expected format: HH:MM:SS)") + } + return nil +} + +// IsCreditCard checks if a string is a valid credit card number using the Luhn algorithm. +func IsCreditCard(value interface{}) error { + str, ok := value.(string) + if !ok { + return errors.New("value must be a string") + } + // Remove spaces and dashes + str = strings.ReplaceAll(str, " ", "") + str = strings.ReplaceAll(str, "-", "") + + // Check if the string is a valid number + if _, err := strconv.Atoi(str); err != nil { + return errors.New("value is not a valid credit card number") + } + + // Luhn algorithm + sum := 0 + alternate := false + for i := len(str) - 1; i >= 0; i-- { + digit, _ := strconv.Atoi(string(str[i])) + if alternate { + digit *= 2 + if digit > 9 { + digit = digit - 9 + } + } + sum += digit + alternate = !alternate + } + + if sum%10 != 0 { + return errors.New("value is not a valid credit card number") + } + return nil +} + +// IsHexColor checks if a string is a valid hexadecimal color code. +func IsHexColor(value interface{}) error { + str, ok := value.(string) + if !ok { + return errors.New("value must be a string") + } + hexColorRegex := `^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$` + matched, err := regexp.MatchString(hexColorRegex, str) + if err != nil || !matched { + return errors.New("value is not a valid hexadecimal color code") + } + return nil +} + +// IsJSON checks if a string is valid JSON. +func IsJSON(value interface{}) error { + str, ok := value.(string) + if !ok { + return errors.New("value must be a string") + } + var js json.RawMessage + if err := json.Unmarshal([]byte(str), &js); err != nil { + return errors.New("value is not valid JSON") + } + return nil +} + +// IsIP checks if a string is a valid IP address (IPv4 or IPv6). +func IsIP(value interface{}) error { + str, ok := value.(string) + if !ok { + return errors.New("value must be a string") + } + if net.ParseIP(str) == nil { + return errors.New("value is not a valid IP address") + } + return nil +} + +// IsAlpha checks if a string contains only alphabetic characters. +func IsAlpha(value interface{}) error { + str, ok := value.(string) + if !ok { + return errors.New("value must be a string") + } + alphaRegex := `^[a-zA-Z]+$` + matched, err := regexp.MatchString(alphaRegex, str) + if err != nil || !matched { + return errors.New("value must contain only alphabetic characters") + } + return nil +} + +// IsAlphaNumeric checks if a string contains only alphanumeric characters. +func IsAlphaNumeric(value interface{}) error { + str, ok := value.(string) + if !ok { + return errors.New("value must be a string") + } + alphaNumericRegex := `^[a-zA-Z0-9]+$` + matched, err := regexp.MatchString(alphaNumericRegex, str) + if err != nil || !matched { + return errors.New("value must contain only alphanumeric characters") + } + return nil +} + +// IsArabic checks if a string contains only Arabic characters (including spaces and common Arabic punctuation). +func IsArabic(value interface{}) error { + str, ok := value.(string) + if !ok { + return errors.New("value must be a string") + } + + // Regex to match Arabic characters (Unicode block for Arabic and Arabic Supplement) + arabicRegex := `^[\p{Arabic}\s]+$` + matched, err := regexp.MatchString(arabicRegex, str) + if err != nil { + return errors.New("an error occurred while validating the string") + } + if !matched { + return errors.New("value must contain only Arabic characters") + } + return nil +} + +// IsAlphaArabic checks if a string contains only Arabic and Latin alphabetic characters. +func IsAlphaArabic(value interface{}) error { + str, ok := value.(string) + if !ok { + return errors.New("value must be a string") + } + // Regex to match Arabic and Latin alphabetic characters + alphaArabicRegex := `^[\p{Arabic}\p{Latin}\s]+$` + matched, err := regexp.MatchString(alphaArabicRegex, str) + if err != nil || !matched { + return errors.New("value must contain only Arabic and Latin alphabetic characters") + } + return nil +} + +// IsBase64 checks if a string is valid Base64-encoded data. +func IsBase64(value interface{}) error { + str, ok := value.(string) + if !ok { + return errors.New("value must be a string") + } + _, err := base64.StdEncoding.DecodeString(str) + if err != nil { + return errors.New("value is not valid Base64") + } + return nil +} diff --git a/validator/is_test.go b/validator/is_test.go new file mode 100644 index 0000000..b024764 --- /dev/null +++ b/validator/is_test.go @@ -0,0 +1,498 @@ +package validator + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestIsNotEmpty tests the IsNotEmpty validator. +func TestIsNotEmpty(t *testing.T) { + tests := []struct { + name string + value interface{} + expected error + }{ + // Strings + {name: "Non-empty string", value: "hello", expected: nil}, + {name: "Empty string", value: "", expected: errors.New("value is empty")}, + + // Numbers + {name: "Non-zero int", value: 42, expected: nil}, + {name: "Zero int", value: 0, expected: errors.New("value is zero")}, + {name: "Non-zero float", value: 3.14, expected: nil}, + {name: "Zero float", value: 0.0, expected: errors.New("value is zero")}, + + // Booleans + {name: "True boolean", value: true, expected: nil}, + {name: "False boolean", value: false, expected: errors.New("value is false")}, + + // Slices + {name: "Non-empty slice", value: []int{1, 2, 3}, expected: nil}, + {name: "Empty slice", value: []int{}, expected: errors.New("value is empty")}, + + // Maps + {name: "Non-empty map", value: map[string]int{"a": 1}, expected: nil}, + {name: "Empty map", value: map[string]int{}, expected: errors.New("value is empty")}, + + // Nil + {name: "Nil value", value: nil, expected: errors.New("value is nil")}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := IsNotEmpty(tt.value) + if tt.expected == nil { + assert.NoError(t, err) + } else { + assert.EqualError(t, err, tt.expected.Error()) + } + }) + } +} + +// TestIsAlphanumeric tests the IsAlphanumeric validator. +func TestIsAlphanumeric(t *testing.T) { + tests := []struct { + name string + input interface{} + error error + }{ + {"valid alphanumeric", "abc123", nil}, + {"invalid characters", "abc@123", errors.New("value contains invalid characters")}, + {"not a string", 123, errors.New("value is not a string")}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := IsAlphanumeric(test.input) + require.Equal(t, test.error, err) + }) + } +} + +// TestIsEmail tests the IsEmail validator. +func TestIsEmail(t *testing.T) { + tests := []struct { + name string + input interface{} + error error + }{ + {"valid email", "user@example.com", nil}, + {"invalid email", "invalid-email", errors.New("value is not a valid email address")}, + {"not a string", 123, errors.New("value is not a string")}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := IsEmail(test.input) + require.Equal(t, test.error, err) + }) + } +} + +func TestIsIn(t *testing.T) { + isIn := IsIn("apple", "banana", "cherry") + + tests := []struct { + name string + input interface{} + error error + }{ + {"valid value", "apple", nil}, + {"invalid value", "grape", errors.New("value must be one of [apple banana cherry]")}, + {"wrong type", 123, errors.New("value must be one of [apple banana cherry]")}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := isIn(test.input) + require.Equal(t, test.error, err) + }) + } +} + +func TestIsString(t *testing.T) { + tests := []struct { + name string + input interface{} + error error + }{ + {"valid string", "hello", nil}, + {"invalid type (int)", 123, errors.New("value must be a string")}, + {"invalid type (float)", 123.45, errors.New("value must be a string")}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := IsString(test.input) + require.Equal(t, test.error, err) + }) + } +} + +func TestIsNumber(t *testing.T) { + tests := []struct { + name string + input interface{} + error error + }{ + {"valid integer", 123, nil}, + {"valid float", 123.45, nil}, + {"invalid type (string)", "123", errors.New("value must be a number")}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := IsNumber(test.input) + require.Equal(t, test.error, err) + }) + } +} + +func TestIsInt(t *testing.T) { + tests := []struct { + name string + input interface{} + error error + }{ + {"valid integer", 123, nil}, + {"invalid type (float)", 123.45, errors.New("value must be an integer")}, + {"invalid type (string)", "123", errors.New("value must be an integer")}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := IsInt(test.input) + require.Equal(t, test.error, err) + }) + } +} + +func TestIsFloat(t *testing.T) { + tests := []struct { + name string + input interface{} + error error + }{ + {"valid float", 123.45, nil}, + {"invalid type (int)", 123, errors.New("value must be a float")}, + {"invalid type (string)", "123.45", errors.New("value must be a float")}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := IsFloat(test.input) + require.Equal(t, test.error, err) + }) + } +} + +func TestIsBool(t *testing.T) { + tests := []struct { + name string + input interface{} + error error + }{ + {"valid bool (true)", true, nil}, + {"valid bool (false)", false, nil}, + {"invalid type (string)", "true", errors.New("value must be a boolean")}, + {"invalid type (int)", 1, errors.New("value must be a boolean")}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := IsBool(test.input) + require.Equal(t, test.error, err) + }) + } +} + +func TestIsSlice(t *testing.T) { + tests := []struct { + name string + input interface{} + error error + }{ + {"valid slice", []int{1, 2, 3}, nil}, + {"invalid type (string)", "hello", errors.New("value must be a slice")}, + {"invalid type (int)", 123, errors.New("value must be a slice")}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := IsSlice(test.input) + require.Equal(t, test.error, err) + }) + } +} + +func TestIsMap(t *testing.T) { + tests := []struct { + name string + input interface{} + error error + }{ + {"valid map", map[string]int{"a": 1, "b": 2}, nil}, + {"invalid type (string)", "hello", errors.New("value must be a map")}, + {"invalid type (int)", 123, errors.New("value must be a map")}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := IsMap(test.input) + require.Equal(t, test.error, err) + }) + } +} + +func TestIsURL(t *testing.T) { + tests := []struct { + name string + input interface{} + error error + }{ + {"valid URL", "https://example.com", nil}, + {"invalid URL", "example.com", errors.New("value is not a valid URL")}, + {"invalid type (int)", 123, errors.New("value must be a string")}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := IsURL(test.input) + require.Equal(t, test.error, err) + }) + } +} + +func TestIsUUID(t *testing.T) { + tests := []struct { + name string + input interface{} + error error + }{ + {"valid UUID", "123e4567-e89b-12d3-a456-426614174000", nil}, + {"invalid UUID", "invalid-uuid", errors.New("value is not a valid UUID")}, + {"invalid type (int)", 123, errors.New("value must be a string")}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := IsUUID(test.input) + require.Equal(t, test.error, err) + }) + } +} + +func TestIsDate(t *testing.T) { + tests := []struct { + name string + input interface{} + error error + }{ + {"valid date", "2023-10-01", nil}, + {"invalid date", "01-10-2023", errors.New("value is not a valid date (expected format: YYYY-MM-DD)")}, + {"invalid type (int)", 123, errors.New("value must be a string")}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := IsDate(test.input) + require.Equal(t, test.error, err) + }) + } +} + +func TestIsTime(t *testing.T) { + tests := []struct { + name string + input interface{} + error error + }{ + {"valid time", "15:04:05", nil}, + {"invalid time", "25:61:61", errors.New("value is not a valid time (expected format: HH:MM:SS)")}, + {"invalid type (int)", 123, errors.New("value must be a string")}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := IsTime(test.input) + require.Equal(t, test.error, err) + }) + } +} + +func TestIsCreditCard(t *testing.T) { + tests := []struct { + name string + input interface{} + error error + }{ + {"valid credit card", "4111 1111 1111 1111", nil}, + {"invalid credit card", "1234 5678 9012 3456", errors.New("value is not a valid credit card number")}, + {"invalid type (int)", 123, errors.New("value must be a string")}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := IsCreditCard(test.input) + require.Equal(t, test.error, err) + }) + } +} + +func TestIsHexColor(t *testing.T) { + tests := []struct { + name string + input interface{} + error error + }{ + {"valid hex color (6 digits)", "#FFFFFF", nil}, + {"valid hex color (3 digits)", "#FFF", nil}, + {"invalid hex color", "#ZZZ", errors.New("value is not a valid hexadecimal color code")}, + {"invalid type (int)", 123, errors.New("value must be a string")}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := IsHexColor(test.input) + require.Equal(t, test.error, err) + }) + } +} + +func TestIsJSON(t *testing.T) { + tests := []struct { + name string + input interface{} + error error + }{ + {"valid JSON", `{"key": "value"}`, nil}, + {"invalid JSON", `{"key": "value"`, errors.New("value is not valid JSON")}, + {"invalid type (int)", 123, errors.New("value must be a string")}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := IsJSON(test.input) + require.Equal(t, test.error, err) + }) + } +} + +func TestIsIP(t *testing.T) { + tests := []struct { + name string + input interface{} + error error + }{ + {"valid IPv4", "192.168.1.1", nil}, + {"valid IPv6", "2001:0db8:85a3:0000:0000:8a2e:0370:7334", nil}, + {"invalid IP", "invalid-ip", errors.New("value is not a valid IP address")}, + {"invalid type (int)", 123, errors.New("value must be a string")}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := IsIP(test.input) + require.Equal(t, test.error, err) + }) + } +} + +func TestIsAlpha(t *testing.T) { + tests := []struct { + name string + input interface{} + error error + }{ + {"valid alpha", "Hello", nil}, + {"invalid alpha", "Hello123", errors.New("value must contain only alphabetic characters")}, + {"invalid type (int)", 123, errors.New("value must be a string")}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := IsAlpha(test.input) + require.Equal(t, test.error, err) + }) + } +} + +func TestIsAlphaNumeric(t *testing.T) { + tests := []struct { + name string + input interface{} + error error + }{ + {"valid alphanumeric", "Hello123", nil}, + {"invalid alphanumeric", "Hello@123", errors.New("value must contain only alphanumeric characters")}, + {"invalid type (int)", 123, errors.New("value must be a string")}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := IsAlphaNumeric(test.input) + require.Equal(t, test.error, err) + }) + } +} + +func TestIsBase64(t *testing.T) { + tests := []struct { + name string + input interface{} + error error + }{ + {"valid Base64", "aGVsbG8=", nil}, // "hello" in Base64 + {"invalid Base64", "aGVsbG8", errors.New("value is not valid Base64")}, + {"invalid type (int)", 123, errors.New("value must be a string")}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := IsBase64(test.input) + require.Equal(t, test.error, err) + }) + } +} + +func TestIsArabic(t *testing.T) { + tests := []struct { + name string + input interface{} + error error + }{ + {"valid Arabic", "مرحبا", nil}, + {"invalid Arabic", "Hello123", errors.New("value must contain only Arabic characters")}, + {"invalid type (int)", 123, errors.New("value must be a string")}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := IsArabic(test.input) + require.Equal(t, test.error, err) + }) + } +} + +func TestIsAlphaArabic(t *testing.T) { + tests := []struct { + name string + input interface{} + error error + }{ + {"valid Arabic and Latin", "مرحبا Hello", nil}, + {"invalid Arabic and Latin", "مرحبا123", errors.New("value must contain only Arabic and Latin alphabetic characters")}, + {"invalid type (int)", 123, errors.New("value must be a string")}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := IsAlphaArabic(test.input) + require.Equal(t, test.error, err) + }) + } +} diff --git a/validator/range.go b/validator/range.go new file mode 100644 index 0000000..77cccf4 --- /dev/null +++ b/validator/range.go @@ -0,0 +1,111 @@ +package validator + +import ( + "errors" + "fmt" + "reflect" +) + +// MinLength checks if a string meets a minimum length requirement. +func MinLength(min int) ValidatorFunc { + return func(value interface{}) error { + str, ok := value.(string) + if !ok { + return errors.New("value is not a string") + } + if len(str) < min { + return fmt.Errorf("value must be at least %d characters long", min) + } + return nil + } +} + +// MaxLength checks if a string meets a maximum length requirement. +func MaxLength(max int) ValidatorFunc { + return func(value interface{}) error { + str, ok := value.(string) + if !ok { + return errors.New("value is not a string") + } + if len(str) > max { + return fmt.Errorf("value must be at most %d characters long", max) + } + return nil + } +} + +// Length checks if a string meets a length requirement within a range. +func Length(min, max int) ValidatorFunc { + return func(value interface{}) error { + str, ok := value.(string) + if !ok { + return errors.New("value is not a string") + } + length := len(str) + if length < min || length > max { + return fmt.Errorf("value must be between %d and %d characters long", min, max) + } + return nil + } +} + +// MaxValue checks if a numeric value is less than or equal to a maximum value. +func Max(max float64) ValidatorFunc { + return func(value interface{}) error { + v := reflect.ValueOf(value) + switch v.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + if float64(v.Int()) > max { + return fmt.Errorf("value must be less than or equal to %v", max) + } + case reflect.Float32, reflect.Float64: + if v.Float() > max { + return fmt.Errorf("value must be less than or equal to %v", max) + } + default: + return errors.New("value must be a number") + } + return nil + } +} + +// MinValue checks if a numeric value is greater than or equal to a minimum value. +func Min(min float64) ValidatorFunc { + return func(value interface{}) error { + v := reflect.ValueOf(value) + switch v.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + if float64(v.Int()) < min { + return fmt.Errorf("value must be greater than or equal to %v", min) + } + case reflect.Float32, reflect.Float64: + if v.Float() < min { + return fmt.Errorf("value must be greater than or equal to %v", min) + } + default: + return errors.New("value must be a number") + } + return nil + } +} + +// Each checks if every element in a slice or array satisfies the provided validator function. +func Each(validatorFunc ValidatorFunc) ValidatorFunc { + return func(value interface{}) error { + // Check if the value is a slice or array + v := reflect.ValueOf(value) + if v.Kind() != reflect.Slice && v.Kind() != reflect.Array { + return errors.New("value must be a slice or array") + } + + // Iterate over each element and apply the validator function + for i := 0; i < v.Len(); i++ { + element := v.Index(i).Interface() + if err := validatorFunc(element); err != nil { + return fmt.Errorf("element at index %d: %v", i, err) + } + } + + return nil + } +} diff --git a/validator/range_test.go b/validator/range_test.go new file mode 100644 index 0000000..7d9c360 --- /dev/null +++ b/validator/range_test.go @@ -0,0 +1,146 @@ +package validator + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/require" +) + +// TestMinLength tests the MinLength validator. +func TestMinLength(t *testing.T) { + minLength := MinLength(5) + + tests := []struct { + name string + input string + error error + }{ + {"valid length", "testing", nil}, + {"too short", "test", errors.New("value must be at least 5 characters long")}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := minLength(test.input) + require.Equal(t, test.error, err) + }) + } +} + +// TestMaxLength tests the MaxLength validator. +func TestMaxLength(t *testing.T) { + maxLength := MaxLength(5) + + tests := []struct { + name string + input string + error error + }{ + {"valid length", "test", nil}, + {"too long", "testing", errors.New("value must be at most 5 characters long")}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := maxLength(test.input) + require.Equal(t, test.error, err) + }) + } +} + +func TestMaxValue(t *testing.T) { + maxValue := Max(100) + + tests := []struct { + name string + input interface{} + error error + }{ + {"valid integer", 50, nil}, + {"valid float", 99.9, nil}, + {"equal to max", 100, nil}, + {"greater than max (integer)", 101, errors.New("value must be less than or equal to 100")}, + {"greater than max (float)", 100.1, errors.New("value must be less than or equal to 100")}, + {"invalid type (string)", "100", errors.New("value must be a number")}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := maxValue(test.input) + require.Equal(t, test.error, err) + }) + } +} + +func TestMinValue(t *testing.T) { + minValue := Min(10) + + tests := []struct { + name string + input interface{} + error error + }{ + {"valid integer", 50, nil}, + {"valid float", 10.1, nil}, + {"equal to min", 10, nil}, + {"less than min (integer)", 9, errors.New("value must be greater than or equal to 10")}, + {"less than min (float)", 9.9, errors.New("value must be greater than or equal to 10")}, + {"invalid type (string)", "10", errors.New("value must be a number")}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := minValue(test.input) + require.Equal(t, test.error, err) + }) + } +} + +func TestEach(t *testing.T) { + // Test with IsString validator + t.Run("Each element is a string", func(t *testing.T) { + input := []interface{}{"hello", "world", "123"} + err := Each(IsString)(input) + require.NoError(t, err) + }) + + t.Run("Each element is not a string", func(t *testing.T) { + input := []interface{}{"hello", 123, "world"} + err := Each(IsString)(input) + require.EqualError(t, err, "element at index 1: value must be a string") + }) + + // Test with IsNumber validator + t.Run("Each element is a number", func(t *testing.T) { + input := []interface{}{1, 2.5, 3} + err := Each(IsNumber)(input) + require.NoError(t, err) + }) + + t.Run("Each element is not a number", func(t *testing.T) { + input := []interface{}{1, "2.5", 3} + err := Each(IsNumber)(input) + require.EqualError(t, err, "element at index 1: value must be a number") + }) + + // Test with IsArabic validator + t.Run("Each element is Arabic", func(t *testing.T) { + input := []interface{}{"مرحبا", "العالم", "١٢٣"} + err := Each(IsArabic)(input) + require.NoError(t, err) + }) + + t.Run("Each element is not Arabic", func(t *testing.T) { + input := []interface{}{"مرحبا", "Hello", "العالم"} + err := Each(IsArabic)(input) + require.EqualError(t, err, "element at index 1: value must contain only Arabic characters") + }) + + // Test with non-slice/array input + t.Run("Input is not a slice or array", func(t *testing.T) { + input := "hello" + err := Each(IsString)(input) + require.EqualError(t, err, "value must be a slice or array") + }) +} diff --git a/validator/validator_test.go b/validator/validator_test.go index 0353492..eaf155b 100644 --- a/validator/validator_test.go +++ b/validator/validator_test.go @@ -4,137 +4,9 @@ import ( "errors" "testing" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -// TestIsNotEmpty tests the IsNotEmpty validator. -func TestIsNotEmpty(t *testing.T) { - tests := []struct { - name string - value interface{} - expected error - }{ - // Strings - {name: "Non-empty string", value: "hello", expected: nil}, - {name: "Empty string", value: "", expected: errors.New("value is empty")}, - - // Numbers - {name: "Non-zero int", value: 42, expected: nil}, - {name: "Zero int", value: 0, expected: errors.New("value is zero")}, - {name: "Non-zero float", value: 3.14, expected: nil}, - {name: "Zero float", value: 0.0, expected: errors.New("value is zero")}, - - // Booleans - {name: "True boolean", value: true, expected: nil}, - {name: "False boolean", value: false, expected: errors.New("value is false")}, - - // Slices - {name: "Non-empty slice", value: []int{1, 2, 3}, expected: nil}, - {name: "Empty slice", value: []int{}, expected: errors.New("value is empty")}, - - // Maps - {name: "Non-empty map", value: map[string]int{"a": 1}, expected: nil}, - {name: "Empty map", value: map[string]int{}, expected: errors.New("value is empty")}, - - // Nil - {name: "Nil value", value: nil, expected: errors.New("value is nil")}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := IsNotEmpty(tt.value) - if tt.expected == nil { - assert.NoError(t, err) - } else { - assert.EqualError(t, err, tt.expected.Error()) - } - }) - } -} - -// TestIsAlphanumeric tests the IsAlphanumeric validator. -func TestIsAlphanumeric(t *testing.T) { - tests := []struct { - name string - input interface{} - error error - }{ - {"valid alphanumeric", "abc123", nil}, - {"invalid characters", "abc@123", errors.New("value contains invalid characters")}, - {"not a string", 123, errors.New("value is not a string")}, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - err := IsAlphanumeric(test.input) - require.Equal(t, test.error, err) - }) - } -} - -// TestMinLength tests the MinLength validator. -func TestMinLength(t *testing.T) { - minLength := MinLength(5) - - tests := []struct { - name string - input string - error error - }{ - {"valid length", "testing", nil}, - {"too short", "test", errors.New("value must be at least 5 characters long")}, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - err := minLength(test.input) - require.Equal(t, test.error, err) - }) - } -} - -// TestMaxLength tests the MaxLength validator. -func TestMaxLength(t *testing.T) { - maxLength := MaxLength(5) - - tests := []struct { - name string - input string - error error - }{ - {"valid length", "test", nil}, - {"too long", "testing", errors.New("value must be at most 5 characters long")}, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - err := maxLength(test.input) - require.Equal(t, test.error, err) - }) - } -} - -// TestIsEmail tests the IsEmail validator. -func TestIsEmail(t *testing.T) { - tests := []struct { - name string - input interface{} - error error - }{ - {"valid email", "user@example.com", nil}, - {"invalid email", "invalid-email", errors.New("value is not a valid email address")}, - {"not a string", 123, errors.New("value is not a string")}, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - err := IsEmail(test.input) - require.Equal(t, test.error, err) - }) - } -} - // TestRegex tests the Regex validator. func TestRegex(t *testing.T) { regex := Regex(`^\d+$`) // Matches strings with only digits diff --git a/validator/validators.go b/validator/validators.go index 9a40dc9..7662bc7 100644 --- a/validator/validators.go +++ b/validator/validators.go @@ -3,103 +3,9 @@ package validator import ( "errors" "fmt" - "net/mail" - "reflect" "regexp" ) -// IsNotEmpty checks if a value is not empty. -func IsNotEmpty(value interface{}) error { - if value == nil { - return errors.New("value is nil") - } - - v := reflect.ValueOf(value) - - switch v.Kind() { - case reflect.String, reflect.Slice, reflect.Map, reflect.Array: - if v.Len() == 0 { - return errors.New("value is empty") - } - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - if v.Int() == 0 { - return errors.New("value is zero") - } - case reflect.Float32, reflect.Float64: - if v.Float() == 0 { - return errors.New("value is zero") - } - case reflect.Bool: - if !v.Bool() { - return errors.New("value is false") - } - default: - // For unsupported types, assume the value is not empty - return nil - } - - return nil -} - -// IsAlphanumeric checks if a string contains only alphanumeric characters. -func IsAlphanumeric(value interface{}) error { - str, ok := value.(string) - if !ok { - return errors.New("value is not a string") - } - for _, char := range str { - if !((char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') || (char >= '0' && char <= '9')) { - return errors.New("value contains invalid characters") - } - } - return nil -} - -// MinLength checks if a string meets a minimum length requirement. -func MinLength(min int) ValidatorFunc { - return func(value interface{}) error { - str, ok := value.(string) - if !ok { - return errors.New("value is not a string") - } - if len(str) < min { - return fmt.Errorf("value must be at least %d characters long", min) - } - return nil - } -} - -// MaxLength checks if a string meets a maximum length requirement. -func MaxLength(max int) ValidatorFunc { - return func(value interface{}) error { - str, ok := value.(string) - if !ok { - return errors.New("value is not a string") - } - if len(str) > max { - return fmt.Errorf("value must be at most %d characters long", max) - } - return nil - } -} - -// IsEmail checks if a string is a valid email address. -func IsEmail(value interface{}) error { - // Check if the input is a string - str, ok := value.(string) - if !ok { - return errors.New("value is not a string") - } - - // Use net/mail to validate the email - _, err := mail.ParseAddress(str) - if err != nil { - return errors.New("value is not a valid email address") - } - - return nil -} - // Regex validates a string against a regular expression. func Regex(pattern string) ValidatorFunc { re, err := regexp.Compile(pattern)