148 lines
4.7 KiB
Go
148 lines
4.7 KiB
Go
// Port of web/kit/Validation.ts.
|
|
package webui
|
|
|
|
import (
|
|
"regexp"
|
|
"strings"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
// validationNonDigit matches every non-digit rune (the JS /\D/g).
|
|
var validationNonDigit = regexp.MustCompile(`\D`)
|
|
|
|
// validationDigits strips every non-digit character.
|
|
func validationDigits(s string) string {
|
|
return validationNonDigit.ReplaceAllString(s, "")
|
|
}
|
|
|
|
// Validation describes a single form field's validation inputs. Solid's reactive
|
|
// Accessor<T> values collapse to plain values here: the caller reads its signals
|
|
// at the call site and passes the current values. FieldBlur is optional (nil
|
|
// falls back to Field), as is IsValidFunc.
|
|
type Validation struct {
|
|
ID string // snake_case identifier that is "touched"
|
|
Name string // human-readable field name, used in messages
|
|
Required bool // whether an empty value is an error
|
|
Touched map[string]bool // which field IDs the user has interacted with
|
|
Field string // the live field value
|
|
FieldBlur *string // the value at last blur; nil uses Field
|
|
IsValidFunc func(string) bool
|
|
InvalidMsg string // custom "is invalid" message; empty uses a default
|
|
}
|
|
|
|
// CreateValidation computes the validation error message for a field, or "" when
|
|
// valid. This is the collapsed (non-reactive) form of the Solid createMemo: the
|
|
// caller re-invokes it whenever the underlying signals change.
|
|
func CreateValidation(v Validation) string {
|
|
field := strings.TrimSpace(v.Field)
|
|
// When no blur value is provided, fall back to the live value so the
|
|
// "has value but not blurred yet" guard is always false and validation
|
|
// runs against the live value instead.
|
|
fieldBlur := field
|
|
if v.FieldBlur != nil {
|
|
fieldBlur = strings.TrimSpace(*v.FieldBlur)
|
|
}
|
|
|
|
if !v.Touched[v.ID] || (field != "" && fieldBlur == "") || (v.IsValidFunc != nil && v.IsValidFunc(field)) {
|
|
return ""
|
|
}
|
|
if v.Required && field == "" {
|
|
return v.Name + " is required"
|
|
}
|
|
if v.IsValidFunc != nil && !v.IsValidFunc(fieldBlur) {
|
|
if v.InvalidMsg != "" {
|
|
return v.InvalidMsg
|
|
}
|
|
return v.Name + " is invalid"
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// IsPhoneNumberValid reports whether phoneNumber has exactly 10 digits.
|
|
func IsPhoneNumberValid(phoneNumber string) bool {
|
|
return len(validationDigits(phoneNumber)) == 10
|
|
}
|
|
|
|
var validationEmailRegex = regexp.MustCompile("^[-!#$%&'*+/0-9=?A-Z^_a-z`{|}~](\\.?[-!#$%&'*+/0-9=?A-Z^_a-z`{|}~])*@[a-zA-Z0-9](-*\\.?[a-zA-Z0-9])*\\.[a-zA-Z](-?[a-zA-Z0-9])+$")
|
|
|
|
// IsEmailValid reports whether email is a syntactically valid email address.
|
|
func IsEmailValid(email string) bool {
|
|
if email == "" {
|
|
return false
|
|
}
|
|
|
|
emailParts := strings.Split(email, "@")
|
|
if len(emailParts) != 2 {
|
|
return false
|
|
}
|
|
|
|
account := emailParts[0]
|
|
address := emailParts[1]
|
|
|
|
if len(account) > 64 {
|
|
return false
|
|
} else if len(address) > 255 {
|
|
return false
|
|
}
|
|
|
|
for _, part := range strings.Split(address, ".") {
|
|
if len(part) > 63 {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return validationEmailRegex.MatchString(email)
|
|
}
|
|
|
|
var validationURLRegex = regexp.MustCompile("[(http(s)?)://(www\\.)?a-zA-Z0-9@:%._\\+~#=]{2,256}\\.[a-z]{2,6}\\b([-a-zA-Z0-9@:%_\\+.~#?&//=]*)")
|
|
|
|
// IsURLValid reports whether url contains a substring that looks like a URL.
|
|
func IsURLValid(url string) bool {
|
|
return validationURLRegex.MatchString(url)
|
|
}
|
|
|
|
// IsZipCodeValid reports whether zip has exactly 5 or 9 digits.
|
|
func IsZipCodeValid(zip string) bool {
|
|
rawZip := validationDigits(zip)
|
|
return len(rawZip) == 5 || len(rawZip) == 9
|
|
}
|
|
|
|
// IsTaxIDValid reports whether id has exactly 9 digits.
|
|
func IsTaxIDValid(id string) bool {
|
|
return len(validationDigits(id)) == 9
|
|
}
|
|
|
|
// IsAtLeastMinChars reports whether input is at least minLen characters long.
|
|
func IsAtLeastMinChars(input string, minLen int) bool {
|
|
return utf8.RuneCountInString(input) >= minLen
|
|
}
|
|
|
|
// IsWithinMaxChars reports whether input is at most maxLen characters long.
|
|
func IsWithinMaxChars(input string, maxLen int) bool {
|
|
return utf8.RuneCountInString(input) <= maxLen
|
|
}
|
|
|
|
var validationNameRegex = regexp.MustCompile(`^[\p{L}]*[\p{L} '\-]*[\p{L}]$`)
|
|
|
|
// IsNameValid reports whether name consists only of letters, spaces, hyphens,
|
|
// and apostrophes (and begins/ends with a letter).
|
|
func IsNameValid(name string) bool {
|
|
return validationNameRegex.MatchString(name)
|
|
}
|
|
|
|
var validationUsernameRegex = regexp.MustCompile(`^[A-Za-z0-9]*$`)
|
|
|
|
// IsUsernameValid reports an error message when the username is invalid (5-50
|
|
// alphanumeric characters), or "" when valid.
|
|
func IsUsernameValid(username string) string {
|
|
if n := utf8.RuneCountInString(username); n < 5 || n > 50 {
|
|
return "Username must have 5-50 characters"
|
|
}
|
|
|
|
if !validationUsernameRegex.MatchString(username) {
|
|
return "Username must only contain alphanumeric characters"
|
|
}
|
|
|
|
return "" // Valid
|
|
}
|