Writing Validations

Model-based validations are a great way of handling data validations within your application. Instead of validating everything on the front-side (everywhere the data object gets touched), you have your validations at the model level. This results in less code to write and less maintenance any time you want to change what data is acceptable within the model. There are three functions that can be called on the connection for model validations and saving:

  • ValidateAndSave: will validate and save the new or existing object.
  • ValidateAndCreate: will validate and save the new object.
  • ValidateAndUpdate: will validate and save the existing object.

We have seen ValidateAndSave before in a couple of places. The cool thing about ValidateAndSave is that it wraps ValidateAndCreate and ValidateAndUpdate. Therefore, if you're not sure if the model you're saving at run-time is new or existing this gives you a shortcut version so that you don't have to write the conditional yourself. However, this comes at the cost of having that conditional. If you're inserting thousands of new rows at once it would be better to use ValidateAndCreate to remove that extra operation. There are likewise three validation operations available to you when defining the model:

  • Validate: will validate the new or existing object. This validation gets called every time.
  • ValidateCreate: will validate the new object.
  • ValidateUpdate: will validate the existing object.

These methods are defined in your model file. So, going back to our previous example our model currently looks something like this:

package models

import (
    "encoding/json"
    "time"
    "github.com/gobuffalo/pop"
    "github.com/gobuffalo/validate"
    "github.com/gobuffalo/validate/validators"
    "github.com/satori/go.uuid"
)

type User struct {
    ID        uuid.UUID `json:"id" db:"id"`
    CreatedAt time.Time `json:"created_at" db:"created_at"`
    UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
    Title     string    `json:"title" db:"title"`
    FirstName string    `json:"first_name" db:"first_name"`
    LastName  string    `json:"last_name" db:"last_name"`
    Bio       string    `json:"bio" db:"bio"`
    Location  string    `json:"location" db:"location"`
}

// String is not required by pop and may be deleted
func (u User) String() string {
    ju, _ := json.Marshal(u)
    return string(ju)
}

// Users is not required by pop and may be deleted
type Users []User

// String is not required by pop and may be deleted
func (u Users) String() string {
    ju, _ := json.Marshal(u)
    return string(ju)
}

// Validate gets run every time you call a "pop.Validate*" (pop.ValidateAndSave, pop.ValidateAndCreate, pop.ValidateAndUpdate) method.
// This method is not required and may be deleted.
func (u *User) Validate(tx *pop.Connection) (*validate.Errors, error) {
    return validate.Validate(
        &validators.StringIsPresent{Field: u.Title, Name: "Title"},
        &validators.StringIsPresent{Field: u.FirstName, Name: "FirstName"},
        &validators.StringIsPresent{Field: u.LastName, Name: "LastName"},
        &validators.StringIsPresent{Field: u.Bio, Name: "Bio"},
    ), nil
}

// ValidateCreate gets run every time you call "pop.ValidateAndCreate" method.
// This method is not required and may be deleted.
func (u *User) ValidateCreate(tx *pop.Connection) (*validate.Errors, error) {
    return validate.NewErrors(), nil
}

// ValidateUpdate gets run every time you call "pop.ValidateAndUpdate" method.
// This method is not required and may be deleted.
func (u *User) ValidateUpdate(tx *pop.Connection) (*validate.Errors, error) {
    return validate.NewErrors(), nil
}

Remember how we added the Location field in the Quick Start? We only added it to the User struct, but not on the Validate function. This would allow us to create records where the Location field is empty. This may be fine for your use case, but we need some value here. Update the Validate function:

func (u *User) Validate(tx *pop.Connection) (*validate.Errors, error) {
    return validate.Validate(
        &validators.StringIsPresent{Field: u.Title, Name: "Title"},
        &validators.StringIsPresent{Field: u.FirstName, Name: "FirstName"},
        &validators.StringIsPresent{Field: u.LastName, Name: "LastName"},
        &validators.StringIsPresent{Field: u.Bio, Name: "Bio"},
        &validators.StringIsPresent{Field: u.Location, Name: "Location"},
    ), nil
}

