Remove deadcode

Signed-off-by: Bryan Frimin <bryan@getprobo.com>
This commit is contained in:
Bryan Frimin
2026-03-13 16:50:35 +01:00
parent 7fd4221199
commit ef76a8d2e1
76 changed files with 137 additions and 4424 deletions

View File

@@ -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))
}

View File

@@ -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())
}
})

View File

@@ -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())
}
}

View File

@@ -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())
}
}

View File

@@ -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

View File

@@ -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()

View File

@@ -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))
}

View File

@@ -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
}
}

View File

@@ -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)
}
})
}

View File

@@ -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()")
}
})

View File

@@ -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)
}

View File

@@ -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")
}
})
}

View File

@@ -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
}
}

View File

@@ -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)
}
})
}
}

View File

@@ -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
}
}

View File

@@ -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.
//

View File

@@ -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"

View File

@@ -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
}
}

View File

@@ -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)
}
})
}
}

View File

@@ -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") {

View File

@@ -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
}
}

View File

@@ -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

View File

@@ -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 {

View File

@@ -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