Skip to content

Commit

Permalink
Add validation rule: min, max
Browse files Browse the repository at this point in the history
  • Loading branch information
nao1215 committed May 13, 2024
1 parent 077a3c2 commit 54664f1
Show file tree
Hide file tree
Showing 6 changed files with 232 additions and 75 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ You set the validation rules following the "validate:" tag according to the rule
| gte | Check whether value is greater than or equal to the specified value <br> e.g. `validate:"gte=1"` |
| lt | Check whether value is less than the specified value <br> e.g. `validate:"lt=1"` |
| lte | Check whether value is less than or equal to the specified value <br> e.g. `validate:"lte=1"` |
| min | Check whether value is greater than or equal to the specified value <br> e.g. `validate:"min=1"` |
| max | Check whether value is less than or equal to the specified value <br> e.g. `validate:"max=100"` |

## License
[MIT License](./LICENSE)
Expand Down
229 changes: 154 additions & 75 deletions csv_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package csv

import (
"bytes"
"os"
"path/filepath"
"testing"
Expand All @@ -10,81 +11,6 @@ import (

func TestCSV_Decode(t *testing.T) {
t.Parallel()

t.Run("all error: `id,name,age,password` header", func(t *testing.T) {
t.Parallel()

f, err := os.Open(filepath.Join("testdata", "all_error.csv"))
if err != nil {
t.Fatal(err)
}

c, err := NewCSV(f)
if err != nil {
t.Fatal(err)
}

type person struct {
ID int `validate:"numeric,gte=1"`
Name string `validate:"alpha"`
Age int `validate:"numeric,gt=-1,lt=120,gte=0"`
Password string `validate:"required,alphanumeric"`
IsAdmin bool `validate:"boolean"`
Zero int `validate:"numeric,eq=0,lte=1,ne=1"`
}
people := make([]person, 0)

got := c.Decode(&people)
for i, err := range got {
switch i {
case 0:
if err.Error() != "line:2 column id: target is not greater than or equal to the threshold value: threshold=1.000000, value=0.000000" {
t.Errorf("CSV.Decode() got errors: %v", err)
}
case 1:
if err.Error() != "line:3 column password: target is not an alphanumeric character: value=password-bad" {
t.Errorf("CSV.Decode() got errors: %v", err)
}
case 2:
if err.Error() != "line:4 column password: target is required but is empty: value=" {
t.Errorf("CSV.Decode() got errors: %v", err)
}
case 3:
if err.Error() != "line:5 column name: target is not an alphabetic character: value=1Joyless" {
t.Errorf("CSV.Decode() got errors: %v", err)
}
case 4:
if err.Error() != "line:5 column zero: target is not equal to the threshold value: threshold=0.000000, value=1.000000" {
t.Errorf("CSV.Decode() got errors: %v", err)
}
case 5:
if err.Error() != "line:5 column zero: target is equal to threshold the value: threshold=1.000000, value=1.000000" {
t.Errorf("CSV.Decode() got errors: %v", err)
}
case 6:
if err.Error() != "line:6 column age: target is not less than the threshold value: threshold=120.000000, value=120.000000" {
t.Errorf("CSV.Decode() got errors: %v", err)
}
case 7:
if err.Error() != "line:7 column is_admin: target is not a boolean: value=2" {
t.Errorf("CSV.Decode() got errors: %v", err)
}
case 8:
if err.Error() != "line:8 column age: target is not greater than the threshold value: threshold=-1.000000, value=-1.000000" {
t.Errorf("CSV.Decode() got errors: %v", err)
}
case 9:
if err.Error() != "line:8 column age: target is not greater than or equal to the threshold value: threshold=0.000000, value=-1.000000" {
t.Errorf("CSV.Decode() got errors: %v", err)
}
case 10:
if err.Error() != "line:9 column id: target is not a numeric character: value=a" {
t.Errorf("CSV.Decode() got errors: %v", err)
}
}
}
})

t.Run("read `id,name,age` header with value", func(t *testing.T) {
t.Parallel()

Expand Down Expand Up @@ -194,4 +120,157 @@ func TestCSV_Decode(t *testing.T) {
t.Errorf("CSV.Decode() mismatch (-got +want):\n%s", diff)
}
})

t.Run("validate min, max: success case", func(t *testing.T) {
t.Parallel()

input := `id,age
1,0
2,1
3,120
4,119
`

c, err := NewCSV(bytes.NewBufferString(input))
if err != nil {
t.Fatal(err)
}

type person struct {
ID int // no validate
Age int `validate:"min=0,max=120.0"`
}

people := make([]person, 0)
errs := c.Decode(&people)
if len(errs) != 0 {
t.Errorf("CSV.Decode() got errors: %v", errs)
}

want := []person{
{ID: 1, Age: 0},
{ID: 2, Age: 1},
{ID: 3, Age: 120},
{ID: 4, Age: 119},
}

if diff := cmp.Diff(people, want); diff != "" {
t.Errorf("CSV.Decode() mismatch (-got +want):\n%s", diff)
}
})
}

func Test_ErrCheck(t *testing.T) {
t.Parallel()

t.Run("error: `id,name,age,password` header", func(t *testing.T) {
t.Parallel()

f, err := os.Open(filepath.Join("testdata", "all_error.csv"))
if err != nil {
t.Fatal(err)
}

c, err := NewCSV(f)
if err != nil {
t.Fatal(err)
}

type person struct {
ID int `validate:"numeric,gte=1"`
Name string `validate:"alpha"`
Age int `validate:"numeric,gt=-1,lt=120,gte=0"`
Password string `validate:"required,alphanumeric"`
IsAdmin bool `validate:"boolean"`
Zero int `validate:"numeric,eq=0,lte=1,ne=1"`
}
people := make([]person, 0)

got := c.Decode(&people)
for i, err := range got {
switch i {
case 0:
if err.Error() != "line:2 column id: target is not greater than or equal to the threshold value: threshold=1.000000, value=0.000000" {
t.Errorf("CSV.Decode() got errors: %v", err)
}
case 1:
if err.Error() != "line:3 column password: target is not an alphanumeric character: value=password-bad" {
t.Errorf("CSV.Decode() got errors: %v", err)
}
case 2:
if err.Error() != "line:4 column password: target is required but is empty: value=" {
t.Errorf("CSV.Decode() got errors: %v", err)
}
case 3:
if err.Error() != "line:5 column name: target is not an alphabetic character: value=1Joyless" {
t.Errorf("CSV.Decode() got errors: %v", err)
}
case 4:
if err.Error() != "line:5 column zero: target is not equal to the threshold value: threshold=0.000000, value=1.000000" {
t.Errorf("CSV.Decode() got errors: %v", err)
}
case 5:
if err.Error() != "line:5 column zero: target is equal to threshold the value: threshold=1.000000, value=1.000000" {
t.Errorf("CSV.Decode() got errors: %v", err)
}
case 6:
if err.Error() != "line:6 column age: target is not less than the threshold value: threshold=120.000000, value=120.000000" {
t.Errorf("CSV.Decode() got errors: %v", err)
}
case 7:
if err.Error() != "line:7 column is_admin: target is not a boolean: value=2" {
t.Errorf("CSV.Decode() got errors: %v", err)
}
case 8:
if err.Error() != "line:8 column age: target is not greater than the threshold value: threshold=-1.000000, value=-1.000000" {
t.Errorf("CSV.Decode() got errors: %v", err)
}
case 9:
if err.Error() != "line:8 column age: target is not greater than or equal to the threshold value: threshold=0.000000, value=-1.000000" {
t.Errorf("CSV.Decode() got errors: %v", err)
}
case 10:
if err.Error() != "line:9 column id: target is not a numeric character: value=a" {
t.Errorf("CSV.Decode() got errors: %v", err)
}
}
}
})

t.Run("validate min, max: error case", func(t *testing.T) {
t.Parallel()

input := `id,age
1,0
2,-1
3,120
4,120.1
`

c, err := NewCSV(bytes.NewBufferString(input))
if err != nil {
t.Fatal(err)
}

type person struct {
ID int // no validate
Age int `validate:"min=0,max=120.0"`
}

people := make([]person, 0)
errs := c.Decode(&people)

for i, err := range errs {
switch i {
case 0:
if err.Error() != "line:3 column age: target is less than the minimum value: threshold=0.000000, value=-1.000000" {
t.Errorf("CSV.Decode() got errors: %v", err)
}
case 1:
if err.Error() != "line:5 column age: target is greater than the maximum value: threshold=120.000000, value=120.100000" {
t.Errorf("CSV.Decode() got errors: %v", err)
}
}
}
})
}
4 changes: 4 additions & 0 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,8 @@ var (
ErrLessThan = errors.New("target is not less than the threshold value")
// ErrLessThanEqual is returned when the target is not less than or equal to the value.
ErrLessThanEqual = errors.New("target is not less than or equal to the threshold value")
// ErrMin is returned when the target is less than the minimum value.
ErrMin = errors.New("target is less than the minimum value")
// ErrMax is returned when the target is greater than the maximum value.
ErrMax = errors.New("target is greater than the maximum value")
)
12 changes: 12 additions & 0 deletions parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,18 @@ func parseValidateTag(tags string) (validators, error) {
return nil, err
}
validatorList = append(validatorList, newLessThanEqualValidator(threshold))
case strings.HasPrefix(t, minTagValue.String()):
threshold, err := parseThreshold(t)
if err != nil {
return nil, err
}
validatorList = append(validatorList, newMinValidator(threshold))
case strings.HasPrefix(t, maxTagValue.String()):
threshold, err := parseThreshold(t)
if err != nil {
return nil, err
}
validatorList = append(validatorList, newMaxValidator(threshold))
}
}
return validatorList, nil
Expand Down
4 changes: 4 additions & 0 deletions tag.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ const (
lessThanTagValue tagValue = "lt"
// lessThanEqualTagValue is the struct tag name for less than or equal fields.
lessThanEqualTagValue tagValue = "lte"
// minTagValue is the struct tag name for minimum fields.
minTagValue tagValue = "min"
// maxTagValue is the struct tag name for maximum fields.
maxTagValue tagValue = "max"
)

