Added error operator for custom validation #1259

This commit is contained in:
Mike Farah 2022-06-25 12:46:24 +10:00
parent 98b411f82e
commit 86c2b03630
6 changed files with 122 additions and 0 deletions

View File

@ -0,0 +1,56 @@
# Error
Use this operation to short-circuit expressions. Useful for validation.
{% hint style="warning" %}
Note that versions prior to 4.18 require the 'eval/e' command to be specified. 
`yq e <exp> <file>`
{% endhint %}
## Validate a particular value
Given a sample.yml file of:
```yaml
a: hello
```
then
```bash
yq 'select(.a == "howdy") or error(".a [" + .a + "] is not howdy!")' sample.yml
```
will output
```bash
Error: .a [hello] is not howdy!
```
## Validate the environment variable is a number - invalid
Running
```bash
numberOfCats="please" yq --null-input 'env(numberOfCats) | select(tag == "!!int") or error("numberOfCats is not a number :(")'
```
will output
```bash
Error: numberOfCats is not a number :(
```
## Validate the environment variable is a number - valid
`with` can be a convenient way of encapsulating validation.
Given a sample.yml file of:
```yaml
name: Bob
favouriteAnimal: cat
```
then
```bash
numberOfCats="3" yq '
with(env(numberOfCats); select(tag == "!!int") or error("numberOfCats is not a number :(")) |
.numPets = env(numberOfCats)
' sample.yml
```
will output
```yaml
name: Bob
favouriteAnimal: cat
numPets: 3
```

View File

@ -0,0 +1,3 @@
# Error
Use this operation to short-circuit expressions. Useful for validation.

View File

@ -363,6 +363,8 @@ func initLexer() (*lex.Lexer, error) {
lexer.Add([]byte(`tz`), opToken(tzOpType)) lexer.Add([]byte(`tz`), opToken(tzOpType))
lexer.Add([]byte(`with_dtf`), opToken(withDtFormatOpType)) lexer.Add([]byte(`with_dtf`), opToken(withDtFormatOpType))
lexer.Add([]byte(`error`), opToken(errorOpType))
lexer.Add([]byte(`toyaml\([0-9]+\)`), encodeWithIndent(YamlOutputFormat)) lexer.Add([]byte(`toyaml\([0-9]+\)`), encodeWithIndent(YamlOutputFormat))
lexer.Add([]byte(`to_yaml\([0-9]+\)`), encodeWithIndent(YamlOutputFormat)) lexer.Add([]byte(`to_yaml\([0-9]+\)`), encodeWithIndent(YamlOutputFormat))

View File

@ -91,6 +91,7 @@ var columnOpType = &operationType{Type: "LINE", NumArgs: 0, Precedence: 50, Hand
var collectOpType = &operationType{Type: "COLLECT", NumArgs: 1, Precedence: 50, Handler: collectOperator} var collectOpType = &operationType{Type: "COLLECT", NumArgs: 1, Precedence: 50, Handler: collectOperator}
var mapOpType = &operationType{Type: "MAP", NumArgs: 1, Precedence: 50, Handler: mapOperator} var mapOpType = &operationType{Type: "MAP", NumArgs: 1, Precedence: 50, Handler: mapOperator}
var errorOpType = &operationType{Type: "ERROR", NumArgs: 1, Precedence: 50, Handler: errorOperator}
var pickOpType = &operationType{Type: "PICK", NumArgs: 1, Precedence: 50, Handler: pickOperator} var pickOpType = &operationType{Type: "PICK", NumArgs: 1, Precedence: 50, Handler: pickOperator}
var evalOpType = &operationType{Type: "EVAL", NumArgs: 1, Precedence: 50, Handler: evalOperator} var evalOpType = &operationType{Type: "EVAL", NumArgs: 1, Precedence: 50, Handler: evalOperator}
var mapValuesOpType = &operationType{Type: "MAP_VALUES", NumArgs: 1, Precedence: 50, Handler: mapValuesOperator} var mapValuesOpType = &operationType{Type: "MAP_VALUES", NumArgs: 1, Precedence: 50, Handler: mapValuesOperator}

View File

@ -0,0 +1,20 @@
package yqlib
import (
"fmt"
)
func errorOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
log.Debugf("-- errorOperation")
rhs, err := d.GetMatchingNodes(context.ReadOnlyClone(), expressionNode.RHS)
if err != nil {
return Context{}, err
}
errorMessage := "aborted"
if rhs.MatchingNodes.Len() > 0 {
errorMessage = rhs.MatchingNodes.Front().Value.(*CandidateNode).Node.Value
}
return Context{}, fmt.Errorf(errorMessage)
}

View File

@ -0,0 +1,40 @@
package yqlib
import "testing"
const validationExpression = `
with(env(numberOfCats); select(tag == "!!int") or error("numberOfCats is not a number :(")) |
.numPets = env(numberOfCats)
`
var errorOperatorScenarios = []expressionScenario{
{
description: "Validate a particular value",
document: `a: hello`,
expression: `select(.a == "howdy") or error(".a [" + .a + "] is not howdy!")`,
expectedError: ".a [hello] is not howdy!",
},
{
description: "Validate the environment variable is a number - invalid",
environmentVariables: map[string]string{"numberOfCats": "please"},
expression: `env(numberOfCats) | select(tag == "!!int") or error("numberOfCats is not a number :(")`,
expectedError: "numberOfCats is not a number :(",
},
{
description: "Validate the environment variable is a number - valid",
subdescription: "`with` can be a convenient way of encapsulating validation.",
environmentVariables: map[string]string{"numberOfCats": "3"},
document: "name: Bob\nfavouriteAnimal: cat\n",
expression: validationExpression,
expected: []string{
"D0, P[], (doc)::name: Bob\nfavouriteAnimal: cat\nnumPets: 3\n",
},
},
}
func TestErrorOperatorScenarios(t *testing.T) {
for _, tt := range errorOperatorScenarios {
testScenario(t, &tt)
}
documentOperatorScenarios(t, "error", errorOperatorScenarios)
}