(There are no model migrations to run for this since we're not actively saving.)

It would be wise to take a look at the underlying library driving validations at https://github.com/markbates/validate but I've outlined some of the pre-packaged validators here. These are available to you out of the box when using pop:

  • EmailIsPresent: for strictly matching email
  • EmailLike: ensures the form of
  • IntArrayIsPresent: Test for integer is in an array
  • IntIsGreaterThan: Test for integer is greater than
  • IntIsLessThan: Test for integer is less than
  • RegexMatch: Test for a regex match
  • StringInclusion: Test if a string is in an array
  • StringIsPresent: Test that the string field has a value
  • StringLengthInRange: Test if a string is within a range
  • StringsMatch: Test if two strings match
  • TimeAfterTime: Test if a given time comes after a tested time
  • TimeIsBeforeTime: Test is a given time comes before a tested time
  • TimeIsPresent: Test if a time field has a value
  • URLIsValid: Test if a URL has a value and is valid
  • UUIDIsValid: Test if a UUID has a value

This group is enough to get you going for most cases. However there may be time where you want to customize your validation. For instance, suppose your business requirements were such that all locations must be in the form of City Name, State. Obviously this isn't a real world example but hopefully it will give you a clue as to how to proceed writing your own validators. These luckily can be placed inline. For example in your User model:

// the validation struct to hold the field name, field value, and any messages
type LocationValidator struct {
    Name    string
    Field   string
    Message string
}

// IsValid performs the validation based on city, state format
func (v *LocationValidator) IsValid(errors *validate.Errors) {
    parts := strings.Split(v.Field, ",")
    if len(parts) != 2 || len(parts[0]) == 0 || len(parts[1]) == 0 {
        if v.Message == "" {
            v.Message = fmt.Sprintf("%s is not a valid location.", v.Name)
        }
        errors.Add(validators.GenerateKey(v.Name), v.Message)
    } else if len(parts) == 2 {
        state := strings.TrimSpace(parts[1])
        // Check that domain is valid
        if len(state) < 2 {
            if v.Message == "" {
                v.Message = fmt.Sprintf("%s does not provide a valid state or state abbreviation.", v.Name)
            }
            errors.Add(validators.GenerateKey(v.Name), v.Message)
        }
    }
}

// Validate gets run every time you call a "pop.Validate*" (pop.ValidateAndSave, pop.ValidateAndCreate, pop.ValidateAndUpdate) method.
// This method is not required and may be deleted.
func (u *User) Validate(tx *pop.Connection) (*validate.Errors, error) {
    return validate.Validate(
        &validators.StringIsPresent{Field: u.Title, Name: "Title"},
        &validators.StringIsPresent{Field: u.FirstName, Name: "FirstName"},
        &validators.StringIsPresent{Field: u.LastName, Name: "LastName"},
        &validators.StringIsPresent{Field: u.Bio, Name: "Bio"},
        &validators.StringIsPresent{Field: u.Location, Name: "Location"},
        &LocationValidator{Field: u.Location, Name: "Location"},
    ), nil
}

Now let's write a test program to ensure it works:

package main

import (
    "log"
    "bitbucket.org/pop-book/models"
    "github.com/gobuffalo/pop"
)

func main() {
    tx, err := pop.Connect("development")
    if err != nil {
        log.Panic(err)
    }

    frank := models.User{Title: "Mr.", FirstName: "Frank", LastName: "Castle", Bio: "USMC, badass.", Location: "nowhere"}
    verrs, err := tx.ValidateAndSave(&frank)
    if verrs.Count() > 0 {
        log.Println(fmt.Sprintf("ERROR WHILE SAVING: %s\n", verrs))
    }
    if err != nil {
        log.Panic(err)
    }
}

Easy enough - now compile and run:

./main
2018/01/09 16:13:05 ERROR WHILE SAVING: Location is not a valid location.

Go into the code, supply a value that's in the format of City, State, and rerun:

package main

import (
    "log"
    "bitbucket.org/pop-book/models"
    "github.com/gobuffalo/pop"
)

func main() {
    tx, err := pop.Connect("development")
    if err != nil {
        log.Panic(err)
    }

    frank := models.User{Title: "Mr.", FirstName: "Frank", LastName: "Castle", Bio: "USMC, badass.", Location: "Hoboken, NJ"}
    verrs, err := tx.ValidateAndSave(&frank)
    if verrs.Count() > 0 {
        log.Println(fmt.Sprintf("ERROR WHILE SAVING: %s\n", verrs))
    }
    if err != nil {
        log.Panic(err)
    }
}

results matching ""

    No results matching ""