Single Responsibility
Each rule should validate one specific aspect. Combine multiple rules rather than creating one complex rule.
// Goodr.MinS(8), StrongPassword, NotCommonPassword
// AvoidComplexPasswordRule() // Does everything
While Souuup provides comprehensive built-in rules, you’ll often need custom validation logic for your specific domain requirements. This guide shows you how to create powerful, reusable custom rules.
A rule in Souuup is simply a function that takes a FieldState[T]
and returns an error if validation fails:
type Rule[T any] func(FieldState[T]) error
The FieldState[T]
contains the value being validated:
type FieldState[T any] struct { Value T // ... internal fields}
// Email validation rulefunc ValidEmail(fs u.FieldState[string]) error { email := fs.Value if !strings.Contains(email, "@") || !strings.Contains(email, ".") { return fmt.Errorf("must be a valid email address") } return nil}
// Usage
Create rules that accept parameters by returning a rule function:
// Rule that checks if string contains any of the given substringsfunc ContainsAny(substrings []string) u.Rule[string] { return func(fs u.FieldState[string]) error { value := fs.Value for _, substr := range substrings { if strings.Contains(value, substr) { return nil // Found one, validation passes } } return fmt.Errorf("must contain at least one of: %v", substrings) }}
// UsagepasswordField := u.Field(password, r.MinS(8), ContainsAny([]string{"!", "@", "#", "$", "%"}), // Must contain special char)
import ( "net/mail" "strings")
// Comprehensive email validation using Go's mail packagefunc ValidEmail(fs u.FieldState[string]) error { email := fs.Value if email == "" { return fmt.Errorf("email cannot be empty") }
if _, err := mail.ParseAddress(email); err != nil { return fmt.Errorf("must be a valid email address") }
return nil}
// Domain-specific email validationfunc EmailFromDomain(domain string) u.Rule[string] { return func(fs u.FieldState[string]) error { email := fs.Value if !strings.HasSuffix(email, "@"+domain) { return fmt.Errorf("email must be from domain %s", domain) } return nil }}
// Usage ValidEmail, EmailFromDomain("company.com"),)
import "regexp"
// Simple phone number validationfunc ValidPhoneNumber(fs u.FieldState[string]) error { phone := fs.Value // Remove common separators cleaned := strings.ReplaceAll(phone, " ", "") cleaned = strings.ReplaceAll(cleaned, "-", "") cleaned = strings.ReplaceAll(cleaned, "(", "") cleaned = strings.ReplaceAll(cleaned, ")", "")
// Check if it's all digits and proper length if len(cleaned) < 10 || len(cleaned) > 15 { return fmt.Errorf("phone number must be between 10 and 15 digits") }
for _, char := range cleaned { if !unicode.IsDigit(char) && char != '+' { return fmt.Errorf("phone number must contain only digits and optional + prefix") } }
return nil}
// Regex-based phone validation for specific formatsfunc USPhoneNumber(fs u.FieldState[string]) error { phone := fs.Value // Matches (XXX) XXX-XXXX or XXX-XXX-XXXX or XXXXXXXXXX pattern := `^(\([0-9]{3}\)|[0-9]{3})[-.\s]?[0-9]{3}[-.\s]?[0-9]{4}$` matched, _ := regexp.MatchString(pattern, phone)
if !matched { return fmt.Errorf("must be a valid US phone number format") }
return nil}
import "net/url"
// URL validationfunc ValidURL(fs u.FieldState[string]) error { urlStr := fs.Value parsedURL, err := url.Parse(urlStr) if err != nil { return fmt.Errorf("must be a valid URL") }
if parsedURL.Scheme == "" { return fmt.Errorf("URL must include a scheme (http:// or https://)") }
return nil}
// HTTPS-only URL validationfunc SecureURL(fs u.FieldState[string]) error { urlStr := fs.Value parsedURL, err := url.Parse(urlStr) if err != nil { return fmt.Errorf("must be a valid URL") }
if parsedURL.Scheme != "https" { return fmt.Errorf("URL must use HTTPS") }
return nil}
// Username pattern validationfunc ValidUsername(fs u.FieldState[string]) error { username := fs.Value
// Must start with letter, contain only letters, numbers, underscores pattern := `^[a-zA-Z][a-zA-Z0-9_]*$` matched, _ := regexp.MatchString(pattern, username)
if !matched { return fmt.Errorf("username must start with a letter and contain only letters, numbers, and underscores") }
return nil}
// Prime number validationfunc IsPrime(fs u.FieldState[int]) error { n := fs.Value if n < 2 { return fmt.Errorf("must be a prime number (>= 2)") }
for i := 2; i*i <= n; i++ { if n%i == 0 { return fmt.Errorf("must be a prime number") } }
return nil}
// Even number validationfunc IsEven(fs u.FieldState[int]) error { if fs.Value%2 != 0 { return fmt.Errorf("must be an even number") } return nil}
// Range validation with custom boundsfunc InRange[T u.Numeric](min, max T) u.Rule[T] { return func(fs u.FieldState[T]) error { value := fs.Value if value < min || value > max { return fmt.Errorf("must be between %v and %v (inclusive)", min, max) } return nil }}
// Percentage validation (0-100)func ValidPercentage(fs u.FieldState[float64]) error { value := fs.Value if value < 0 || value > 100 { return fmt.Errorf("percentage must be between 0 and 100") } return nil}
import ( "time")
// Date format validationfunc DateFormat(layout string) u.Rule[string] { return func(fs u.FieldState[string]) error { dateStr := fs.Value if _, err := time.Parse(layout, dateStr); err != nil { return fmt.Errorf("date must be in format %s", layout) } return nil }}
// Past date validationfunc InThePast(fs u.FieldState[time.Time]) error { if fs.Value.After(time.Now()) { return fmt.Errorf("date must be in the past") } return nil}
// Future date validationfunc InTheFuture(fs u.FieldState[time.Time]) error { if fs.Value.Before(time.Now()) { return fmt.Errorf("date must be in the future") } return nil}
// Age validation from birthdatefunc MinimumAge(minAge int) u.Rule[time.Time] { return func(fs u.FieldState[time.Time]) error { birthDate := fs.Value age := int(time.Since(birthDate).Hours() / 24 / 365)
if age < minAge { return fmt.Errorf("must be at least %d years old", minAge) }
return nil }}
// Business hours validationfunc DuringBusinessHours(fs u.FieldState[time.Time]) error { t := fs.Value hour := t.Hour()
// Business hours: 9 AM to 5 PM, Monday to Friday if t.Weekday() == time.Saturday || t.Weekday() == time.Sunday { return fmt.Errorf("must be during business days (Monday-Friday)") }
if hour < 9 || hour >= 17 { return fmt.Errorf("must be during business hours (9 AM - 5 PM)") }
return nil}
Sometimes you need to validate one field against another:
type PasswordForm struct { Password string ConfirmPassword string}
// Password confirmation rulefunc PasswordsMatch(original, confirmation string) u.Rule[string] { return func(fs u.FieldState[string]) error { if original != confirmation { return fmt.Errorf("passwords do not match") } return nil }}
// Date range validationfunc EndAfterStart(startDate, endDate time.Time) u.Rule[time.Time] { return func(fs u.FieldState[time.Time]) error { if endDate.Before(startDate) { return fmt.Errorf("end date must be after start date") } return nil }}
// Usagefunc ValidatePasswordForm(form PasswordForm) error { schema := u.Schema{ "password": u.Field(form.Password, r.NotZero, r.MinS(8), StrongPassword, // Custom rule ), "confirm_password": u.Field(form.ConfirmPassword, r.NotZero, PasswordsMatch(form.Password, form.ConfirmPassword), ), }
return u.NewSouuup(schema).Validate()}
// Credit card validation using Luhn algorithmfunc ValidCreditCard(fs u.FieldState[string]) error { cardNumber := strings.ReplaceAll(fs.Value, " ", "")
if len(cardNumber) < 13 || len(cardNumber) > 19 { return fmt.Errorf("credit card number must be 13-19 digits") }
// Luhn algorithm implementation sum := 0 alternate := false
for i := len(cardNumber) - 1; i >= 0; i-- { digit := int(cardNumber[i] - '0')
if alternate { digit *= 2 if digit > 9 { digit = digit%10 + digit/10 } }
sum += digit alternate = !alternate }
if sum%10 != 0 { return fmt.Errorf("invalid credit card number") }
return nil}
// Stock ticker symbol validationfunc ValidStockSymbol(fs u.FieldState[string]) error { symbol := fs.Value
// Must be 1-5 uppercase letters if len(symbol) < 1 || len(symbol) > 5 { return fmt.Errorf("stock symbol must be 1-5 characters") }
for _, char := range symbol { if !unicode.IsUpper(char) || !unicode.IsLetter(char) { return fmt.Errorf("stock symbol must contain only uppercase letters") } }
return nil}
// ISBN validationfunc ValidISBN(fs u.FieldState[string]) error { isbn := strings.ReplaceAll(fs.Value, "-", "")
if len(isbn) == 10 { return validateISBN10(isbn) } else if len(isbn) == 13 { return validateISBN13(isbn) }
return fmt.Errorf("ISBN must be 10 or 13 digits")}
func validateISBN10(isbn string) error { // ISBN-10 validation logic sum := 0 for i := 0; i < 9; i++ { digit := int(isbn[i] - '0') sum += digit * (10 - i) }
lastChar := isbn[9] var checkDigit int if lastChar == 'X' { checkDigit = 10 } else { checkDigit = int(lastChar - '0') }
sum += checkDigit
if sum%11 != 0 { return fmt.Errorf("invalid ISBN-10") }
return nil}
Rules that depend on external context or state:
// Database-dependent validationtype UserService struct { db *sql.DB}
func (us *UserService) UniqueEmail(fs u.FieldState[string]) error { email := fs.Value
var count int err := us.db.QueryRow("SELECT COUNT(*) FROM users WHERE email = ?", email).Scan(&count) if err != nil { return fmt.Errorf("failed to check email uniqueness") }
if count > 0 { return fmt.Errorf("email address is already in use") }
return nil}
// Environment-dependent validationfunc ValidEnvironment(allowedEnvs []string) u.Rule[string] { return func(fs u.FieldState[string]) error { env := fs.Value
for _, allowed := range allowedEnvs { if env == allowed { return nil } }
return fmt.Errorf("environment must be one of: %v", allowedEnvs) }}
// Combine multiple rules into onefunc StrongPassword(fs u.FieldState[string]) error { password := fs.Value
// Check minimum length if len(password) < 8 { return fmt.Errorf("password must be at least 8 characters long") }
// Check for uppercase letter hasUpper := false hasLower := false hasDigit := false hasSpecial := false
specialChars := "!@#$%^&*()_+-=[]{}|;:,.<>?"
for _, char := range password { switch { case unicode.IsUpper(char): hasUpper = true case unicode.IsLower(char): hasLower = true case unicode.IsDigit(char): hasDigit = true case strings.ContainsRune(specialChars, char): hasSpecial = true } }
var missing []string if !hasUpper { missing = append(missing, "uppercase letter") } if !hasLower { missing = append(missing, "lowercase letter") } if !hasDigit { missing = append(missing, "digit") } if !hasSpecial { missing = append(missing, "special character") }
if len(missing) > 0 { return fmt.Errorf("password must contain at least one: %s", strings.Join(missing, ", ")) }
return nil}
// Rule factory for common patternsfunc MustContainAll(requirements []string) u.Rule[string] { return func(fs u.FieldState[string]) error { value := strings.ToLower(fs.Value)
for _, req := range requirements { if !strings.Contains(value, strings.ToLower(req)) { return fmt.Errorf("must contain '%s'", req) } }
return nil }}
// Rule that applies only under certain conditionsfunc RequiredIf(condition bool, message string) u.Rule[string] { return func(fs u.FieldState[string]) error { if condition && fs.Value == "" { return fmt.Errorf(message) } return nil }}
// Conditional validation based on other field valuesfunc RequiredIfFieldEquals(otherValue, expectedValue, fieldName string) u.Rule[string] { return func(fs u.FieldState[string]) error { if otherValue == expectedValue && fs.Value == "" { return fmt.Errorf("field is required when %s is '%s'", fieldName, expectedValue) } return nil }}
// Usage exampletype ShippingForm struct { Method string // "pickup" or "delivery" DeliveryAddress string}
func ValidateShipping(form ShippingForm) error { schema := u.Schema{ "method": u.Field(form.Method, r.NotZero, r.InS([]string{"pickup", "delivery"}), ), "delivery_address": u.Field(form.DeliveryAddress, RequiredIfFieldEquals(form.Method, "delivery", "method"), // Only validate address format if it's required func(fs u.FieldState[string]) error { if form.Method == "delivery" && len(fs.Value) < 10 { return fmt.Errorf("delivery address must be at least 10 characters") } return nil }, ), }
return u.NewSouuup(schema).Validate()}
Always test your custom rules thoroughly:
func TestValidEmail(t *testing.T) { tests := []struct { name string email string wantErr bool }{ {"missing @", "userexample.com", true}, {"missing domain", "user@", true}, {"empty email", "", true}, }
for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { fs := u.FieldState[string]{Value: tt.email} err := ValidEmail(fs)
if (err != nil) != tt.wantErr { t.Errorf("ValidEmail() error = %v, wantErr %v", err, tt.wantErr) } }) }}
Single Responsibility
Each rule should validate one specific aspect. Combine multiple rules rather than creating one complex rule.
// Goodr.MinS(8), StrongPassword, NotCommonPassword
// AvoidComplexPasswordRule() // Does everything
Clear Error Messages
Provide specific, actionable error messages that help users understand what’s wrong.
// Good"password must contain at least one uppercase letter"
// Avoid"invalid password"
Handle Edge Cases
Consider empty values, nil pointers, and boundary conditions in your rules.
func ValidEmail(fs u.FieldState[string]) error { if fs.Value == "" { return fmt.Errorf("email cannot be empty") } // ... rest of validation}
Make Rules Reusable
Create parameterized rules that can be configured for different use cases.
func MinAge(age int) u.Rule[time.Time] { return func(fs u.FieldState[time.Time]) error { // Implementation }}
HTTP Integration
Learn how to use custom rules in web API validation.
API Reference
Complete reference of all available functions and interfaces.
Contributing
Consider contributing useful custom rules back to the Souuup project.