From e4408a0417eedf2b32aaf36d5b44c423ee0172a8 Mon Sep 17 00:00:00 2001 From: CHIKAMATSU Naohiro Date: Fri, 6 Sep 2024 21:59:30 +0900 Subject: [PATCH] Add containsany tag --- README.md | 1 + csv_test.go | 33 ++++++++++++++++++++++++++++++++- errors.go | 4 ++++ i18n/en.yaml | 6 ++++++ i18n/ja.yaml | 6 ++++++ i18n/ru.yaml | 6 ++++++ parser.go | 30 ++++++++++++++++++++---------- tag.go | 2 ++ validation.go | 25 +++++++++++++++++++++++++ 9 files changed, 102 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index b3c128f..75b61ec 100644 --- a/README.md +++ b/README.md @@ -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
e.g. `validate:"contains=abc"` | +| containsany | Check whether value contains any of the specified characters
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 | diff --git a/csv_test.go b/csv_test.go index 2fdfae5..e6cd090 100644 --- a/csv_test.go +++ b/csv_test.go @@ -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) + } + } + } + }) + } diff --git a/errors.go b/errors.go index 736594f..9792f11 100644 --- a/errors.go +++ b/errors.go @@ -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" ) diff --git a/i18n/en.yaml b/i18n/en.yaml index 5740d8a..f8fb2b0 100644 --- a/i18n/en.yaml +++ b/i18n/en.yaml @@ -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" diff --git a/i18n/ja.yaml b/i18n/ja.yaml index 88c0eed..259bf71 100644 --- a/i18n/ja.yaml +++ b/i18n/ja.yaml @@ -81,3 +81,9 @@ - id: "ErrInvalidContainsFormat" translation: "'contains'タグの形式が無効です" + +- id: "ErrContainsAny" + translation: "指定された値のいずれも含んでいません" + +- id: "ErrInvalidContainsAnyFormat" + translation: "'containsany'タグの形式が無効です" diff --git a/i18n/ru.yaml b/i18n/ru.yaml index 083d504..ec56211 100644 --- a/i18n/ru.yaml +++ b/i18n/ru.yaml @@ -75,3 +75,9 @@ - id: "ErrInvalidContains" translation: "Формат тега 'contains' недопустим" + +- id: "ErrContainsAny" + translation: "целевое значение не содержит ни одного из указанных значений" + +- id: "ErrInvalidContainsAny" + translation: "Формат тега 'containsany' недопустим" diff --git a/parser.go b/parser.go index 33db7e6..2a639a2 100644 --- a/parser.go +++ b/parser.go @@ -1,6 +1,7 @@ package csv import ( + "errors" "fmt" "reflect" "strconv" @@ -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()): @@ -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 @@ -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") } diff --git a/tag.go b/tag.go index b8e3c4e..901eb53 100644 --- a/tag.go +++ b/tag.go @@ -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. diff --git a/validation.go b/validation.go index b5ad950..89fa00f 100644 --- a/validation.go +++ b/validation.go @@ -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)) +}