wip(helpers): unfinished addition of various helpers

This commit is contained in:
2023-03-16 03:29:44 +00:00
parent 9038699dbf
commit be806c6e1f
2 changed files with 740 additions and 0 deletions

View File

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

View File

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