@@ -25,7 +25,7 @@ func BenchmarkValidate_SingleField(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
v := New()
|
||||
v.Check(&email, "email", Required(), Email())
|
||||
v.Check(&email, "email", Required(), NotEmpty())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ func BenchmarkValidate_MultipleFields(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
v := New()
|
||||
v.Check(&email, "email", Required(), Email())
|
||||
v.Check(&email, "email", Required(), NotEmpty())
|
||||
v.Check(&password, "password", Required(), MinLen(8))
|
||||
v.Check(&age, "age", Min(18), Max(120))
|
||||
}
|
||||
@@ -108,16 +108,6 @@ func BenchmarkValidate_ArrayValidation(b *testing.B) {
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkEmail(b *testing.B) {
|
||||
email := "test@example.com"
|
||||
validator := Email()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = validator(&email)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkURL(b *testing.B) {
|
||||
urlStr := "https://example.com"
|
||||
validator := URL()
|
||||
@@ -214,7 +204,7 @@ func BenchmarkValidate_WithErrors(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
v := New()
|
||||
v.Check(&email, "email", Required(), Email())
|
||||
v.Check(&email, "email", Required(), NotEmpty())
|
||||
if !v.HasErrors() {
|
||||
b.Fatal("expected validation error")
|
||||
}
|
||||
@@ -256,7 +246,7 @@ func BenchmarkValidate_ComplexForm(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
v := New()
|
||||
v.Check(&user.Email, "email", Required(), Email())
|
||||
v.Check(&user.Email, "email", Required(), NotEmpty())
|
||||
v.Check(&user.Name, "name", Required(), MinLen(2))
|
||||
v.Check(&user.Age, "age", Min(18), Max(120))
|
||||
v.Check(user.Website, "website", URL())
|
||||
|
||||
@@ -19,13 +19,15 @@ import (
|
||||
"testing"
|
||||
|
||||
"go.gearno.de/x/ref"
|
||||
"go.probo.inc/probo/pkg/mail"
|
||||
)
|
||||
|
||||
func TestValidator_Validate(t *testing.T) {
|
||||
t.Run("single field validation", func(t *testing.T) {
|
||||
v := New()
|
||||
email := "test@example.com"
|
||||
v.Check(&email, "email", Required(), Email())
|
||||
email := mail.Addr("test@example.com")
|
||||
|
||||
v.Check(&email, "email", Required(), NotEmpty())
|
||||
|
||||
if v.HasErrors() {
|
||||
t.Errorf("expected no errors, got: %v", v.Errors())
|
||||
@@ -34,10 +36,10 @@ func TestValidator_Validate(t *testing.T) {
|
||||
|
||||
t.Run("multiple field validations", func(t *testing.T) {
|
||||
v := New()
|
||||
email := ""
|
||||
email := mail.Nil
|
||||
password := "123"
|
||||
|
||||
v.Check(&email, "email", Required(), Email())
|
||||
v.Check(email, "email", NotEmpty())
|
||||
v.Check(&password, "password", Required(), MinLen(8))
|
||||
|
||||
if !v.HasErrors() {
|
||||
@@ -69,8 +71,8 @@ func TestValidator_CheckNested(t *testing.T) {
|
||||
v := New()
|
||||
|
||||
v.CheckNested("user", func(nv *Validator) {
|
||||
email := "invalid"
|
||||
nv.Check(&email, "email", Email())
|
||||
email := mail.Nil
|
||||
nv.Check(&email, "email", NotEmpty())
|
||||
|
||||
nv.CheckNested("address", func(av *Validator) {
|
||||
city := ""
|
||||
@@ -186,7 +188,7 @@ func TestOptionalFieldExample(t *testing.T) {
|
||||
|
||||
v := New()
|
||||
|
||||
v.Check(&req.Email, "email", Required(), Email())
|
||||
v.Check(&req.Email, "email", Required(), NotEmpty())
|
||||
v.Check(&req.Name, "name", Required(), MinLen(2))
|
||||
v.Check(req.Website, "website", URL())
|
||||
v.Check(req.PhoneNumber, "phoneNumber", MinLen(10))
|
||||
@@ -232,7 +234,7 @@ func TestRealWorldExample(t *testing.T) {
|
||||
}
|
||||
|
||||
user := User{
|
||||
Email: "invalid-email",
|
||||
Email: "",
|
||||
Password: "123",
|
||||
Age: 15,
|
||||
Website: ref.Ref("not-a-url"),
|
||||
@@ -245,7 +247,7 @@ func TestRealWorldExample(t *testing.T) {
|
||||
v := New()
|
||||
|
||||
// Validate user fields
|
||||
v.Check(&user.Email, "email", Required(), Email())
|
||||
v.Check(&user.Email, "email", Required(), NotEmpty())
|
||||
v.Check(&user.Password, "password", Required(), MinLen(8))
|
||||
v.Check(&user.Age, "age", Min(18), Max(120))
|
||||
v.Check(user.Website, "website", URL())
|
||||
@@ -262,7 +264,7 @@ func TestRealWorldExample(t *testing.T) {
|
||||
|
||||
errors := v.Errors()
|
||||
expectedErrors := map[string]ErrorCode{
|
||||
"email": ErrorCodeInvalidEmail,
|
||||
"email": ErrorCodeRequired,
|
||||
"password": ErrorCodeTooShort,
|
||||
"age": ErrorCodeOutOfRange,
|
||||
"website": ErrorCodeInvalidURL,
|
||||
@@ -373,10 +375,10 @@ func TestDuplicateValidators(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("duplicate Email creates two errors", func(t *testing.T) {
|
||||
t.Run("duplicate NotEmpty creates two errors", func(t *testing.T) {
|
||||
v := New()
|
||||
email := "invalid"
|
||||
v.Check(&email, "email", Email(), Email())
|
||||
email := mail.Nil
|
||||
v.Check(&email, "email", NotEmpty(), NotEmpty())
|
||||
|
||||
errors := v.Errors()
|
||||
if len(errors) != 2 {
|
||||
@@ -405,22 +407,23 @@ func TestDuplicateValidators(t *testing.T) {
|
||||
|
||||
func TestStandardErrorPattern(t *testing.T) {
|
||||
// Simulates a typical validation function
|
||||
validateUser := func(email, password string) error {
|
||||
validateUser := func(email mail.Addr, password string) error {
|
||||
v := New()
|
||||
v.Check(&email, "email", Required(), Email())
|
||||
v.Check(&email, "email", Required(), NotEmpty())
|
||||
v.Check(&password, "password", Required(), MinLen(8))
|
||||
return v.Error()
|
||||
}
|
||||
|
||||
t.Run("valid data returns nil", func(t *testing.T) {
|
||||
err := validateUser("user@example.com", "password123")
|
||||
if err != nil {
|
||||
email := mail.Addr("user@example.com")
|
||||
|
||||
if err := validateUser(email, "password123"); err != nil {
|
||||
t.Errorf("expected nil, got: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid data returns ValidationErrors as error", func(t *testing.T) {
|
||||
err := validateUser("", "123")
|
||||
err := validateUser(mail.Nil, "123")
|
||||
if err == nil {
|
||||
t.Fatal("expected validation errors")
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@ package validator
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"go.probo.inc/probo/pkg/mail"
|
||||
)
|
||||
|
||||
// Required validates that a field has a value.
|
||||
@@ -59,6 +61,10 @@ func NotEmpty() ValidatorFunc {
|
||||
if strings.TrimSpace(v) == "" {
|
||||
return newValidationError(ErrorCodeRequired, "field cannot be empty")
|
||||
}
|
||||
case mail.Addr:
|
||||
if v == mail.Nil {
|
||||
return newValidationError(ErrorCodeRequired, "field cannot be empty")
|
||||
}
|
||||
default:
|
||||
rv := reflect.ValueOf(actualValue)
|
||||
if rv.Kind() == reflect.Slice && rv.Len() == 0 {
|
||||
|
||||
@@ -23,36 +23,10 @@ import (
|
||||
|
||||
var (
|
||||
emailRegex = regexp.MustCompile(`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`)
|
||||
gidRegex = regexp.MustCompile(`^gid://[a-zA-Z0-9\-_]+/[a-zA-Z0-9\-_]+/[a-zA-Z0-9\-_]+$`)
|
||||
uuidRegex = regexp.MustCompile(`^[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}$`)
|
||||
domainRegex = regexp.MustCompile(`^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)*[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$`)
|
||||
)
|
||||
|
||||
// Email validates that a string is a valid email address.
|
||||
func Email() ValidatorFunc {
|
||||
return func(value any) *ValidationError {
|
||||
actualValue, isNil := dereferenceValue(value)
|
||||
if isNil {
|
||||
return nil
|
||||
}
|
||||
|
||||
str, ok := actualValue.(string)
|
||||
if !ok {
|
||||
return newValidationError(ErrorCodeInvalidEmail, "value must be a string")
|
||||
}
|
||||
|
||||
if str == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !emailRegex.MatchString(str) {
|
||||
return newValidationError(ErrorCodeInvalidEmail, "invalid email address")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// URL validates that a string is a valid URL with http or https scheme.
|
||||
func URL() ValidatorFunc {
|
||||
return func(value any) *ValidationError {
|
||||
|
||||
@@ -21,35 +21,6 @@ import (
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
)
|
||||
|
||||
func TestEmail(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
value any
|
||||
wantError bool
|
||||
}{
|
||||
{"valid email", "test@example.com", false},
|
||||
{"valid email with plus", "test+tag@example.com", false},
|
||||
{"invalid email no @", "testexample.com", true},
|
||||
{"invalid email no domain", "test@", true},
|
||||
{"invalid email no TLD", "test@example", true},
|
||||
{"empty string", "", false}, // Empty is allowed, use Required() to enforce
|
||||
{"nil pointer", (*string)(nil), false}, // Skip validation
|
||||
{"non-string", 123, true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := Email()(tt.value)
|
||||
if (err != nil) != tt.wantError {
|
||||
t.Errorf("Email() error = %v, wantError %v", err, tt.wantError)
|
||||
}
|
||||
if err != nil && err.Code != ErrorCodeInvalidEmail {
|
||||
t.Errorf("Expected error code %s, got %s", ErrorCodeInvalidEmail, err.Code)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestURL(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
Reference in New Issue
Block a user