From f3a8efe69ad346ffaa01ce7dfdbe8cb04b21d9c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luis=20Eduardo=20Jer=C3=A9z=20Gir=C3=B3n?= Date: Mon, 22 Jul 2024 20:33:23 -0600 Subject: [PATCH] Add email and struct validation --- internal/validate/README.md | 5 ++ internal/validate/email.go | 17 ++++ internal/validate/email_test.go | 25 ++++++ internal/validate/struct.go | 98 +++++++++++++++++++++++ internal/validate/struct_test.go | 130 +++++++++++++++++++++++++++++++ 5 files changed, 275 insertions(+) create mode 100644 internal/validate/README.md create mode 100644 internal/validate/email.go create mode 100644 internal/validate/email_test.go create mode 100644 internal/validate/struct.go create mode 100644 internal/validate/struct_test.go diff --git a/internal/validate/README.md b/internal/validate/README.md new file mode 100644 index 0000000..0244bc3 --- /dev/null +++ b/internal/validate/README.md @@ -0,0 +1,5 @@ +# Validate functions + +All the functions in this directory are used to validate data, all of them _MUST BE PURE FUNCTIONS_. + +https://en.wikipedia.org/wiki/Pure_function diff --git a/internal/validate/email.go b/internal/validate/email.go new file mode 100644 index 0000000..304a06a --- /dev/null +++ b/internal/validate/email.go @@ -0,0 +1,17 @@ +package validate + +import "regexp" + +// Email validates an email address. +// It returns a boolean indicating whether +// the email is valid or not. +func Email(email string) bool { + // Regular expression to match email format + regex := `^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$` + + // Compile the regular expression + re := regexp.MustCompile(regex) + + // Match the email against the regular expression + return re.MatchString(email) +} diff --git a/internal/validate/email_test.go b/internal/validate/email_test.go new file mode 100644 index 0000000..3d5bfaa --- /dev/null +++ b/internal/validate/email_test.go @@ -0,0 +1,25 @@ +package validate + +import "testing" + +func TestEmail(t *testing.T) { + tests := []struct { + email string + valid bool + }{ + {"", false}, + {"test", false}, + {"test@", false}, + {"@example.com", false}, + {"test@example", false}, + {"test@example.com", true}, + {"test@example.com.gt", true}, + } + + for _, testItem := range tests { + isValid := Email(testItem.email) + if isValid != testItem.valid { + t.Errorf("Email(%s) expected %v, got %v", testItem.email, testItem.valid, isValid) + } + } +} diff --git a/internal/validate/struct.go b/internal/validate/struct.go new file mode 100644 index 0000000..0e6eb29 --- /dev/null +++ b/internal/validate/struct.go @@ -0,0 +1,98 @@ +package validate + +import ( + "fmt" + + "github.com/go-playground/validator/v10" +) + +// StructError is the error returned by Struct. +type StructError struct { + errs []error +} + +// SetErrs sets the errors. +func (e *StructError) SetErrs(errs []error) { + e.errs = errs +} + +// AddErr adds an error. +func (e *StructError) AddErr(err error) { + e.errs = append(e.errs, err) +} + +// HasErrs returns true if there are errors. +func (e *StructError) HasErrs() bool { + return len(e.errs) > 0 +} + +// Error returns all the errors as a string separated by commas. +func (e *StructError) Error() string { + errStr := "" + for idx, err := range e.errs { + if idx > 0 { + errStr += ", " + } + errStr += err.Error() + } + + return errStr +} + +// Errors returns all the errors as a slice of strings. +func (e *StructError) Errors() []string { + errStrs := make([]string, len(e.errs)) + for idx, err := range e.errs { + errStrs[idx] = err.Error() + } + + return errStrs +} + +// ErrorsRaw returns all the errors as a slice of errors. +func (e *StructError) ErrorsRaw() []error { + return e.errs +} + +// Struct validates the given struct using go-playground/validator. +func Struct[T any](sPointer *T) *StructError { + err := validator.New().Struct(sPointer) + + if err != nil { + errs := StructError{} + + if _, ok := err.(*validator.InvalidValidationError); ok { + errs.AddErr(fmt.Errorf("validation error (check if it's a struct): %s", err)) + } + + if validationErrors, ok := err.(validator.ValidationErrors); ok { + for _, validationError := range validationErrors { + errs.AddErr(fmt.Errorf( + "error in field %s: %s", + validationError.StructField(), + validationError.Tag(), + )) + } + } + + return &errs + } + + return nil +} + +// StructSlice validates the given slice of structs using go-playground/validator. +func StructSlice[T any](sPointerSlice *[]T) *StructError { + for i, sPointer := range *sPointerSlice { + num := i + 1 + if err := Struct(&sPointer); err != nil { + se := &StructError{} + errs := []error{fmt.Errorf("error in row %d", num)} + errs = append(errs, err.ErrorsRaw()...) + se.SetErrs(errs) + return se + } + } + + return nil +} diff --git a/internal/validate/struct_test.go b/internal/validate/struct_test.go new file mode 100644 index 0000000..5226fb4 --- /dev/null +++ b/internal/validate/struct_test.go @@ -0,0 +1,130 @@ +package validate + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/assert" +) + +type TestStruct struct { + Field string `validate:"required"` +} + +func TestValidateStruct(t *testing.T) { + t.Run("Success", func(t *testing.T) { + s := TestStruct{ + Field: "value", + } + err := Struct(&s) + + assert.Nil(t, err) + assert.Equal(t, "value", s.Field) + }) + + t.Run("Fail", func(t *testing.T) { + s := TestStruct{ + Field: "", + } + err := Struct(&s) + + assert.NotNil(t, err) + assert.IsType(t, &StructError{}, err) + }) +} + +func TestStructSlice(t *testing.T) { + t.Run("Success", func(t *testing.T) { + s := []TestStruct{ + {Field: "value1"}, + {Field: "value2"}, + } + err := StructSlice(&s) + + assert.Nil(t, err) + }) + + t.Run("Fail on row 1", func(t *testing.T) { + s := []TestStruct{ + {Field: ""}, + {Field: "value2"}, + } + err := StructSlice(&s) + + assert.NotNil(t, err) + assert.IsType(t, &StructError{}, err) + assert.Contains(t, err.Error(), "error in row 1") + }) + + t.Run("Fail on row 2", func(t *testing.T) { + s := []TestStruct{ + {Field: "value1"}, + {Field: ""}, + } + err := StructSlice(&s) + + assert.NotNil(t, err) + assert.IsType(t, &StructError{}, err) + assert.Contains(t, err.Error(), "error in row 2") + }) +} + +func TestStructError(t *testing.T) { + t.Run("Error method", func(t *testing.T) { + err := &StructError{ + errs: []error{errors.New("error1"), errors.New("error2")}, + } + assert.Equal(t, "error1, error2", err.Error()) + }) + + t.Run("Errors method", func(t *testing.T) { + err := &StructError{ + errs: []error{errors.New("error1"), errors.New("error2")}, + } + assert.Equal(t, []string{"error1", "error2"}, err.Errors()) + }) + + t.Run("ErrorsRaw method", func(t *testing.T) { + err := &StructError{ + errs: []error{errors.New("error1"), errors.New("error2")}, + } + assert.Equal( + t, + []error{errors.New("error1"), errors.New("error2")}, + err.ErrorsRaw(), + ) + }) + + t.Run("AddErr method", func(t *testing.T) { + err := &StructError{} + err.AddErr(errors.New("error1")) + err.AddErr(errors.New("error2")) + assert.Equal(t, []string{"error1", "error2"}, err.Errors()) + }) + + t.Run("SetErrs method", func(t *testing.T) { + err := &StructError{} + err.SetErrs([]error{errors.New("error1"), errors.New("error2")}) + assert.Equal(t, []string{"error1", "error2"}, err.Errors()) + }) + + t.Run("SetErrs method (overwrite)", func(t *testing.T) { + err := &StructError{ + errs: []error{errors.New("error0")}, + } + err.SetErrs([]error{errors.New("error1"), errors.New("error2")}) + assert.Equal(t, []string{"error1", "error2"}, err.Errors()) + }) + + t.Run("HasErrs method (with errors)", func(t *testing.T) { + err := &StructError{ + errs: []error{errors.New("error1"), errors.New("error2")}, + } + assert.True(t, err.HasErrs()) + }) + + t.Run("HasErrs method (without errors)", func(t *testing.T) { + err := &StructError{} + assert.False(t, err.HasErrs()) + }) +}