Tutorial - Shared validation rules
What you’ll learn
- How to write validation rules that can be shared between multiple endpoints
- How to delegate validation of a field to a Granitic component
Prerequisites
- Follow the Granitic installation instructions
- Read the before you start tutorial
- Followed the setting up a test database section of tutorial 6
- Either have completed tutorial 7 or clone the
tutorial repo and navigate to
json/008/recordstore
in your terminal.
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 config/base.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
comp-def/common.json
and add this component:
"sharedRuleManager": {
"type": "validate.UnparsedRuleManager",
"Rules": "$sharedRules"
}
In the same file, modify the submitArtistValidator
component so its definition looks like:
"submitArtistValidator": {
"type": "validate.RuleValidator",
"DefaultErrorCode": "INVALID_ARTIST",
"Rules": "$submitArtistRules",
"RuleManager": "+sharedRuleManager"
}
We now need to edit config/base.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/base.json
so that they use the shared rule we’ve just defined when
checking the set of ‘related artists’ that are provided when creating a new artist:
"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"]
]
ELEM
is an operation that causes a shared rule to be applied to each element of a slice. We have introduced a new error code
NO_SUCH_RELATED
, so we’ll need to add that to our serviceErrors
in config/base.json
. Add the following:
["C", "NO_SUCH_RELATED", "Related artist does not exist"]
Optional exercise
You’ll notice that the configuration file config/base.json
and the component definition file comp-def/common.json
are getting quite complex now. Try refactoring these into multiple files (e.g config/base.json
, config/validation.json
,
config/messages.json
)
Validation component
We now need to build the component that actually performs the database check. Create a new file db/validate.go
and set its contents to:
package db
import (
"github.com/graniticio/granitic/v2/rdbms"
"github.com/graniticio/granitic/v2/logging"
)
type ArtistExistsChecker struct{
DbClientManager rdbms.ClientManager
Log logging.Logger
}
func (aec *ArtistExistsChecker) ValidInt64(id int64) (bool, error) {
dbc, _ := aec.DbClientManager.Client()
var count int64
// Execute a query that counts how many artists in the database share the ID we are checking
// If the count is zero, that artist doesn't exist.
if _, err := dbc.SelectBindSingleQIDParam("CHECK_ARTIST", "ID", id, &count); err != nil {
return false, err
} else {
return count > 0, nil
}
}
And 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 register this new checker as a component by adding the following to your comp-def/common.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 Go code 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 by navigating to the folder where you have yout tutorial project and running:
grnc-bind && go build && ./recordstore
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."
}
]
}
}
Notice that the error data is showing you which field and (for slice types) the index of the problem element.
Recap
- Validation rules can be defined globally so that 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.