From cdd10be611fb2c989294142d97892b6fcaf276c8 Mon Sep 17 00:00:00 2001 From: CHIKAMATSU Naohiro Date: Thu, 5 Sep 2024 22:51:53 +0900 Subject: [PATCH] Add email tag --- README.md | 6 ++++++ csv_test.go | 37 +++++++++++++++++++++++++++++++++++++ errors.go | 2 ++ i18n/en.yaml | 3 +++ i18n/ja.yaml | 3 +++ i18n/ru.yaml | 3 +++ parser.go | 2 ++ tag.go | 2 ++ validation.go | 31 ++++++++++++++++++++++++++++++- 9 files changed, 88 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9db706e..9e17b71 100644 --- a/README.md +++ b/README.md @@ -133,6 +133,12 @@ You set the validation rules following the "validate:" tag according to the rule | numeric | Check whether value is numeric or not | | uppercase | Check whether value is uppercase or not | +#### Format + +| Tag Name | Description | +|-------------------|---------------------------------------------------| +| email | Check whether value is an email address or not | + #### Comparisons | Tag Name | Description | diff --git a/csv_test.go b/csv_test.go index 0ce6549..fb2b59a 100644 --- a/csv_test.go +++ b/csv_test.go @@ -528,4 +528,41 @@ ABC } } }) + + t.Run("validate email", func(t *testing.T) { + t.Parallel() + + input := `email +simple@example.com +very.common@example.com +disposable.style.email.with+symbol@example.com +user.name+tag+sorting@example.com +admin@mailserver1 +badあ@example.com +` + + c, err := NewCSV(bytes.NewBufferString(input)) + if err != nil { + t.Fatal(err) + } + + type email struct { + Email string `validate:"email"` + } + + emails := make([]email, 0) + errs := c.Decode(&emails) + for i, err := range errs { + switch i { + case 0: + if err.Error() != "line:6 column email: target is not a valid email address: value=admin@mailserver1" { + t.Errorf("CSV.Decode() got errors: %v", err) + } + case 1: + if err.Error() != "line:7 column email: target is not a valid email address: value=badあ@example.com" { + t.Errorf("CSV.Decode() got errors: %v", err) + } + } + } + }) } diff --git a/errors.go b/errors.go index 47f0dac..8312a34 100644 --- a/errors.go +++ b/errors.go @@ -96,4 +96,6 @@ var ( ErrUppercaseID = "ErrUppercase" // ErrASCIIID is the error ID used when the target is not an ASCII character. ErrASCIIID = "ErrASCII" + // ErrEmailID is the error ID used when the target is not an email. + ErrEmailID = "ErrEmail" ) diff --git a/i18n/en.yaml b/i18n/en.yaml index c6f6ba6..b48dd29 100644 --- a/i18n/en.yaml +++ b/i18n/en.yaml @@ -66,3 +66,6 @@ - id: "ErrASCII" translation: "target is not an ASCII character" + +- id: "ErrEmail" + translation: "target is not a valid email address" diff --git a/i18n/ja.yaml b/i18n/ja.yaml index c4024e4..c226a4c 100644 --- a/i18n/ja.yaml +++ b/i18n/ja.yaml @@ -72,3 +72,6 @@ - id: "ErrASCII" translation: "値がASCII文字ではありません" + +- id: "ErrEmail" + translation: "値がメールアドレスではありません" diff --git a/i18n/ru.yaml b/i18n/ru.yaml index 201c334..9b5da98 100644 --- a/i18n/ru.yaml +++ b/i18n/ru.yaml @@ -66,3 +66,6 @@ - id: "ErrASCII" translation: "целевое значение не является ASCII символом" + +- id: "ErrEmail" + translation: "целевое значение не является адресом электронной почты" diff --git a/parser.go b/parser.go index 789d530..968e29c 100644 --- a/parser.go +++ b/parser.go @@ -133,6 +133,8 @@ func (c *CSV) parseValidateTag(tags string) (validators, error) { validatorList = append(validatorList, newUppercaseValidator()) case strings.HasPrefix(t, asciiTagValue.String()): validatorList = append(validatorList, newASCIIValidator()) + case strings.HasPrefix(t, emailTagValue.String()): + validatorList = append(validatorList, newEmailValidator()) } } return validatorList, nil diff --git a/tag.go b/tag.go index d43fe2e..c4e89f9 100644 --- a/tag.go +++ b/tag.go @@ -48,6 +48,8 @@ const ( uppercaseTagValue tagValue = "uppercase" // asciiTagValue is the struct tag name for ascii fields. asciiTagValue tagValue = "ascii" + // emailTagValue is the struct tag name for email fields. + emailTagValue tagValue = "email" ) // String returns the string representation of the tag. diff --git a/validation.go b/validation.go index 48f2b98..c0a2be9 100644 --- a/validation.go +++ b/validation.go @@ -2,6 +2,7 @@ package csv import ( "fmt" + "regexp" "strconv" "strings" @@ -461,15 +462,43 @@ func newASCIIValidator() *asciiValidator { // Do validates the target is an ASCII string. func (a *asciiValidator) Do(localizer *i18n.Localizer, target any) error { + const maxASCII = 127 + v, ok := target.(string) if !ok { return NewError(localizer, ErrASCIIID, fmt.Sprintf("value=%v", target)) } for _, r := range v { - if r > 127 { + if r > maxASCII { return NewError(localizer, ErrASCIIID, fmt.Sprintf("value=%v", target)) } } return nil } + +// emailValidator is a struct that contains the validation rules for an email column. +type emailValidator struct { + regexp *regexp.Regexp +} + +// newEmailValidator returns a new emailValidator. +func newEmailValidator() *emailValidator { + const emailRegexPattern = `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$` + return &emailValidator{ + regexp: regexp.MustCompile(emailRegexPattern), + } +} + +// Do validates the target is an email. +func (e *emailValidator) Do(localizer *i18n.Localizer, target any) error { + v, ok := target.(string) + if !ok { + return NewError(localizer, ErrEmailID, fmt.Sprintf("value=%v", target)) + } + + if !e.regexp.MatchString(v) { + return NewError(localizer, ErrEmailID, fmt.Sprintf("value=%v", target)) + } + return nil +}