From ae17363f8b24b8d9dcd50acecde0a8f69c26e943 Mon Sep 17 00:00:00 2001
From: Matthew Dillon <mrdillon@alaska.edu>
Date: Tue, 13 Oct 2015 15:28:44 -0700
Subject: [PATCH] Model validation, initial cut

Fixes #11.
---
 api/characteristics.go    |  6 ++++
 api/measurements.go       |  6 ++++
 api/species.go            |  6 ++++
 api/strains.go            |  6 ++++
 api/users.go              | 14 ++++----
 models/characteristics.go | 18 ++++++++++
 models/interfaces.go      | 16 ++++++++-
 models/measurements.go    | 18 ++++++++++
 models/species.go         | 18 ++++++++++
 models/strains.go         | 18 ++++++++++
 models/users.go           | 71 +++++++++++++++------------------------
 types/validation-error.go | 17 ++++++++++
 12 files changed, 162 insertions(+), 52 deletions(-)
 create mode 100644 types/validation-error.go

diff --git a/api/characteristics.go b/api/characteristics.go
index 2fa1965..11f2d52 100644
--- a/api/characteristics.go
+++ b/api/characteristics.go
@@ -137,6 +137,9 @@ func (c CharacteristicService) Update(id int64, e *types.Entity, genus string, c
 		if err == errors.ErrCharacteristicNotUpdated {
 			return newJSONError(err, http.StatusBadRequest)
 		}
+		if err, ok := err.(types.ValidationError); ok {
+			return &types.AppError{Error: err, Status: helpers.StatusUnprocessableEntity}
+		}
 		return newJSONError(err, http.StatusInternalServerError)
 	}
 
