feat: add field validation API with signup form example

Introduces Field, Rule, ValidateAll, ResetFields, and AddError for
declarative input validation. Includes built-in rules (Required,
MinLen, MaxLen, Min, Max, Email, Pattern, Custom) and a signup
example exercising the full API surface.
This commit is contained in:
Ryan Hamamura
2026-02-11 14:42:44 -10:00
parent e636970f7b
commit 5362614c3e
6 changed files with 595 additions and 0 deletions

128
rule.go Normal file
View File

@@ -0,0 +1,128 @@
package via
import (
"fmt"
"regexp"
"strconv"
"strings"
)
// Rule defines a single validation check for a Field.
type Rule struct {
validate func(val string) error
}
// Required rejects empty or whitespace-only values.
func Required(msg ...string) Rule {
m := "This field is required"
if len(msg) > 0 {
m = msg[0]
}
return Rule{func(val string) error {
if strings.TrimSpace(val) == "" {
return fmt.Errorf("%s", m)
}
return nil
}}
}
// MinLen rejects values shorter than n characters.
func MinLen(n int, msg ...string) Rule {
m := fmt.Sprintf("Must be at least %d characters", n)
if len(msg) > 0 {
m = msg[0]
}
return Rule{func(val string) error {
if len(val) < n {
return fmt.Errorf("%s", m)
}
return nil
}}
}
// MaxLen rejects values longer than n characters.
func MaxLen(n int, msg ...string) Rule {
m := fmt.Sprintf("Must be at most %d characters", n)
if len(msg) > 0 {
m = msg[0]
}
return Rule{func(val string) error {
if len(val) > n {
return fmt.Errorf("%s", m)
}
return nil
}}
}
// Min parses the value as an integer and rejects values less than n.
func Min(n int, msg ...string) Rule {
m := fmt.Sprintf("Must be at least %d", n)
if len(msg) > 0 {
m = msg[0]
}
return Rule{func(val string) error {
v, err := strconv.Atoi(val)
if err != nil {
return fmt.Errorf("%s", m)
}
if v < n {
return fmt.Errorf("%s", m)
}
return nil
}}
}
// Max parses the value as an integer and rejects values greater than n.
func Max(n int, msg ...string) Rule {
m := fmt.Sprintf("Must be at most %d", n)
if len(msg) > 0 {
m = msg[0]
}
return Rule{func(val string) error {
v, err := strconv.Atoi(val)
if err != nil {
return fmt.Errorf("%s", m)
}
if v > n {
return fmt.Errorf("%s", m)
}
return nil
}}
}
// Pattern rejects values that don't match the regular expression re.
func Pattern(re string, msg ...string) Rule {
m := "Invalid format"
if len(msg) > 0 {
m = msg[0]
}
compiled := regexp.MustCompile(re)
return Rule{func(val string) error {
if !compiled.MatchString(val) {
return fmt.Errorf("%s", m)
}
return nil
}}
}
var emailRegexp = regexp.MustCompile(`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`)
// Email rejects values that don't look like an email address.
func Email(msg ...string) Rule {
m := "Invalid email address"
if len(msg) > 0 {
m = msg[0]
}
return Rule{func(val string) error {
if !emailRegexp.MatchString(val) {
return fmt.Errorf("%s", m)
}
return nil
}}
}
// Custom creates a rule from a user-provided validation function.
// The function should return nil for valid input and an error for invalid input.
func Custom(fn func(string) error) Rule {
return Rule{validate: fn}
}