Skip to content

Commit

Permalink
Add range rule implementation and tests (#8)
Browse files Browse the repository at this point in the history
* Add range rule implementation and tests

* Add more tests for range validation

* Use the right copyright header...
  • Loading branch information
sgleizes authored and qiangxue committed Aug 31, 2016
1 parent c5ea90f commit c33fb6f
Show file tree
Hide file tree
Showing 5 changed files with 277 additions and 1 deletion.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

```
Expand Down Expand Up @@ -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).
Expand Down
87 changes: 87 additions & 0 deletions range.go
Original file line number Diff line number Diff line change
@@ -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
}
66 changes: 66 additions & 0 deletions range_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
33 changes: 33 additions & 0 deletions util.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
88 changes: 88 additions & 0 deletions util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down

0 comments on commit c33fb6f

Please sign in to comment.