@@ -176,6 +179,9 @@ func (c CharacteristicService) Create(e *types.Entity, genus string, claims *typ
 	payload.Characteristic.CharacteristicTypeID = id
 
 	if err := models.Create(payload.Characteristic.CharacteristicBase); err != nil {
+		if err, ok := err.(types.ValidationError); ok {
+			return &types.AppError{Error: err, Status: helpers.StatusUnprocessableEntity}
+		}
 		return newJSONError(err, http.StatusInternalServerError)
 	}
 
diff --git a/api/measurements.go b/api/measurements.go
index 205eaa9..53303f4 100644
--- a/api/measurements.go
+++ b/api/measurements.go
@@ -99,6 +99,9 @@ func (m MeasurementService) Update(id int64, e *types.Entity, genus string, clai
 		if err == errors.ErrMeasurementNotUpdated {
 			return newJSONError(err, http.StatusBadRequest)
 		}
+		if err, ok := err.(types.ValidationError); ok {
+			return &types.AppError{Error: err, Status: helpers.StatusUnprocessableEntity}
+		}
 		return newJSONError(err, http.StatusInternalServerError)
 	}
 
@@ -132,6 +135,9 @@ func (m MeasurementService) Create(e *types.Entity, genus string, claims *types.
 	payload.Measurement.UpdatedBy = claims.Sub
 
 	if err := models.Create(payload.Measurement.MeasurementBase); err != nil {
+		if err, ok := err.(types.ValidationError); ok {
+			return &types.AppError{Error: err, Status: helpers.StatusUnprocessableEntity}
+		}
 		return newJSONError(err, http.StatusInternalServerError)
 	}
 
diff --git a/api/species.go b/api/species.go
index 9ecd680..6d1516d 100644
--- a/api/species.go
+++ b/api/species.go
@@ -97,6 +97,9 @@ func (s SpeciesService) Update(id int64, e *types.Entity, genus string, claims *
 		if err == errors.ErrSpeciesNotUpdated {
 			return newJSONError(err, http.StatusBadRequest)
 		}
+		if err, ok := err.(types.ValidationError); ok {
+			return &types.AppError{Error: err, Status: helpers.StatusUnprocessableEntity}
+		}
 		return newJSONError(err, http.StatusInternalServerError)
 	}
 
@@ -133,6 +136,9 @@ func (s SpeciesService) Create(e *types.Entity, genus string, claims *types.Clai
 	payload.Species.SpeciesBase.GenusID = genusID
 
 	if err := models.Create(payload.Species.SpeciesBase); err != nil {
+		if err, ok := err.(types.ValidationError); ok {
+			return &types.AppError{Error: err, Status: helpers.StatusUnprocessableEntity}
+		}
 		return newJSONError(err, http.StatusInternalServerError)
 	}
 
diff --git a/api/strains.go b/api/strains.go
index 4cdbad8..4789eaf 100644
--- a/api/strains.go
+++ b/api/strains.go
@@ -159,6 +159,9 @@ func (s StrainService) Update(id int64, e *types.Entity, genus string, claims *t
 		if err == errors.ErrStrainNotUpdated {
 			return newJSONError(err, http.StatusBadRequest)
 		}
+		if err, ok := err.(types.ValidationError); ok {
+			return &types.AppError{Error: err, Status: helpers.StatusUnprocessableEntity}
+		}
 		return newJSONError(err, http.StatusInternalServerError)
 	}
 
@@ -190,6 +193,9 @@ func (s StrainService) Create(e *types.Entity, genus string, claims *types.Claim
 	payload.Strain.UpdatedBy = claims.Sub
 
 	if err := models.Create(payload.Strain.StrainBase); err != nil {
+		if err, ok := err.(types.ValidationError); ok {
+			return &types.AppError{Error: err, Status: helpers.StatusUnprocessableEntity}
+		}
 		return newJSONError(err, http.StatusInternalServerError)
 	}
 
diff --git a/api/users.go b/api/users.go
index a2591d6..c79c7db 100644
--- a/api/users.go
+++ b/api/users.go
@@ -105,14 +105,13 @@ func (u UserService) Update(id int64, e *types.Entity, dummy string, claims *typ
 	user.Verified = originalUser.Verified
 	user.UpdatedAt = helpers.CurrentTime()
 
-	if err := user.Validate(); err != nil {
-		return &types.AppError{Error: err, Status: helpers.StatusUnprocessableEntity}
-	}
-
 	if err := models.Update(user.UserBase); err != nil {
 		if err == errors.ErrUserNotUpdated {
 			return newJSONError(err, http.StatusBadRequest)
 		}
+		if err, ok := err.(types.ValidationError); ok {
+			return &types.AppError{Error: err, Status: helpers.StatusUnprocessableEntity}
+		}
 		return newJSONError(err, http.StatusInternalServerError)
 	}
 
@@ -124,9 +123,7 @@ func (u UserService) Update(id int64, e *types.Entity, dummy string, claims *typ
 // Create initializes a new user.
 func (u UserService) Create(e *types.Entity, dummy string, claims *types.Claims) *types.AppError {
 	user := (*e).(*payloads.User).User
-	if err := user.Validate(); err != nil {
-		return &types.AppError{Error: err, Status: helpers.StatusUnprocessableEntity}
-	}
+
 	ct := helpers.CurrentTime()
 	user.CreatedAt = ct
 	user.UpdatedAt = ct
@@ -144,6 +141,9 @@ func (u UserService) Create(e *types.Entity, dummy string, claims *types.Claims)
 				return newJSONError(errors.ErrEmailAddressTaken, http.StatusInternalServerError)
 			}
 		}
+		if err, ok := err.(types.ValidationError); ok {
+			return &types.AppError{Error: err, Status: helpers.StatusUnprocessableEntity}
+		}
 		return newJSONError(err, http.StatusInternalServerError)
 	}
 
diff --git a/models/characteristics.go b/models/characteristics.go
index 3be62b6..8d73f84 100644
--- a/models/characteristics.go
+++ b/models/characteristics.go
@@ -38,6 +38,24 @@ func (c *CharacteristicBase) DeleteError() error {
 	return errors.ErrCharacteristicNotDeleted
 }
 
+func (c *CharacteristicBase) validate() types.ValidationError {
+	cv := make(types.ValidationError, 0)
+
+	if c.CharacteristicName == "" {
+		cv["Name"] = []string{helpers.MustProvideAValue}
+	}
+
+	if c.CharacteristicTypeID == 0 {
+		cv["Characteristic Type"] = []string{helpers.MustProvideAValue}
+	}
+
+	if len(cv) > 0 {
+		return cv
+	}
+
+	return nil
+}
+
 // CharacteristicBase is what the DB expects for write operations
 type CharacteristicBase struct {
 	ID                   int64           `json:"id,omitempty"`
diff --git a/models/interfaces.go b/models/interfaces.go
index c0e010b..725dc63 100644
--- a/models/interfaces.go
+++ b/models/interfaces.go
@@ -1,16 +1,24 @@
 package models
 
-import "github.com/thermokarst/bactdb/Godeps/_workspace/src/github.com/jmoiron/modl"
+import (
+	"github.com/thermokarst/bactdb/Godeps/_workspace/src/github.com/jmoiron/modl"
+	"github.com/thermokarst/bactdb/types"
+)
 
 type base interface {
 	PreInsert(modl.SqlExecutor) error
 	PreUpdate(modl.SqlExecutor) error
 	UpdateError() error
 	DeleteError() error
+	validate() types.ValidationError
 }
 
 // Create will create a new DB record of a model.
 func Create(b base) error {
+	if err := b.validate(); err != nil {
+		return err
+	}
+
 	if err := DBH.Insert(b); err != nil {
 		return nil
 	}
@@ -19,6 +27,10 @@ func Create(b base) error {
 
 // Update runs a DB update on a model.
 func Update(b base) error {
+	if err := b.validate(); err != nil {
+		return err
+	}
+
 	count, err := DBH.Update(b)
 	if err != nil {
 		return err
@@ -26,6 +38,7 @@ func Update(b base) error {
 	if count != 1 {
 		return b.UpdateError()
 	}
+
 	return nil
 }
 
@@ -38,5 +51,6 @@ func Delete(b base) error {
 	if count != 1 {
 		return b.DeleteError()
 	}
+
 	return nil
 }
diff --git a/models/measurements.go b/models/measurements.go
index 4fdf733..e234b2c 100644
--- a/models/measurements.go
+++ b/models/measurements.go
@@ -39,6 +39,24 @@ func (m *MeasurementBase) DeleteError() error {
 	return errors.ErrMeasurementNotDeleted
 }
 
+func (m *MeasurementBase) validate() types.ValidationError {
+	mv := make(types.ValidationError, 0)
+
+	if m.StrainID == 0 {
+		mv["Strain"] = []string{helpers.MustProvideAValue}
+	}
+
+	if m.CharacteristicID == 0 {
+		mv["Characteristic"] = []string{helpers.MustProvideAValue}
+	}
+
+	if len(mv) > 0 {
+		return mv
+	}
+
+	return nil
+}
+
 // MeasurementBase is what the DB expects for write operations
 // There are three types of supported measurements: fixed-text, free-text,
 // & numerical. The table has a constraint that will allow at most one
diff --git a/models/species.go b/models/species.go
index 5bec47c..62a5e8a 100644
--- a/models/species.go
+++ b/models/species.go
@@ -39,6 +39,24 @@ func (s *SpeciesBase) DeleteError() error {
 	return errors.ErrSpeciesNotDeleted
 }
 
+func (s *SpeciesBase) validate() types.ValidationError {
+	sv := make(types.ValidationError, 0)
+
+	if s.GenusID == 0 {
+		sv["Genus"] = []string{helpers.MustProvideAValue}
+	}
+
+	if s.SpeciesName == "" {
+		sv["Species"] = []string{helpers.MustProvideAValue}
+	}
+
+	if len(sv) > 0 {
+		return sv
+	}
+
+	return nil
+}
+
 // SpeciesBase is what the DB expects for write operations.
 type SpeciesBase struct {
 	ID                  int64            `db:"id" json:"id"`
diff --git a/models/strains.go b/models/strains.go
index a2389aa..6420878 100644
--- a/models/strains.go
+++ b/models/strains.go
@@ -39,6 +39,24 @@ func (s *StrainBase) DeleteError() error {
 	return errors.ErrStrainNotDeleted
 }
 
+func (s *StrainBase) validate() types.ValidationError {
+	sv := make(types.ValidationError, 0)
+
+	if s.SpeciesID == 0 {
+		sv["Species"] = []string{helpers.MustProvideAValue}
+	}
+
+	if s.StrainName == "" {
+		sv["Name"] = []string{helpers.MustProvideAValue}
+	}
+
+	if len(sv) > 0 {
+		return sv
+	}
+
+	return nil
+}
+
 // StrainBase is what the DB expects for write operations.
 type StrainBase struct {
 	ID                  int64            `db:"id" json:"id"`
diff --git a/models/users.go b/models/users.go
index d9cf9bb..5f18d45 100644
--- a/models/users.go
+++ b/models/users.go
@@ -2,7 +2,6 @@ package models
 
 import (
 	"database/sql"
-	"encoding/json"
 	"regexp"
 
 	"github.com/thermokarst/bactdb/Godeps/_workspace/src/github.com/jmoiron/modl"
@@ -40,6 +39,33 @@ func (u *UserBase) DeleteError() error {
 	return errors.ErrUserNotDeleted
 }
 
+func (u *UserBase) validate() types.ValidationError {
+	uv := make(types.ValidationError, 0)
+
+	if u.Name == "" {
+		uv["Name"] = []string{helpers.MustProvideAValue}
+	}
+
+	if u.Email == "" {
+		uv["Email"] = []string{helpers.MustProvideAValue}
+	}
+
+	regex, _ := regexp.Compile(`(\w[-._\w]*\w@\w[-._\w]*\w\.\w{2,3})`)
+	if u.Email != "" && !regex.MatchString(u.Email) {
+		uv["Email"] = []string{"Must provide a valid email address"}
+	}
+
+	if len(u.Password) < 8 {
+		uv["Password"] = []string{"Password must be at least 8 characters"}
+	}
+
+	if len(uv) > 0 {
+		return uv
+	}
+
+	return nil
+}
+
 // UserBase is what the DB expects to see for write operations.
 type UserBase struct {
 	ID        int64          `json:"id,omitempty"`
@@ -68,17 +94,6 @@ type UserValidation struct {
 	Role     []string `json:"role,omitempty"`
 }
 
-// Error returns the JSON-encoded error response for any validation errors.
-func (uv UserValidation) Error() string {
-	errs, err := json.Marshal(struct {
-		UserValidation `json:"errors"`
-	}{uv})
-	if err != nil {
-		return err.Error()
-	}
-	return string(errs)
-}
-
 // Users are multiple user entities.
 type Users []*User
 
@@ -87,38 +102,6 @@ type UserMeta struct {
 	CanAdd bool `json:"canAdd"`
 }
 
-// Validate validates a user record.
-func (u *User) Validate() error {
-	var uv UserValidation
-	validationError := false
-
-	if u.Name == "" {
-		uv.Name = append(uv.Name, helpers.MustProvideAValue)
-		validationError = true
-	}
-
-	if u.Email == "" {
-		uv.Email = append(uv.Email, helpers.MustProvideAValue)
-		validationError = true
-	}
-
-	regex, _ := regexp.Compile(`(\w[-._\w]*\w@\w[-._\w]*\w\.\w{2,3})`)
-	if u.Email != "" && !regex.MatchString(u.Email) {
-		uv.Email = append(uv.Email, "Must provide a valid email address")
-		validationError = true
-	}
-
-	if len(u.Password) < 8 {
-		uv.Password = append(uv.Password, "Password must be at least 8 characters")
-		validationError = true
-	}
-
-	if validationError {
-		return uv
-	}
-	return nil
-}
-
 // DbAuthenticate authenticates a user.
 // For thermokarst/jwt: authentication callback
 func DbAuthenticate(email string, password string) error {
diff --git a/types/validation-error.go b/types/validation-error.go
new file mode 100644
index 0000000..8ffe2d7
--- /dev/null
+++ b/types/validation-error.go
@@ -0,0 +1,17 @@
+package types
+
+import "encoding/json"
+
+type ValidationError map[string][]string
+
+func (v ValidationError) Error() string {
+	errs, err := json.Marshal(struct {
+		ValidationError `json:"errors"`
+	}{v})
+
+	if err != nil {
+		return err.Error()
+	}
+
+	return string(errs)
+}