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
15 dateFormats = []string{
16 "2006-01-02",
17 "2006-01-02T15:04:05Z",
18 "2006-01-02T15:04:05-07:00",
19 "2006-01-02 15:04:05",
20 }
21)
22
23// ValidationError represents a validation error
24type ValidationError struct {
25 Field string
26 Message string
27}
28
29func (e ValidationError) Error() string {
30 return fmt.Sprintf("validation error for field '%s': %s", e.Field, e.Message)
31}
32
33// ValidationErrors represents multiple validation errors
34type ValidationErrors []ValidationError
35
36func (e ValidationErrors) Error() string {
37 if len(e) == 0 {
38 return "no validation errors"
39 }
40
41 if len(e) == 1 {
42 return e[0].Error()
43 }
44
45 var messages []string
46 for _, err := range e {
47 messages = append(messages, err.Error())
48 }
49 return fmt.Sprintf("multiple validation errors: %s", strings.Join(messages, "; "))
50}
51
52// RequiredString validates that a string field is not empty
53func RequiredString(name, value string) error {
54 if strings.TrimSpace(value) == "" {
55 return ValidationError{Field: name, Message: "is required and cannot be empty"}
56 }
57 return nil
58}
59
60// ValidURL validates that a string is a valid URL
61func ValidURL(name, value string) error {
62 if value == "" {
63 return nil
64 }
65
66 parsed, err := url.Parse(value)
67 if err != nil {
68 return ValidationError{Field: name, Message: "must be a valid URL"}
69 }
70
71 if parsed.Scheme != "http" && parsed.Scheme != "https" {
72 return ValidationError{Field: name, Message: "must use http or https scheme"}
73 }
74
75 return nil
76}
77
78// ValidEmail validates that a string is a valid email address
79func ValidEmail(name, value string) error {
80 if value == "" {
81 return nil
82 }
83
84 if !emailRegex.MatchString(value) {
85 return ValidationError{Field: name, Message: "must be a valid email address"}
86 }
87
88 return nil
89}
90
91// StringLength validates string length constraints
92func StringLength(name, value string, min, max int) error {
93 length := len(strings.TrimSpace(value))
94
95 if min > 0 && length < min {
96 return ValidationError{Field: name, Message: fmt.Sprintf("must be at least %d characters long", min)}
97 }
98
99 if max > 0 && length > max {
100 return ValidationError{Field: name, Message: fmt.Sprintf("must not exceed %d characters", max)}
101 }
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
118 return ValidationError{
119 Field: name,
120 Message: "must be a valid date (YYYY-MM-DD, YYYY-MM-DDTHH:MM:SSZ, etc.)",
121 }
122}
123
124// PositiveID validates that an ID is positive
125func PositiveID(name string, value int64) error {
126 if value <= 0 {
127 return ValidationError{Field: name, Message: "must be a positive integer"}
128 }
129 return nil
130}
131
132// ValidEnum validates that a value is one of the allowed enum values
133func ValidEnum(name, value string, allowedValues []string) error {
134 if value == "" {
135 return nil
136 }
137
138 if slices.Contains(allowedValues, value) {
139 return nil
140 }
141
142 message := fmt.Sprintf("must be one of: %s", strings.Join(allowedValues, ", "))
143 return ValidationError{Field: name, Message: message}
144}
145
146// ValidFilePath validates that a string looks like a valid file path
147func ValidFilePath(name, value string) error {
148 if value == "" {
149 return nil
150 }
151
152 if strings.Contains(value, "..") {
153 return ValidationError{Field: name, Message: "cannot contain '..' path traversal"}
154 }
155
156 if strings.ContainsAny(value, "<>:\"|?*") {
157 return ValidationError{Field: name, Message: "contains invalid characters"}
158 }
159
160 return nil
161}
162
163// Validator provides a fluent interface for validation
164type Validator struct {
165 errors ValidationErrors
166}
167
168// NewValidator creates a new validator instance
169func NewValidator() *Validator {
170 return &Validator{}
171}
172
173// Check adds a validation check
174func (v *Validator) Check(err error) *Validator {
175 if err != nil {
176 if valErr, ok := err.(ValidationError); ok {
177 v.errors = append(v.errors, valErr)
178 } else {
179 v.errors = append(v.errors, ValidationError{Field: "unknown", Message: err.Error()})
180 }
181 }
182 return v
183}
184
185// IsValid returns true if no validation errors occurred
186func (v *Validator) IsValid() bool {
187 return len(v.errors) == 0
188}
189
190// Errors returns all validation errors
191func (v *Validator) Errors() error {
192 if len(v.errors) == 0 {
193 return nil
194 }
195 return v.errors
196}