@@ -32,7 +32,7 @@ func TestCheckEach_EmptyTypedSlice(t *testing.T) {
|
||||
t.Error("callback should not be called for empty slice")
|
||||
})
|
||||
|
||||
if v.HasErrors() {
|
||||
if v.Error() != nil {
|
||||
t.Errorf("unexpected error for empty slice: %v", v.Error())
|
||||
}
|
||||
}
|
||||
@@ -63,7 +63,7 @@ func TestCheckEach_NonEmptyTypedSlice(t *testing.T) {
|
||||
t.Errorf("expected callback to be called 2 times, got %d", callCount)
|
||||
}
|
||||
|
||||
if v.HasErrors() {
|
||||
if v.Error() != nil {
|
||||
t.Errorf("unexpected error: %v", v.Error())
|
||||
}
|
||||
}
|
||||
@@ -79,7 +79,7 @@ func TestCheckEach_NilTypedSlice(t *testing.T) {
|
||||
t.Error("callback should not be called for nil slice")
|
||||
})
|
||||
|
||||
if v.HasErrors() {
|
||||
if v.Error() != nil {
|
||||
t.Errorf("unexpected error for nil slice: %v", v.Error())
|
||||
}
|
||||
}
|
||||
@@ -108,7 +108,7 @@ func TestCheckEach_PointerToNonEmptySlice(t *testing.T) {
|
||||
t.Errorf("expected callback to be called 3 times, got %d", callCount)
|
||||
}
|
||||
|
||||
if v.HasErrors() {
|
||||
if v.Error() != nil {
|
||||
t.Errorf("unexpected error for pointer to slice: %v", v.Error())
|
||||
}
|
||||
}
|
||||
@@ -124,7 +124,7 @@ func TestCheckEach_PointerToEmptySlice(t *testing.T) {
|
||||
t.Error("callback should not be called for empty slice")
|
||||
})
|
||||
|
||||
if v.HasErrors() {
|
||||
if v.Error() != nil {
|
||||
t.Errorf("unexpected error for pointer to empty slice: %v", v.Error())
|
||||
}
|
||||
}
|
||||
@@ -139,7 +139,7 @@ func TestCheckEach_NilPointerToSlice(t *testing.T) {
|
||||
t.Error("callback should not be called for nil pointer to slice")
|
||||
})
|
||||
|
||||
if v.HasErrors() {
|
||||
if v.Error() != nil {
|
||||
t.Errorf("unexpected error for nil pointer to slice: %v", v.Error())
|
||||
}
|
||||
}
|
||||
@@ -169,7 +169,7 @@ func TestCheckEach_DoublePointerToSlice(t *testing.T) {
|
||||
t.Errorf("expected callback to be called 2 times, got %d", callCount)
|
||||
}
|
||||
|
||||
if v.HasErrors() {
|
||||
if v.Error() != nil {
|
||||
t.Errorf("unexpected error for double pointer to slice: %v", v.Error())
|
||||
}
|
||||
}
|
||||
@@ -184,11 +184,11 @@ func TestCheckEach_NonSliceValue(t *testing.T) {
|
||||
t.Error("callback should not be called for non-slice value")
|
||||
})
|
||||
|
||||
if !v.HasErrors() {
|
||||
if v.Error() == nil {
|
||||
t.Error("expected error for non-slice value")
|
||||
}
|
||||
|
||||
errors := v.Errors()
|
||||
errors := v.Error().(ValidationErrors)
|
||||
if len(errors) != 1 {
|
||||
t.Errorf("expected 1 error, got %d", len(errors))
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ func TestDoublePointerValidation(t *testing.T) {
|
||||
|
||||
v.Check(doublePtr, "name", validator.Required(), validator.NotEmpty(), validator.MaxLen(1000))
|
||||
|
||||
if v.HasErrors() {
|
||||
if v.Error() != nil {
|
||||
t.Errorf("expected no errors, got: %v", v.Error())
|
||||
}
|
||||
})
|
||||
@@ -42,7 +42,7 @@ func TestDoublePointerValidation(t *testing.T) {
|
||||
|
||||
v.Check(doublePtr, "name", validator.Required(), validator.NotEmpty())
|
||||
|
||||
if !v.HasErrors() {
|
||||
if v.Error() == nil {
|
||||
t.Error("expected errors for empty string")
|
||||
}
|
||||
})
|
||||
@@ -55,7 +55,7 @@ func TestDoublePointerValidation(t *testing.T) {
|
||||
|
||||
v.Check(doublePtr, "name", validator.Required(), validator.MaxLen(10))
|
||||
|
||||
if !v.HasErrors() {
|
||||
if v.Error() == nil {
|
||||
t.Error("expected errors for string exceeding max length")
|
||||
}
|
||||
})
|
||||
@@ -66,7 +66,7 @@ func TestDoublePointerValidation(t *testing.T) {
|
||||
|
||||
v.Check(doublePtr, "name", validator.NotEmpty(), validator.MaxLen(1000))
|
||||
|
||||
if v.HasErrors() {
|
||||
if v.Error() != nil {
|
||||
t.Errorf("expected no errors for nil optional field, got: %v", v.Error())
|
||||
}
|
||||
})
|
||||
@@ -78,7 +78,7 @@ func TestDoublePointerValidation(t *testing.T) {
|
||||
|
||||
v.Check(doublePtr, "name", validator.NotEmpty(), validator.MaxLen(1000))
|
||||
|
||||
if v.HasErrors() {
|
||||
if v.Error() != nil {
|
||||
t.Errorf("expected no errors for nil optional field, got: %v", v.Error())
|
||||
}
|
||||
})
|
||||
@@ -91,7 +91,7 @@ func TestDoublePointerValidation(t *testing.T) {
|
||||
|
||||
v.Check(doublePtr, "name", validator.NotEmpty(), validator.MaxLen(1000))
|
||||
|
||||
if v.HasErrors() {
|
||||
if v.Error() != nil {
|
||||
t.Errorf("expected no errors, got: %v", v.Error())
|
||||
}
|
||||
})
|
||||
|
||||
@@ -69,11 +69,11 @@ func TestOneOf_CustomStringType(t *testing.T) {
|
||||
v.Check(tt.value, "asset_type", OneOfSlice(tt.allowed))
|
||||
|
||||
if tt.expectError {
|
||||
if !v.HasErrors() {
|
||||
if v.Error() == nil {
|
||||
t.Error("expected error but got none")
|
||||
}
|
||||
} else {
|
||||
if v.HasErrors() {
|
||||
if v.Error() != nil {
|
||||
t.Errorf("unexpected error: %v", v.Error())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,11 +56,11 @@ func TestOptional_WithGIDPointer(t *testing.T) {
|
||||
v.Check(tt.value, "owner_id", GID(100))
|
||||
|
||||
if tt.expectError {
|
||||
if !v.HasErrors() {
|
||||
if v.Error() == nil {
|
||||
t.Error("expected error but got none")
|
||||
}
|
||||
} else {
|
||||
if v.HasErrors() {
|
||||
if v.Error() != nil {
|
||||
t.Errorf("unexpected error: %v", v.Error())
|
||||
}
|
||||
}
|
||||
@@ -100,14 +100,14 @@ func TestOptional_WithCustomTypePointer(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
v := New()
|
||||
v.Check(tt.value, "asset_type", OneOf("VALID", "ANOTHER"))
|
||||
v.Check(tt.value, "asset_type", OneOfSlice([]string{"VALID", "ANOTHER"}))
|
||||
|
||||
if tt.expectError {
|
||||
if !v.HasErrors() {
|
||||
if v.Error() == nil {
|
||||
t.Error("expected error but got none")
|
||||
}
|
||||
} else {
|
||||
if v.HasErrors() {
|
||||
if v.Error() != nil {
|
||||
t.Errorf("unexpected error: %v", v.Error())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
@@ -97,29 +96,6 @@ func (v *Validator) CheckEach(items any, field string, fn func(index int, item a
|
||||
}
|
||||
}
|
||||
|
||||
func (v *Validator) CheckNested(field string, fn func(v *Validator)) {
|
||||
nestedValidator := New()
|
||||
fn(nestedValidator)
|
||||
|
||||
for _, err := range nestedValidator.errors {
|
||||
prefixedErr := &ValidationError{
|
||||
Field: fmt.Sprintf("%s.%s", field, err.Field),
|
||||
Code: err.Code,
|
||||
Message: err.Message,
|
||||
Value: err.Value,
|
||||
}
|
||||
v.errors = append(v.errors, prefixedErr)
|
||||
}
|
||||
}
|
||||
|
||||
func (v *Validator) HasErrors() bool {
|
||||
return len(v.errors) > 0
|
||||
}
|
||||
|
||||
func (v *Validator) Errors() ValidationErrors {
|
||||
return v.errors
|
||||
}
|
||||
|
||||
func (v *Validator) Error() error {
|
||||
if len(v.errors) == 0 {
|
||||
return nil
|
||||
|
||||
@@ -53,61 +53,6 @@ func BenchmarkValidate_OptionalField(b *testing.B) {
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkValidate_NestedStruct(b *testing.B) {
|
||||
type Address struct {
|
||||
City string
|
||||
ZipCode string
|
||||
}
|
||||
|
||||
type User struct {
|
||||
Name string
|
||||
Address Address
|
||||
}
|
||||
|
||||
user := User{
|
||||
Name: "John Doe",
|
||||
Address: Address{
|
||||
City: "New York",
|
||||
ZipCode: "10001",
|
||||
},
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
v := New()
|
||||
v.Check(&user.Name, "name", Required())
|
||||
v.CheckNested("address", func(av *Validator) {
|
||||
av.Check(&user.Address.City, "city", Required())
|
||||
av.Check(&user.Address.ZipCode, "zipCode", Pattern(`^\d{5}$`, ""))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkValidate_ArrayValidation(b *testing.B) {
|
||||
type Item struct {
|
||||
Name string
|
||||
Price int
|
||||
}
|
||||
|
||||
items := []Item{
|
||||
{Name: "Item 1", Price: 100},
|
||||
{Name: "Item 2", Price: 200},
|
||||
{Name: "Item 3", Price: 300},
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
v := New()
|
||||
for j, item := range items {
|
||||
v.CheckNested("items[0]", func(iv *Validator) {
|
||||
iv.Check(&item.Name, "name", Required())
|
||||
iv.Check(&item.Price, "price", Min(0))
|
||||
_ = j
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkURL(b *testing.B) {
|
||||
urlStr := "https://example.com"
|
||||
validator := URL()
|
||||
@@ -118,16 +63,6 @@ func BenchmarkURL(b *testing.B) {
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkUUID(b *testing.B) {
|
||||
uuid := "550e8400-e29b-41d4-a716-446655440000"
|
||||
validator := UUID()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = validator(&uuid)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMinLen(b *testing.B) {
|
||||
str := "hello world"
|
||||
validator := MinLen(5)
|
||||
@@ -148,36 +83,6 @@ func BenchmarkMin(b *testing.B) {
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMinFloat(b *testing.B) {
|
||||
num := 99.99
|
||||
validator := MinFloat(0.01)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = validator(&num)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMaxFloat(b *testing.B) {
|
||||
num := 50.50
|
||||
validator := MaxFloat(99.99)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = validator(&num)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkRangeFloat(b *testing.B) {
|
||||
num := 50.50
|
||||
validator := RangeFloat(0.01, 99.99)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = validator(&num)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkNotEmpty(b *testing.B) {
|
||||
str := "hello world"
|
||||
validator := NotEmpty()
|
||||
@@ -188,16 +93,6 @@ func BenchmarkNotEmpty(b *testing.B) {
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPattern(b *testing.B) {
|
||||
zipCode := "12345"
|
||||
validator := Pattern(`^\d{5}$`, "")
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = validator(&zipCode)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkValidate_WithErrors(b *testing.B) {
|
||||
email := ""
|
||||
|
||||
@@ -205,7 +100,7 @@ func BenchmarkValidate_WithErrors(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
v := New()
|
||||
v.Check(&email, "email", Required(), NotEmpty())
|
||||
if !v.HasErrors() {
|
||||
if v.Error() == nil {
|
||||
b.Fatal("expected validation error")
|
||||
}
|
||||
}
|
||||
@@ -251,73 +146,6 @@ func BenchmarkValidate_ComplexForm(b *testing.B) {
|
||||
v.Check(&user.Age, "age", Min(18), Max(120))
|
||||
v.Check(user.Website, "website", URL())
|
||||
v.Check(user.PhoneNumber, "phoneNumber", MinLen(10))
|
||||
v.Check(&user.Price, "price", MinFloat(0.01))
|
||||
|
||||
v.CheckNested("address", func(av *Validator) {
|
||||
av.Check(&user.Address.Street, "street", Required())
|
||||
av.Check(&user.Address.City, "city", Required())
|
||||
av.Check(&user.Address.ZipCode, "zipCode", Pattern(`^\d{5}$`, ""))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMinItems(b *testing.B) {
|
||||
items := []string{"a", "b", "c"}
|
||||
validator := MinItems(2)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = validator(&items)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMaxItems(b *testing.B) {
|
||||
items := []string{"a", "b", "c"}
|
||||
validator := MaxItems(5)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = validator(&items)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkUniqueItems(b *testing.B) {
|
||||
items := []string{"a", "b", "c"}
|
||||
validator := UniqueItems()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = validator(&items)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAlphaNumeric(b *testing.B) {
|
||||
str := "abc123DEF456"
|
||||
validator := AlphaNumeric()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = validator(&str)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkNoSpaces(b *testing.B) {
|
||||
str := "hello-world-test"
|
||||
validator := NoSpaces()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = validator(&str)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSlug(b *testing.B) {
|
||||
str := "hello-world-123"
|
||||
validator := Slug()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = validator(&str)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -343,47 +171,6 @@ func BenchmarkBefore(b *testing.B) {
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFutureDate(b *testing.B) {
|
||||
future := time.Now().Add(24 * time.Hour)
|
||||
validator := FutureDate()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = validator(&future)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPastDate(b *testing.B) {
|
||||
past := time.Now().Add(-24 * time.Hour)
|
||||
validator := PastDate()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = validator(&past)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkEqualTo(b *testing.B) {
|
||||
str1 := "password"
|
||||
str2 := "password"
|
||||
validator := EqualTo(&str2)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = validator(&str1)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkNotEqualTo(b *testing.B) {
|
||||
str1 := "password"
|
||||
str2 := "different"
|
||||
validator := NotEqualTo(&str2)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = validator(&str1)
|
||||
}
|
||||
}
|
||||
func BenchmarkDomain(b *testing.B) {
|
||||
str := "api.example.com"
|
||||
validator := Domain()
|
||||
@@ -394,16 +181,6 @@ func BenchmarkDomain(b *testing.B) {
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkHTTPUrl(b *testing.B) {
|
||||
str := "http://api.example.com/v1/users"
|
||||
validator := HTTPUrl()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = validator(&str)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkHTTPSUrl(b *testing.B) {
|
||||
str := "https://api.example.com/v1/users"
|
||||
validator := HTTPSUrl()
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"go.probo.inc/probo/pkg/mail"
|
||||
@@ -28,8 +27,8 @@ func TestValidator_Validate(t *testing.T) {
|
||||
|
||||
v.Check(&email, "email", Required(), NotEmpty())
|
||||
|
||||
if v.HasErrors() {
|
||||
t.Errorf("expected no errors, got: %v", v.Errors())
|
||||
if v.Error() != nil {
|
||||
t.Errorf("expected no errors, got: %v", v.Error().(ValidationErrors))
|
||||
}
|
||||
})
|
||||
|
||||
@@ -41,11 +40,11 @@ func TestValidator_Validate(t *testing.T) {
|
||||
v.Check(email, "email", NotEmpty())
|
||||
v.Check(&password, "password", Required(), MinLen(8))
|
||||
|
||||
if !v.HasErrors() {
|
||||
if v.Error() == nil {
|
||||
t.Error("expected validation errors")
|
||||
}
|
||||
|
||||
errors := v.Errors()
|
||||
errors := v.Error().(ValidationErrors)
|
||||
// email: 1 error (Required), password: 1 error (MinLen - Required passes because it's not empty)
|
||||
if len(errors) != 2 {
|
||||
t.Errorf("expected 2 errors, got %d: %v", len(errors), errors)
|
||||
@@ -58,7 +57,7 @@ func TestValidator_Validate(t *testing.T) {
|
||||
|
||||
v.Check(&value, "password", MinLen(8), MaxLen(5))
|
||||
|
||||
errors := v.Errors()
|
||||
errors := v.Error().(ValidationErrors)
|
||||
// Both MinLen and MaxLen will fail (too short and somehow conflicts, but logically MinLen will fail)
|
||||
if len(errors) < 1 {
|
||||
t.Errorf("expected at least 1 error, got %d", len(errors))
|
||||
@@ -66,41 +65,6 @@ func TestValidator_Validate(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestValidator_CheckNested(t *testing.T) {
|
||||
v := New()
|
||||
|
||||
v.CheckNested("user", func(nv *Validator) {
|
||||
email := mail.Nil
|
||||
nv.Check(&email, "email", NotEmpty())
|
||||
|
||||
nv.CheckNested("address", func(av *Validator) {
|
||||
city := ""
|
||||
av.Check(&city, "city", Required())
|
||||
})
|
||||
})
|
||||
|
||||
if !v.HasErrors() {
|
||||
t.Error("expected validation errors")
|
||||
}
|
||||
|
||||
errors := v.Errors()
|
||||
if len(errors) != 2 {
|
||||
t.Errorf("expected 2 errors, got %d", len(errors))
|
||||
}
|
||||
|
||||
// Check field paths
|
||||
expectedFields := map[string]bool{
|
||||
"user.email": true,
|
||||
"user.address.city": true,
|
||||
}
|
||||
|
||||
for _, err := range errors {
|
||||
if !expectedFields[err.Field] {
|
||||
t.Errorf("unexpected field path: %s", err.Field)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidator_Error(t *testing.T) {
|
||||
t.Run("no errors", func(t *testing.T) {
|
||||
v := New()
|
||||
@@ -192,11 +156,11 @@ func TestOptionalFieldExample(t *testing.T) {
|
||||
v.Check(req.PhoneNumber, "phoneNumber", MinLen(10))
|
||||
v.Check(req.Age, "age", Min(18), Max(120))
|
||||
|
||||
if !v.HasErrors() {
|
||||
if v.Error() == nil {
|
||||
t.Fatal("expected validation errors")
|
||||
}
|
||||
|
||||
errors := v.Errors()
|
||||
errors := v.Error().(ValidationErrors)
|
||||
|
||||
websiteErr := errors.ByField("website")
|
||||
if len(websiteErr) != 1 {
|
||||
@@ -216,140 +180,17 @@ func TestOptionalFieldExample(t *testing.T) {
|
||||
t.Logf("Optional field validation errors: %s", errors.Error())
|
||||
}
|
||||
|
||||
func TestRealWorldExample(t *testing.T) {
|
||||
// Simulate a user registration form
|
||||
type Address struct {
|
||||
City string
|
||||
ZipCode string
|
||||
}
|
||||
|
||||
type User struct {
|
||||
Email string
|
||||
Password string
|
||||
Age int
|
||||
Website *string
|
||||
Address Address
|
||||
}
|
||||
|
||||
user := User{
|
||||
Email: "",
|
||||
Password: "123",
|
||||
Age: 15,
|
||||
Website: new("not-a-url"),
|
||||
Address: Address{
|
||||
City: "",
|
||||
ZipCode: "12345",
|
||||
},
|
||||
}
|
||||
|
||||
v := New()
|
||||
|
||||
// Validate user fields
|
||||
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())
|
||||
|
||||
// Validate nested address
|
||||
v.CheckNested("address", func(av *Validator) {
|
||||
av.Check(&user.Address.City, "city", Required())
|
||||
av.Check(&user.Address.ZipCode, "zipCode", Pattern(`^\d{5}$`, "must be 5 digits"))
|
||||
})
|
||||
|
||||
if !v.HasErrors() {
|
||||
t.Fatal("expected validation errors")
|
||||
}
|
||||
|
||||
errors := v.Errors()
|
||||
expectedErrors := map[string]ErrorCode{
|
||||
"email": ErrorCodeRequired,
|
||||
"password": ErrorCodeTooShort,
|
||||
"age": ErrorCodeOutOfRange,
|
||||
"website": ErrorCodeInvalidURL,
|
||||
"address.city": ErrorCodeRequired,
|
||||
}
|
||||
|
||||
// Check that we have the expected errors
|
||||
for field, expectedCode := range expectedErrors {
|
||||
found := false
|
||||
for _, err := range errors {
|
||||
if err.Field == field && err.Code == expectedCode {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("expected error for field '%s' with code '%s'", field, expectedCode)
|
||||
}
|
||||
}
|
||||
|
||||
// Print errors for debugging
|
||||
t.Logf("Validation errors: %s", errors.Error())
|
||||
}
|
||||
|
||||
func TestArrayValidation(t *testing.T) {
|
||||
type Item struct {
|
||||
Name string
|
||||
Price int
|
||||
}
|
||||
|
||||
items := []Item{
|
||||
{Name: "", Price: -10},
|
||||
{Name: "Valid", Price: 100},
|
||||
{Name: "X", Price: 10},
|
||||
}
|
||||
|
||||
v := New()
|
||||
|
||||
// Validate each item
|
||||
for i, item := range items {
|
||||
field := fmt.Sprintf("items[%d]", i)
|
||||
v.CheckNested(field, func(iv *Validator) {
|
||||
iv.Check(&item.Name, "name", Required(), MinLen(2))
|
||||
iv.Check(&item.Price, "price", Min(0))
|
||||
})
|
||||
}
|
||||
|
||||
if !v.HasErrors() {
|
||||
t.Fatal("expected validation errors")
|
||||
}
|
||||
|
||||
errors := v.Errors()
|
||||
|
||||
// Check for specific field paths
|
||||
expectedFields := []string{
|
||||
"items[0].name",
|
||||
"items[0].price",
|
||||
"items[2].name",
|
||||
}
|
||||
|
||||
for _, expectedField := range expectedFields {
|
||||
found := false
|
||||
for _, err := range errors {
|
||||
if err.Field == expectedField {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("expected error for field '%s'", expectedField)
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("Array validation errors: %s", errors.Error())
|
||||
}
|
||||
|
||||
func TestDuplicateValidators(t *testing.T) {
|
||||
t.Run("duplicate MinLen creates two errors", func(t *testing.T) {
|
||||
v := New()
|
||||
name := "abc"
|
||||
v.Check(&name, "name", MinLen(5), MinLen(5))
|
||||
|
||||
if !v.HasErrors() {
|
||||
if v.Error() == nil {
|
||||
t.Error("expected validation errors")
|
||||
}
|
||||
|
||||
errors := v.Errors()
|
||||
errors := v.Error().(ValidationErrors)
|
||||
if len(errors) != 2 {
|
||||
t.Errorf("expected 2 errors (one per MinLen), got %d", len(errors))
|
||||
}
|
||||
@@ -367,7 +208,7 @@ func TestDuplicateValidators(t *testing.T) {
|
||||
name := ""
|
||||
v.Check(&name, "name", Required(), Required())
|
||||
|
||||
errors := v.Errors()
|
||||
errors := v.Error().(ValidationErrors)
|
||||
if len(errors) != 2 {
|
||||
t.Errorf("expected 2 errors, got %d", len(errors))
|
||||
}
|
||||
@@ -378,7 +219,7 @@ func TestDuplicateValidators(t *testing.T) {
|
||||
email := mail.Nil
|
||||
v.Check(&email, "email", NotEmpty(), NotEmpty())
|
||||
|
||||
errors := v.Errors()
|
||||
errors := v.Error().(ValidationErrors)
|
||||
if len(errors) != 2 {
|
||||
t.Errorf("expected 2 errors, got %d", len(errors))
|
||||
}
|
||||
@@ -389,7 +230,7 @@ func TestDuplicateValidators(t *testing.T) {
|
||||
name := "test"
|
||||
v.Check(&name, "name", MinLen(5), MinLen(10))
|
||||
|
||||
errors := v.Errors()
|
||||
errors := v.Error().(ValidationErrors)
|
||||
if len(errors) != 2 {
|
||||
t.Errorf("expected 2 errors, got %d", len(errors))
|
||||
}
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
package validator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// MinItems validates that a slice or array has at least the specified minimum number of items.
|
||||
func MinItems(min int) ValidatorFunc {
|
||||
return func(value any) *ValidationError {
|
||||
v := reflect.ValueOf(value)
|
||||
if v.Kind() == reflect.Pointer {
|
||||
if v.IsNil() {
|
||||
return nil
|
||||
}
|
||||
v = v.Elem()
|
||||
}
|
||||
|
||||
if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {
|
||||
return newValidationError(ErrorCodeInvalidFormat, "value must be a slice or array")
|
||||
}
|
||||
|
||||
if v.Len() < min {
|
||||
return newValidationError(
|
||||
ErrorCodeOutOfRange,
|
||||
fmt.Sprintf("must contain at least %d items", min),
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// MaxItems validates that a slice or array does not exceed the specified maximum number of items.
|
||||
func MaxItems(max int) ValidatorFunc {
|
||||
return func(value any) *ValidationError {
|
||||
v := reflect.ValueOf(value)
|
||||
if v.Kind() == reflect.Pointer {
|
||||
if v.IsNil() {
|
||||
return nil
|
||||
}
|
||||
v = v.Elem()
|
||||
}
|
||||
|
||||
if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {
|
||||
return newValidationError(ErrorCodeInvalidFormat, "value must be a slice or array")
|
||||
}
|
||||
|
||||
if v.Len() > max {
|
||||
return newValidationError(
|
||||
ErrorCodeOutOfRange,
|
||||
fmt.Sprintf("must contain at most %d items", max),
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// UniqueItems validates that all items in a slice or array are unique.
|
||||
func UniqueItems() ValidatorFunc {
|
||||
return func(value any) *ValidationError {
|
||||
v := reflect.ValueOf(value)
|
||||
if v.Kind() == reflect.Pointer {
|
||||
if v.IsNil() {
|
||||
return nil
|
||||
}
|
||||
v = v.Elem()
|
||||
}
|
||||
|
||||
if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {
|
||||
return newValidationError(ErrorCodeInvalidFormat, "value must be a slice or array")
|
||||
}
|
||||
|
||||
if v.Len() == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
elemType := v.Type().Elem()
|
||||
if !elemType.Comparable() {
|
||||
return newValidationError(ErrorCodeInvalidFormat, "cannot validate uniqueness for non-comparable types")
|
||||
}
|
||||
|
||||
seen := make(map[any]bool)
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
item := v.Index(i).Interface()
|
||||
if seen[item] {
|
||||
return newValidationError(ErrorCodeInvalidFormat, "items must be unique")
|
||||
}
|
||||
seen[item] = true
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -1,215 +0,0 @@
|
||||
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
package validator
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMinItems(t *testing.T) {
|
||||
t.Run("valid slice", func(t *testing.T) {
|
||||
items := []string{"a", "b", "c"}
|
||||
err := MinItems(2)(&items)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error, got: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("exact minimum", func(t *testing.T) {
|
||||
items := []int{1, 2}
|
||||
err := MinItems(2)(&items)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error, got: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("too few items", func(t *testing.T) {
|
||||
items := []string{"a"}
|
||||
err := MinItems(2)(&items)
|
||||
if err == nil {
|
||||
t.Fatal("expected validation error")
|
||||
} else if err.Code != ErrorCodeOutOfRange {
|
||||
t.Errorf("expected error code %s, got %s", ErrorCodeOutOfRange, err.Code)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("nil slice", func(t *testing.T) {
|
||||
var items *[]string
|
||||
err := MinItems(2)(items)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error for nil, got: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("non-slice value", func(t *testing.T) {
|
||||
value := "not a slice"
|
||||
err := MinItems(2)(&value)
|
||||
if err == nil || err.Code != ErrorCodeInvalidFormat {
|
||||
t.Error("expected invalid format error")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestMaxItems(t *testing.T) {
|
||||
t.Run("valid slice", func(t *testing.T) {
|
||||
items := []string{"a", "b"}
|
||||
err := MaxItems(5)(&items)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error, got: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("exact maximum", func(t *testing.T) {
|
||||
items := []int{1, 2, 3}
|
||||
err := MaxItems(3)(&items)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error, got: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("too many items", func(t *testing.T) {
|
||||
items := []string{"a", "b", "c", "d"}
|
||||
err := MaxItems(2)(&items)
|
||||
if err == nil {
|
||||
t.Fatal("expected validation error")
|
||||
} else if err.Code != ErrorCodeOutOfRange {
|
||||
t.Errorf("expected error code %s, got %s", ErrorCodeOutOfRange, err.Code)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("nil slice", func(t *testing.T) {
|
||||
var items *[]string
|
||||
err := MaxItems(2)(items)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error for nil, got: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestUniqueItems(t *testing.T) {
|
||||
t.Run("unique items", func(t *testing.T) {
|
||||
items := []string{"a", "b", "c"}
|
||||
err := UniqueItems()(&items)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error, got: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("duplicate items", func(t *testing.T) {
|
||||
items := []string{"a", "b", "a"}
|
||||
err := UniqueItems()(&items)
|
||||
if err == nil {
|
||||
t.Fatal("expected validation error")
|
||||
} else if err.Code != ErrorCodeInvalidFormat {
|
||||
t.Errorf("expected error code %s, got %s", ErrorCodeInvalidFormat, err.Code)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("unique integers", func(t *testing.T) {
|
||||
items := []int{1, 2, 3}
|
||||
err := UniqueItems()(&items)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error, got: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("duplicate integers", func(t *testing.T) {
|
||||
items := []int{1, 2, 1}
|
||||
err := UniqueItems()(&items)
|
||||
if err == nil {
|
||||
t.Error("expected validation error")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("nil slice", func(t *testing.T) {
|
||||
var items *[]string
|
||||
err := UniqueItems()(items)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error for nil, got: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("empty slice", func(t *testing.T) {
|
||||
items := []string{}
|
||||
err := UniqueItems()(&items)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error for empty slice, got: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("non-comparable type - slice of slices", func(t *testing.T) {
|
||||
items := [][]int{{1, 2}, {3, 4}}
|
||||
err := UniqueItems()(&items)
|
||||
if err == nil {
|
||||
t.Fatal("expected validation error for non-comparable type")
|
||||
} else {
|
||||
if err.Code != ErrorCodeInvalidFormat {
|
||||
t.Errorf("expected error code %s, got %s", ErrorCodeInvalidFormat, err.Code)
|
||||
}
|
||||
if err.Message != "cannot validate uniqueness for non-comparable types" {
|
||||
t.Errorf("unexpected error message: %s", err.Message)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("non-comparable type - slice of maps", func(t *testing.T) {
|
||||
items := []map[string]int{{"a": 1}, {"b": 2}}
|
||||
err := UniqueItems()(&items)
|
||||
if err == nil {
|
||||
t.Fatal("expected validation error for non-comparable type")
|
||||
} else if err.Code != ErrorCodeInvalidFormat {
|
||||
t.Errorf("expected error code %s, got %s", ErrorCodeInvalidFormat, err.Code)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("non-comparable type - struct with slice field", func(t *testing.T) {
|
||||
type NonComparable struct {
|
||||
Items []int
|
||||
}
|
||||
items := []NonComparable{{Items: []int{1, 2}}, {Items: []int{3, 4}}}
|
||||
err := UniqueItems()(&items)
|
||||
if err == nil {
|
||||
t.Fatal("expected validation error for non-comparable type")
|
||||
} else if err.Code != ErrorCodeInvalidFormat {
|
||||
t.Errorf("expected error code %s, got %s", ErrorCodeInvalidFormat, err.Code)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("comparable struct with unique values", func(t *testing.T) {
|
||||
type ComparableStruct struct {
|
||||
ID int
|
||||
Name string
|
||||
}
|
||||
items := []ComparableStruct{{ID: 1, Name: "a"}, {ID: 2, Name: "b"}}
|
||||
err := UniqueItems()(&items)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error for comparable structs, got: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("comparable struct with duplicate values", func(t *testing.T) {
|
||||
type ComparableStruct struct {
|
||||
ID int
|
||||
Name string
|
||||
}
|
||||
items := []ComparableStruct{{ID: 1, Name: "a"}, {ID: 2, Name: "b"}, {ID: 1, Name: "a"}}
|
||||
err := UniqueItems()(&items)
|
||||
if err == nil {
|
||||
t.Fatal("expected validation error for duplicate comparable structs")
|
||||
} else if err.Code != ErrorCodeInvalidFormat {
|
||||
t.Errorf("expected error code %s, got %s", ErrorCodeInvalidFormat, err.Code)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -23,8 +23,8 @@ func TestOptionalByDefault(t *testing.T) {
|
||||
v := New()
|
||||
v.Check(nil, "field", MinLen(5))
|
||||
|
||||
if v.HasErrors() {
|
||||
t.Errorf("expected no errors for nil (optional by default), got: %v", v.Errors())
|
||||
if v.Error() != nil {
|
||||
t.Errorf("expected no errors for nil (optional by default), got: %v", v.Error())
|
||||
}
|
||||
})
|
||||
|
||||
@@ -33,8 +33,8 @@ func TestOptionalByDefault(t *testing.T) {
|
||||
var str *string
|
||||
v.Check(str, "field", MinLen(5))
|
||||
|
||||
if v.HasErrors() {
|
||||
t.Errorf("expected no errors for nil pointer (optional by default), got: %v", v.Errors())
|
||||
if v.Error() != nil {
|
||||
t.Errorf("expected no errors for nil pointer (optional by default), got: %v", v.Error())
|
||||
}
|
||||
})
|
||||
|
||||
@@ -43,8 +43,8 @@ func TestOptionalByDefault(t *testing.T) {
|
||||
str := "hello world"
|
||||
v.Check(&str, "field", MinLen(5))
|
||||
|
||||
if v.HasErrors() {
|
||||
t.Errorf("expected no errors, got: %v", v.Errors())
|
||||
if v.Error() != nil {
|
||||
t.Errorf("expected no errors, got: %v", v.Error())
|
||||
}
|
||||
})
|
||||
|
||||
@@ -53,7 +53,7 @@ func TestOptionalByDefault(t *testing.T) {
|
||||
str := "hi"
|
||||
v.Check(&str, "field", MinLen(5))
|
||||
|
||||
if !v.HasErrors() {
|
||||
if v.Error() == nil {
|
||||
t.Error("expected validation error")
|
||||
}
|
||||
})
|
||||
@@ -63,8 +63,8 @@ func TestOptionalByDefault(t *testing.T) {
|
||||
str := "hello"
|
||||
v.Check(&str, "field", MinLen(3), MaxLen(10))
|
||||
|
||||
if v.HasErrors() {
|
||||
t.Errorf("expected no errors, got: %v", v.Errors())
|
||||
if v.Error() != nil {
|
||||
t.Errorf("expected no errors, got: %v", v.Error())
|
||||
}
|
||||
})
|
||||
|
||||
@@ -73,7 +73,7 @@ func TestOptionalByDefault(t *testing.T) {
|
||||
str := ""
|
||||
v.Check(&str, "field", MinLen(5))
|
||||
|
||||
if !v.HasErrors() {
|
||||
if v.Error() == nil {
|
||||
t.Error("expected validation error for empty string")
|
||||
}
|
||||
})
|
||||
@@ -83,7 +83,7 @@ func TestOptionalByDefault(t *testing.T) {
|
||||
var str *string
|
||||
v.Check(str, "field", Required())
|
||||
|
||||
if !v.HasErrors() {
|
||||
if v.Error() == nil {
|
||||
t.Error("expected validation error for nil with Required()")
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
package validator
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"time"
|
||||
)
|
||||
|
||||
// EqualTo validates that a value equals another value using deep equality.
|
||||
// Special handling for time.Time to compare instants rather than internal structure.
|
||||
func EqualTo(other any) ValidatorFunc {
|
||||
return func(value any) *ValidationError {
|
||||
if !areEqual(value, other) {
|
||||
return newValidationError(ErrorCodeInvalidFormat, "values must match")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// NotEqualTo validates that a value does not equal another value using deep equality.
|
||||
// Special handling for time.Time to compare instants rather than internal structure.
|
||||
func NotEqualTo(other any) ValidatorFunc {
|
||||
return func(value any) *ValidationError {
|
||||
if areEqual(value, other) {
|
||||
return newValidationError(ErrorCodeInvalidFormat, "values must not match")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// areEqual compares two values for equality with special handling for time.Time.
|
||||
func areEqual(a, b any) bool {
|
||||
// Dereference both values
|
||||
aVal, aIsNil := dereferenceValue(a)
|
||||
bVal, bIsNil := dereferenceValue(b)
|
||||
|
||||
// If both are nil, they're equal
|
||||
if aIsNil && bIsNil {
|
||||
return true
|
||||
}
|
||||
|
||||
// If only one is nil, they're not equal
|
||||
if aIsNil || bIsNil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Special handling for time.Time
|
||||
aTime, aIsTime := aVal.(time.Time)
|
||||
bTime, bIsTime := bVal.(time.Time)
|
||||
|
||||
if aIsTime && bIsTime {
|
||||
// Use time.Time.Equal() which compares the instant, ignoring location and monotonic clock
|
||||
return aTime.Equal(bTime)
|
||||
}
|
||||
|
||||
// Fall back to reflect.DeepEqual for all other types
|
||||
return reflect.DeepEqual(aVal, bVal)
|
||||
}
|
||||
@@ -1,182 +0,0 @@
|
||||
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
package validator
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestEqualTo(t *testing.T) {
|
||||
t.Run("equal strings", func(t *testing.T) {
|
||||
str1 := "password"
|
||||
str2 := "password"
|
||||
err := EqualTo(&str2)(&str1)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error, got: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("different strings", func(t *testing.T) {
|
||||
str1 := "password"
|
||||
str2 := "different"
|
||||
err := EqualTo(&str2)(&str1)
|
||||
if err == nil {
|
||||
t.Error("expected validation error")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("equal integers", func(t *testing.T) {
|
||||
num1 := 42
|
||||
num2 := 42
|
||||
err := EqualTo(&num2)(&num1)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error, got: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("different integers", func(t *testing.T) {
|
||||
num1 := 42
|
||||
num2 := 43
|
||||
err := EqualTo(&num2)(&num1)
|
||||
if err == nil {
|
||||
t.Error("expected validation error")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestNotEqualTo(t *testing.T) {
|
||||
t.Run("different strings", func(t *testing.T) {
|
||||
str1 := "password"
|
||||
str2 := "different"
|
||||
err := NotEqualTo(&str2)(&str1)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error, got: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("equal strings", func(t *testing.T) {
|
||||
str1 := "password"
|
||||
str2 := "password"
|
||||
err := NotEqualTo(&str2)(&str1)
|
||||
if err == nil {
|
||||
t.Error("expected validation error")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestEqualTo_TimeComparison(t *testing.T) {
|
||||
t.Run("same instant same location", func(t *testing.T) {
|
||||
time1 := time.Date(2025, 11, 5, 12, 0, 0, 0, time.UTC)
|
||||
time2 := time.Date(2025, 11, 5, 12, 0, 0, 0, time.UTC)
|
||||
err := EqualTo(time2)(time1)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error for same instant, got: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("same instant different location", func(t *testing.T) {
|
||||
// Create the same instant in different time zones
|
||||
utcTime := time.Date(2025, 11, 5, 12, 0, 0, 0, time.UTC)
|
||||
est, _ := time.LoadLocation("America/New_York")
|
||||
estTime := time.Date(2025, 11, 5, 7, 0, 0, 0, est) // 7am EST = 12pm UTC
|
||||
|
||||
err := EqualTo(utcTime)(estTime)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error for same instant in different locations, got: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("different instants same location", func(t *testing.T) {
|
||||
time1 := time.Date(2025, 11, 5, 12, 0, 0, 0, time.UTC)
|
||||
time2 := time.Date(2025, 11, 5, 13, 0, 0, 0, time.UTC)
|
||||
err := EqualTo(time2)(time1)
|
||||
if err == nil {
|
||||
t.Error("expected validation error for different instants")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("pointer to time same instant", func(t *testing.T) {
|
||||
time1 := time.Date(2025, 11, 5, 12, 0, 0, 0, time.UTC)
|
||||
time2 := time.Date(2025, 11, 5, 12, 0, 0, 0, time.UTC)
|
||||
err := EqualTo(&time2)(&time1)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error for pointer to same instant, got: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("nil time pointers", func(t *testing.T) {
|
||||
var time1 *time.Time
|
||||
var time2 *time.Time
|
||||
err := EqualTo(time2)(time1)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error for nil time pointers, got: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("one nil one non-nil time pointer", func(t *testing.T) {
|
||||
var time1 *time.Time
|
||||
time2 := time.Date(2025, 11, 5, 12, 0, 0, 0, time.UTC)
|
||||
err := EqualTo(&time2)(time1)
|
||||
if err == nil {
|
||||
t.Error("expected validation error for nil vs non-nil time")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("same instant with monotonic clock difference", func(t *testing.T) {
|
||||
// Simulate times with different monotonic clock data
|
||||
baseTime := time.Date(2025, 11, 5, 12, 0, 0, 0, time.UTC)
|
||||
time1 := baseTime
|
||||
time.Sleep(1 * time.Millisecond) // Advances monotonic clock
|
||||
time2 := baseTime
|
||||
|
||||
// Even though monotonic clocks differ, the instants are the same
|
||||
err := EqualTo(time2)(time1)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error despite monotonic clock difference, got: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestNotEqualTo_TimeComparison(t *testing.T) {
|
||||
t.Run("different instants", func(t *testing.T) {
|
||||
time1 := time.Date(2025, 11, 5, 12, 0, 0, 0, time.UTC)
|
||||
time2 := time.Date(2025, 11, 5, 13, 0, 0, 0, time.UTC)
|
||||
err := NotEqualTo(time2)(time1)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error for different instants, got: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("same instant same location", func(t *testing.T) {
|
||||
time1 := time.Date(2025, 11, 5, 12, 0, 0, 0, time.UTC)
|
||||
time2 := time.Date(2025, 11, 5, 12, 0, 0, 0, time.UTC)
|
||||
err := NotEqualTo(time2)(time1)
|
||||
if err == nil {
|
||||
t.Error("expected validation error for same instant")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("same instant different location", func(t *testing.T) {
|
||||
utcTime := time.Date(2025, 11, 5, 12, 0, 0, 0, time.UTC)
|
||||
est, _ := time.LoadLocation("America/New_York")
|
||||
estTime := time.Date(2025, 11, 5, 7, 0, 0, 0, est)
|
||||
|
||||
err := NotEqualTo(utcTime)(estTime)
|
||||
if err == nil {
|
||||
t.Error("expected validation error for same instant in different locations")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
package validator
|
||||
|
||||
// Custom creates a custom validator with a specified error code, message, and validation function.
|
||||
// The validation function should return true if the value is valid, false otherwise.
|
||||
func Custom(code ErrorCode, message string, fn func(value any) bool) ValidatorFunc {
|
||||
return func(value any) *ValidationError {
|
||||
if !fn(value) {
|
||||
return newValidationError(code, message)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
package validator
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCustom(t *testing.T) {
|
||||
validator := Custom(ErrorCodeCustom, "value must be positive", func(value any) bool {
|
||||
if num, ok := value.(int); ok {
|
||||
return num > 0
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
value any
|
||||
wantError bool
|
||||
}{
|
||||
{"valid positive", 5, false},
|
||||
{"invalid zero", 0, true},
|
||||
{"invalid negative", -5, true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := validator(tt.value)
|
||||
if (err != nil) != tt.wantError {
|
||||
t.Errorf("Custom() error = %v, wantError %v", err, tt.wantError)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
//go:embed data/disposable-email-domains/disposable_email_blocklist.conf
|
||||
disposableEmailsRaw []byte
|
||||
testEmails = []string{
|
||||
"acme.com",
|
||||
"acme.net",
|
||||
"acme.org",
|
||||
"ethereal.email",
|
||||
"example.com",
|
||||
"example.net",
|
||||
"example.org",
|
||||
"mailhog.local",
|
||||
"mailslurp.com",
|
||||
"test.com",
|
||||
"test.net",
|
||||
"test.org",
|
||||
"localhost.localdomain",
|
||||
}
|
||||
blacklistedEmails = append(
|
||||
strings.Split(strings.TrimSpace(string(disposableEmailsRaw)), "\n"),
|
||||
testEmails...,
|
||||
)
|
||||
notOneOfBlacklisted = NotOneOfSlice(blacklistedEmails)
|
||||
)
|
||||
|
||||
func NotBlacklisted() ValidatorFunc {
|
||||
return func(value any) *ValidationError {
|
||||
err := notOneOfBlacklisted(value)
|
||||
|
||||
if err != nil {
|
||||
return newValidationError(
|
||||
ErrorCodeInvalidEmail,
|
||||
"must not be blacklisted",
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,6 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
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])?$`)
|
||||
)
|
||||
|
||||
@@ -61,40 +60,6 @@ func URL() ValidatorFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// HTTPUrl validates that a string is a valid HTTP URL (not HTTPS).
|
||||
func HTTPUrl() ValidatorFunc {
|
||||
return func(value any) *ValidationError {
|
||||
actualValue, isNil := dereferenceValue(value)
|
||||
if isNil {
|
||||
return nil
|
||||
}
|
||||
|
||||
str, ok := actualValue.(string)
|
||||
if !ok {
|
||||
return newValidationError(ErrorCodeInvalidURL, "value must be a string")
|
||||
}
|
||||
|
||||
if str == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
parsedURL, err := url.Parse(str)
|
||||
if err != nil {
|
||||
return newValidationError(ErrorCodeInvalidURL, "invalid URL format")
|
||||
}
|
||||
|
||||
if parsedURL.Scheme != "http" {
|
||||
return newValidationError(ErrorCodeInvalidURL, "URL must use http scheme")
|
||||
}
|
||||
|
||||
if parsedURL.Host == "" {
|
||||
return newValidationError(ErrorCodeInvalidURL, "URL must have a host")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// HTTPSUrl validates that a string is a valid HTTPS URL (not HTTP).
|
||||
func HTTPSUrl() ValidatorFunc {
|
||||
return func(value any) *ValidationError {
|
||||
@@ -129,31 +94,6 @@ func HTTPSUrl() ValidatorFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// UUID validates that a string is a valid UUID.
|
||||
func UUID() ValidatorFunc {
|
||||
return func(value any) *ValidationError {
|
||||
actualValue, isNil := dereferenceValue(value)
|
||||
if isNil {
|
||||
return nil
|
||||
}
|
||||
|
||||
str, ok := actualValue.(string)
|
||||
if !ok {
|
||||
return newValidationError(ErrorCodeInvalidFormat, "value must be a string")
|
||||
}
|
||||
|
||||
if str == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !uuidRegex.MatchString(str) {
|
||||
return newValidationError(ErrorCodeInvalidFormat, "invalid UUID format")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// GID validates that a string is a valid GID using gid.ParseGID.
|
||||
// Optionally validates the entity type if provided.
|
||||
//
|
||||
|
||||
@@ -51,75 +51,6 @@ func TestURL(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPUrl(t *testing.T) {
|
||||
t.Run("valid http URL", func(t *testing.T) {
|
||||
str := "http://example.com"
|
||||
err := HTTPUrl()(&str)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error, got: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("valid http URL with path", func(t *testing.T) {
|
||||
str := "http://example.com/path/to/resource"
|
||||
err := HTTPUrl()(&str)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error, got: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("valid http URL with query", func(t *testing.T) {
|
||||
str := "http://example.com?foo=bar"
|
||||
err := HTTPUrl()(&str)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error, got: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid - https scheme", func(t *testing.T) {
|
||||
str := "https://example.com"
|
||||
err := HTTPUrl()(&str)
|
||||
if err == nil {
|
||||
t.Fatal("expected validation error for https")
|
||||
}
|
||||
if err.Message != "URL must use http scheme" {
|
||||
t.Errorf("unexpected error message: %s", err.Message)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid - no scheme", func(t *testing.T) {
|
||||
str := "example.com"
|
||||
err := HTTPUrl()(&str)
|
||||
if err == nil {
|
||||
t.Error("expected validation error for missing scheme")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid - no host", func(t *testing.T) {
|
||||
str := "http://"
|
||||
err := HTTPUrl()(&str)
|
||||
if err == nil {
|
||||
t.Error("expected validation error for missing host")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("empty string", func(t *testing.T) {
|
||||
str := ""
|
||||
err := HTTPUrl()(&str)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error for empty string, got: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("nil pointer", func(t *testing.T) {
|
||||
var str *string
|
||||
err := HTTPUrl()(str)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error for nil, got: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestHTTPSUrl(t *testing.T) {
|
||||
t.Run("valid https URL", func(t *testing.T) {
|
||||
str := "https://example.com"
|
||||
|
||||
@@ -78,126 +78,3 @@ func Max(max int) ValidatorFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// Range validates that a number is within the specified range (inclusive).
|
||||
func Range(min, max int) ValidatorFunc {
|
||||
return func(value any) *ValidationError {
|
||||
actualValue, isNil := dereferenceValue(value)
|
||||
if isNil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var num int
|
||||
switch v := actualValue.(type) {
|
||||
case int:
|
||||
num = v
|
||||
case int32:
|
||||
num = int(v)
|
||||
case int64:
|
||||
num = int(v)
|
||||
default:
|
||||
return newValidationError(ErrorCodeInvalidFormat, "value must be a number")
|
||||
}
|
||||
|
||||
if num < min || num > max {
|
||||
return newValidationError(
|
||||
ErrorCodeOutOfRange,
|
||||
fmt.Sprintf("must be between %d and %d", min, max),
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// MinFloat validates that a floating-point number is at least the specified minimum value.
|
||||
func MinFloat(min float64) ValidatorFunc {
|
||||
return func(value any) *ValidationError {
|
||||
actualValue, isNil := dereferenceValue(value)
|
||||
if isNil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var num float64
|
||||
switch v := actualValue.(type) {
|
||||
case float32:
|
||||
num = float64(v)
|
||||
case float64:
|
||||
num = v
|
||||
case int:
|
||||
num = float64(v)
|
||||
default:
|
||||
return newValidationError(ErrorCodeInvalidFormat, "value must be a number")
|
||||
}
|
||||
|
||||
if num < min {
|
||||
return newValidationError(
|
||||
ErrorCodeOutOfRange,
|
||||
fmt.Sprintf("must be at least %g", min),
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// MaxFloat validates that a floating-point number does not exceed the specified maximum value.
|
||||
func MaxFloat(max float64) ValidatorFunc {
|
||||
return func(value any) *ValidationError {
|
||||
actualValue, isNil := dereferenceValue(value)
|
||||
if isNil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var num float64
|
||||
switch v := actualValue.(type) {
|
||||
case float32:
|
||||
num = float64(v)
|
||||
case float64:
|
||||
num = v
|
||||
case int:
|
||||
num = float64(v)
|
||||
default:
|
||||
return newValidationError(ErrorCodeInvalidFormat, "value must be a number")
|
||||
}
|
||||
|
||||
if num > max {
|
||||
return newValidationError(
|
||||
ErrorCodeOutOfRange,
|
||||
fmt.Sprintf("must be at most %g", max),
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// RangeFloat validates that a floating-point number is within the specified range (inclusive).
|
||||
func RangeFloat(min, max float64) ValidatorFunc {
|
||||
return func(value any) *ValidationError {
|
||||
actualValue, isNil := dereferenceValue(value)
|
||||
if isNil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var num float64
|
||||
switch v := actualValue.(type) {
|
||||
case float32:
|
||||
num = float64(v)
|
||||
case float64:
|
||||
num = v
|
||||
case int:
|
||||
num = float64(v)
|
||||
default:
|
||||
return newValidationError(ErrorCodeInvalidFormat, "value must be a number")
|
||||
}
|
||||
|
||||
if num < min || num > max {
|
||||
return newValidationError(
|
||||
ErrorCodeOutOfRange,
|
||||
fmt.Sprintf("must be between %g and %g", min, max),
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,28 +66,3 @@ func TestMax(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRange(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
value any
|
||||
min int
|
||||
max int
|
||||
wantError bool
|
||||
}{
|
||||
{"in range", 5, 1, 10, false},
|
||||
{"at min", 1, 1, 10, false},
|
||||
{"at max", 10, 1, 10, false},
|
||||
{"below range", 0, 1, 10, true},
|
||||
{"above range", 11, 1, 10, true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := Range(tt.min, tt.max)(tt.value)
|
||||
if (err != nil) != tt.wantError {
|
||||
t.Errorf("Range() error = %v, wantError %v", err, tt.wantError)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -298,8 +298,8 @@ func TestNoHTML(t *testing.T) {
|
||||
title := "Product Title 2024"
|
||||
v.Check(&title, "title", Required(), NoHTML(), MinLen(3), MaxLen(100))
|
||||
|
||||
if v.HasErrors() {
|
||||
t.Errorf("expected no errors, got: %v", v.Errors())
|
||||
if v.Error() != nil {
|
||||
t.Errorf("expected no errors, got: %v", v.Error())
|
||||
}
|
||||
})
|
||||
|
||||
@@ -308,8 +308,8 @@ func TestNoHTML(t *testing.T) {
|
||||
title := "José García-O'Brien"
|
||||
v.Check(&title, "title", Required(), NoHTML(), PrintableText(), MinLen(3), MaxLen(100))
|
||||
|
||||
if v.HasErrors() {
|
||||
t.Errorf("expected no errors, got: %v", v.Errors())
|
||||
if v.Error() != nil {
|
||||
t.Errorf("expected no errors, got: %v", v.Error())
|
||||
}
|
||||
})
|
||||
|
||||
@@ -318,12 +318,12 @@ func TestNoHTML(t *testing.T) {
|
||||
malicious := "<script>alert('xss')</script>"
|
||||
v.Check(&malicious, "content", Required(), NoHTML(), PrintableText())
|
||||
|
||||
if !v.HasErrors() {
|
||||
if v.Error() == nil {
|
||||
t.Error("expected validation errors")
|
||||
}
|
||||
|
||||
// Should have error from NoHTML
|
||||
errors := v.Errors()
|
||||
errors := v.Error().(ValidationErrors)
|
||||
found := false
|
||||
for _, err := range errors {
|
||||
if strings.Contains(err.Message, "HTML tags") {
|
||||
@@ -341,12 +341,12 @@ func TestNoHTML(t *testing.T) {
|
||||
malicious := "<b>test\x00text</b>"
|
||||
v.Check(&malicious, "content", NoHTML(), PrintableText())
|
||||
|
||||
if !v.HasErrors() {
|
||||
if v.Error() == nil {
|
||||
t.Error("expected validation errors")
|
||||
}
|
||||
|
||||
// Should have at least one error (NoHTML will catch it first)
|
||||
if len(v.Errors()) < 1 {
|
||||
if ve := v.Error(); ve == nil || len(ve.(ValidationErrors)) < 1 {
|
||||
t.Error("expected at least one validation error")
|
||||
}
|
||||
})
|
||||
@@ -635,8 +635,8 @@ func TestPrintableText(t *testing.T) {
|
||||
title := "Product Title 2024"
|
||||
v.Check(&title, "title", Required(), PrintableText(), MinLen(3), MaxLen(100))
|
||||
|
||||
if v.HasErrors() {
|
||||
t.Errorf("expected no errors, got: %v", v.Errors())
|
||||
if v.Error() != nil {
|
||||
t.Errorf("expected no errors, got: %v", v.Error())
|
||||
}
|
||||
})
|
||||
|
||||
@@ -840,8 +840,8 @@ func TestSafeText(t *testing.T) {
|
||||
title := "Product Title 2024"
|
||||
v.Check(&title, "title", SafeText(100))
|
||||
|
||||
if v.HasErrors() {
|
||||
t.Errorf("expected no errors, got: %v", v.Errors())
|
||||
if v.Error() != nil {
|
||||
t.Errorf("expected no errors, got: %v", v.Error())
|
||||
}
|
||||
})
|
||||
|
||||
@@ -850,11 +850,11 @@ func TestSafeText(t *testing.T) {
|
||||
malicious := "<script>alert('xss')</script>"
|
||||
v.Check(&malicious, "content", SafeText(100))
|
||||
|
||||
if !v.HasErrors() {
|
||||
if v.Error() == nil {
|
||||
t.Error("expected validation errors")
|
||||
}
|
||||
|
||||
errors := v.Errors()
|
||||
errors := v.Error().(ValidationErrors)
|
||||
found := false
|
||||
for _, err := range errors {
|
||||
if strings.Contains(err.Message, "HTML tags") {
|
||||
|
||||
@@ -17,15 +17,9 @@ package validator
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
alphaNumericRegex = regexp.MustCompile(`^[a-zA-Z0-9]+$`)
|
||||
slugRegex = regexp.MustCompile(`^[a-z0-9]+(?:-[a-z0-9]+)*$`)
|
||||
)
|
||||
|
||||
// MinLen validates that a string has at least the specified minimum length.
|
||||
func MinLen(minLength int) ValidatorFunc {
|
||||
return func(value any) *ValidationError {
|
||||
@@ -74,107 +68,6 @@ func MaxLen(maxLength int) ValidatorFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// Pattern validates that a string matches the specified regular expression pattern.
|
||||
func Pattern(pattern string, message string) ValidatorFunc {
|
||||
regex := regexp.MustCompile(pattern)
|
||||
|
||||
return func(value any) *ValidationError {
|
||||
actualValue, isNil := dereferenceValue(value)
|
||||
if isNil {
|
||||
return nil
|
||||
}
|
||||
|
||||
str, ok := actualValue.(string)
|
||||
if !ok {
|
||||
return newValidationError(ErrorCodeInvalidFormat, "value must be a string")
|
||||
}
|
||||
|
||||
if !regex.MatchString(str) {
|
||||
if message == "" {
|
||||
message = fmt.Sprintf("must match pattern: %s", pattern)
|
||||
}
|
||||
return newValidationError(ErrorCodeInvalidFormat, message)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// AlphaNumeric validates that a string contains only letters and numbers.
|
||||
func AlphaNumeric() ValidatorFunc {
|
||||
return func(value any) *ValidationError {
|
||||
actualValue, isNil := dereferenceValue(value)
|
||||
if isNil {
|
||||
return nil
|
||||
}
|
||||
|
||||
str, ok := actualValue.(string)
|
||||
if !ok {
|
||||
return newValidationError(ErrorCodeInvalidFormat, "value must be a string")
|
||||
}
|
||||
|
||||
if str == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !alphaNumericRegex.MatchString(str) {
|
||||
return newValidationError(ErrorCodeInvalidFormat, "must contain only letters and numbers")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// NoSpaces validates that a string does not contain any spaces.
|
||||
func NoSpaces() ValidatorFunc {
|
||||
return func(value any) *ValidationError {
|
||||
actualValue, isNil := dereferenceValue(value)
|
||||
if isNil {
|
||||
return nil
|
||||
}
|
||||
|
||||
str, ok := actualValue.(string)
|
||||
if !ok {
|
||||
return newValidationError(ErrorCodeInvalidFormat, "value must be a string")
|
||||
}
|
||||
|
||||
if str == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if strings.Contains(str, " ") {
|
||||
return newValidationError(ErrorCodeInvalidFormat, "must not contain spaces")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Slug validates that a string is a valid URL slug (lowercase letters, numbers, and hyphens).
|
||||
func Slug() ValidatorFunc {
|
||||
return func(value any) *ValidationError {
|
||||
actualValue, isNil := dereferenceValue(value)
|
||||
if isNil {
|
||||
return nil
|
||||
}
|
||||
|
||||
str, ok := actualValue.(string)
|
||||
if !ok {
|
||||
return newValidationError(ErrorCodeInvalidFormat, "value must be a string")
|
||||
}
|
||||
|
||||
if str == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !slugRegex.MatchString(str) {
|
||||
return newValidationError(ErrorCodeInvalidFormat, "must be a valid slug (lowercase letters, numbers, and hyphens)")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// OneOfSlice validates that a value is one of the allowed values in the slice.
|
||||
// Accepts a slice of any type. Compares by value first, then by string representation.
|
||||
func OneOfSlice[T any](allowed []T) ValidatorFunc {
|
||||
@@ -224,105 +117,3 @@ func OneOfSlice[T any](allowed []T) ValidatorFunc {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// NotOneOfSlice validates that a value is not one of the values in the slice.
|
||||
// Accepts a slice of any type. Compares by value first, then by string representation.
|
||||
func NotOneOfSlice[T any](disallowed []T) ValidatorFunc {
|
||||
// Build disallowed map with string keys for flexible comparison
|
||||
disallowedMap := make(map[string]bool)
|
||||
disallowedStrings := make([]string, 0, len(disallowed))
|
||||
|
||||
for _, v := range disallowed {
|
||||
str := fmt.Sprint(v)
|
||||
disallowedMap[str] = true
|
||||
disallowedStrings = append(disallowedStrings, str)
|
||||
}
|
||||
|
||||
return func(value any) *ValidationError {
|
||||
// Handle nil values first
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Dereference all pointer levels
|
||||
actualValue := value
|
||||
val := reflect.ValueOf(value)
|
||||
for val.Kind() == reflect.Pointer {
|
||||
if val.IsNil() {
|
||||
return nil
|
||||
}
|
||||
val = val.Elem()
|
||||
actualValue = val.Interface()
|
||||
}
|
||||
|
||||
// First try exact match with DeepEqual
|
||||
for _, disallowedVal := range disallowed {
|
||||
if reflect.DeepEqual(actualValue, disallowedVal) {
|
||||
return newValidationError(
|
||||
ErrorCodeInvalidEnum,
|
||||
fmt.Sprintf("must not be one of: %s", strings.Join(disallowedStrings, ", ")),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Then try string comparison (for custom string types)
|
||||
valueStr := fmt.Sprint(actualValue)
|
||||
if disallowedMap[valueStr] {
|
||||
return newValidationError(
|
||||
ErrorCodeInvalidEnum,
|
||||
fmt.Sprintf("must not be one of: %s", strings.Join(disallowedStrings, ", ")),
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// OneOf validates that a value is one of the allowed values.
|
||||
// Accepts strings or types that implement fmt.Stringer as variadic arguments.
|
||||
func OneOf(allowed ...any) ValidatorFunc {
|
||||
allowedMap := make(map[string]bool)
|
||||
allowedStrings := make([]string, 0, len(allowed))
|
||||
|
||||
for _, v := range allowed {
|
||||
var str string
|
||||
switch val := v.(type) {
|
||||
case string:
|
||||
str = val
|
||||
case fmt.Stringer:
|
||||
str = val.String()
|
||||
default:
|
||||
str = fmt.Sprint(val)
|
||||
}
|
||||
allowedMap[str] = true
|
||||
allowedStrings = append(allowedStrings, str)
|
||||
}
|
||||
|
||||
return func(value any) *ValidationError {
|
||||
actualValue, isNil := dereferenceValue(value)
|
||||
if isNil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var str string
|
||||
switch v := actualValue.(type) {
|
||||
case string:
|
||||
str = v
|
||||
default:
|
||||
if stringer, ok := actualValue.(fmt.Stringer); ok {
|
||||
str = stringer.String()
|
||||
} else {
|
||||
return newValidationError(ErrorCodeInvalidEnum, "value must be a string or implement fmt.Stringer")
|
||||
}
|
||||
}
|
||||
|
||||
if !allowedMap[str] {
|
||||
return newValidationError(
|
||||
ErrorCodeInvalidEnum,
|
||||
fmt.Sprintf("must be one of: %s", strings.Join(allowedStrings, ", ")),
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,215 +67,6 @@ func TestMaxLen(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPattern(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
value any
|
||||
pattern string
|
||||
message string
|
||||
wantError bool
|
||||
}{
|
||||
{"valid pattern", "abc123", `^[a-z0-9]+$`, "", false},
|
||||
{"invalid pattern", "ABC123", `^[a-z0-9]+$`, "", true},
|
||||
{"custom message", "invalid", `^valid$`, "must be 'valid'", true},
|
||||
{"nil pointer", (*string)(nil), `^test$`, "", false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := Pattern(tt.pattern, tt.message)(tt.value)
|
||||
if (err != nil) != tt.wantError {
|
||||
t.Errorf("Pattern() error = %v, wantError %v", err, tt.wantError)
|
||||
}
|
||||
if err != nil && tt.message != "" && err.Message != tt.message {
|
||||
t.Errorf("Expected message '%s', got '%s'", tt.message, err.Message)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlphaNumeric(t *testing.T) {
|
||||
t.Run("valid alphanumeric", func(t *testing.T) {
|
||||
str := "abc123"
|
||||
err := AlphaNumeric()(&str)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error, got: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("only letters", func(t *testing.T) {
|
||||
str := "abcDEF"
|
||||
err := AlphaNumeric()(&str)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error, got: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("only numbers", func(t *testing.T) {
|
||||
str := "123456"
|
||||
err := AlphaNumeric()(&str)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error, got: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("contains spaces", func(t *testing.T) {
|
||||
str := "abc 123"
|
||||
err := AlphaNumeric()(&str)
|
||||
if err == nil {
|
||||
t.Error("expected validation error")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("contains special characters", func(t *testing.T) {
|
||||
str := "abc-123"
|
||||
err := AlphaNumeric()(&str)
|
||||
if err == nil {
|
||||
t.Error("expected validation error")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("empty string", func(t *testing.T) {
|
||||
str := ""
|
||||
err := AlphaNumeric()(&str)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error for empty string, got: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("nil pointer", func(t *testing.T) {
|
||||
var str *string
|
||||
err := AlphaNumeric()(str)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error for nil, got: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestNoSpaces(t *testing.T) {
|
||||
t.Run("no spaces", func(t *testing.T) {
|
||||
str := "hello-world"
|
||||
err := NoSpaces()(&str)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error, got: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("contains spaces", func(t *testing.T) {
|
||||
str := "hello world"
|
||||
err := NoSpaces()(&str)
|
||||
if err == nil {
|
||||
t.Error("expected validation error")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("multiple spaces", func(t *testing.T) {
|
||||
str := "hello world test"
|
||||
err := NoSpaces()(&str)
|
||||
if err == nil {
|
||||
t.Error("expected validation error")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("empty string", func(t *testing.T) {
|
||||
str := ""
|
||||
err := NoSpaces()(&str)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error for empty string, got: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("nil pointer", func(t *testing.T) {
|
||||
var str *string
|
||||
err := NoSpaces()(str)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error for nil, got: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSlug(t *testing.T) {
|
||||
t.Run("valid slug", func(t *testing.T) {
|
||||
str := "hello-world"
|
||||
err := Slug()(&str)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error, got: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("valid slug with numbers", func(t *testing.T) {
|
||||
str := "hello-world-123"
|
||||
err := Slug()(&str)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error, got: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("single word", func(t *testing.T) {
|
||||
str := "hello"
|
||||
err := Slug()(&str)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error, got: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("contains uppercase", func(t *testing.T) {
|
||||
str := "Hello-World"
|
||||
err := Slug()(&str)
|
||||
if err == nil {
|
||||
t.Error("expected validation error for uppercase")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("contains spaces", func(t *testing.T) {
|
||||
str := "hello world"
|
||||
err := Slug()(&str)
|
||||
if err == nil {
|
||||
t.Error("expected validation error for spaces")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("contains underscores", func(t *testing.T) {
|
||||
str := "hello_world"
|
||||
err := Slug()(&str)
|
||||
if err == nil {
|
||||
t.Error("expected validation error for underscores")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("starts with hyphen", func(t *testing.T) {
|
||||
str := "-hello"
|
||||
err := Slug()(&str)
|
||||
if err == nil {
|
||||
t.Error("expected validation error for leading hyphen")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ends with hyphen", func(t *testing.T) {
|
||||
str := "hello-"
|
||||
err := Slug()(&str)
|
||||
if err == nil {
|
||||
t.Error("expected validation error for trailing hyphen")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("empty string", func(t *testing.T) {
|
||||
str := ""
|
||||
err := Slug()(&str)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error for empty string, got: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("nil pointer", func(t *testing.T) {
|
||||
var str *string
|
||||
err := Slug()(str)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error for nil, got: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestOneOf(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
@@ -93,96 +93,6 @@ func Before(t any) ValidatorFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// FutureDate validates that a time is in the future.
|
||||
func FutureDate() ValidatorFunc {
|
||||
return func(value any) *ValidationError {
|
||||
actualValue, isNil := dereferenceValue(value)
|
||||
if isNil {
|
||||
return nil
|
||||
}
|
||||
|
||||
timeVal, ok := actualValue.(time.Time)
|
||||
if !ok {
|
||||
return newValidationError(ErrorCodeInvalidFormat, "value must be a time.Time")
|
||||
}
|
||||
|
||||
if !timeVal.After(time.Now()) {
|
||||
return newValidationError(ErrorCodeOutOfRange, "must be a future date")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// PastDate validates that a time is in the past.
|
||||
func PastDate() ValidatorFunc {
|
||||
return func(value any) *ValidationError {
|
||||
actualValue, isNil := dereferenceValue(value)
|
||||
if isNil {
|
||||
return nil
|
||||
}
|
||||
|
||||
timeVal, ok := actualValue.(time.Time)
|
||||
if !ok {
|
||||
return newValidationError(ErrorCodeInvalidFormat, "value must be a time.Time")
|
||||
}
|
||||
|
||||
if !timeVal.Before(time.Now()) {
|
||||
return newValidationError(ErrorCodeOutOfRange, "must be a past date")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// MinDuration validates that a duration is at least the specified minimum value.
|
||||
func MinDuration(min time.Duration) ValidatorFunc {
|
||||
return func(value any) *ValidationError {
|
||||
actualValue, isNil := dereferenceValue(value)
|
||||
if isNil {
|
||||
return nil
|
||||
}
|
||||
|
||||
duration, ok := actualValue.(time.Duration)
|
||||
if !ok {
|
||||
return newValidationError(ErrorCodeInvalidFormat, "value must be a time.Duration")
|
||||
}
|
||||
|
||||
if duration < min {
|
||||
return newValidationError(
|
||||
ErrorCodeOutOfRange,
|
||||
fmt.Sprintf("must be at least %s", min),
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// MaxDuration validates that a duration does not exceed the specified maximum value.
|
||||
func MaxDuration(max time.Duration) ValidatorFunc {
|
||||
return func(value any) *ValidationError {
|
||||
actualValue, isNil := dereferenceValue(value)
|
||||
if isNil {
|
||||
return nil
|
||||
}
|
||||
|
||||
duration, ok := actualValue.(time.Duration)
|
||||
if !ok {
|
||||
return newValidationError(ErrorCodeInvalidFormat, "value must be a time.Duration")
|
||||
}
|
||||
|
||||
if duration > max {
|
||||
return newValidationError(
|
||||
ErrorCodeOutOfRange,
|
||||
fmt.Sprintf("must be at most %s", max),
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// RangeDuration validates that a duration is within the specified range (inclusive).
|
||||
func RangeDuration(min, max time.Duration) ValidatorFunc {
|
||||
return func(value any) *ValidationError {
|
||||
|
||||
@@ -95,144 +95,6 @@ func TestBefore(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestFutureDate(t *testing.T) {
|
||||
future := time.Now().Add(24 * time.Hour)
|
||||
past := time.Now().Add(-24 * time.Hour)
|
||||
|
||||
t.Run("future date", func(t *testing.T) {
|
||||
err := FutureDate()(&future)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error, got: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("past date", func(t *testing.T) {
|
||||
err := FutureDate()(&past)
|
||||
if err == nil {
|
||||
t.Fatal("expected validation error")
|
||||
}
|
||||
if err.Code != ErrorCodeOutOfRange {
|
||||
t.Errorf("expected error code %s, got %s", ErrorCodeOutOfRange, err.Code)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("nil pointer", func(t *testing.T) {
|
||||
var timeVal *time.Time
|
||||
err := FutureDate()(timeVal)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error for nil, got: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestPastDate(t *testing.T) {
|
||||
future := time.Now().Add(24 * time.Hour)
|
||||
past := time.Now().Add(-24 * time.Hour)
|
||||
|
||||
t.Run("past date", func(t *testing.T) {
|
||||
err := PastDate()(&past)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error, got: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("future date", func(t *testing.T) {
|
||||
err := PastDate()(&future)
|
||||
if err == nil {
|
||||
t.Fatal("expected validation error")
|
||||
}
|
||||
if err.Code != ErrorCodeOutOfRange {
|
||||
t.Errorf("expected error code %s, got %s", ErrorCodeOutOfRange, err.Code)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("nil pointer", func(t *testing.T) {
|
||||
var timeVal *time.Time
|
||||
err := PastDate()(timeVal)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error for nil, got: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestMinDuration(t *testing.T) {
|
||||
minDuration := 10 * time.Minute
|
||||
|
||||
t.Run("duration above minimum", func(t *testing.T) {
|
||||
duration := 20 * time.Minute
|
||||
err := MinDuration(minDuration)(&duration)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error, got: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("duration equal to minimum", func(t *testing.T) {
|
||||
duration := 10 * time.Minute
|
||||
err := MinDuration(minDuration)(&duration)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error, got: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("duration below minimum", func(t *testing.T) {
|
||||
duration := 5 * time.Minute
|
||||
err := MinDuration(minDuration)(&duration)
|
||||
if err == nil {
|
||||
t.Fatal("expected validation error")
|
||||
}
|
||||
if err.Code != ErrorCodeOutOfRange {
|
||||
t.Errorf("expected error code %s, got %s", ErrorCodeOutOfRange, err.Code)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("nil pointer", func(t *testing.T) {
|
||||
var duration *time.Duration
|
||||
err := MinDuration(minDuration)(duration)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error for nil, got: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestMaxDuration(t *testing.T) {
|
||||
maxDuration := 1 * time.Hour
|
||||
|
||||
t.Run("duration below maximum", func(t *testing.T) {
|
||||
duration := 30 * time.Minute
|
||||
err := MaxDuration(maxDuration)(&duration)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error, got: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("duration equal to maximum", func(t *testing.T) {
|
||||
duration := 1 * time.Hour
|
||||
err := MaxDuration(maxDuration)(&duration)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error, got: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("duration above maximum", func(t *testing.T) {
|
||||
duration := 2 * time.Hour
|
||||
err := MaxDuration(maxDuration)(&duration)
|
||||
if err == nil {
|
||||
t.Fatal("expected validation error")
|
||||
}
|
||||
if err.Code != ErrorCodeOutOfRange {
|
||||
t.Errorf("expected error code %s, got %s", ErrorCodeOutOfRange, err.Code)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("nil pointer", func(t *testing.T) {
|
||||
var duration *time.Duration
|
||||
err := MaxDuration(maxDuration)(duration)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error for nil, got: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestRangeDuration(t *testing.T) {
|
||||
minDuration := 10 * time.Minute
|
||||
maxDuration := 1 * time.Hour
|
||||
|
||||
Reference in New Issue
Block a user