From 76de70c87ec0f60020ff1be45e56430a9f683c89 Mon Sep 17 00:00:00 2001 From: CHIKAMATSU Naohiro Date: Mon, 13 May 2024 20:58:34 +0900 Subject: [PATCH] Add "len" tag value --- README.md | 1 + csv_test.go | 75 ++++++++++++++++++++++++++++++++++++++++++++++++++- errors.go | 2 ++ go.mod | 1 + go.sum | 2 ++ parser.go | 6 +++++ tag.go | 2 ++ validation.go | 26 ++++++++++++++++++ 8 files changed, 114 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0c4da41..b82e442 100644 --- a/README.md +++ b/README.md @@ -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
e.g. `validate:"lte=1"` | | min | Check whether value is greater than or equal to the specified value
e.g. `validate:"min=1"` | | max | Check whether value is less than or equal to the specified value
e.g. `validate:"max=100"` | +| len | Check whether the length of the value is equal to the specified value
e.g. `validate:"len=10"` | ## License [MIT License](./LICENSE) diff --git a/csv_test.go b/csv_test.go index 3bddccd..319e97d 100644 --- a/csv_test.go +++ b/csv_test.go @@ -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) { @@ -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) @@ -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) + } + } + } + }) } diff --git a/errors.go b/errors.go index b526ef4..02cd5ac 100644 --- a/errors.go +++ b/errors.go @@ -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") ) diff --git a/go.mod b/go.mod index ee2a710..e1c4e5f 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index c8b26fe..eaabd08 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/parser.go b/parser.go index 4680124..eaf867a 100644 --- a/parser.go +++ b/parser.go @@ -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 diff --git a/tag.go b/tag.go index 0696a0c..85e5e3a 100644 --- a/tag.go +++ b/tag.go @@ -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. diff --git a/validation.go b/validation.go index ce5f1c7..a17fbf3 100644 --- a/validation.go +++ b/validation.go @@ -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. @@ -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 +}