From be806c6e1fecb8a3c497bed4b067f308d4b456af Mon Sep 17 00:00:00 2001 From: Jim Myhrberg Date: Thu, 16 Mar 2023 03:29:44 +0000 Subject: [PATCH] wip(helpers): unfinished addition of various helpers --- helpers.go | 180 ++++++++++++++++ helpers_test.go | 560 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 740 insertions(+) diff --git a/helpers.go b/helpers.go index a58f375..0f31368 100644 --- a/helpers.go +++ b/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 +} diff --git a/helpers_test.go b/helpers_test.go index b15461c..930e666 100644 --- a/helpers_test.go +++ b/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 +}