diff --git a/controller/controller_test.go b/controller/controller_test.go index 149ad65..62b5d94 100644 --- a/controller/controller_test.go +++ b/controller/controller_test.go @@ -63,8 +63,8 @@ func TestController_RenderPage(t *testing.T) { p.Name = "home" p.Layout = "main" p.Cache.Enabled = false - p.Headers["a"] = "b" - p.Headers["c"] = "d" + p.Headers["A"] = "b" + p.Headers["C"] = "d" p.StatusCode = http.StatusCreated return ctx, rec, ctr, p } @@ -89,7 +89,7 @@ func TestController_RenderPage(t *testing.T) { } // Check the template cache - parsed, err := c.TemplateRenderer.Load("controller", p.Name) + parsed, err := c.TemplateRenderer.Load("page", p.Name) assert.NoError(t, err) // Check that all expected templates were parsed. diff --git a/controller/form.go b/controller/form.go index 4dbb9f9..78680e8 100644 --- a/controller/form.go +++ b/controller/form.go @@ -6,24 +6,29 @@ import ( "github.com/labstack/echo/v4" ) +// FormSubmission represents the state of the submission of a form, not including the form itself type FormSubmission struct { + // IsSubmitted indicates if the form has been submitted IsSubmitted bool + // Errors stores a slice of error message strings keyed by form struct field name Errors map[string][]string } +// Process processes a submission for a form func (f *FormSubmission) Process(ctx echo.Context, form interface{}) error { f.Errors = make(map[string][]string) f.IsSubmitted = true // Validate the form if err := ctx.Validate(form); err != nil { - f.setErrorMessages(form, err) + f.setErrorMessages(err) } return nil } +// HasErrors indicates if the submission has any validation errors func (f FormSubmission) HasErrors() bool { if f.Errors == nil { return false @@ -31,10 +36,12 @@ func (f FormSubmission) HasErrors() bool { return len(f.Errors) > 0 } +// FieldHasErrors indicates if a given field on the form has any validation errors func (f FormSubmission) FieldHasErrors(fieldName string) bool { return len(f.GetFieldErrors(fieldName)) > 0 } +// SetFieldError sets an error message for a given field name func (f *FormSubmission) SetFieldError(fieldName string, message string) { if f.Errors == nil { f.Errors = make(map[string][]string) @@ -42,19 +49,15 @@ func (f *FormSubmission) SetFieldError(fieldName string, message string) { f.Errors[fieldName] = append(f.Errors[fieldName], message) } +// GetFieldErrors gets the errors for a given field name func (f FormSubmission) GetFieldErrors(fieldName string) []string { if f.Errors == nil { return []string{} } - - errors, has := f.Errors[fieldName] - if !has { - return []string{} - } - - return errors + return f.Errors[fieldName] } +// GetFieldStatusClass returns an HTML class based on the status of the field func (f FormSubmission) GetFieldStatusClass(fieldName string) string { if f.IsSubmitted { if f.FieldHasErrors(fieldName) { @@ -65,11 +68,14 @@ func (f FormSubmission) GetFieldStatusClass(fieldName string) string { return "" } +// IsDone indicates if the submission is considered done which is when it has been submitted +// and there are no errors. func (f FormSubmission) IsDone() bool { return f.IsSubmitted && !f.HasErrors() } -func (f *FormSubmission) setErrorMessages(form interface{}, err error) { +// setErrorMessages sets errors messages on the submission for all fields that failed validation +func (f *FormSubmission) setErrorMessages(err error) { // Only this is supported right now ves, ok := err.(validator.ValidationErrors) if !ok { diff --git a/controller/form_test.go b/controller/form_test.go new file mode 100644 index 0000000..1679dd4 --- /dev/null +++ b/controller/form_test.go @@ -0,0 +1,36 @@ +package controller + +import ( + "testing" + + "goweb/tests" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestFormSubmission(t *testing.T) { + type formTest struct { + Name string `validate:"required"` + Email string `validate:"required,email"` + Submission FormSubmission + } + + ctx, _ := tests.NewContext(c.Web, "/") + form := formTest{ + Name: "", + Email: "a@a.com", + } + err := form.Submission.Process(ctx, form) + assert.NoError(t, err) + + assert.True(t, form.Submission.HasErrors()) + assert.True(t, form.Submission.FieldHasErrors("Name")) + assert.False(t, form.Submission.FieldHasErrors("Email")) + require.Len(t, form.Submission.GetFieldErrors("Name"), 1) + assert.Len(t, form.Submission.GetFieldErrors("Email"), 0) + assert.Equal(t, "This field is required.", form.Submission.GetFieldErrors("Name")[0]) + assert.Equal(t, "is-danger", form.Submission.GetFieldStatusClass("Name")) + assert.Equal(t, "is-success", form.Submission.GetFieldStatusClass("Email")) + assert.False(t, form.Submission.IsDone()) +} diff --git a/routes/router.go b/routes/router.go index c568c7f..41e27ad 100644 --- a/routes/router.go +++ b/routes/router.go @@ -8,8 +8,6 @@ import ( "goweb/middleware" "goweb/services" - "github.com/go-playground/validator/v10" - "github.com/gorilla/sessions" "github.com/labstack/echo-contrib/session" @@ -17,19 +15,6 @@ import ( echomw "github.com/labstack/echo/v4/middleware" ) -type Validator struct { - validator *validator.Validate -} - -func (v *Validator) Validate(i interface{}) error { - if err := v.validator.Struct(i); err != nil { - return err - } - return nil -} - -// TODO: This is doing more than building the router - func BuildRouter(c *services.Container) { // Static files with proper cache control // funcmap.File() should be used in templates to append a cache key to the URL in order to break cache @@ -73,9 +58,6 @@ func BuildRouter(c *services.Container) { err := Error{Controller: ctr} c.Web.HTTPErrorHandler = err.Get - // Validator - c.Web.Validator = &Validator{validator: validator.New()} - // Routes navRoutes(c, g, ctr) userRoutes(c, g, ctr) diff --git a/services/container.go b/services/container.go index dd8509e..b8cd7e1 100644 --- a/services/container.go +++ b/services/container.go @@ -21,6 +21,9 @@ import ( // Container contains all services used by the application and provides an easy way to handle dependency // injection including within tests type Container struct { + // Validator stores a validator + Validator *Validator + // Web stores the web framework Web *echo.Echo @@ -53,6 +56,7 @@ type Container struct { func NewContainer() *Container { c := new(Container) c.initConfig() + c.initValidator() c.initWeb() c.initCache() c.initDatabase() @@ -87,6 +91,11 @@ func (c *Container) initConfig() { c.Config = &cfg } +// initValidator initializes the validator +func (c *Container) initValidator() { + c.Validator = NewValidator() +} + // initWeb initializes the web framework func (c *Container) initWeb() { c.Web = echo.New() @@ -98,6 +107,8 @@ func (c *Container) initWeb() { default: c.Web.Logger.SetLevel(log.DEBUG) } + + c.Web.Validator = c.Validator } // initCache initializes the cache diff --git a/services/container_test.go b/services/container_test.go index 8cbe18e..dc875d9 100644 --- a/services/container_test.go +++ b/services/container_test.go @@ -9,6 +9,7 @@ import ( func TestNewContainer(t *testing.T) { assert.NotNil(t, c.Web) assert.NotNil(t, c.Config) + assert.NotNil(t, c.Validator) assert.NotNil(t, c.Cache) assert.NotNil(t, c.Database) assert.NotNil(t, c.ORM) diff --git a/services/validator.go b/services/validator.go new file mode 100644 index 0000000..863976d --- /dev/null +++ b/services/validator.go @@ -0,0 +1,26 @@ +package services + +import ( + "github.com/go-playground/validator/v10" +) + +// Validator provides validation mainly validating structs within the web context +type Validator struct { + // validator stores the underlying validator + validator *validator.Validate +} + +// NewValidator creats a new Validator +func NewValidator() *Validator { + return &Validator{ + validator: validator.New(), + } +} + +// Validate validates a struct +func (v *Validator) Validate(i interface{}) error { + if err := v.validator.Struct(i); err != nil { + return err + } + return nil +} diff --git a/services/validator_test.go b/services/validator_test.go new file mode 100644 index 0000000..3faa2b5 --- /dev/null +++ b/services/validator_test.go @@ -0,0 +1,19 @@ +package services + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestValidator(t *testing.T) { + type example struct { + Value string `validate:"required"` + } + e := example{} + err := c.Validator.Validate(e) + assert.Error(t, err) + e.Value = "a" + err = c.Validator.Validate(e) + assert.NoError(t, err) +}