Skip to content

Commit

Permalink
Merge pull request #15 from nao1215/nchika/containsany
Browse files Browse the repository at this point in the history
Add containsany tag
  • Loading branch information
nao1215 authored Sep 6, 2024
2 parents d3c14c2 + e4408a0 commit 1a87a59
Show file tree
Hide file tree
Showing 9 changed files with 102 additions and 11 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ You set the validation rules following the "validate:" tag according to the rule
| ascii | Check whether value is ASCII or not |
| boolean | Check whether value is boolean or not. |
| contains | Check whether value contains the specified substring <br> e.g. `validate:"contains=abc"` |
| containsany | Check whether value contains any of the specified characters <br> e.g. `validate:"containsany=abc def"` |
| lowercase | Check whether value is lowercase or not |
| numeric | Check whether value is numeric or not |
| uppercase | Check whether value is uppercase or not |
Expand Down
33 changes: 32 additions & 1 deletion csv_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -617,10 +617,41 @@ example sentence
for i, err := range errs {
switch i {
case 0:
if err.Error() != "target is not one of the values: contains=needle bad_value" {
if err.Error() != "'contains' tag format is invalid: contains=needle bad_value" {
t.Errorf("CSV.Decode() got errors: %v", err)
}
}
}
})

t.Run("validate containsany", func(t *testing.T) {
t.Parallel()

input := `name
you can't find a needle in a haystack
example sentence
I sleep in a bed
`

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

type containsAny struct {
Name string `validate:"containsany=needle bed"`
}

containsAnyList := make([]containsAny, 0)
errs := c.Decode(&containsAnyList)
for i, err := range errs {
switch i {
case 0:
if err.Error() != "line:3 column name: target does not contain any of the specified values: containsany=needle bed, value=example sentence" {
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 @@ -102,4 +102,8 @@ var (
ErrContainsID = "ErrContains"
// ErrInvalidContainsFormatID is the error ID used when the contains format is invalid.
ErrInvalidContainsFormatID = "ErrInvalidContainsFormat"
// ErrContainsAnyID is the error ID used when the target does not contain any of the specified values.
ErrContainsAnyID = "ErrContainsAny"
// ErrInvalidContainsAnyFormatID is the error ID used when the contains any format is invalid.
ErrInvalidContainsAnyFormatID = "ErrInvalidContainsAnyFormat"
)
6 changes: 6 additions & 0 deletions i18n/en.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,9 @@

- id: "ErrInvalidContainsFormat"
translation: "'contains' tag format is invalid"

- id: "ErrContainsAny"
translation: "target does not contain any of the specified values"

- id: "ErrInvalidContainsAnyFormat"
translation: "'containsany' tag format is invalid"
6 changes: 6 additions & 0 deletions i18n/ja.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,9 @@

- id: "ErrInvalidContainsFormat"
translation: "'contains'タグの形式が無効です"

- id: "ErrContainsAny"
translation: "指定された値のいずれも含んでいません"

- id: "ErrInvalidContainsAnyFormat"
translation: "'containsany'タグの形式が無効です"
6 changes: 6 additions & 0 deletions i18n/ru.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,9 @@

- id: "ErrInvalidContains"
translation: "Формат тега 'contains' недопустим"

- id: "ErrContainsAny"
translation: "целевое значение не содержит ни одного из указанных значений"

- id: "ErrInvalidContainsAny"
translation: "Формат тега 'containsany' недопустим"
30 changes: 20 additions & 10 deletions parser.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package csv

import (
"errors"
"fmt"
"reflect"
"strconv"
Expand Down Expand Up @@ -122,9 +123,9 @@ func (c *CSV) parseValidateTag(tags string) (validators, error) {
}
validatorList = append(validatorList, newLengthValidator(threshold))
case strings.HasPrefix(t, oneOfTagValue.String()):
oneOf, err := c.parseOneOf(t)
oneOf, err := c.parseSpecifiedValues(t)
if err != nil {
return nil, err
return nil, NewError(c.i18nLocalizer, ErrInvalidOneOfFormatID, t)
}
validatorList = append(validatorList, newOneOfValidator(oneOf))
case strings.HasPrefix(t, lowercaseTagValue.String()):
Expand All @@ -135,15 +136,24 @@ func (c *CSV) parseValidateTag(tags string) (validators, error) {
validatorList = append(validatorList, newASCIIValidator())
case strings.HasPrefix(t, emailTagValue.String()):
validatorList = append(validatorList, newEmailValidator())
case strings.HasPrefix(t, containsTagValue.String()):
oneOf, err := c.parseOneOf(t)
case strings.HasPrefix(t, containsTagValue.String()) && !strings.HasPrefix(t, containsAnyTagValue.String()):
values, err := c.parseSpecifiedValues(t)
if err != nil {
return nil, err
}
if len(oneOf) != 1 {
return nil, NewError(c.i18nLocalizer, ErrInvalidOneOfFormatID, t)
if len(values) != 1 {
return nil, NewError(c.i18nLocalizer, ErrInvalidContainsFormatID, t)
}
validatorList = append(validatorList, newContainsValidator(values[0]))
case strings.HasPrefix(t, containsAnyTagValue.String()):
values, err := c.parseSpecifiedValues(t)
if err != nil {
return nil, err
}
if len(values) == 0 {
return nil, NewError(c.i18nLocalizer, ErrInvalidContainsAnyFormatID, t)
}
validatorList = append(validatorList, newContainsValidator(oneOf[0]))
validatorList = append(validatorList, newContainsAnyValidator(values))
}
}
return validatorList, nil
Expand All @@ -164,13 +174,13 @@ func (c *CSV) parseThreshold(tagValue string) (float64, error) {
return 0, NewError(c.i18nLocalizer, ErrInvalidThresholdFormatID, tagValue)
}

// parseOneOf parses the oneOf value.
// parseSpecifiedValues parses the tag values.
// tagValue is the value of the struct tag. e.g. oneof=male female prefer_not_to
func (c *CSV) parseOneOf(tagValue string) ([]string, error) {
func (c *CSV) parseSpecifiedValues(tagValue string) ([]string, error) {
parts := strings.Split(tagValue, "=")

if len(parts) == 2 {
return strings.Split(parts[1], " "), nil
}
return nil, NewError(c.i18nLocalizer, ErrInvalidOneOfFormatID, tagValue)
return nil, errors.New("invalid tag values format")
}
2 changes: 2 additions & 0 deletions tag.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ const (
emailTagValue tagValue = "email"
// containsTagValue is the struct tag name for contains fields.
containsTagValue tagValue = "contains"
// containsAnyTagValue is the struct tag name for contains any fields.
containsAnyTagValue tagValue = "containsany"
)

// String returns the string representation of the tag.
Expand Down
25 changes: 25 additions & 0 deletions validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -525,3 +525,28 @@ func (c *containsValidator) Do(localizer *i18n.Localizer, target any) error {
}
return nil
}

// containsAnyValidator is a struct that contains the validation rules for a contains any column.
type containsAnyValidator struct {
contains []string
}

// newContainsAnyValidator returns a new containsAnyValidator.
func newContainsAnyValidator(contains []string) *containsAnyValidator {
return &containsAnyValidator{contains: contains}
}

// Do validates the target contains any of the contains values.
func (c *containsAnyValidator) Do(localizer *i18n.Localizer, target any) error {
v, ok := target.(string)
if !ok {
return NewError(localizer, ErrContainsAnyID, fmt.Sprintf("value=%v", target))
}

for _, s := range c.contains {
if strings.Contains(v, s) {
return nil
}
}
return NewError(localizer, ErrContainsAnyID, fmt.Sprintf("containsany=%s, value=%v", strings.Join(c.contains, " "), target))
}

0 comments on commit 1a87a59

Please sign in to comment.