cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists 馃崈
charm
leaflet
readability
golang
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}