Skip to content

Commit

Permalink
Allow when= to access value, parent, and root nodes
Browse files Browse the repository at this point in the history
Resolves #733
  • Loading branch information
jtigger committed Sep 10, 2022
1 parent b80f736 commit cfad249
Show file tree
Hide file tree
Showing 10 changed files with 137 additions and 37 deletions.
11 changes: 11 additions & 0 deletions pkg/validations/filetests/when=/can-use-parent-value.tpltest
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
counters:
enabled: true
#@assert/validate ("fail", lambda v: fail("fails")), when=lambda par: par["enabled"]
foo: ""
#@assert/validate ("fail", lambda v: fail("fails")), when=lambda par: not par["enabled"]
bar: ""

+++

ERR:
- "foo" (stdin:4) requires "fail"; fail: fails (by stdin:3)
11 changes: 11 additions & 0 deletions pkg/validations/filetests/when=/can-use-root-value.tpltest
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
enabled: true
counters:
#@assert/validate ("fail", lambda v: fail("fails")), when=lambda root: not root[0]["enabled"]
foo: ""
#@assert/validate ("fail", lambda v: fail("fails")), when=lambda root, document: root[0]["enabled"] and len(root[0]["counters"]) == 2
bar: ""

+++

ERR:
- "bar" (stdin:6) requires "fail"; fail: fails (by stdin:5)
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
#@ load("@ytt:assert", "assert")

#@assert/validate min_len=5, when=lambda v: fail("error while checking when=")
#@assert/validate min_len=5, when=lambda v: fail("error from within lambda")
foo: bar

+++

ERR:
- fail: error while checking when=
Validating "foo": Failure evaluating when=: fail: error from within lambda
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ foo: bar
+++

