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:
28
context.go
28
context.go
@@ -480,6 +480,34 @@ func (c *Context) unsubscribeAll() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Field creates a signal with validation rules attached.
|
||||||
|
// The initial value seeds both the signal and the reset target.
|
||||||
|
func (c *Context) Field(initial any, rules ...Rule) *Field {
|
||||||
|
return &Field{
|
||||||
|
signal: c.Signal(initial),
|
||||||
|
rules: rules,
|
||||||
|
initialVal: initial,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateAll runs Validate on every field, returning true only if all pass.
|
||||||
|
func (c *Context) ValidateAll(fields ...*Field) bool {
|
||||||
|
ok := true
|
||||||
|
for _, f := range fields {
|
||||||
|
if !f.Validate() {
|
||||||
|
ok = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetFields resets every field to its initial value and clears errors.
|
||||||
|
func (c *Context) ResetFields(fields ...*Field) {
|
||||||
|
for _, f := range fields {
|
||||||
|
f.Reset()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func newContext(id string, route string, v *V) *Context {
|
func newContext(id string, route string, v *V) *Context {
|
||||||
if v == nil {
|
if v == nil {
|
||||||
panic("create context failed: app pointer is nil")
|
panic("create context failed: app pointer is nil")
|
||||||
|
|||||||
58
field.go
Normal file
58
field.go
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
package via
|
||||||
|
|
||||||
|
// Field is a signal with built-in validation rules and error state.
|
||||||
|
// It embeds *signal, so all signal methods (Bind, String, Int, Bool, SetValue, Text, ID)
|
||||||
|
// work transparently.
|
||||||
|
type Field struct {
|
||||||
|
*signal
|
||||||
|
rules []Rule
|
||||||
|
errors []string
|
||||||
|
initialVal any
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate runs all rules against the current value.
|
||||||
|
// Clears previous errors, populates new ones, returns true if all rules pass.
|
||||||
|
func (f *Field) Validate() bool {
|
||||||
|
f.errors = nil
|
||||||
|
val := f.String()
|
||||||
|
for _, r := range f.rules {
|
||||||
|
if err := r.validate(val); err != nil {
|
||||||
|
f.errors = append(f.errors, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return len(f.errors) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasError returns true if this field has any validation errors.
|
||||||
|
func (f *Field) HasError() bool {
|
||||||
|
return len(f.errors) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// FirstError returns the first validation error message, or "" if valid.
|
||||||
|
func (f *Field) FirstError() string {
|
||||||
|
if len(f.errors) > 0 {
|
||||||
|
return f.errors[0]
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Errors returns all current validation error messages.
|
||||||
|
func (f *Field) Errors() []string {
|
||||||
|
return f.errors
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddError manually adds an error message (useful for server-side or cross-field validation).
|
||||||
|
func (f *Field) AddError(msg string) {
|
||||||
|
f.errors = append(f.errors, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearErrors removes all validation errors from this field.
|
||||||
|
func (f *Field) ClearErrors() {
|
||||||
|
f.errors = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset restores the field value to its initial value and clears all errors.
|
||||||
|
func (f *Field) Reset() {
|
||||||
|
f.SetValue(f.initialVal)
|
||||||
|
f.errors = nil
|
||||||
|
}
|
||||||
178
field_test.go
Normal file
178
field_test.go
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
package via
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/ryanhamamura/via/h"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newTestField(initial any, rules ...Rule) *Field {
|
||||||
|
v := New()
|
||||||
|
var f *Field
|
||||||
|
v.Page("/", func(c *Context) {
|
||||||
|
f = c.Field(initial, rules...)
|
||||||
|
c.View(func() h.H { return h.Div() })
|
||||||
|
})
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFieldCreation(t *testing.T) {
|
||||||
|
f := newTestField("hello", Required())
|
||||||
|
assert.Equal(t, "hello", f.String())
|
||||||
|
assert.NotEmpty(t, f.ID())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFieldSignalDelegation(t *testing.T) {
|
||||||
|
f := newTestField(42)
|
||||||
|
assert.Equal(t, "42", f.String())
|
||||||
|
assert.Equal(t, 42, f.Int())
|
||||||
|
|
||||||
|
f.SetValue("new")
|
||||||
|
assert.Equal(t, "new", f.String())
|
||||||
|
|
||||||
|
// Bind returns an h.H element
|
||||||
|
assert.NotNil(t, f.Bind())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFieldValidateSingleRule(t *testing.T) {
|
||||||
|
f := newTestField("", Required())
|
||||||
|
assert.False(t, f.Validate())
|
||||||
|
assert.True(t, f.HasError())
|
||||||
|
assert.Equal(t, "This field is required", f.FirstError())
|
||||||
|
|
||||||
|
f.SetValue("ok")
|
||||||
|
assert.True(t, f.Validate())
|
||||||
|
assert.False(t, f.HasError())
|
||||||
|
assert.Equal(t, "", f.FirstError())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFieldValidateMultipleRules(t *testing.T) {
|
||||||
|
f := newTestField("ab", Required(), MinLen(3))
|
||||||
|
assert.False(t, f.Validate())
|
||||||
|
errs := f.Errors()
|
||||||
|
assert.Len(t, errs, 1)
|
||||||
|
assert.Equal(t, "Must be at least 3 characters", errs[0])
|
||||||
|
|
||||||
|
f.SetValue("")
|
||||||
|
assert.False(t, f.Validate())
|
||||||
|
errs = f.Errors()
|
||||||
|
assert.Len(t, errs, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFieldErrors(t *testing.T) {
|
||||||
|
f := newTestField("")
|
||||||
|
assert.Nil(t, f.Errors())
|
||||||
|
assert.False(t, f.HasError())
|
||||||
|
assert.Equal(t, "", f.FirstError())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFieldAddError(t *testing.T) {
|
||||||
|
f := newTestField("ok")
|
||||||
|
f.AddError("username taken")
|
||||||
|
assert.True(t, f.HasError())
|
||||||
|
assert.Equal(t, "username taken", f.FirstError())
|
||||||
|
assert.Len(t, f.Errors(), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFieldClearErrors(t *testing.T) {
|
||||||
|
f := newTestField("", Required())
|
||||||
|
f.Validate()
|
||||||
|
assert.True(t, f.HasError())
|
||||||
|
f.ClearErrors()
|
||||||
|
assert.False(t, f.HasError())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFieldReset(t *testing.T) {
|
||||||
|
f := newTestField("initial", Required(), MinLen(3))
|
||||||
|
f.SetValue("changed")
|
||||||
|
f.AddError("some error")
|
||||||
|
|
||||||
|
f.Reset()
|
||||||
|
assert.Equal(t, "initial", f.String())
|
||||||
|
assert.False(t, f.HasError())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateAll(t *testing.T) {
|
||||||
|
v := New()
|
||||||
|
var username, email *Field
|
||||||
|
v.Page("/", func(c *Context) {
|
||||||
|
username = c.Field("", Required(), MinLen(3))
|
||||||
|
email = c.Field("", Required(), Email())
|
||||||
|
c.View(func() h.H { return h.Div() })
|
||||||
|
})
|
||||||
|
|
||||||
|
// both empty → both fail
|
||||||
|
assert.False(t, false) // sanity
|
||||||
|
ok := username.Validate() && email.Validate()
|
||||||
|
assert.False(t, ok)
|
||||||
|
|
||||||
|
// simulate ValidateAll via context
|
||||||
|
v2 := New()
|
||||||
|
var u2, e2 *Field
|
||||||
|
v2.Page("/", func(c *Context) {
|
||||||
|
u2 = c.Field("joe", Required(), MinLen(3))
|
||||||
|
e2 = c.Field("joe@x.com", Required(), Email())
|
||||||
|
c.View(func() h.H { return h.Div() })
|
||||||
|
|
||||||
|
assert.True(t, c.ValidateAll(u2, e2))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateAllPartialFailure(t *testing.T) {
|
||||||
|
v := New()
|
||||||
|
v.Page("/", func(c *Context) {
|
||||||
|
good := c.Field("valid", Required())
|
||||||
|
bad := c.Field("", Required())
|
||||||
|
c.View(func() h.H { return h.Div() })
|
||||||
|
|
||||||
|
// ValidateAll must run ALL fields even if first passes
|
||||||
|
ok := c.ValidateAll(good, bad)
|
||||||
|
assert.False(t, ok)
|
||||||
|
assert.False(t, good.HasError())
|
||||||
|
assert.True(t, bad.HasError())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResetFields(t *testing.T) {
|
||||||
|
v := New()
|
||||||
|
v.Page("/", func(c *Context) {
|
||||||
|
a := c.Field("a", Required())
|
||||||
|
b := c.Field("b", Required())
|
||||||
|
c.View(func() h.H { return h.Div() })
|
||||||
|
|
||||||
|
a.SetValue("changed-a")
|
||||||
|
b.SetValue("changed-b")
|
||||||
|
a.AddError("err")
|
||||||
|
|
||||||
|
c.ResetFields(a, b)
|
||||||
|
assert.Equal(t, "a", a.String())
|
||||||
|
assert.Equal(t, "b", b.String())
|
||||||
|
assert.False(t, a.HasError())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFieldValidateClearsPreviousErrors(t *testing.T) {
|
||||||
|
f := newTestField("", Required())
|
||||||
|
f.Validate()
|
||||||
|
assert.True(t, f.HasError())
|
||||||
|
|
||||||
|
f.SetValue("ok")
|
||||||
|
f.Validate()
|
||||||
|
assert.False(t, f.HasError())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFieldCustomValidator(t *testing.T) {
|
||||||
|
f := newTestField("bad", Custom(func(val string) error {
|
||||||
|
if val == "bad" {
|
||||||
|
return fmt.Errorf("no bad words")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}))
|
||||||
|
assert.False(t, f.Validate())
|
||||||
|
assert.Equal(t, "no bad words", f.FirstError())
|
||||||
|
|
||||||
|
f.SetValue("good")
|
||||||
|
assert.True(t, f.Validate())
|
||||||
|
}
|
||||||
87
internal/examples/signup/main.go
Normal file
87
internal/examples/signup/main.go
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ryanhamamura/via"
|
||||||
|
"github.com/ryanhamamura/via/h"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
v := via.New()
|
||||||
|
v.Config(via.Options{
|
||||||
|
DocumentTitle: "Signup",
|
||||||
|
ServerAddress: ":8080",
|
||||||
|
})
|
||||||
|
|
||||||
|
v.AppendToHead(h.StyleEl(h.Raw(`
|
||||||
|
body { font-family: system-ui, sans-serif; max-width: 420px; margin: 2rem auto; padding: 0 1rem; }
|
||||||
|
label { display: block; font-weight: 600; margin-top: 1rem; }
|
||||||
|
input { display: block; width: 100%; padding: 0.4rem; margin-top: 0.25rem; box-sizing: border-box; }
|
||||||
|
.error { color: #c00; font-size: 0.85rem; margin-top: 0.2rem; }
|
||||||
|
.success { color: #080; margin-top: 1rem; }
|
||||||
|
.actions { margin-top: 1.5rem; display: flex; gap: 0.5rem; }
|
||||||
|
`)))
|
||||||
|
|
||||||
|
v.Page("/", func(c *via.Context) {
|
||||||
|
username := c.Field("", via.Required(), via.MinLen(3), via.MaxLen(20))
|
||||||
|
email := c.Field("", via.Required(), via.Email())
|
||||||
|
age := c.Field("", via.Required(), via.Min(13), via.Max(120))
|
||||||
|
// Optional field — only validated when non-empty
|
||||||
|
website := c.Field("", via.Custom(func(val string) error { return nil }), via.Pattern(`^$|^https?://\S+$`, "Must be a valid URL"))
|
||||||
|
|
||||||
|
var success string
|
||||||
|
|
||||||
|
signup := c.Action(func() {
|
||||||
|
success = ""
|
||||||
|
if !c.ValidateAll(username, email, age, website) {
|
||||||
|
c.Sync()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Server-side check
|
||||||
|
if username.String() == "admin" {
|
||||||
|
username.AddError("Username is already taken")
|
||||||
|
c.Sync()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
success = "Account created for " + username.String() + "!"
|
||||||
|
c.ResetFields(username, email, age, website)
|
||||||
|
c.Sync()
|
||||||
|
})
|
||||||
|
|
||||||
|
reset := c.Action(func() {
|
||||||
|
success = ""
|
||||||
|
c.ResetFields(username, email, age, website)
|
||||||
|
c.Sync()
|
||||||
|
})
|
||||||
|
|
||||||
|
c.View(func() h.H {
|
||||||
|
return h.Div(
|
||||||
|
h.H1(h.Text("Sign Up")),
|
||||||
|
|
||||||
|
h.Label(h.Text("Username")),
|
||||||
|
h.Input(h.Type("text"), h.Placeholder("pick a username"), username.Bind()),
|
||||||
|
h.If(username.HasError(), h.Div(h.Class("error"), h.Text(username.FirstError()))),
|
||||||
|
|
||||||
|
h.Label(h.Text("Email")),
|
||||||
|
h.Input(h.Type("email"), h.Placeholder("you@example.com"), email.Bind()),
|
||||||
|
h.If(email.HasError(), h.Div(h.Class("error"), h.Text(email.FirstError()))),
|
||||||
|
|
||||||
|
h.Label(h.Text("Age")),
|
||||||
|
h.Input(h.Type("number"), h.Placeholder("your age"), age.Bind()),
|
||||||
|
h.If(age.HasError(), h.Div(h.Class("error"), h.Text(age.FirstError()))),
|
||||||
|
|
||||||
|
h.Label(h.Text("Website (optional)")),
|
||||||
|
h.Input(h.Type("url"), h.Placeholder("https://example.com"), website.Bind()),
|
||||||
|
h.If(website.HasError(), h.Div(h.Class("error"), h.Text(website.FirstError()))),
|
||||||
|
|
||||||
|
h.Div(h.Class("actions"),
|
||||||
|
h.Button(h.Text("Sign Up"), signup.OnClick()),
|
||||||
|
h.Button(h.Text("Reset"), reset.OnClick()),
|
||||||
|
),
|
||||||
|
|
||||||
|
h.If(success != "", h.P(h.Class("success"), h.Text(success))),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
v.Start()
|
||||||
|
}
|
||||||
128
rule.go
Normal file
128
rule.go
Normal 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}
|
||||||
|
}
|
||||||
116
rule_test.go
Normal file
116
rule_test.go
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
package via
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRequired(t *testing.T) {
|
||||||
|
r := Required()
|
||||||
|
assert.NoError(t, r.validate("hello"))
|
||||||
|
assert.Error(t, r.validate(""))
|
||||||
|
assert.Error(t, r.validate(" "))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRequiredCustomMessage(t *testing.T) {
|
||||||
|
r := Required("name needed")
|
||||||
|
err := r.validate("")
|
||||||
|
assert.EqualError(t, err, "name needed")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMinLen(t *testing.T) {
|
||||||
|
r := MinLen(3)
|
||||||
|
assert.NoError(t, r.validate("abc"))
|
||||||
|
assert.NoError(t, r.validate("abcd"))
|
||||||
|
assert.Error(t, r.validate("ab"))
|
||||||
|
assert.Error(t, r.validate(""))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMinLenCustomMessage(t *testing.T) {
|
||||||
|
r := MinLen(5, "too short")
|
||||||
|
err := r.validate("ab")
|
||||||
|
assert.EqualError(t, err, "too short")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMaxLen(t *testing.T) {
|
||||||
|
r := MaxLen(5)
|
||||||
|
assert.NoError(t, r.validate("abc"))
|
||||||
|
assert.NoError(t, r.validate("abcde"))
|
||||||
|
assert.Error(t, r.validate("abcdef"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMaxLenCustomMessage(t *testing.T) {
|
||||||
|
r := MaxLen(2, "too long")
|
||||||
|
err := r.validate("abc")
|
||||||
|
assert.EqualError(t, err, "too long")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMin(t *testing.T) {
|
||||||
|
r := Min(5)
|
||||||
|
assert.NoError(t, r.validate("5"))
|
||||||
|
assert.NoError(t, r.validate("10"))
|
||||||
|
assert.Error(t, r.validate("4"))
|
||||||
|
assert.Error(t, r.validate("abc"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMinCustomMessage(t *testing.T) {
|
||||||
|
r := Min(10, "need 10+")
|
||||||
|
err := r.validate("3")
|
||||||
|
assert.EqualError(t, err, "need 10+")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMax(t *testing.T) {
|
||||||
|
r := Max(10)
|
||||||
|
assert.NoError(t, r.validate("10"))
|
||||||
|
assert.NoError(t, r.validate("5"))
|
||||||
|
assert.Error(t, r.validate("11"))
|
||||||
|
assert.Error(t, r.validate("abc"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMaxCustomMessage(t *testing.T) {
|
||||||
|
r := Max(5, "too big")
|
||||||
|
err := r.validate("6")
|
||||||
|
assert.EqualError(t, err, "too big")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPattern(t *testing.T) {
|
||||||
|
r := Pattern(`^\d{3}$`)
|
||||||
|
assert.NoError(t, r.validate("123"))
|
||||||
|
assert.Error(t, r.validate("12"))
|
||||||
|
assert.Error(t, r.validate("abcd"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPatternCustomMessage(t *testing.T) {
|
||||||
|
r := Pattern(`^\d+$`, "digits only")
|
||||||
|
err := r.validate("abc")
|
||||||
|
assert.EqualError(t, err, "digits only")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEmail(t *testing.T) {
|
||||||
|
r := Email()
|
||||||
|
assert.NoError(t, r.validate("user@example.com"))
|
||||||
|
assert.NoError(t, r.validate("a.b+c@foo.co"))
|
||||||
|
assert.Error(t, r.validate("notanemail"))
|
||||||
|
assert.Error(t, r.validate("@example.com"))
|
||||||
|
assert.Error(t, r.validate("user@"))
|
||||||
|
assert.Error(t, r.validate(""))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEmailCustomMessage(t *testing.T) {
|
||||||
|
r := Email("bad email")
|
||||||
|
err := r.validate("nope")
|
||||||
|
assert.EqualError(t, err, "bad email")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCustom(t *testing.T) {
|
||||||
|
r := Custom(func(val string) error {
|
||||||
|
if val != "magic" {
|
||||||
|
return fmt.Errorf("must be magic")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
assert.NoError(t, r.validate("magic"))
|
||||||
|
assert.EqualError(t, r.validate("other"), "must be magic")
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user