From c33fb6fe778566f2a6b3af786a325e33a5cb08e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Gleizes?= Date: Thu, 1 Sep 2016 00:59:48 +0200 Subject: [PATCH] Add range rule implementation and tests (#8) * Add range rule implementation and tests * Add more tests for range validation * Use the right copyright header... --- README.md | 4 ++- range.go | 87 ++++++++++++++++++++++++++++++++++++++++++++++++++ range_test.go | 66 ++++++++++++++++++++++++++++++++++++++ util.go | 33 +++++++++++++++++++ util_test.go | 88 +++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 277 insertions(+), 1 deletion(-) create mode 100644 range.go create mode 100644 range_test.go diff --git a/README.md b/README.md index 1401990..891ef21 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ Run the following command to install the package: ``` go get github.com/go-ozzo/ozzo-validation ``` - + You may also get specified release of the package by: ``` @@ -270,6 +270,8 @@ The following rules are provided in the `validation` package: * `In(...interface{})`: checks if a value can be found in the given list of values. * `Length(min, max int)`: checks if the length of a value is within the specified range. This rule should only be used for validating strings, slices, maps, and arrays. +* `Range(min, max int)`: checks if a value is within the specified range. + This rule should only be used for validating int, uint and float types. * `Match(*regexp.Regexp)`: checks if a value matches the specified regular expression. This rule should only be used for strings and byte slices. * `Required`: checks if a value is not empty (neither nil nor zero). diff --git a/range.go b/range.go new file mode 100644 index 0000000..21795da --- /dev/null +++ b/range.go @@ -0,0 +1,87 @@ +// Copyright 2016 Qiang Xue. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package validation + +import ( + "errors" + "fmt" + "reflect" +) + +// Range returns a validation rule that checks if a value is within the given range: [min,max]. +// Note that the value being checked and the boundary values must be of the same type. +func Range(min interface{}, max interface{}) *rangeRule { + return &rangeRule{ + min: min, + max: max, + message: fmt.Sprintf("must be between %v and %v", min, max), + } +} + +type rangeRule struct { + min interface{} + max interface{} + message string +} + +// Validate checks if the given value is valid or not. +func (r *rangeRule) Validate(value interface{}) error { + value, isNil := Indirect(value) + if isNil { + return nil + } + + rv := reflect.ValueOf(value) + switch rv.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + min, err := ToInt(r.min) + if err != nil { + return err + } + max, err := ToInt(r.max) + if err != nil { + return err + } + if min <= rv.Int() && rv.Int() <= max { + return nil + } + + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + min, err := ToUint(r.min) + if err != nil { + return err + } + max, err := ToUint(r.max) + if err != nil { + return err + } + if min <= rv.Uint() && rv.Uint() <= max { + return nil + } + + case reflect.Float32, reflect.Float64: + min, err := ToFloat(r.min) + if err != nil { + return err + } + max, err := ToFloat(r.max) + if err != nil { + return err + } + if min <= rv.Float() && rv.Float() <= max { + return nil + } + + default: + r.message = fmt.Sprintf("cannot apply range rule on type %v", rv.Kind()) + } + return errors.New(r.message) +} + +// Error sets the error message for the rule. +func (r *rangeRule) Error(message string) *rangeRule { + r.message = message + return r +} diff --git a/range_test.go b/range_test.go new file mode 100644 index 0000000..5bb9ccb --- /dev/null +++ b/range_test.go @@ -0,0 +1,66 @@ +// Copyright 2016 Qiang Xue. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package validation + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestRange(t *testing.T) { + var i int + var u uint + var f float64 + + tests := []struct { + tag string + min, max interface{} + value interface{} + err string + }{ + {"t1", 2, 4, "", "cannot apply range rule on type string"}, + {"t2", 2, 4, "abc", "cannot apply range rule on type string"}, + {"t3", 2, 4, []int{1, 2}, "cannot apply range rule on type slice"}, + {"t4", 2, 4, map[string]int{"A": 1}, "cannot apply range rule on type map"}, + {"t5", 0, 2, nil, ""}, + + {"t6", 2, 4, 2, ""}, + {"t7", 2, 4, 3, ""}, + {"t8", 2, 4, 4, ""}, + {"t9", 0, 2, &i, ""}, + {"t10", 1, 2, &i, "must be between 1 and 2"}, + {"t11", 2, 4, 5, "must be between 2 and 4"}, + {"t12", uint(2), 4, 3, "cannot convert uint to int64"}, + {"t13", 2, uint(4), 3, "cannot convert uint to int64"}, + + {"t14", uint(0), uint(2), &u, ""}, + {"t15", uint(1), uint(2), &u, "must be between 1 and 2"}, + {"t16", uint(0), uint(1), uint(1), ""}, + {"t17", uint(0), uint(1), uint(2), "must be between 0 and 1"}, + {"t18", 0, uint(2), uint(1), "cannot convert int to uint64"}, + {"t19", uint(0), 2, uint(1), "cannot convert int to uint64"}, + + {"t20", 0.0, 2.0, &f, ""}, + {"t21", 1.0, 2.0, &f, "must be between 1 and 2"}, + {"t22", 0.0, 0.1, 1.0, "must be between 0 and 0.1"}, + {"t23", 0, 1.0, 0.5, "cannot convert int to float64"}, + {"t24", 0.0, 1, 1.0, "cannot convert int to float64"}, + } + + for _, test := range tests { + r := Range(test.min, test.max) + err := r.Validate(test.value) + assertError(t, test.err, err, test.tag) + } +} + +func TestRangeError(t *testing.T) { + r := Range(10, 20) + assert.Equal(t, "must be between 10 and 20", r.message) + + r.Error("123") + assert.Equal(t, "123", r.message) +} diff --git a/util.go b/util.go index 6854030..2c1c588 100644 --- a/util.go +++ b/util.go @@ -55,6 +55,39 @@ func LengthOfValue(value interface{}) (int, error) { return 0, fmt.Errorf("cannot get the length of %v", v.Kind()) } +// ToInt converts the given value to an int64. +// An error is returned for all incompatible types. +func ToInt(value interface{}) (int64, error) { + v := reflect.ValueOf(value) + switch v.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return v.Int(), nil + } + return 0, fmt.Errorf("cannot convert %v to int64", v.Kind()) +} + +// ToUint converts the given value to an uint64. +// An error is returned for all incompatible types. +func ToUint(value interface{}) (uint64, error) { + v := reflect.ValueOf(value) + switch v.Kind() { + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return v.Uint(), nil + } + return 0, fmt.Errorf("cannot convert %v to uint64", v.Kind()) +} + +// ToFloat converts the given value to a float64. +// An error is returned for all incompatible types. +func ToFloat(value interface{}) (float64, error) { + v := reflect.ValueOf(value) + switch v.Kind() { + case reflect.Float32, reflect.Float64: + return v.Float(), nil + } + return 0, fmt.Errorf("cannot convert %v to float64", v.Kind()) +} + // IsEmpty checks if a value is empty or not. // A value is considered empty if // - integer, float: zero diff --git a/util_test.go b/util_test.go index 0bbcbee..b9dda28 100644 --- a/util_test.go +++ b/util_test.go @@ -103,6 +103,94 @@ func TestLengthOfValue(t *testing.T) { } } +func TestToInt(t *testing.T) { + var a int + + tests := []struct { + tag string + value interface{} + result int64 + err string + }{ + {"t1", 1, 1, ""}, + {"t2", int8(1), 1, ""}, + {"t3", int16(1), 1, ""}, + {"t4", int32(1), 1, ""}, + {"t5", int64(1), 1, ""}, + {"t6", &a, 0, "cannot convert ptr to int64"}, + {"t7", uint(1), 0, "cannot convert uint to int64"}, + {"t8", float64(1), 0, "cannot convert float64 to int64"}, + {"t9", "abc", 0, "cannot convert string to int64"}, + {"t10", []int{1, 2}, 0, "cannot convert slice to int64"}, + {"t11", map[string]int{"A": 1}, 0, "cannot convert map to int64"}, + } + + for _, test := range tests { + l, err := ToInt(test.value) + assert.Equal(t, test.result, l, test.tag) + assertError(t, test.err, err, test.tag) + } +} + +func TestToUint(t *testing.T) { + var a int + var b uint + + tests := []struct { + tag string + value interface{} + result uint64 + err string + }{ + {"t1", uint(1), 1, ""}, + {"t2", uint8(1), 1, ""}, + {"t3", uint16(1), 1, ""}, + {"t4", uint32(1), 1, ""}, + {"t5", uint64(1), 1, ""}, + {"t6", 1, 0, "cannot convert int to uint64"}, + {"t7", &a, 0, "cannot convert ptr to uint64"}, + {"t8", &b, 0, "cannot convert ptr to uint64"}, + {"t9", float64(1), 0, "cannot convert float64 to uint64"}, + {"t10", "abc", 0, "cannot convert string to uint64"}, + {"t11", []int{1, 2}, 0, "cannot convert slice to uint64"}, + {"t12", map[string]int{"A": 1}, 0, "cannot convert map to uint64"}, + } + + for _, test := range tests { + l, err := ToUint(test.value) + assert.Equal(t, test.result, l, test.tag) + assertError(t, test.err, err, test.tag) + } +} + +func TestToFloat(t *testing.T) { + var a int + var b uint + + tests := []struct { + tag string + value interface{} + result float64 + err string + }{ + {"t1", float32(1), 1, ""}, + {"t2", float64(1), 1, ""}, + {"t3", 1, 0, "cannot convert int to float64"}, + {"t4", uint(1), 0, "cannot convert uint to float64"}, + {"t5", &a, 0, "cannot convert ptr to float64"}, + {"t6", &b, 0, "cannot convert ptr to float64"}, + {"t7", "abc", 0, "cannot convert string to float64"}, + {"t8", []int{1, 2}, 0, "cannot convert slice to float64"}, + {"t9", map[string]int{"A": 1}, 0, "cannot convert map to float64"}, + } + + for _, test := range tests { + l, err := ToFloat(test.value) + assert.Equal(t, test.result, l, test.tag) + assertError(t, test.err, err, test.tag) + } +} + func TestIsEmpty(t *testing.T) { var s1 string var s2 string = "a"