Skip to content

Commit

Permalink
Merge pull request #4 from nao1215/nchika/add-len-tag-value
Browse files Browse the repository at this point in the history
Add "len" tag value
  • Loading branch information
nao1215 authored May 13, 2024
2 parents 0f98e46 + 76de70c commit 1069485
Show file tree
Hide file tree
Showing 8 changed files with 114 additions and 1 deletion.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ You set the validation rules following the "validate:" tag according to the rule
| 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"` |
| len | Check whether the length of the value is equal to the specified value <br> e.g. `validate:"len=10"` |

## License
[MIT License](./LICENSE)
Expand Down
75 changes: 74 additions & 1 deletion csv_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,41 @@ func TestCSV_Decode(t *testing.T) {
t.Errorf("CSV.Decode() mismatch (-got +want):\n%s", diff)
}
})

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

input := `id,name
1,abc
2,あいう
3,👩‍❤‍💋‍👩🇷🇺😂
`
c, err := NewCSV(bytes.NewBufferString(input))
if err != nil {
t.Fatal(err)
}

type person struct {
ID int // no validate
Name string `validate:"len=3"`
}
persons := make([]person, 0)

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

want := []person{
{ID: 1, Name: "abc"},
{ID: 2, Name: "あいう"},
{ID: 3, Name: "👩‍❤‍💋‍👩🇷🇺😂"},
}

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

func Test_ErrCheck(t *testing.T) {
Expand Down Expand Up @@ -246,7 +281,6 @@ func Test_ErrCheck(t *testing.T) {
3,120
4,120.1
`

c, err := NewCSV(bytes.NewBufferString(input))
if err != nil {
t.Fatal(err)
Expand All @@ -273,4 +307,43 @@ func Test_ErrCheck(t *testing.T) {
}
}
})

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

input := `id,name
1,abcd
2,あいうえ
3,👩‍❤‍💋‍👩🇷🇺😂🏯
`
c, err := NewCSV(bytes.NewBufferString(input))
if err != nil {
t.Fatal(err)
}

type person struct {
ID int // no validate
Name string `validate:"len=3"`
}
persons := make([]person, 0)

errs := c.Decode(&persons)

for i, err := range errs {
switch i {
case 0:
if err.Error() != "line:2 column name: target length is not equal to the threshold value: length threshold=3, value=abcd" {
t.Errorf("CSV.Decode() got errors: %v", err)
}
case 1:
if err.Error() != "line:3 column name: target length is not equal to the threshold value: length threshold=3, value=あいうえ" {
t.Errorf("CSV.Decode() got errors: %v", err)
}
case 2:
if err.Error() != "line:4 column name: target length is not equal to the threshold value: length threshold=3, value=👩‍❤‍💋‍👩🇷🇺😂🏯" {
t.Errorf("CSV.Decode() got errors: %v", err)
}
}
}
})
}
2 changes: 2 additions & 0 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,6 @@ var (
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")
// ErrLength is returned when the target length is not equal to the value.
ErrLength = errors.New("target length is not equal to the threshold value")
)
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ go 1.20
require (
github.com/google/go-cmp v0.6.0
github.com/motemen/go-testutil v0.0.0-20231019055648-af6add1c10c8
github.com/rivo/uniseg v0.4.7
)
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/motemen/go-testutil v0.0.0-20231019055648-af6add1c10c8 h1:bSyU9M98Y4Rrs8X1tYkO8hyHsGw1kWCWyG6FnCQ0l/E=
github.com/motemen/go-testutil v0.0.0-20231019055648-af6add1c10c8/go.mod h1:fz3ptMGvFb+/JIPQadvSpFND5BuGi7cJka/JgG7njN8=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
6 changes: 6 additions & 0 deletions parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,12 @@ func parseValidateTag(tags string) (validators, error) {
return nil, err
}
validatorList = append(validatorList, newMaxValidator(threshold))
case strings.HasPrefix(t, lengthTagValue.String()):
threshold, err := parseThreshold(t)
if err != nil {
return nil, err
}
validatorList = append(validatorList, newLengthValidator(threshold))
}
}
return validatorList, nil
Expand Down
2 changes: 2 additions & 0 deletions tag.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ const (
minTagValue tagValue = "min"
// maxTagValue is the struct tag name for maximum fields.
maxTagValue tagValue = "max"
// lengthTagValue is the struct tag name for length fields.
lengthTagValue tagValue = "len"
)

// String returns the string representation of the tag.
Expand Down
26 changes: 26 additions & 0 deletions validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package csv
import (
"fmt"
"strconv"

"github.com/rivo/uniseg"
)

// validator is a struct that contains the validation rules for a column.
Expand Down Expand Up @@ -356,3 +358,27 @@ func (m *maxValidator) Do(target any) error {
}
return nil
}

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

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

// Do validates the target length is equal to the threshold.
func (l *lengthValidator) Do(target any) error {
v, ok := target.(string)
if !ok {
return fmt.Errorf("%w: value=%v", ErrLength, target) //nolint
}

count := uniseg.GraphemeClusterCount(v)
if count != int(l.threshold) {
return fmt.Errorf("%w: length threshold=%d, value=%s", ErrLength, int(l.threshold), v) //nolint
}
return nil
}

0 comments on commit 1069485

Please sign in to comment.