diff --git a/README.md b/README.md index 9e17b71..b3c128f 100644 --- a/README.md +++ b/README.md @@ -129,6 +129,7 @@ You set the validation rules following the "validate:" tag according to the rule | alphanumeric | Check whether value is alphanumeric or not | | 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"` | | 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 fb2b59a..2fdfae5 100644 --- a/csv_test.go +++ b/csv_test.go @@ -565,4 +565,62 @@ badあ@example.com } } }) + + t.Run("validate contains", func(t *testing.T) { + t.Parallel() + + input := `name +search for a needle in a haystack +example sentence +` + + c, err := NewCSV(bytes.NewBufferString(input)) + if err != nil { + t.Fatal(err) + } + + type contains struct { + Name string `validate:"contains=needle"` + } + + containsList := make([]contains, 0) + errs := c.Decode(&containsList) + for i, err := range errs { + switch i { + case 0: + if err.Error() != "line:3 column name: target does not contain the specified value: contains=needle, value=example sentence" { + t.Errorf("CSV.Decode() got errors: %v", err) + } + } + } + }) + + t.Run("invalid contains tag format", func(t *testing.T) { + t.Parallel() + + input := `name +search for a needle in a haystack +example sentence +` + + c, err := NewCSV(bytes.NewBufferString(input)) + if err != nil { + t.Fatal(err) + } + + type contains struct { + Name string `validate:"contains=needle bad_value"` + } + + containsList := make([]contains, 0) + errs := c.Decode(&containsList) + for i, err := range errs { + switch i { + case 0: + if err.Error() != "target is not one of the values: contains=needle bad_value" { + t.Errorf("CSV.Decode() got errors: %v", err) + } + } + } + }) } diff --git a/errors.go b/errors.go index 8312a34..736594f 100644 --- a/errors.go +++ b/errors.go @@ -98,4 +98,8 @@ var ( ErrASCIIID = "ErrASCII" // ErrEmailID is the error ID used when the target is not an email. ErrEmailID = "ErrEmail" + // ErrContainsID is the error ID used when the target does not contain the specified value. + ErrContainsID = "ErrContains" + // ErrInvalidContainsFormatID is the error ID used when the contains format is invalid. + ErrInvalidContainsFormatID = "ErrInvalidContainsFormat" ) diff --git a/i18n/en.yaml b/i18n/en.yaml index b48dd29..5740d8a 100644 --- a/i18n/en.yaml +++ b/i18n/en.yaml @@ -69,3 +69,9 @@ - id: "ErrEmail" translation: "target is not a valid email address" + +- id: "ErrContains" + translation: "target does not contain the specified value" + +- id: "ErrInvalidContainsFormat" + translation: "'contains' tag format is invalid" diff --git a/i18n/ja.yaml b/i18n/ja.yaml index c226a4c..88c0eed 100644 --- a/i18n/ja.yaml +++ b/i18n/ja.yaml @@ -75,3 +75,9 @@ - id: "ErrEmail" translation: "値がメールアドレスではありません" + +- id: "ErrContains" + translation: "指定された値を含んでいません" + +- id: "ErrInvalidContainsFormat" + translation: "'contains'タグの形式が無効です" diff --git a/i18n/ru.yaml b/i18n/ru.yaml index 9b5da98..083d504 100644 --- a/i18n/ru.yaml +++ b/i18n/ru.yaml @@ -69,3 +69,9 @@ - id: "ErrEmail" translation: "целевое значение не является адресом электронной почты" + +- id: "ErrContains" + translation: "целевое значение не содержит подстроку" + +- id: "ErrInvalidContains" + translation: "Формат тега 'contains' недопустим" diff --git a/parser.go b/parser.go index 968e29c..33db7e6 100644 --- a/parser.go +++ b/parser.go @@ -135,6 +135,15 @@ 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) + if err != nil { + return nil, err + } + if len(oneOf) != 1 { + return nil, NewError(c.i18nLocalizer, ErrInvalidOneOfFormatID, t) + } + validatorList = append(validatorList, newContainsValidator(oneOf[0])) } } return validatorList, nil diff --git a/tag.go b/tag.go index c4e89f9..b8e3c4e 100644 --- a/tag.go +++ b/tag.go @@ -50,6 +50,8 @@ const ( asciiTagValue tagValue = "ascii" // emailTagValue is the struct tag name for email fields. emailTagValue tagValue = "email" + // containsTagValue is the struct tag name for contains fields. + containsTagValue tagValue = "contains" ) // String returns the string representation of the tag. diff --git a/validation.go b/validation.go index c0a2be9..b5ad950 100644 --- a/validation.go +++ b/validation.go @@ -502,3 +502,26 @@ func (e *emailValidator) Do(localizer *i18n.Localizer, target any) error { } return nil } + +// containsValidator is a struct that contains the validation rules for a contains column. +type containsValidator struct { + contains string +} + +// newContainsValidator returns a new containsValidator. +func newContainsValidator(contains string) *containsValidator { + return &containsValidator{contains: contains} +} + +// Do validates the target contains the contains value. +func (c *containsValidator) Do(localizer *i18n.Localizer, target any) error { + v, ok := target.(string) + if !ok { + return NewError(localizer, ErrContainsID, fmt.Sprintf("value=%v", target)) + } + + if !strings.Contains(v, c.contains) { + return NewError(localizer, ErrContainsID, fmt.Sprintf("contains=%s, value=%v", c.contains, target)) + } + return nil +}