// 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 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 }