mirror of
https://github.com/romdo/go-validate.git
synced 2026-02-19 08:06:40 +00:00
wip(helpers): unfinished addition of various helpers
This commit is contained in:
180
helpers.go
180
helpers.go
@@ -1,7 +1,9 @@
|
||||
package validate
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
// RequireField returns a Error type for the given field if provided value is
|
||||
@@ -34,3 +36,181 @@ func RequireField(field string, value interface{}) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func InRange(field string, value interface{}, min, max float64) error {
|
||||
if value == nil {
|
||||
return &Error{Field: field, Msg: "cannot be nil"}
|
||||
}
|
||||
|
||||
kind := reflect.TypeOf(value).Kind()
|
||||
var floatValue float64
|
||||
|
||||
switch kind { //nolint:exhaustive
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
floatValue = float64(reflect.ValueOf(value).Int())
|
||||
case reflect.Uint,
|
||||
reflect.Uint8,
|
||||
reflect.Uint16,
|
||||
reflect.Uint32,
|
||||
reflect.Uint64:
|
||||
floatValue = float64(reflect.ValueOf(value).Uint())
|
||||
case reflect.Float32, reflect.Float64:
|
||||
floatValue = reflect.ValueOf(value).Float()
|
||||
default:
|
||||
return &Error{
|
||||
Field: field,
|
||||
Msg: fmt.Sprintf("unsupported type %T for InRange", value),
|
||||
}
|
||||
}
|
||||
|
||||
if floatValue < min || floatValue > max {
|
||||
return &Error{
|
||||
Field: field,
|
||||
Msg: fmt.Sprintf("must be in range [%.6f, %.6f]", min, max),
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func MinLength(field string, value interface{}, minLength int) error {
|
||||
if value == nil {
|
||||
return &Error{Field: field, Msg: "cannot be nil"}
|
||||
}
|
||||
|
||||
if minLength < 0 {
|
||||
return &Error{Field: field, Msg: "minLength must be non-negative"}
|
||||
}
|
||||
|
||||
kind := reflect.TypeOf(value).Kind()
|
||||
var length int
|
||||
|
||||
switch kind { //nolint:exhaustive
|
||||
case reflect.String:
|
||||
length = len(reflect.ValueOf(value).String())
|
||||
case reflect.Slice, reflect.Array, reflect.Map:
|
||||
length = reflect.ValueOf(value).Len()
|
||||
default:
|
||||
return &Error{
|
||||
Field: field,
|
||||
Msg: fmt.Sprintf("unsupported type %T for MinLength", value),
|
||||
}
|
||||
}
|
||||
|
||||
if length < minLength {
|
||||
return &Error{
|
||||
Field: field,
|
||||
Msg: fmt.Sprintf("must have a minimum length of %d", minLength),
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func MaxLength(field string, value interface{}, maxLength int) error {
|
||||
if value == nil {
|
||||
return &Error{Field: field, Msg: "cannot be nil"}
|
||||
}
|
||||
|
||||
if maxLength < 0 {
|
||||
return &Error{Field: field, Msg: "maxLength must be non-negative"}
|
||||
}
|
||||
|
||||
kind := reflect.TypeOf(value).Kind()
|
||||
var length int
|
||||
|
||||
switch kind { //nolint:exhaustive
|
||||
case reflect.String:
|
||||
length = len(reflect.ValueOf(value).String())
|
||||
case reflect.Slice, reflect.Array, reflect.Map:
|
||||
length = reflect.ValueOf(value).Len()
|
||||
default:
|
||||
return &Error{
|
||||
Field: field,
|
||||
Msg: fmt.Sprintf("unsupported type %T for MaxLength", value),
|
||||
}
|
||||
}
|
||||
|
||||
if length > maxLength {
|
||||
return &Error{
|
||||
Field: field,
|
||||
Msg: fmt.Sprintf("must have a maximum length of %d", maxLength),
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MatchesRegexp checks if the value of a field matches the specified regular
|
||||
// expression. It returns an error if the value doesn't match the pattern.
|
||||
func MatchRegexp(
|
||||
field string,
|
||||
value interface{},
|
||||
pattern *regexp.Regexp,
|
||||
) error {
|
||||
if pattern == nil {
|
||||
return &Error{Field: field, Msg: "pattern cannot be nil"}
|
||||
}
|
||||
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
if !pattern.MatchString(v) {
|
||||
return &Error{
|
||||
Field: field,
|
||||
Msg: fmt.Sprintf(
|
||||
"does not match pattern '%s': '%s'", pattern, v,
|
||||
),
|
||||
}
|
||||
}
|
||||
case []byte:
|
||||
if !pattern.Match(v) {
|
||||
return &Error{
|
||||
Field: field,
|
||||
Msg: fmt.Sprintf(
|
||||
"does not match pattern '%s': '%s'", pattern, string(v),
|
||||
),
|
||||
}
|
||||
}
|
||||
default:
|
||||
return &Error{
|
||||
Field: field,
|
||||
Msg: fmt.Sprintf(
|
||||
"unsupported type %T for MatchRegexp", value,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func NotNil(field string, value interface{}) error {
|
||||
val := reflect.ValueOf(value)
|
||||
kind := val.Kind()
|
||||
isNil := false
|
||||
|
||||
if kind == reflect.Ptr && !val.IsNil() {
|
||||
val = val.Elem()
|
||||
kind = val.Kind()
|
||||
}
|
||||
|
||||
switch kind { //nolint:exhaustive
|
||||
case reflect.Chan,
|
||||
reflect.Func,
|
||||
reflect.Interface,
|
||||
reflect.Map,
|
||||
reflect.Slice,
|
||||
reflect.Ptr:
|
||||
if val.IsNil() {
|
||||
isNil = true
|
||||
}
|
||||
case reflect.Invalid:
|
||||
isNil = true
|
||||
default:
|
||||
}
|
||||
|
||||
if isNil {
|
||||
return &Error{Field: field, Msg: "must not be nil"}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
560
helpers_test.go
560
helpers_test.go
@@ -1,6 +1,9 @@
|
||||
package validate
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -404,3 +407,560 @@ func TestRequireField(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInRange(t *testing.T) {
|
||||
type args struct {
|
||||
field string
|
||||
value interface{}
|
||||
min float64
|
||||
max float64
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want error
|
||||
}{
|
||||
{
|
||||
name: "int in range",
|
||||
args: args{
|
||||
field: "Age",
|
||||
value: int(25),
|
||||
min: 18,
|
||||
max: 65,
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "int below range",
|
||||
args: args{
|
||||
field: "Age",
|
||||
value: int(15),
|
||||
min: 18,
|
||||
max: 65,
|
||||
},
|
||||
want: &Error{
|
||||
Field: "Age",
|
||||
Msg: "must be in range [18.000000, 65.000000]",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "int above range",
|
||||
args: args{
|
||||
field: "Age",
|
||||
value: int(70),
|
||||
min: 18,
|
||||
max: 65,
|
||||
},
|
||||
want: &Error{
|
||||
Field: "Age",
|
||||
Msg: "must be in range [18.000000, 65.000000]",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "float in range",
|
||||
args: args{
|
||||
field: "Rating",
|
||||
value: float64(4.5),
|
||||
min: 1,
|
||||
max: 5,
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "float below range",
|
||||
args: args{
|
||||
field: "Rating",
|
||||
value: float64(0.5),
|
||||
min: 1,
|
||||
max: 5,
|
||||
},
|
||||
want: &Error{
|
||||
Field: "Rating",
|
||||
Msg: "must be in range [1.000000, 5.000000]",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "float above range",
|
||||
args: args{
|
||||
field: "Rating",
|
||||
value: float64(5.5),
|
||||
min: 1,
|
||||
max: 5,
|
||||
},
|
||||
want: &Error{
|
||||
Field: "Rating",
|
||||
Msg: "must be in range [1.000000, 5.000000]",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "unsupported type",
|
||||
args: args{
|
||||
field: "Tags",
|
||||
value: []string{"tag1", "tag2"},
|
||||
min: 1,
|
||||
max: 5,
|
||||
},
|
||||
want: &Error{
|
||||
Field: "Tags",
|
||||
Msg: "unsupported type []string for InRange",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := InRange(
|
||||
tt.args.field,
|
||||
tt.args.value,
|
||||
tt.args.min,
|
||||
tt.args.max,
|
||||
)
|
||||
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMinLength(t *testing.T) {
|
||||
type args struct {
|
||||
field string
|
||||
value interface{}
|
||||
minLength int
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want error
|
||||
}{
|
||||
{
|
||||
name: "nil",
|
||||
args: args{
|
||||
field: "Title",
|
||||
value: nil,
|
||||
minLength: 5,
|
||||
},
|
||||
want: &Error{Field: "Title", Msg: "cannot be nil"},
|
||||
},
|
||||
{
|
||||
name: "negative minLength",
|
||||
args: args{
|
||||
field: "Title",
|
||||
value: "hello",
|
||||
minLength: -1,
|
||||
},
|
||||
want: &Error{Field: "Title", Msg: "minLength must be non-negative"},
|
||||
},
|
||||
{
|
||||
name: "string valid",
|
||||
args: args{
|
||||
field: "Title",
|
||||
value: "hello",
|
||||
minLength: 5,
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "string invalid",
|
||||
args: args{
|
||||
field: "Title",
|
||||
value: "hello",
|
||||
minLength: 6,
|
||||
},
|
||||
want: &Error{
|
||||
Field: "Title",
|
||||
Msg: "must have a minimum length of 6",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "slice valid",
|
||||
args: args{
|
||||
field: "Tags",
|
||||
value: []string{"tag1", "tag2"},
|
||||
minLength: 1,
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "slice invalid",
|
||||
args: args{
|
||||
field: "Tags",
|
||||
value: []string{"tag1", "tag2"},
|
||||
minLength: 3,
|
||||
},
|
||||
want: &Error{Field: "Tags", Msg: "must have a minimum length of 3"},
|
||||
},
|
||||
{
|
||||
name: "map valid",
|
||||
args: args{
|
||||
field: "Lookup",
|
||||
value: map[string]string{"foo": "bar"},
|
||||
minLength: 1,
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "map invalid",
|
||||
args: args{
|
||||
field: "Lookup",
|
||||
value: map[string]string{"foo": "bar"},
|
||||
minLength: 2,
|
||||
},
|
||||
want: &Error{
|
||||
Field: "Lookup",
|
||||
Msg: "must have a minimum length of 2",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "unsupported type",
|
||||
args: args{
|
||||
field: "Number",
|
||||
value: 123,
|
||||
minLength: 2,
|
||||
},
|
||||
want: &Error{
|
||||
Field: "Number",
|
||||
Msg: "unsupported type int for MinLength",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := MinLength(tt.args.field, tt.args.value, tt.args.minLength)
|
||||
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMaxLength(t *testing.T) {
|
||||
type args struct {
|
||||
field string
|
||||
value interface{}
|
||||
maxLength int
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want error
|
||||
}{
|
||||
{
|
||||
name: "nil",
|
||||
args: args{
|
||||
field: "Title",
|
||||
value: nil,
|
||||
maxLength: 5,
|
||||
},
|
||||
want: &Error{Field: "Title", Msg: "cannot be nil"},
|
||||
},
|
||||
{
|
||||
name: "negative maxLength",
|
||||
args: args{
|
||||
field: "Title",
|
||||
value: "hello",
|
||||
maxLength: -1,
|
||||
},
|
||||
want: &Error{Field: "Title", Msg: "maxLength must be non-negative"},
|
||||
},
|
||||
{
|
||||
name: "string valid",
|
||||
args: args{
|
||||
field: "Title",
|
||||
value: "hello",
|
||||
maxLength: 5,
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "string invalid",
|
||||
args: args{
|
||||
field: "Title",
|
||||
value: "hello",
|
||||
maxLength: 4,
|
||||
},
|
||||
want: &Error{
|
||||
Field: "Title",
|
||||
Msg: "must have a maximum length of 4",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "slice valid",
|
||||
args: args{
|
||||
field: "Tags",
|
||||
value: []string{"tag1", "tag2"},
|
||||
maxLength: 2,
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "slice invalid",
|
||||
args: args{
|
||||
field: "Tags",
|
||||
value: []string{"tag1", "tag2"},
|
||||
maxLength: 1,
|
||||
},
|
||||
want: &Error{Field: "Tags", Msg: "must have a maximum length of 1"},
|
||||
},
|
||||
{
|
||||
name: "map valid",
|
||||
args: args{
|
||||
field: "Lookup",
|
||||
value: map[string]string{"foo": "bar"},
|
||||
maxLength: 1,
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "map invalid",
|
||||
args: args{
|
||||
field: "Lookup",
|
||||
value: map[string]string{"foo": "bar"},
|
||||
maxLength: 0,
|
||||
},
|
||||
want: &Error{
|
||||
Field: "Lookup",
|
||||
Msg: "must have a maximum length of 0",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "unsupported type",
|
||||
args: args{
|
||||
field: "Number",
|
||||
value: 42,
|
||||
maxLength: 5,
|
||||
},
|
||||
want: &Error{
|
||||
Field: "Number",
|
||||
Msg: "unsupported type int for MaxLength",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := MaxLength(tt.args.field, tt.args.value, tt.args.maxLength)
|
||||
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMatchRegexp(t *testing.T) {
|
||||
usernameRegexp := regexp.MustCompile(`^[a-z]+\d+$`)
|
||||
passwordRegexp := regexp.MustCompile(`^.*[A-Z]+.*$`)
|
||||
|
||||
type args struct {
|
||||
field string
|
||||
value interface{}
|
||||
pattern *regexp.Regexp
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want error
|
||||
}{
|
||||
{
|
||||
name: "string matches pattern",
|
||||
args: args{
|
||||
field: "username",
|
||||
value: "johndoe123",
|
||||
pattern: usernameRegexp,
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "string does not match pattern",
|
||||
args: args{
|
||||
field: "username",
|
||||
value: "JohnDoe123",
|
||||
pattern: usernameRegexp,
|
||||
},
|
||||
want: &Error{
|
||||
Field: "username",
|
||||
Msg: "does not match pattern '^[a-z]+\\d+$': 'JohnDoe123'",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "byte slice matches pattern",
|
||||
args: args{
|
||||
field: "username",
|
||||
value: []byte("johndoe123"),
|
||||
pattern: usernameRegexp,
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "byte slice does not match pattern",
|
||||
args: args{
|
||||
field: "username",
|
||||
value: []byte("JohnDoe123"),
|
||||
pattern: usernameRegexp,
|
||||
},
|
||||
want: &Error{
|
||||
Field: "username",
|
||||
Msg: "does not match pattern '^[a-z]+\\d+$': 'JohnDoe123'",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "unsupported type",
|
||||
args: args{
|
||||
field: "password",
|
||||
value: 123456,
|
||||
pattern: passwordRegexp,
|
||||
},
|
||||
want: &Error{
|
||||
Field: "password",
|
||||
Msg: "unsupported type int for MatchRegexp",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := MatchRegexp(tt.args.field, tt.args.value, tt.args.pattern)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNotNil(t *testing.T) {
|
||||
type args struct {
|
||||
field string
|
||||
value interface{}
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want error
|
||||
}{
|
||||
{
|
||||
name: "nil pointer",
|
||||
args: args{
|
||||
field: "Name",
|
||||
value: (*string)(nil),
|
||||
},
|
||||
want: &Error{
|
||||
Field: "Name",
|
||||
Msg: "must not be nil",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "non-nil pointer",
|
||||
args: args{
|
||||
field: "Name",
|
||||
value: new(string),
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "nil slice",
|
||||
args: args{
|
||||
field: "Tags",
|
||||
value: []string(nil),
|
||||
},
|
||||
want: &Error{
|
||||
Field: "Tags",
|
||||
Msg: "must not be nil",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "non-nil slice",
|
||||
args: args{
|
||||
field: "Tags",
|
||||
value: []string{},
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "nil map",
|
||||
args: args{
|
||||
field: "Metadata",
|
||||
value: map[string]string(nil),
|
||||
},
|
||||
want: &Error{
|
||||
Field: "Metadata",
|
||||
Msg: "must not be nil",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "non-nil map",
|
||||
args: args{
|
||||
field: "Metadata",
|
||||
value: map[string]string{},
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "nil interface",
|
||||
args: args{
|
||||
field: "Data",
|
||||
value: fmt.Stringer(nil),
|
||||
},
|
||||
want: &Error{Field: "Data", Msg: "must not be nil"},
|
||||
},
|
||||
{
|
||||
name: "non-nil interface",
|
||||
args: args{
|
||||
field: "Data",
|
||||
// Using *bytes.Buffer as an example of a non-nil fmt.Stringer.
|
||||
value: fmt.Stringer(bytes.NewBuffer(nil)),
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "nil function",
|
||||
args: args{
|
||||
field: "Callback",
|
||||
value: (func())(nil),
|
||||
},
|
||||
want: &Error{
|
||||
Field: "Callback",
|
||||
Msg: "must not be nil",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "non-nil function",
|
||||
args: args{
|
||||
field: "Callback",
|
||||
value: func() {},
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "non-nil struct",
|
||||
args: args{
|
||||
field: "Person",
|
||||
value: struct{ Name string }{Name: "Alice"},
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "pointer to nil pointer",
|
||||
args: args{
|
||||
field: "Data",
|
||||
value: pointerToPointer(nil),
|
||||
},
|
||||
want: &Error{
|
||||
Field: "Data",
|
||||
Msg: "must not be nil",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "pointer to non-nil pointer",
|
||||
args: args{
|
||||
field: "Data",
|
||||
value: pointerToPointer(new(int)),
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := NotNil(tt.args.field, tt.args.value)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func pointerToPointer(v *int) **int {
|
||||
return &v
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user