Major refactoring to hexagonal (ports & adapters) architecture: - Add service layer (apikey_service, project_service) for business logic - Add webhook system with dispatcher and delivery tracking - Add command queue with priority-based processing - Add rate limiting with sliding window algorithm - Add audit logging for command execution - Add OpenTelemetry integration (traces, metrics, spans) - Add circuit breaker for fault tolerance - Add cached repository wrapper for performance - Add comprehensive validation package - Add Kubernetes client integration for pod management - Add database migrations (allowed_ips, audit_log, rate_limiting, queue, webhooks) - Add network policy and PodDisruptionBudget for k8s - Remove legacy executor and projects/registry packages - Untrack secrets.yaml (now managed via envault) - Add coverage.out to .gitignore - Add e2e test infrastructure with docker-compose - Add comprehensive documentation (API, architecture, operations, plans) - Add golangci-lint config and pre-commit hook Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
280 lines
7.4 KiB
Go
280 lines
7.4 KiB
Go
// 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
|
|
}
|
|
}
|