API Reference
Explore the complete API reference for all available functions and rules.
Learn from practical examples that demonstrate how to use Souuup in real-world scenarios. These examples show common patterns and best practices for different types of applications.
A complete example of validating user registration requests in a web API:
package main
import ( "encoding/json" "fmt" "log" "net/http" "strings" "unicode"
"github.com/cachesdev/souuup/r" "github.com/cachesdev/souuup/u")
type UserRegistration struct { Username string `json:"username"` Email string `json:"email"` Password string `json:"password"` ConfirmPassword string `json:"confirmPassword"` FirstName string `json:"firstName"` LastName string `json:"lastName"` Age int `json:"age"` Terms bool `json:"terms"`}
// Custom validation rulesfunc 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}
func StrongPassword(fs u.FieldState[string]) error { password := fs.Value
if len(password) < 8 { return fmt.Errorf("must be at least 8 characters long") }
hasUpper, hasLower, hasDigit := false, false, false for _, char := range password { switch { case unicode.IsUpper(char): hasUpper = true case unicode.IsLower(char): hasLower = true case unicode.IsDigit(char): hasDigit = true } }
if !hasUpper || !hasLower || !hasDigit { return fmt.Errorf("must contain uppercase, lowercase, and digit") }
return nil}
func PasswordsMatch(password, confirm string) u.Rule[string] { return func(fs u.FieldState[string]) error { if password != confirm { return fmt.Errorf("passwords do not match") } return nil }}
func ValidateUserRegistration(req UserRegistration) error { schema := u.Schema{ "username": u.Field(req.Username, r.NotZero, r.MinS(3), r.MaxS(20), ), "email": u.Field(req.Email, r.NotZero, ValidEmail, ), "password": u.Field(req.Password, r.NotZero, StrongPassword, PasswordsMatch(req.Password, req.ConfirmPassword), ), "firstName": u.Field(req.FirstName, r.NotZero, r.MinS(2), r.MaxS(50), ), "lastName": u.Field(req.LastName, r.NotZero, r.MinS(2), r.MaxS(50), ), "age": u.Field(req.Age, r.MinN(13), r.MaxN(120), ), "terms": u.Field(req.Terms, r.NotZero, // Must be true ), }
return u.NewSouuup(schema).Validate()}
func registerHandler(w http.ResponseWriter, req *http.Request) { if req.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return }
var registration UserRegistration if err := json.NewDecoder(req.Body).Decode(®istration); err != nil { http.Error(w, "Invalid JSON", http.StatusBadRequest) return }
if err := ValidateUserRegistration(registration); err != nil { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusBadRequest) json.NewEncoder(w).Encode(map[string]any{ "success": false, "message": "Validation failed", "errors": err.(*u.ValidationError).ToMap(), }) return }
// Process successful registration... w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]any{ "success": true, "message": "User registered successfully", })}
func main() { http.HandleFunc("/register", registerHandler)
fmt.Println("Server starting on :8080") log.Fatal(http.ListenAndServe(":8080", nil))}
Example for validating e-commerce product data:
type Product struct { Name string `json:"name"` SKU string `json:"sku"` Description string `json:"description"` Price float64 `json:"price"` Category string `json:"category"` Tags []string `json:"tags"` Weight float64 `json:"weight"` Dimensions struct { Length float64 `json:"length"` Width float64 `json:"width"` Height float64 `json:"height"` } `json:"dimensions"` InStock bool `json:"inStock"`}
func ValidSKU(fs u.FieldState[string]) error { sku := fs.Value // SKU format: ABC-123-XYZ parts := strings.Split(sku, "-") if len(parts) != 3 { return fmt.Errorf("SKU must follow format ABC-123-XYZ") }
if len(parts[0]) != 3 || len(parts[2]) != 3 { return fmt.Errorf("SKU prefix and suffix must be 3 characters") }
if len(parts[1]) != 3 { return fmt.Errorf("SKU middle part must be 3 digits") }
return nil}
func ValidateProduct(product Product) error { schema := u.Schema{ "name": u.Field(product.Name, r.NotZero, r.MinS(3), r.MaxS(100), ), "sku": u.Field(product.SKU, r.NotZero, ValidSKU, ), "description": u.Field(product.Description, r.NotZero, r.MinS(10), r.MaxS(2000), ), "price": u.Field(product.Price, r.Gt(0.0), r.Lte(99999.99), ), "category": u.Field(product.Category, r.NotZero, r.InS([]string{"electronics", "clothing", "books", "home", "sports"}), ), "tags": u.Field(product.Tags, r.MinLen[string](1), r.MaxLen[string](10), r.Every(r.MinS(2)), r.Every(r.MaxS(20)), ), "weight": u.Field(product.Weight, r.Gt(0.0), r.Lte(1000.0), // Max 1000kg ), "dimensions": u.Schema{ "length": u.Field(product.Dimensions.Length, r.Gt(0.0)), "width": u.Field(product.Dimensions.Width, r.Gt(0.0)), "height": u.Field(product.Dimensions.Height, r.Gt(0.0)), }, "inStock": u.Field(product.InStock), // No validation needed }
return u.NewSouuup(schema).Validate()}
A typical contact form validation scenario:
type ContactForm struct { Name string `json:"name"` Email string `json:"email"` Phone string `json:"phone,omitempty"` Subject string `json:"subject"` Message string `json:"message"` ContactMethod string `json:"contactMethod"` Newsletter bool `json:"newsletter"`}
func ValidPhone(fs u.FieldState[string]) error { if fs.Value == "" { return nil // Optional field }
phone := strings.ReplaceAll(fs.Value, " ", "") phone = strings.ReplaceAll(phone, "-", "") phone = strings.ReplaceAll(phone, "(", "") phone = strings.ReplaceAll(phone, ")", "")
if len(phone) < 10 { return fmt.Errorf("phone number must be at least 10 digits") }
return nil}
func ValidateContactForm(form ContactForm) error { schema := u.Schema{ "name": u.Field(form.Name, r.NotZero, r.MinS(2), r.MaxS(100), ), "email": u.Field(form.Email, r.NotZero, ValidEmail, ), "phone": u.Field(form.Phone, ValidPhone), "subject": u.Field(form.Subject, r.NotZero, r.MinS(5), r.MaxS(200), ), "message": u.Field(form.Message, r.NotZero, r.MinS(10), r.MaxS(5000), ), "contactMethod": u.Field(form.ContactMethod, r.NotZero, r.InS([]string{"email", "phone", "either"}), ), }
return u.NewSouuup(schema).Validate()}
Complex form with conditional validation:
type SurveyResponse struct { Age int `json:"age"` Gender string `json:"gender"` Employment string `json:"employment"` JobTitle string `json:"jobTitle,omitempty"` Company string `json:"company,omitempty"` Salary *int `json:"salary,omitempty"` Education string `json:"education"` Interests []string `json:"interests"` Satisfaction int `json:"satisfaction"` Feedback string `json:"feedback,omitempty"`}
func RequiredIfEmployed(employment, jobTitle string) u.Rule[string] { return func(fs u.FieldState[string]) error { if employment == "employed" && fs.Value == "" { return fmt.Errorf("job title is required for employed respondents") } return nil }}
func ValidateSurvey(survey SurveyResponse) error { schema := u.Schema{ "age": u.Field(survey.Age, r.MinN(16), r.MaxN(120), ), "gender": u.Field(survey.Gender, r.NotZero, r.InS([]string{"male", "female", "non-binary", "prefer-not-to-say"}), ), "employment": u.Field(survey.Employment, r.NotZero, r.InS([]string{"employed", "unemployed", "student", "retired"}), ), "jobTitle": u.Field(survey.JobTitle, RequiredIfEmployed(survey.Employment, survey.JobTitle), func(fs u.FieldState[string]) error { if survey.Employment == "employed" && len(fs.Value) < 2 { return fmt.Errorf("job title must be at least 2 characters") } return nil }, ), "education": u.Field(survey.Education, r.NotZero, r.InS([]string{"high-school", "bachelors", "masters", "phd", "other"}), ), "interests": u.Field(survey.Interests, r.MinLen[string](1), r.MaxLen[string](10), r.Every(r.MinS(2)), ), "satisfaction": u.Field(survey.Satisfaction, r.MinN(1), r.MaxN(10), ), }
return u.NewSouuup(schema).Validate()}
Validating data from CSV imports:
type CustomerImport struct { CustomerID string `csv:"customer_id"` FirstName string `csv:"first_name"` LastName string `csv:"last_name"` Email string `csv:"email"` Phone string `csv:"phone"` BirthDate string `csv:"birth_date"` Country string `csv:"country"` LifetimeValue float64 `csv:"lifetime_value"`}
func ValidDateFormat(layout string) u.Rule[string] { return func(fs u.FieldState[string]) error { if _, err := time.Parse(layout, fs.Value); err != nil { return fmt.Errorf("date must be in format %s", layout) } return nil }}
func ValidCountryCode(fs u.FieldState[string]) error { // This could be a comprehensive list or API call validCodes := map[string]bool{ "US": true, "CA": true, "GB": true, "DE": true, "FR": true, "AU": true, "JP": true, "BR": true, "IN": true, "CN": true, }
if !validCodes[fs.Value] { return fmt.Errorf("invalid country code") }
return nil}
func ValidateCustomerImport(customer CustomerImport, rowNum int) error { schema := u.Schema{ "customer_id": u.Field(customer.CustomerID, r.NotZero, r.MinS(3), r.MaxS(20), ), "first_name": u.Field(customer.FirstName, r.NotZero, r.MinS(1), r.MaxS(50), ), "last_name": u.Field(customer.LastName, r.NotZero, r.MinS(1), r.MaxS(50), ), "email": u.Field(customer.Email, r.NotZero, ValidEmail, ), "phone": u.Field(customer.Phone, ValidPhone), "birth_date": u.Field(customer.BirthDate, r.NotZero, ValidDateFormat("2006-01-02"), ), "country": u.Field(customer.Country, r.NotZero, ValidCountryCode, ), "lifetime_value": u.Field(customer.LifetimeValue, r.MinN(0.0), ), }
s := u.NewSouuup(schema) if err := s.Validate(); err != nil { return fmt.Errorf("row %d: %w", rowNum, err) }
return nil}
// Process CSV filefunc ProcessCustomerCSV(filename string) error { file, err := os.Open(filename) if err != nil { return err } defer file.Close()
reader := csv.NewReader(file) records, err := reader.ReadAll() if err != nil { return err }
var errors []error
for i, record := range records[1:] { // Skip header customer := CustomerImport{ CustomerID: record[0], FirstName: record[1], LastName: record[2], Email: record[3], Phone: record[4], BirthDate: record[5], Country: record[6], LifetimeValue: parseFloat(record[7]), }
if err := ValidateCustomerImport(customer, i+2); err != nil { errors = append(errors, err) } }
if len(errors) > 0 { for _, err := range errors { fmt.Printf("Validation error: %v\n", err) } return fmt.Errorf("found %d validation errors", len(errors)) }
return nil}
Validating application configuration:
type DatabaseConfig struct { Host string `yaml:"host"` Port int `yaml:"port"` Username string `yaml:"username"` Password string `yaml:"password"` Database string `yaml:"database"` MaxConnections int `yaml:"max_connections"` ConnectTimeout int `yaml:"connect_timeout"` SSL bool `yaml:"ssl"`}
type ServerConfig struct { Host string `yaml:"host"` Port int `yaml:"port"` ReadTimeout int `yaml:"read_timeout"` WriteTimeout int `yaml:"write_timeout"` TLS struct { Enabled bool `yaml:"enabled"` CertFile string `yaml:"cert_file"` KeyFile string `yaml:"key_file"` } `yaml:"tls"`}
type AppConfig struct { Environment string `yaml:"environment"` Debug bool `yaml:"debug"` Database DatabaseConfig `yaml:"database"` Server ServerConfig `yaml:"server"` Features struct { Analytics bool `yaml:"analytics"` Metrics bool `yaml:"metrics"` Logging bool `yaml:"logging"` } `yaml:"features"`}
func ValidPort(fs u.FieldState[int]) error { port := fs.Value if port < 1 || port > 65535 { return fmt.Errorf("port must be between 1 and 65535") } return nil}
func FileExists(fs u.FieldState[string]) error { if fs.Value == "" { return nil // Optional field }
if _, err := os.Stat(fs.Value); os.IsNotExist(err) { return fmt.Errorf("file does not exist: %s", fs.Value) }
return nil}
func ValidateAppConfig(config AppConfig) error { schema := u.Schema{ "environment": u.Field(config.Environment, r.NotZero, r.InS([]string{"development", "staging", "production"}), ), "database": u.Schema{ "host": u.Field(config.Database.Host, r.NotZero, r.MinS(1), ), "port": u.Field(config.Database.Port, ValidPort, ), "username": u.Field(config.Database.Username, r.NotZero, r.MinS(1), ), "password": u.Field(config.Database.Password, r.NotZero, r.MinS(8), ), "database": u.Field(config.Database.Database, r.NotZero, r.MinS(1), ), "max_connections": u.Field(config.Database.MaxConnections, r.MinN(1), r.MaxN(1000), ), "connect_timeout": u.Field(config.Database.ConnectTimeout, r.MinN(1), r.MaxN(300), // 5 minutes max ), }, "server": u.Schema{ "host": u.Field(config.Server.Host, r.NotZero, ), "port": u.Field(config.Server.Port, ValidPort, ), "read_timeout": u.Field(config.Server.ReadTimeout, r.MinN(1), r.MaxN(300), ), "write_timeout": u.Field(config.Server.WriteTimeout, r.MinN(1), r.MaxN(300), ), "tls": u.Schema{ "cert_file": u.Field(config.Server.TLS.CertFile, func(fs u.FieldState[string]) error { if config.Server.TLS.Enabled && fs.Value == "" { return fmt.Errorf("cert file is required when TLS is enabled") } return FileExists(fs) }, ), "key_file": u.Field(config.Server.TLS.KeyFile, func(fs u.FieldState[string]) error { if config.Server.TLS.Enabled && fs.Value == "" { return fmt.Errorf("key file is required when TLS is enabled") } return FileExists(fs) }, ), }, }, }
return u.NewSouuup(schema).Validate()}
Example of how to test your validation logic:
func TestUserRegistrationValidation(t *testing.T) { tests := []struct { name string user UserRegistration wantErr bool errField string }{ { name: "valid user", user: UserRegistration{ Username: "johndoe", Password: "Password123", ConfirmPassword: "Password123", FirstName: "John", LastName: "Doe", Age: 25, Terms: true, }, wantErr: false, }, { name: "short username", user: UserRegistration{ Username: "jo", // ... other valid fields }, wantErr: true, errField: "username", }, { name: "passwords don't match", user: UserRegistration{ Username: "johndoe", Password: "Password123", ConfirmPassword: "DifferentPassword", // ... other valid fields }, wantErr: true, errField: "password", }, }
for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := ValidateUserRegistration(tt.user)
if (err != nil) != tt.wantErr { t.Errorf("ValidateUserRegistration() error = %v, wantErr %v", err, tt.wantErr) return }
if tt.wantErr && tt.errField != "" { validationErr, ok := err.(*u.ValidationError) if !ok { t.Errorf("Expected ValidationError, got %T", err) return }
errorMap := validationErr.ToMap() if _, exists := errorMap[tt.errField]; !exists { t.Errorf("Expected error for field %s, but it wasn't found in %v", tt.errField, errorMap) } } }) }}
Creating user-friendly error responses:
type APIError struct { Code string `json:"code"` Message string `json:"message"` Field string `json:"field,omitempty"`}
type ValidationResponse struct { Success bool `json:"success"` Message string `json:"message"` Errors []APIError `json:"errors,omitempty"`}
func FormatValidationErrors(err error) ValidationResponse { validationErr, ok := err.(*u.ValidationError) if !ok { return ValidationResponse{ Success: false, Message: "Internal validation error", Errors: []APIError{{ Code: "VALIDATION_ERROR", Message: err.Error(), }}, } }
var apiErrors []APIError errorMap := validationErr.ToMap()
for field, fieldErrors := range errorMap { if errors, ok := fieldErrors["errors"]; ok { if errorList, ok := errors.([]u.RuleError); ok { for _, ruleErr := range errorList { apiErrors = append(apiErrors, APIError{ Code: "VALIDATION_FAILED", Message: string(ruleErr), Field: field, }) } } } }
return ValidationResponse{ Success: false, Message: "Validation failed", Errors: apiErrors, }}
These examples demonstrate the flexibility and power of Souuup for various validation scenarios. Each example can be adapted to your specific requirements and domain logic.
API Reference
Explore the complete API reference for all available functions and rules.
Custom Rules
Learn how to create your own validation rules for specific requirements.
GitHub Repository
Check out the source code and contribute to the project.