From ce2fbd5617b0312a51bc21675f658a8b13cd7bb0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 18 Jul 2025 13:46:19 +0000 Subject: [PATCH 1/4] Implement email validation tag feature Co-authored-by: leocavalcante <183722+leocavalcante@users.noreply.github.com> --- README.md | 2 + _examples/email_test/test_email.go | 87 +++++++++++++++++++++++ _examples/email_test/user_validator.go | 25 +++++++ types/string_utils.go | 19 ++++- types/string_utils_test.go | 80 +++++++++++++++++++++ validgen/get_test_elements_string_test.go | 13 ++++ validgen/parser_validation.go | 1 + validgen/parser_validation_test.go | 9 +++ validgen/test_elements.go | 1 + 9 files changed, 236 insertions(+), 1 deletion(-) create mode 100644 _examples/email_test/test_email.go create mode 100644 _examples/email_test/user_validator.go create mode 100644 types/string_utils_test.go diff --git a/README.md b/README.md index 68feb09..ec9e083 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,7 @@ The following validations will be implemented: - in (in): must be one of the following values - nin (not in): must not be one of the following values - required (required): is required +- email (email): must be a valid email format (empty is valid for optional fields) The following table shows the validations and possible types, where "I" means "Implemented", "W" means "Will be implemented" and "-" means "Will not be implemented": @@ -66,6 +67,7 @@ The following table shows the validations and possible types, where "I" means "I | in | I | W | W | W | W | W | - | W | | nin | W | W | W | W | W | W | - | W | | required | I | W | W | W | W | W | W | W | +| email | I | - | - | - | - | - | - | - | # Steps to run the unit tests diff --git a/_examples/email_test/test_email.go b/_examples/email_test/test_email.go new file mode 100644 index 0000000..035969d --- /dev/null +++ b/_examples/email_test/test_email.go @@ -0,0 +1,87 @@ +package main + +import ( + "fmt" +) + +type User struct { + Email1 string `validate:"required,email"` + Email2 string `validate:"email"` +} + +func main() { + // Test case 1: Empty required email (should fail) + u1 := &User{ + Email1: "", + Email2: "", + } + if errs := UserValidate(u1); len(errs) > 0 { + fmt.Printf("User1: %+v Errors: ", u1) + for _, err := range errs { + fmt.Printf("%s; ", err) + } + fmt.Println() + } else { + fmt.Printf("User1: %+v is valid\n", u1) + } + + // Test case 2: Invalid required email (should fail) + u2 := &User{ + Email1: "invalid.email", + Email2: "", + } + if errs := UserValidate(u2); len(errs) > 0 { + fmt.Printf("User2: %+v Errors: ", u2) + for _, err := range errs { + fmt.Printf("%s; ", err) + } + fmt.Println() + } else { + fmt.Printf("User2: %+v is valid\n", u2) + } + + // Test case 3: Valid required email, empty optional email (should pass) + u3 := &User{ + Email1: "valid@example.com", + Email2: "", + } + if errs := UserValidate(u3); len(errs) > 0 { + fmt.Printf("User3: %+v Errors: ", u3) + for _, err := range errs { + fmt.Printf("%s; ", err) + } + fmt.Println() + } else { + fmt.Printf("User3: %+v is valid\n", u3) + } + + // Test case 4: Valid required email, valid optional email (should pass) + u4 := &User{ + Email1: "user@domain.com", + Email2: "optional@test.org", + } + if errs := UserValidate(u4); len(errs) > 0 { + fmt.Printf("User4: %+v Errors: ", u4) + for _, err := range errs { + fmt.Printf("%s; ", err) + } + fmt.Println() + } else { + fmt.Printf("User4: %+v is valid\n", u4) + } + + // Test case 5: Valid required email, invalid optional email (should fail) + u5 := &User{ + Email1: "user@domain.com", + Email2: "invalid.email", + } + if errs := UserValidate(u5); len(errs) > 0 { + fmt.Printf("User5: %+v Errors: ", u5) + for _, err := range errs { + fmt.Printf("%s; ", err) + } + fmt.Println() + } else { + fmt.Printf("User5: %+v is valid\n", u5) + } +} \ No newline at end of file diff --git a/_examples/email_test/user_validator.go b/_examples/email_test/user_validator.go new file mode 100644 index 0000000..0c5b15d --- /dev/null +++ b/_examples/email_test/user_validator.go @@ -0,0 +1,25 @@ +// Code generated by ValidGen. DO NOT EDIT. + +package main + +import ( + "github.com/opencodeco/validgen/types" +) + +func UserValidate(obj *User) []error { + var errs []error + + if !(obj.Email1 != "") { + errs = append(errs, types.NewValidationError("Email1 is required")) + } + + if !(types.IsValidEmail(obj.Email1) == true) { + errs = append(errs, types.NewValidationError("Email1 must be a valid email")) + } + + if !(types.IsValidEmail(obj.Email2) == true) { + errs = append(errs, types.NewValidationError("Email2 must be a valid email")) + } + + return errs +} diff --git a/types/string_utils.go b/types/string_utils.go index ca3e58d..cda20ab 100644 --- a/types/string_utils.go +++ b/types/string_utils.go @@ -1,7 +1,24 @@ package types -import "strings" +import ( + "regexp" + "strings" +) func ToLower(str string) string { return strings.ToLower(str) } + +// IsValidEmail validates if a string is a valid email format +// Returns true for valid email format, false otherwise +// Empty string returns true (for optional email fields) +func IsValidEmail(email string) bool { + if email == "" { + return true // Empty email is valid for optional fields + } + + // Basic email regex pattern + // This covers most common email formats but is not exhaustive + emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`) + return emailRegex.MatchString(email) +} diff --git a/types/string_utils_test.go b/types/string_utils_test.go new file mode 100644 index 0000000..27c1480 --- /dev/null +++ b/types/string_utils_test.go @@ -0,0 +1,80 @@ +package types + +import "testing" + +func TestIsValidEmail(t *testing.T) { + tests := []struct { + name string + email string + want bool + }{ + { + name: "empty email (valid for optional fields)", + email: "", + want: true, + }, + { + name: "valid simple email", + email: "test@example.com", + want: true, + }, + { + name: "valid email with subdomain", + email: "user@mail.example.com", + want: true, + }, + { + name: "valid email with numbers and special chars", + email: "user123+tag@example.co.uk", + want: true, + }, + { + name: "valid email with dots and underscores", + email: "first.last_name@domain.org", + want: true, + }, + { + name: "invalid email without @", + email: "invalid.email.com", + want: false, + }, + { + name: "invalid email without domain", + email: "user@", + want: false, + }, + { + name: "invalid email without local part", + email: "@domain.com", + want: false, + }, + { + name: "invalid email without TLD", + email: "user@domain", + want: false, + }, + { + name: "invalid email with spaces", + email: "user @domain.com", + want: false, + }, + { + name: "invalid email with multiple @", + email: "user@@domain.com", + want: false, + }, + { + name: "invalid email with short TLD", + email: "user@domain.c", + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := IsValidEmail(tt.email); got != tt.want { + t.Errorf("IsValidEmail(%q) = %v, want %v", tt.email, got, tt.want) + } + }) + } +} \ No newline at end of file diff --git a/validgen/get_test_elements_string_test.go b/validgen/get_test_elements_string_test.go index aab1c7d..372cd56 100644 --- a/validgen/get_test_elements_string_test.go +++ b/validgen/get_test_elements_string_test.go @@ -145,6 +145,19 @@ func TestGetTestElementsWithStringFields(t *testing.T) { errorMessage: "InField must be one of ' a ' ' b ' ' c '", }, }, + { + name: "Email validation", + args: args{ + fieldName: "EmailField", + fieldValidation: "email", + }, + want: TestElements{ + leftOperand: "types.IsValidEmail(obj.EmailField)", + operator: "==", + rightOperands: []string{`true`}, + errorMessage: "EmailField must be a valid email", + }, + }, } for _, tt := range tests { diff --git a/validgen/parser_validation.go b/validgen/parser_validation.go index 66df7b9..9b0deeb 100644 --- a/validgen/parser_validation.go +++ b/validgen/parser_validation.go @@ -34,6 +34,7 @@ func ParserValidation(fieldValidation string) (*Validation, error) { "neq": ONE_VALUE, "neq_ignore_case": ONE_VALUE, "in": MANY_VALUES, + "email": ZERO_VALUE, } validation, values, err := parserValidationString(fieldValidation) diff --git a/validgen/parser_validation_test.go b/validgen/parser_validation_test.go index bf4e301..7bf5f3a 100644 --- a/validgen/parser_validation_test.go +++ b/validgen/parser_validation_test.go @@ -95,6 +95,15 @@ func Test_ValidParserValidation(t *testing.T) { Values: []string{"a", "b", "c"}, }, }, + { + name: "email validation without value", + validation: "email", + want: &Validation{ + Operation: "email", + ExpectedValues: ZERO_VALUE, + Values: []string{}, + }, + }, } for _, tt := range tests { diff --git a/validgen/test_elements.go b/validgen/test_elements.go index f0b6ec3..71ee71c 100644 --- a/validgen/test_elements.go +++ b/validgen/test_elements.go @@ -41,6 +41,7 @@ func GetTestElements(fieldName, fieldValidation, fieldType string) (TestElements "neq,string": {"{{.Name}}", "!=", `"{{.Target}}"`, "{{.Name}} must not be equal to '{{.Target}}'"}, "neq_ignore_case,string": {"types.ToLower({{.Name}})", "!=", `"{{.Target}}"`, "{{.Name}} must not be equal to '{{.Target}}'"}, "in,string": {"{{.Name}}", "==", `"{{.Target}}"`, "{{.Name}} must be one of {{.Targets}}"}, + "email,string": {"types.IsValidEmail({{.Name}})", "==", `true`, "{{.Name}} must be a valid email"}, } validation, err := ParserValidation(fieldValidation) From 8c8c906670dbe55a7e671d80cff26e8043fde552 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 18 Jul 2025 14:35:37 +0000 Subject: [PATCH 2/4] Add comprehensive end-to-end tests for email validation Co-authored-by: leocavalcante <183722+leocavalcante@users.noreply.github.com> --- tests/endtoend/string.go | 50 ++++++++++++++++++++++++++ tests/endtoend/stringtype_validator.go | 12 +++++++ 2 files changed, 62 insertions(+) diff --git a/tests/endtoend/string.go b/tests/endtoend/string.go index fc876a7..3d4b6b3 100644 --- a/tests/endtoend/string.go +++ b/tests/endtoend/string.go @@ -11,12 +11,15 @@ type StringType struct { FieldNeq string `validate:"neq=cba"` FieldNeqIC string `validate:"neq_ignore_case=YeS"` FieldIn string `validate:"in=ab bc cd"` + EmailReq string `validate:"required,email"` + EmailOpt string `validate:"email"` } func string_tests() { var expectedMsgErrors []string var errs []error + // Test case 1: All validation failures including email v := &StringType{ FieldEq: "123", FieldEqIC: "abc", @@ -25,6 +28,8 @@ func string_tests() { FieldNeq: "cba", FieldNeqIC: "yeS", FieldIn: "abc", + EmailReq: "", // Empty required email + EmailOpt: "invalid", // Invalid optional email } expectedMsgErrors = []string{ "FieldReq is required", @@ -35,12 +40,15 @@ func string_tests() { "FieldNeq must not be equal to 'cba'", "FieldNeqIC must not be equal to 'yes'", "FieldIn must be one of 'ab' 'bc' 'cd'", + "EmailReq is required", + "EmailOpt must be a valid email", } errs = StringTypeValidate(v) if !expectedMsgErrorsOk(errs, expectedMsgErrors) { log.Fatalf("error = %v, wantErr %v", errs, expectedMsgErrors) } + // Test case 2: Invalid required email format v = &StringType{ FieldReq: "123", FieldEq: "aabbcc", @@ -50,6 +58,48 @@ func string_tests() { FieldNeq: "ops", FieldNeqIC: "No", FieldIn: "bc", + EmailReq: "invalid.email.format", // Invalid required email + EmailOpt: "", // Empty optional email (valid) + } + expectedMsgErrors = []string{ + "EmailReq must be a valid email", + } + errs = StringTypeValidate(v) + if !expectedMsgErrorsOk(errs, expectedMsgErrors) { + log.Fatalf("error = %v, wantErr %v", errs, expectedMsgErrors) + } + + // Test case 3: All valid including valid emails + v = &StringType{ + FieldReq: "123", + FieldEq: "aabbcc", + FieldEqIC: "yEs", + FieldMinMax: "12345678", + FieldLen: "abcdefgh", + FieldNeq: "ops", + FieldNeqIC: "No", + FieldIn: "bc", + EmailReq: "user@example.com", // Valid required email + EmailOpt: "optional@test.org", // Valid optional email + } + expectedMsgErrors = nil + errs = StringTypeValidate(v) + if !expectedMsgErrorsOk(errs, expectedMsgErrors) { + log.Fatalf("error = %v, wantErr %v", errs, expectedMsgErrors) + } + + // Test case 4: Valid required email, empty optional email + v = &StringType{ + FieldReq: "123", + FieldEq: "aabbcc", + FieldEqIC: "yEs", + FieldMinMax: "12345678", + FieldLen: "abcdefgh", + FieldNeq: "ops", + FieldNeqIC: "No", + FieldIn: "bc", + EmailReq: "required@domain.com", // Valid required email + EmailOpt: "", // Empty optional email (valid) } expectedMsgErrors = nil errs = StringTypeValidate(v) diff --git a/tests/endtoend/stringtype_validator.go b/tests/endtoend/stringtype_validator.go index 755d74b..a98b570 100644 --- a/tests/endtoend/stringtype_validator.go +++ b/tests/endtoend/stringtype_validator.go @@ -45,5 +45,17 @@ func StringTypeValidate(obj *StringType) []error { errs = append(errs, types.NewValidationError("FieldIn must be one of 'ab' 'bc' 'cd'")) } + if !(obj.EmailReq != "") { + errs = append(errs, types.NewValidationError("EmailReq is required")) + } + + if !(types.IsValidEmail(obj.EmailReq) == true) { + errs = append(errs, types.NewValidationError("EmailReq must be a valid email")) + } + + if !(types.IsValidEmail(obj.EmailOpt) == true) { + errs = append(errs, types.NewValidationError("EmailOpt must be a valid email")) + } + return errs } From e723c03ded33b64d1e903f3683cc67a24bb53b63 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 18 Jul 2025 14:47:09 +0000 Subject: [PATCH 3/4] Optimize email validation with pre-compiled regex for better performance Co-authored-by: leocavalcante <183722+leocavalcante@users.noreply.github.com> --- types/string_utils.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/types/string_utils.go b/types/string_utils.go index cda20ab..e59d002 100644 --- a/types/string_utils.go +++ b/types/string_utils.go @@ -5,6 +5,10 @@ import ( "strings" ) +// emailRegex is a pre-compiled regex for email validation +// This avoids recompiling the regex on every validation call +var emailRegex = regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`) + func ToLower(str string) string { return strings.ToLower(str) } @@ -17,8 +21,6 @@ func IsValidEmail(email string) bool { return true // Empty email is valid for optional fields } - // Basic email regex pattern - // This covers most common email formats but is not exhaustive - emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`) + // Use pre-compiled regex for better performance return emailRegex.MatchString(email) } From fe56677a53f1eae56358b802ef30b9464efd9fb4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 18 Jul 2025 17:42:15 +0000 Subject: [PATCH 4/4] refactor: consolidate string tests into two test cases as requested Co-authored-by: alexgarzao <5495536+alexgarzao@users.noreply.github.com> --- tests/endtoend/string.go | 50 ++++------------------------------------ 1 file changed, 5 insertions(+), 45 deletions(-) diff --git a/tests/endtoend/string.go b/tests/endtoend/string.go index 3d4b6b3..754b395 100644 --- a/tests/endtoend/string.go +++ b/tests/endtoend/string.go @@ -19,7 +19,7 @@ func string_tests() { var expectedMsgErrors []string var errs []error - // Test case 1: All validation failures including email + // Test case 1: All failure scenarios v := &StringType{ FieldEq: "123", FieldEqIC: "abc", @@ -28,8 +28,8 @@ func string_tests() { FieldNeq: "cba", FieldNeqIC: "yeS", FieldIn: "abc", - EmailReq: "", // Empty required email - EmailOpt: "invalid", // Invalid optional email + EmailReq: "invalid.email.format", // Invalid required email + EmailOpt: "invalid", // Invalid optional email } expectedMsgErrors = []string{ "FieldReq is required", @@ -40,36 +40,15 @@ func string_tests() { "FieldNeq must not be equal to 'cba'", "FieldNeqIC must not be equal to 'yes'", "FieldIn must be one of 'ab' 'bc' 'cd'", - "EmailReq is required", - "EmailOpt must be a valid email", - } - errs = StringTypeValidate(v) - if !expectedMsgErrorsOk(errs, expectedMsgErrors) { - log.Fatalf("error = %v, wantErr %v", errs, expectedMsgErrors) - } - - // Test case 2: Invalid required email format - v = &StringType{ - FieldReq: "123", - FieldEq: "aabbcc", - FieldEqIC: "yEs", - FieldMinMax: "12345678", - FieldLen: "abcdefgh", - FieldNeq: "ops", - FieldNeqIC: "No", - FieldIn: "bc", - EmailReq: "invalid.email.format", // Invalid required email - EmailOpt: "", // Empty optional email (valid) - } - expectedMsgErrors = []string{ "EmailReq must be a valid email", + "EmailOpt must be a valid email", } errs = StringTypeValidate(v) if !expectedMsgErrorsOk(errs, expectedMsgErrors) { log.Fatalf("error = %v, wantErr %v", errs, expectedMsgErrors) } - // Test case 3: All valid including valid emails + // Test case 2: All valid input v = &StringType{ FieldReq: "123", FieldEq: "aabbcc", @@ -80,25 +59,6 @@ func string_tests() { FieldNeqIC: "No", FieldIn: "bc", EmailReq: "user@example.com", // Valid required email - EmailOpt: "optional@test.org", // Valid optional email - } - expectedMsgErrors = nil - errs = StringTypeValidate(v) - if !expectedMsgErrorsOk(errs, expectedMsgErrors) { - log.Fatalf("error = %v, wantErr %v", errs, expectedMsgErrors) - } - - // Test case 4: Valid required email, empty optional email - v = &StringType{ - FieldReq: "123", - FieldEq: "aabbcc", - FieldEqIC: "yEs", - FieldMinMax: "12345678", - FieldLen: "abcdefgh", - FieldNeq: "ops", - FieldNeqIC: "No", - FieldIn: "bc", - EmailReq: "required@domain.com", // Valid required email EmailOpt: "", // Empty optional email (valid) } expectedMsgErrors = nil