// String returns the string representation of the tag.
Expand Down
56 changes: 56 additions & 0 deletions validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -300,3 +300,59 @@ func (l *lessThanEqualValidator) Do(target any) error {
}
return nil
}

// minValidator is a struct that contains the validation rules for a minimum column.
type minValidator struct {
threshold float64
}

// newMinValidator returns a new minValidator.
func newMinValidator(threshold float64) *minValidator {
return &minValidator{threshold: threshold}
}

// Do validates the target is greater than or equal to the threshold.
func (m *minValidator) Do(target any) error {
v, ok := target.(string)
if !ok {
return fmt.Errorf("%w: value=%v", ErrMin, target) //nolint
}

value, err := strconv.ParseFloat(v, 64)
if err != nil {
return fmt.Errorf("%w: value=%v", ErrMin, target) //nolint
}

if value < m.threshold {
return fmt.Errorf("%w: threshold=%f, value=%f", ErrMin, m.threshold, value) //nolint
}
return nil
}

// maxValidator is a struct that contains the validation rules for a maximum column.
type maxValidator struct {
threshold float64
}

// newMaxValidator returns a new maxValidator.
func newMaxValidator(threshold float64) *maxValidator {
return &maxValidator{threshold: threshold}
}

// Do validates the target is less than or equal to the threshold.
func (m *maxValidator) Do(target any) error {
v, ok := target.(string)
if !ok {
return fmt.Errorf("%w: value=%v", ErrMax, target) //nolint
}

value, err := strconv.ParseFloat(v, 64)
if err != nil {
return fmt.Errorf("%w: value=%v", ErrMax, target) //nolint
}

if value > m.threshold {
return fmt.Errorf("%w: threshold=%f, value=%f", ErrMax, m.threshold, value) //nolint
}
return nil
}

0 comments on commit 54664f1

Please sign in to comment.