Tutorial - Shared validation rules

What you’ll learn

  1. How to write validation rules that can be shared between multiple endpoints
  2. How to delegate validation of a field to a Granitic component

Prerequisites

  1. Follow the Granitic installation instructions
  2. Read the before you start tutorial
  3. Followed the setting up a test database section of tutorial 6
  4. Either have completed tutorial 7 or open a terminal and run:
cd $GOPATH/src/github.com/graniticio
git clone https://github.com/graniticio/granitic-examples.git
cd $GOPATH/src/github.com/graniticio/granitic-examples/tutorial
./prepare-tutorial.sh 8

Test database

If you didn’t follow tutorial 6, please work through the ‘Setting up a test database‘ section which explains how to run Docker and MySQL with a pre-built test database.

Shared rules

The validation rules we’ve expressed in resource/config/config.json currently look like:

"submitArtistRules": [
  ["Name",             "STR",  "REQ:NAME_MISSING", "TRIM", "STOPALL", "LEN:5-50:NAME_BAD_LENGTH", "BREAK", "REG:^[A-Z]| +$:NAME_BAD_CONTENT"],
  ["FirstYearActive",  "INT",  "RANGE:1700|2100:FIRST_ACTIVE_INVALID"]
]

These rules are specific to submitting an artist, but some rules (like checking to see if an artist exists) are likely to be useful in a number of places. Granitic provides a mechanism for defining rules in a way in which they can be shared. Open resource/components/components.json and add this component:

"sharedRuleManager": {
  "type": "validate.UnparsedRuleManager",
  "Rules": "conf:sharedRules"
}

In the same file, modify the submitArtistValidator component so its definition looks like:

"submitArtistValidator": {
  "type": "validate.RuleValidator",
  "DefaultErrorCode": "INVALID_ARTIST",
  "Rules": "conf:submitArtistRules",
  "RuleManager": "ref:sharedRuleManager"
}

We now need to edit resource/config/config.json to add some shared rules. Add the following:

"sharedRules": {
  "artistExistsRule": ["INT", "EXT:artistExistsChecker"]
}

EXT (short for external) is an operation that delegates validation of a field to another Granitic component, in this case a component named artistExistsChecker that will need to implement the validate.ExternalInt64Validator interface.

We need to alter the existing submitArtistRules in config.json so that they use the shared rule on ID we’re given:

"submitArtistRules": [
  ["Name",             "STR",  "REQ:NAME_MISSING", "TRIM", "STOPALL", "LEN:5-50:NAME_BAD_LENGTH", "BREAK", "REG:^[A-Z]| +$:NAME_BAD_CONTENT"],
  ["FirstYearActive",  "INT",  "RANGE:1700|2100:FIRST_ACTIVE_INVALID"],
  ["RelatedArtists", "SLICE",  "ELEM:artistExistsRule:NO_SUCH_RELATED"]
]

The ELEM is an operation that causes a shared rule to be applied to each element of a slice. We have introduced an new error code NO_SUCH_RELATED, so we’ll need to add that to our serviceErrors in config.json. Add the following:

  ["C", "NO_SUCH_RELATED", "Related artist does not exist"]

Validation component

We now need to build the component that actually performs the database check. Create a new file recordstore/db/validate.go and set its contents to:

package db

import (
  "github.com/graniticio/granitic/rdbms"
  "github.com/graniticio/granitic/logging"
)

type ArtistExistsChecker struct{
  DbClientManager rdbms.RdbmsClientManager
  Log logging.Logger
}

func (aec *ArtistExistsChecker) ValidInt64(id int64) (bool, error) {

  dbc, _ := aec.DbClientManager.Client()

  var count int64

  if _, err := dbc.SelectBindSingleQIdParam("CHECK_ARTIST", "Id", id, &count); err != nil {
    return false, err
  } else {
    return count > 0, nil
  }
}

An we’ll need to add a new query to resource/queries/artist:

ID:CHECK_ARTIST

SELECT
    COUNT(id)
FROM
    artist
WHERE
    id = ${Id}

The last step is to add the following to your components.json file:

"artistExistsChecker": {
  "type": "db.ArtistExistsChecker"
}

Binding database results to a basic type

Previous examples have shown how to bind database results into a struct or slice of structs. In the above

  dbc.SelectBindSingleQIdParam("CHECK_ARTIST", "Id", id, &count)

we are binding the results of the database call to an int64. You may supply a basic type (string, int etc) instead of a struct when your query is guaranteed to return a single row with a single column.

Building and testing

Start your service:

cd $GOPATH/src/granitic-tutorial/recordstore
grnc-bind && go build && ./recordstore -c resource/config

and POST the following JSON to http://localhost:8080/artist

{
  "Name": "Another Artist",
  "RelatedArtists": [-1, 1, 9999]
}

(see the data capture tutorial for instructions on using a browser plugin to do this) and you should get a result like:

{
  "ByField":{
    "RelatedArtists[0]":[
      {
        "Code":"C-NO_SUCH_RELATED",
        "Message":"Related artist does not exist."
      }
    ],
    "RelatedArtists[2]":[
      {
        "Code":"C-NO_SUCH_RELATED",
        "Message":"Related artist does not exist."
      }
    ]
  }
}

Recap

  • Validation rules can be defined globally so they can be re-used by multiple endpoints
  • When validating slices, the validation of each element can be delegated to another validation rule
  • When validating ints, floats and strings your validation rule can delegate to another component, as long as it implements validate.ExternalInt64Validator, validate.ExternalFloat64Validator or validate.ExternalStringValidator
  • Database results can be bound to a basic type as long as your query returns one row with one column.