Skip to content

Commit

Permalink
Add contains tag
Browse files Browse the repository at this point in the history
  • Loading branch information
nao1215 committed Sep 6, 2024
1 parent 9133095 commit b5dd909
Show file tree
Hide file tree
Showing 9 changed files with 115 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <br> 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 |
Expand Down
58 changes: 58 additions & 0 deletions csv_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
})
}
4 changes: 4 additions & 0 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
6 changes: 6 additions & 0 deletions i18n/en.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
6 changes: 6 additions & 0 deletions i18n/ja.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,9 @@

- id: "ErrEmail"
translation: "値がメールアドレスではありません"

- id: "ErrContains"
translation: "指定された値を含んでいません"

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

- id: "ErrEmail"
translation: "целевое значение не является адресом электронной почты"

- id: "ErrContains"
translation: "целевое значение не содержит подстроку"

- id: "ErrInvalidContains"
translation: "Формат тега 'contains' недопустим"
9 changes: 9 additions & 0 deletions parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions tag.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
23 changes: 23 additions & 0 deletions validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

0 comments on commit b5dd909

Please sign in to comment.