// Package validate provides reusable input validation utilities with structured errors. // It complements the sanitize package which focuses on security-critical sanitization. // Use validate for business rule validation; use sanitize for security-critical input sanitization. package validate import ( "fmt" "regexp" "strings" ) // ValidationError represents a single field validation failure. type ValidationError struct { Field string Message string } // Error implements the error interface. func (e ValidationError) Error() string { return fmt.Sprintf("%s: %s", e.Field, e.Message) } // ValidationErrors is a collection of validation errors. type ValidationErrors []ValidationError // Error implements the error interface, returning all errors as a single message. func (e ValidationErrors) Error() string { if len(e) == 0 { return "" } if len(e) == 1 { return e[0].Error() } var sb strings.Builder sb.WriteString("validation failed: ") for i, err := range e { if i > 0 { sb.WriteString("; ") } sb.WriteString(err.Error()) } return sb.String() } // HasErrors returns true if there are any validation errors. func (e ValidationErrors) HasErrors() bool { return len(e) > 0 } // Fields returns a map of field names to their error messages. // Useful for structured API error responses. func (e ValidationErrors) Fields() map[string]string { result := make(map[string]string, len(e)) for _, err := range e { // Only keep the first error per field if _, exists := result[err.Field]; !exists { result[err.Field] = err.Message } } return result } // Validator accumulates validation errors for composable validation. type Validator struct { errors ValidationErrors } // New creates a new Validator for composable validation. func New() *Validator { return &Validator{} } // Required validates that a string is not empty. func (v *Validator) Required(value, field string) *Validator { if strings.TrimSpace(value) == "" { v.errors = append(v.errors, ValidationError{ Field: field, Message: "is required", }) } return v } // RequiredSlice validates that a slice has at least one element. func (v *Validator) RequiredSlice(value []string, field string) *Validator { if len(value) == 0 { v.errors = append(v.errors, ValidationError{ Field: field, Message: "is required", }) } return v } // StringLength validates that a string's length is within bounds. // Pass 0 for min to skip minimum check, or 0 for max to skip maximum check. func (v *Validator) StringLength(value, field string, min, max int) *Validator { length := len(value) if min > 0 && length < min { v.errors = append(v.errors, ValidationError{ Field: field, Message: fmt.Sprintf("must be at least %d characters", min), }) } if max > 0 && length > max { v.errors = append(v.errors, ValidationError{ Field: field, Message: fmt.Sprintf("must be at most %d characters", max), }) } return v } // Pattern validates that a string matches a regular expression. func (v *Validator) Pattern(value, field string, pattern *regexp.Regexp, description string) *Validator { if value != "" && !pattern.MatchString(value) { v.errors = append(v.errors, ValidationError{ Field: field, Message: description, }) } return v } // Custom adds a custom validation check. func (v *Validator) Custom(valid bool, field, message string) *Validator { if !valid { v.errors = append(v.errors, ValidationError{ Field: field, Message: message, }) } return v } // AddError adds a validation error directly. func (v *Validator) AddError(field, message string) *Validator { v.errors = append(v.errors, ValidationError{ Field: field, Message: message, }) return v } // Error returns the accumulated validation errors, or nil if there are none. func (v *Validator) Error() error { if len(v.errors) == 0 { return nil } return v.errors } // Errors returns the validation errors slice directly. func (v *Validator) Errors() ValidationErrors { return v.errors } // HasErrors returns true if any validation errors have been recorded. func (v *Validator) HasErrors() bool { return len(v.errors) > 0 } // --- Standalone validation functions --- // Required validates that a string is not empty. // Returns a ValidationError if validation fails, nil otherwise. func Required(value, field string) error { if strings.TrimSpace(value) == "" { return ValidationError{ Field: field, Message: "is required", } } return nil } // RequiredSlice validates that a slice has at least one element. func RequiredSlice(value []string, field string) error { if len(value) == 0 { return ValidationError{ Field: field, Message: "is required", } } return nil } // StringLength validates that a string's length is within bounds. // Pass 0 for min to skip minimum check, or 0 for max to skip maximum check. func StringLength(value, field string, min, max int) error { length := len(value) if min > 0 && length < min { return ValidationError{ Field: field, Message: fmt.Sprintf("must be at least %d characters", min), } } if max > 0 && length > max { return ValidationError{ Field: field, Message: fmt.Sprintf("must be at most %d characters", max), } } return nil } // Pattern validates that a string matches a regular expression. // Returns nil for empty strings (use Required for that check). func Pattern(value, field string, pattern *regexp.Regexp, description string) error { if value != "" && !pattern.MatchString(value) { return ValidationError{ Field: field, Message: description, } } return nil } // --- Common patterns --- // Common pre-compiled regex patterns for reuse. var ( // AlphanumericDashUnderscore matches alphanumeric strings with dashes and underscores. AlphanumericDashUnderscore = regexp.MustCompile(`^[a-zA-Z0-9_-]+$`) // AlphanumericDashUnderscoreDot matches alphanumeric strings with dashes, underscores, and dots. AlphanumericDashUnderscoreDot = regexp.MustCompile(`^[a-zA-Z0-9._-]+$`) // Email matches a basic email pattern. Email = regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`) // UUID matches a UUID format. UUID = regexp.MustCompile(`^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$`) // Slug matches URL-safe slugs (lowercase alphanumeric with dashes). Slug = regexp.MustCompile(`^[a-z0-9]+(?:-[a-z0-9]+)*$`) ) // --- Convenience validators for common patterns --- // Name validates a name field (alphanumeric with dashes/underscores, 1-64 chars). // This matches the existing isValidName pattern in claude_config.go. func Name(value, field string) error { v := New() v.Required(value, field) v.StringLength(value, field, 1, 64) v.Pattern(value, field, AlphanumericDashUnderscore, "must be alphanumeric with dashes or underscores") return v.Error() } // IsValidationError returns true if the error is a ValidationError or ValidationErrors. func IsValidationError(err error) bool { if err == nil { return false } switch err.(type) { case ValidationError, ValidationErrors: return true default: return false } } // AsValidationErrors converts an error to ValidationErrors if possible. // Returns nil if the error is not a validation error. func AsValidationErrors(err error) ValidationErrors { if err == nil { return nil } switch e := err.(type) { case ValidationErrors: return e case ValidationError: return ValidationErrors{e} default: return nil } }