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/tests/endtoend/string.go b/tests/endtoend/string.go index fc876a7..754b395 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 failure scenarios v := &StringType{ FieldEq: "123", FieldEqIC: "abc", @@ -25,6 +28,8 @@ func string_tests() { FieldNeq: "cba", FieldNeqIC: "yeS", FieldIn: "abc", + EmailReq: "invalid.email.format", // Invalid 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 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 2: All valid input v = &StringType{ FieldReq: "123", FieldEq: "aabbcc", @@ -50,6 +58,8 @@ func string_tests() { FieldNeq: "ops", FieldNeqIC: "No", FieldIn: "bc", + EmailReq: "user@example.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 } diff --git a/types/string_utils.go b/types/string_utils.go index ca3e58d..e59d002 100644 --- a/types/string_utils.go +++ b/types/string_utils.go @@ -1,7 +1,26 @@ package types -import "strings" +import ( + "regexp" + "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) } + +// 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 + } + + // Use pre-compiled regex for better performance + 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)