cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists 馃崈
charm leaflet readability golang
at main 186 lines 4.4 kB view raw
1package services 2 3import ( 4 "fmt" 5 "net/url" 6 "regexp" 7 "slices" 8 "strings" 9 "time" 10) 11 12var ( 13 emailRegex = regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`) 14 dateFormats = []string{ 15 "2006-01-02", 16 "2006-01-02T15:04:05Z", 17 "2006-01-02T15:04:05-07:00", 18 "2006-01-02 15:04:05", 19 } 20) 21 22// ValidationError represents a validation error 23type ValidationError struct { 24 Field string 25 Message string 26} 27 28func (e ValidationError) Error() string { 29 return fmt.Sprintf("validation error for field '%s': %s", e.Field, e.Message) 30} 31 32func NewValidationError(f, m string) ValidationError { 33 return ValidationError{Field: f, Message: m} 34} 35 36// ValidationErrors represents multiple validation errors 37type ValidationErrors []ValidationError 38 39func (e ValidationErrors) Error() string { 40 if len(e) == 0 { 41 return "no validation errors" 42 } 43 44 if len(e) == 1 { 45 return e[0].Error() 46 } 47 48 var messages []string 49 for _, err := range e { 50 messages = append(messages, err.Error()) 51 } 52 return fmt.Sprintf("multiple validation errors: %s", strings.Join(messages, "; ")) 53} 54 55// RequiredString validates that a string field is not empty 56func RequiredString(name, value string) error { 57 if strings.TrimSpace(value) == "" { 58 return NewValidationError(name, "is required and cannot be empty") 59 } 60 return nil 61} 62 63// ValidURL validates that a string is a valid URL 64func ValidURL(name, value string) error { 65 if value == "" { 66 return nil 67 } 68 69 parsed, err := url.Parse(value) 70 if err != nil { 71 return ValidationError{Field: name, Message: "must be a valid URL"} 72 } 73 74 if parsed.Scheme != "http" && parsed.Scheme != "https" { 75 return NewValidationError(name, "must use http or https scheme") 76 } 77 78 return nil 79} 80 81// ValidEmail validates that a string is a valid email address 82func ValidEmail(name, value string) error { 83 if value == "" { 84 return nil 85 } 86 87 if !emailRegex.MatchString(value) { 88 return NewValidationError(name, "must be a valid email address") 89 } 90 91 return nil 92} 93 94// StringLength validates string length constraints 95func StringLength(name, value string, min, max int) error { 96 length := len(strings.TrimSpace(value)) 97 if min > 0 && length < min { 98 return NewValidationError(name, fmt.Sprintf("must be at least %d characters long", min)) 99 } 100 if max > 0 && length > max { 101 return NewValidationError(name, fmt.Sprintf("must not exceed %d characters", max)) 102 } 103 return nil 104} 105 106// ValidDate validates that a string can be parsed as a date in supported formats 107func ValidDate(name, value string) error { 108 if value == "" { 109 return nil 110 } 111 112 for _, format := range dateFormats { 113 if _, err := time.Parse(format, value); err == nil { 114 return nil 115 } 116 } 117 return NewValidationError(name, "must be a valid date (YYYY-MM-DD, YYYY-MM-DDTHH:MM:SSZ, etc.)") 118} 119 120// PositiveID validates that an ID is positive 121func PositiveID(name string, value int64) error { 122 if value <= 0 { 123 return NewValidationError(name, "must be a positive integer") 124 } 125 return nil 126} 127 128// ValidEnum validates that a value is one of the allowed enum values 129func ValidEnum(name, value string, allowedValues []string) error { 130 if value == "" { 131 return nil 132 } 133 if slices.Contains(allowedValues, value) { 134 return nil 135 } 136 return NewValidationError(name, fmt.Sprintf("must be one of: %s", strings.Join(allowedValues, ", "))) 137} 138 139// ValidFilePath validates that a string looks like a valid file path 140func ValidFilePath(name, value string) error { 141 if value == "" { 142 return nil 143 } 144 if strings.Contains(value, "..") { 145 return NewValidationError(name, "cannot contain '..' path traversal") 146 } 147 if strings.ContainsAny(value, "<>:\"|?*") { 148 return NewValidationError(name, "contains invalid characters") 149 } 150 return nil 151} 152 153// Validator provides a fluent interface for validation 154type Validator struct { 155 errors ValidationErrors 156} 157 158// NewValidator creates a new validator instance 159func NewValidator() *Validator { 160 return &Validator{} 161} 162 163// Check adds a validation check 164func (v *Validator) Check(err error) *Validator { 165 if err != nil { 166 if valErr, ok := err.(ValidationError); ok { 167 v.errors = append(v.errors, valErr) 168 } else { 169 v.errors = append(v.errors, NewValidationError("unknown", err.Error())) 170 } 171 } 172 return v 173} 174 175// IsValid returns true if no validation errors occurred 176func (v *Validator) IsValid() bool { 177 return len(v.errors) == 0 178} 179 180// Errors returns all validation errors 181func (v *Validator) Errors() error { 182 if len(v.errors) == 0 { 183 return nil 184 } 185 return v.errors 186}