Skip to content

Commit

Permalink
validate: Expose hooks to inject custom behavior during traversal (#406)
Browse files Browse the repository at this point in the history
* add ratcheting validator

* add todo

* add more tests

* add SchemaValidation option to customize validators for properties and subfields

* dont override both options with one unset

* call NewValidator for each subindex

makes usage consistent with other places, also makes moving to a new index for validation explicit
  • Loading branch information
alexzielenski authored Jul 14, 2023
1 parent b193308 commit 0653797
Show file tree
Hide file tree
Showing 6 changed files with 44 additions and 22 deletions.
8 changes: 4 additions & 4 deletions pkg/validation/validate/object_validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ func (o *objectValidator) Validate(data interface{}) *Result {
// Cases: properties which are not regular properties and have not been matched by the PatternProperties validator
if o.AdditionalProperties != nil && o.AdditionalProperties.Schema != nil {
// AdditionalProperties as Schema
res.Merge(NewSchemaValidator(o.AdditionalProperties.Schema, o.Root, o.Path+"."+key, o.KnownFormats, o.Options.Options()...).Validate(value))
res.Merge(o.Options.NewValidatorForField(key, o.AdditionalProperties.Schema, o.Root, o.Path+"."+key, o.KnownFormats, o.Options.Options()...).Validate(value))
} else if regularProperty && !(matched || succeededOnce) {
// TODO: this is dead code since regularProperty=false here
res.AddErrors(errors.FailedAllPatternProperties(o.Path, o.In, key))
Expand All @@ -121,7 +121,7 @@ func (o *objectValidator) Validate(data interface{}) *Result {

// Recursively validates each property against its schema
if v, ok := val[pName]; ok {
r := NewSchemaValidator(&pSchema, o.Root, rName, o.KnownFormats, o.Options.Options()...).Validate(v)
r := o.Options.NewValidatorForField(pName, &pSchema, o.Root, rName, o.KnownFormats, o.Options.Options()...).Validate(v)
res.Merge(r)
}
}
Expand All @@ -144,7 +144,7 @@ func (o *objectValidator) Validate(data interface{}) *Result {
if !regularProperty && (matched /*|| succeededOnce*/) {
for _, pName := range patterns {
if v, ok := o.PatternProperties[pName]; ok {
res.Merge(NewSchemaValidator(&v, o.Root, o.Path+"."+key, o.KnownFormats, o.Options.Options()...).Validate(value))
res.Merge(o.Options.NewValidatorForField(key, &v, o.Root, o.Path+"."+key, o.KnownFormats, o.Options.Options()...).Validate(value))
}
}
}
Expand All @@ -163,7 +163,7 @@ func (o *objectValidator) validatePatternProperty(key string, value interface{},
if match, _ := regexp.MatchString(k, key); match {
patterns = append(patterns, k)
matched = true
validator := NewSchemaValidator(&sch, o.Root, o.Path+"."+key, o.KnownFormats, o.Options.Options()...)
validator := o.Options.NewValidatorForField(key, &sch, o.Root, o.Path+"."+key, o.KnownFormats, o.Options.Options()...)

res := validator.Validate(value)
result.Merge(res)
Expand Down
4 changes: 2 additions & 2 deletions pkg/validation/validate/object_validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,15 @@ func itemsFixture() map[string]interface{} {
}
}

func expectAllValid(t *testing.T, ov valueValidator, dataValid, dataInvalid map[string]interface{}) {
func expectAllValid(t *testing.T, ov ValueValidator, dataValid, dataInvalid map[string]interface{}) {
res := ov.Validate(dataValid)
assert.Equal(t, 0, len(res.Errors))

res = ov.Validate(dataInvalid)
assert.Equal(t, 0, len(res.Errors))
}

func expectOnlyInvalid(t *testing.T, ov valueValidator, dataValid, dataInvalid map[string]interface{}) {
func expectOnlyInvalid(t *testing.T, ov ValueValidator, dataValid, dataInvalid map[string]interface{}) {
res := ov.Validate(dataValid)
assert.Equal(t, 0, len(res.Errors))

Expand Down
36 changes: 26 additions & 10 deletions pkg/validation/validate/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ type SchemaValidator struct {
Path string
in string
Schema *spec.Schema
validators []valueValidator
validators []ValueValidator
Root interface{}
KnownFormats strfmt.Registry
Options SchemaValidatorOptions
Expand Down Expand Up @@ -78,7 +78,15 @@ func NewSchemaValidator(schema *spec.Schema, rootSchema interface{}, root string
for _, o := range options {
o(&s.Options)
}
s.validators = []valueValidator{

if s.Options.NewValidatorForIndex == nil {
s.Options.NewValidatorForIndex = s.NewValidatorForIndex
}
if s.Options.NewValidatorForField == nil {
s.Options.NewValidatorForField = s.NewValidatorForField
}

s.validators = []ValueValidator{
s.typeValidator(),
s.schemaPropsValidator(),
s.stringValidator(),
Expand All @@ -91,6 +99,14 @@ func NewSchemaValidator(schema *spec.Schema, rootSchema interface{}, root string
return &s
}

func (s *SchemaValidator) NewValidatorForField(field string, schema *spec.Schema, rootSchema interface{}, root string, formats strfmt.Registry, opts ...Option) ValueValidator {
return NewSchemaValidator(schema, rootSchema, root, formats, opts...)
}

func (s *SchemaValidator) NewValidatorForIndex(index int, schema *spec.Schema, rootSchema interface{}, root string, formats strfmt.Registry, opts ...Option) ValueValidator {
return NewSchemaValidator(schema, rootSchema, root, formats, opts...)
}

// SetPath sets the path for this schema validator
func (s *SchemaValidator) SetPath(path string) {
s.Path = path
Expand Down Expand Up @@ -174,19 +190,19 @@ func (s *SchemaValidator) Validate(data interface{}) *Result {
return result
}

func (s *SchemaValidator) typeValidator() valueValidator {
func (s *SchemaValidator) typeValidator() ValueValidator {
return &typeValidator{Type: s.Schema.Type, Nullable: s.Schema.Nullable, Format: s.Schema.Format, In: s.in, Path: s.Path}
}

func (s *SchemaValidator) commonValidator() valueValidator {
func (s *SchemaValidator) commonValidator() ValueValidator {
return &basicCommonValidator{
Path: s.Path,
In: s.in,
Enum: s.Schema.Enum,
}
}

func (s *SchemaValidator) sliceValidator() valueValidator {
func (s *SchemaValidator) sliceValidator() ValueValidator {
return &schemaSliceValidator{
Path: s.Path,
In: s.in,
Expand All @@ -201,7 +217,7 @@ func (s *SchemaValidator) sliceValidator() valueValidator {
}
}

func (s *SchemaValidator) numberValidator() valueValidator {
func (s *SchemaValidator) numberValidator() ValueValidator {
return &numberValidator{
Path: s.Path,
In: s.in,
Expand All @@ -214,7 +230,7 @@ func (s *SchemaValidator) numberValidator() valueValidator {
}
}

func (s *SchemaValidator) stringValidator() valueValidator {
func (s *SchemaValidator) stringValidator() ValueValidator {
return &stringValidator{
Path: s.Path,
In: s.in,
Expand All @@ -224,7 +240,7 @@ func (s *SchemaValidator) stringValidator() valueValidator {
}
}

func (s *SchemaValidator) formatValidator() valueValidator {
func (s *SchemaValidator) formatValidator() ValueValidator {
return &formatValidator{
Path: s.Path,
In: s.in,
Expand All @@ -233,12 +249,12 @@ func (s *SchemaValidator) formatValidator() valueValidator {
}
}

func (s *SchemaValidator) schemaPropsValidator() valueValidator {
func (s *SchemaValidator) schemaPropsValidator() ValueValidator {
sch := s.Schema
return newSchemaPropsValidator(s.Path, s.in, sch.AllOf, sch.OneOf, sch.AnyOf, sch.Not, sch.Dependencies, s.Root, s.KnownFormats, s.Options.Options()...)
}

func (s *SchemaValidator) objectValidator() valueValidator {
func (s *SchemaValidator) objectValidator() ValueValidator {
return &objectValidator{
Path: s.Path,
In: s.in,
Expand Down
7 changes: 7 additions & 0 deletions pkg/validation/validate/schema_option.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,16 @@

package validate

import (
"k8s.io/kube-openapi/pkg/validation/spec"
"k8s.io/kube-openapi/pkg/validation/strfmt"
)

// SchemaValidatorOptions defines optional rules for schema validation
type SchemaValidatorOptions struct {
validationRulesEnabled bool
NewValidatorForIndex func(index int, schema *spec.Schema, rootSchema interface{}, root string, formats strfmt.Registry, opts ...Option) ValueValidator
NewValidatorForField func(field string, schema *spec.Schema, rootSchema interface{}, root string, formats strfmt.Registry, opts ...Option) ValueValidator
}

// Option sets optional rules for schema validation
Expand Down
7 changes: 3 additions & 4 deletions pkg/validation/validate/slice_validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,8 @@ func (s *schemaSliceValidator) Validate(data interface{}) *Result {
size := val.Len()

if s.Items != nil && s.Items.Schema != nil {
validator := NewSchemaValidator(s.Items.Schema, s.Root, s.Path, s.KnownFormats, s.Options.Options()...)
for i := 0; i < size; i++ {
validator.SetPath(fmt.Sprintf("%s[%d]", s.Path, i))
validator := s.Options.NewValidatorForIndex(i, s.Items.Schema, s.Root, fmt.Sprintf("%s[%d]", s.Path, i), s.KnownFormats, s.Options.Options()...)
value := val.Index(i)
result.Merge(validator.Validate(value.Interface()))
}
Expand All @@ -66,7 +65,7 @@ func (s *schemaSliceValidator) Validate(data interface{}) *Result {
if s.Items != nil && len(s.Items.Schemas) > 0 {
itemsSize = len(s.Items.Schemas)
for i := 0; i < itemsSize; i++ {
validator := NewSchemaValidator(&s.Items.Schemas[i], s.Root, fmt.Sprintf("%s[%d]", s.Path, i), s.KnownFormats, s.Options.Options()...)
validator := s.Options.NewValidatorForIndex(i, &s.Items.Schemas[i], s.Root, fmt.Sprintf("%s[%d]", s.Path, i), s.KnownFormats, s.Options.Options()...)
if val.Len() <= i {
break
}
Expand All @@ -79,7 +78,7 @@ func (s *schemaSliceValidator) Validate(data interface{}) *Result {
}
if s.AdditionalItems.Schema != nil {
for i := itemsSize; i < size-itemsSize+1; i++ {
validator := NewSchemaValidator(s.AdditionalItems.Schema, s.Root, fmt.Sprintf("%s[%d]", s.Path, i), s.KnownFormats, s.Options.Options()...)
validator := s.Options.NewValidatorForIndex(i, s.AdditionalItems.Schema, s.Root, fmt.Sprintf("%s[%d]", s.Path, i), s.KnownFormats, s.Options.Options()...)
result.Merge(validator.Validate(val.Index(i).Interface()))
}
}
Expand Down
4 changes: 2 additions & 2 deletions pkg/validation/validate/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ import (
"k8s.io/kube-openapi/pkg/validation/spec"
)

// valueValidator validates the values it applies to.
type valueValidator interface {
// ValueValidator validates the values it applies to.
type ValueValidator interface {
// SetPath sets the exact path of the validator prior to calling Validate.
// The exact path contains the map keys and array indices to locate the
// value to be validated from the root data element.
Expand Down

0 comments on commit 0653797

Please sign in to comment.