ERR:
- want when= to be bool, got string
Validating "foo": want when= to be bool, got string
106 changes: 74 additions & 32 deletions pkg/validations/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,34 +57,35 @@ type validationKwargs struct {
//
// When a Node's value is invalid, the errors are collected and returned in a Check.
// Otherwise, returns empty Check and nil error.
func Run(node yamlmeta.Node, threadName string) Check {
func Run(node yamlmeta.Node, threadName string) (Check, error) {
if node == nil {
return Check{}
return Check{}, nil
}

validation := newValidationRun(threadName)
err := yamlmeta.Walk(node, validation)
validation := newValidationRun(threadName, node)
err := yamlmeta.WalkWithParent(node, nil, validation)
if err != nil {
return Check{}
return Check{}, err
}

return validation.chk
return validation.chk, nil
}

type validationRun struct {
thread *starlark.Thread
chk Check
root yamlmeta.Node
}

func newValidationRun(threadName string) *validationRun {
return &validationRun{thread: &starlark.Thread{Name: threadName}, chk: Check{[]error{}}}
func newValidationRun(threadName string, root yamlmeta.Node) *validationRun {
return &validationRun{thread: &starlark.Thread{Name: threadName}, chk: Check{[]error{}}, root: root}
}

// Visit if `node` has validations in its meta.
// Runs the validations, any violations from executing the assertions are collected.
// VisitWithParent if `node` has validations in its meta.
// Runs those validations, collecting any violations
//
// This visitor stores error(violations) in the validationRun and returns nil.
func (a *validationRun) Visit(node yamlmeta.Node) error {
func (a *validationRun) VisitWithParent(node yamlmeta.Node, parent yamlmeta.Node) error {
// get rules in node's meta
validations := Get(node)

Expand All @@ -93,9 +94,12 @@ func (a *validationRun) Visit(node yamlmeta.Node) error {
}
for _, v := range validations {
// possible refactor to check validationRun kwargs prior to validating rules
errs := v.Validate(node, a.thread)
if errs != nil {
a.chk.Violations = append(a.chk.Violations, errs...)
violations, err := v.Validate(node, parent, a.root, a.thread)
if err != nil {
return err
}
if violations != nil {
a.chk.Violations = append(a.chk.Violations, violations...)
}
}

Expand All @@ -107,49 +111,56 @@ func (a *validationRun) Visit(node yamlmeta.Node) error {
//
// Returns an error if the assertion returns False (not-None), or assert.fail()s.
// Otherwise, returns nil.
func (v NodeValidation) Validate(node yamlmeta.Node, thread *starlark.Thread) []error {
key, nodeValue := v.newKeyAndStarlarkValue(node)
func (v NodeValidation) Validate(node yamlmeta.Node, parent yamlmeta.Node, root yamlmeta.Node, thread *starlark.Thread) ([]error, error) {
nodeKey, nodeValue := v.newKeyAndStarlarkValue(node)
_, parentValue := v.newKeyAndStarlarkValue(parent)
_, rootValue := v.newKeyAndStarlarkValue(root)

executeRules, err := v.kwargs.shouldValidate(nodeValue, thread)
executeRules, err := v.kwargs.shouldValidate(nodeValue, parentValue, thread, rootValue)
if err != nil {
return []error{err}
return nil, fmt.Errorf("Validating %s: %s", nodeKey, err)
}
if !executeRules {
return nil
return nil, nil
}

var failures []error
for _, r := range byPriority(v.rules) {
result, err := starlark.Call(thread, r.assertion, starlark.Tuple{nodeValue}, []starlark.Tuple{})
var violations []error
for _, rul := range byPriority(v.rules) {
result, err := starlark.Call(thread, rul.assertion, starlark.Tuple{nodeValue}, []starlark.Tuple{})
if err != nil {
failures = append(failures, fmt.Errorf("%s (%s) requires %q; %s (by %s)", key, node.GetPosition().AsCompactString(), r.msg, err.Error(), v.position.AsCompactString()))
if r.isCritical {
violations = append(violations, fmt.Errorf("%s (%s) requires %q; %s (by %s)", nodeKey, node.GetPosition().AsCompactString(), rul.msg, err.Error(), v.position.AsCompactString()))
if rul.isCritical {
break
}
} else {
if !(result == starlark.True) {
failures = append(failures, fmt.Errorf("%s (%s) requires %q (by %s)", key, node.GetPosition().AsCompactString(), r.msg, v.position.AsCompactString()))
if r.isCritical {
violations = append(violations, fmt.Errorf("%s (%s) requires %q (by %s)", nodeKey, node.GetPosition().AsCompactString(), rul.msg, v.position.AsCompactString()))
if rul.isCritical {
break
}
}
}
}
return failures
return violations, nil
}

// shouldValidate uses validationKwargs and the node's value to run checks on the value. If the value satisfies the checks,
// then the NodeValidation's rules should execute, otherwise the rules will be skipped.
func (v validationKwargs) shouldValidate(value starlark.Value, thread *starlark.Thread) (bool, error) {
func (v validationKwargs) shouldValidate(value starlark.Value, parent starlark.Value, thread *starlark.Thread, root starlark.Value) (bool, error) {
_, valueIsNull := value.(starlark.NoneType)
if valueIsNull && !v.notNull {
return false, nil
}

if v.when != nil && !reflect.ValueOf(v.when).IsNil() {
result, err := starlark.Call(thread, v.when, starlark.Tuple{value}, []starlark.Tuple{})
kwargs, err := v.populateArgs(value, parent, root)
if err != nil {
return false, err
return false, fmt.Errorf("Failed to evaluate when=: %s", err)
}

result, err := starlark.Call(thread, v.when, starlark.Tuple{}, kwargs)
if err != nil {
return false, fmt.Errorf("Failure evaluating when=: %s", err)
}

resultBool, isBool := result.(starlark.Bool)
Expand All @@ -163,6 +174,30 @@ func (v validationKwargs) shouldValidate(value starlark.Value, thread *starlark.
return true, nil
}

func (v validationKwargs) populateArgs(value starlark.Value, parent starlark.Value, root starlark.Value) ([]starlark.Tuple, error) {
kwargs := []starlark.Tuple{}
if whenFunc, isFunc := v.when.(*starlark.Function); isFunc {
for idx := 0; idx < whenFunc.NumParams(); idx++ {
name, _ := whenFunc.Param(idx)
switch name {
case "v", "val", "value":
kwargs = append(kwargs, starlark.Tuple{starlark.String(name), value})
case "p", "par", "parent":
kwargs = append(kwargs, starlark.Tuple{starlark.String(name), parent})
case "r", "root", "d", "doc", "document":
// When validation support is extended beyond Data Values and into documents in general, differentiate
// between "root" and "document":
// - since data values validation occurs on a document, "root" and "document" are synonyms, right now.
// - once validations can be defined in templates, then "root" will be a DocSet.
kwargs = append(kwargs, starlark.Tuple{starlark.String(name), root})
default:
return nil, fmt.Errorf("Unknown argument (%s) expected one of [value, parent, document]", name)
}
}
}
return kwargs, nil
}

func (v validationKwargs) asRules() []rule {
var rules []rule

Expand Down Expand Up @@ -258,8 +293,15 @@ func (v NodeValidation) newKeyAndStarlarkValue(node yamlmeta.Node) (string, star
var key string
var nodeValue starlark.Value
switch typedNode := node.(type) {
case *yamlmeta.DocumentSet, *yamlmeta.Array, *yamlmeta.Map:
panic(fmt.Sprintf("validationRun at %s - not supported on %s at %s", v.position.AsCompactString(), yamlmeta.TypeName(node), node.GetPosition().AsCompactString()))
case *yamlmeta.DocumentSet:
key = yamlmeta.TypeName(typedNode)
nodeValue = yamltemplate.NewGoValueWithYAML(typedNode).AsStarlarkValue()
case *yamlmeta.Array:
key = yamlmeta.TypeName(typedNode)
nodeValue = yamltemplate.NewGoValueWithYAML(typedNode).AsStarlarkValue()
case *yamlmeta.Map:
key = yamlmeta.TypeName(typedNode)
nodeValue = yamltemplate.NewGoValueWithYAML(typedNode).AsStarlarkValue()
case *yamlmeta.Document:
key = yamlmeta.TypeName(typedNode)
nodeValue = yamltemplate.NewGoValueWithYAML(typedNode.Value).AsStarlarkValue()
Expand Down
7 changes: 6 additions & 1 deletion pkg/validations/validations_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,12 @@ func EvalAndValidateTemplate(ft filetests.FileTests) filetests.EvaluateTemplate
return nil, filetests.NewTestErr(err, fmt.Errorf("Failed to process @assert/validate annotations: %s", err))
}

chk := validations.Run(result.(yamlmeta.Node), "template-test")
chk, err := validations.Run(result.(yamlmeta.Node), "template-test")
if err != nil {
err := fmt.Errorf("\n%s", err)
return nil, filetests.NewTestErr(err, fmt.Errorf("Unexpected error (did you include the \"ERR:\" marker in the output?):%v", err))
}
// TODO: proper error handling!
if chk.HasViolations() {
err := fmt.Errorf("\n%s", chk.Error())
return nil, filetests.NewTestErr(err, fmt.Errorf("Unexpected violations (did you include the \"ERR:\" marker in the output?):%v", err))
Expand Down
5 changes: 4 additions & 1 deletion pkg/workspace/library_execution.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ func (ll *LibraryExecution) Values(valuesOverlays []*datavalues.Envelope, schema

if !ll.skipDataValuesValidation {
err = ll.validateValues(values)
if err != nil {
return nil, nil, fmt.Errorf("Validating final data values: %s", err)
}
}
return values, libValues, err
}
Expand All @@ -121,7 +124,7 @@ func (ll *LibraryExecution) validateValues(values *datavalues.Envelope) error {
return err
}

assertCheck := validations.Run(values.Doc, "run-data-values-validations")
assertCheck, err := validations.Run(values.Doc, "run-data-values-validations")
if err != nil {
return err
}
Expand Down
28 changes: 28 additions & 0 deletions pkg/yamlmeta/walk.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,31 @@ func Walk(n Node, v Visitor) error {
}
return nil
}

// VisitorWithParent performs an operation on the given Node while traversing the AST, including a reference to "node"'s
// parent node.
//
// Typically defines the action taken during a WalkWithParent().
type VisitorWithParent interface {
VisitWithParent(Node, Node) error
}

// WalkWithParent traverses the tree starting at `n`, recursively, depth-first, invoking `v` on each node and including
// a reference to "node"s parent node as well.
// if `v` returns non-nil error, the traversal is aborted.
func WalkWithParent(node Node, parent Node, v VisitorWithParent) error {
err := v.VisitWithParent(node, parent)
if err != nil {
return err
}

for _, child := range node.GetValues() {
if childNode, ok := child.(Node); ok {
err = WalkWithParent(childNode, node, v)
if err != nil {
return err
}
}
}
return nil
}

0 comments on commit cfad249

Please sign in to comment.