Skip to content

Add full schema pointer matching capability to schema.ValidationErrorMatch() #23

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Nov 12, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Add full schema pointer matching capability to schema.ValidationError…
…Match()

When a schema validation error is related to one of the logic inversion keywords ("not", "oneOf"), the schema pointer
path provided in the schema validation package's validation results data does not extend past that keyword.

Capability has now been added for the error to be matched against the full JSON pointers under the problematic keywords.

In combination with structuring the schemas in a manner that puts the logic inversion keywords at the lowest possible
level, this makes it possible to fully determine the cause of a validation failure.
  • Loading branch information
per1234 committed Nov 12, 2020
commit cd34d32fcb966b024f568e88cf8d634bb45eb426
64 changes: 59 additions & 5 deletions check/checkdata/schema/parsevalidationresult.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package schema

import (
"encoding/json"
"fmt"
"regexp"

"github.com/arduino/go-paths-helper"
Expand Down Expand Up @@ -76,10 +77,10 @@ func validationErrorMatch(
logrus.Tracef("Checking instance pointer: %s match with regexp: %s", validationError.InstancePtr, instancePointerRegexp)
if instancePointerRegexp.MatchString(validationError.InstancePtr) {
logrus.Tracef("Matched!")
logrus.Tracef("Checking schema pointer: %s match with regexp: %s", validationError.SchemaPtr, schemaPointerRegexp)
if schemaPointerRegexp.MatchString(validationError.SchemaPtr) {
matchedSchemaPointer := validationErrorSchemaPointerMatch(schemaPointerRegexp, validationError, schemasPath)
if matchedSchemaPointer != "" {
logrus.Tracef("Matched!")
if validationErrorSchemaPointerValueMatch(schemaPointerValueRegexp, validationError, schemasPath) {
if validationErrorSchemaPointerValueMatch(schemaPointerValueRegexp, validationError.SchemaURL, matchedSchemaPointer, schemasPath) {
logrus.Tracef("Matched!")
logrus.Tracef("Checking failure context: %v match with regexp: %s", validationError.Context, failureContextRegexp)
if validationErrorContextMatch(failureContextRegexp, validationError) {
Expand Down Expand Up @@ -107,14 +108,67 @@ func validationErrorMatch(
return false
}

// validationErrorSchemaPointerMatch matches the JSON schema pointer related to the validation failure against a regular expression.
func validationErrorSchemaPointerMatch(
schemaPointerRegexp *regexp.Regexp,
validationError *jsonschema.ValidationError,
schemasPath *paths.Path,
) string {
logrus.Tracef("Checking schema pointer: %s match with regexp: %s", validationError.SchemaPtr, schemaPointerRegexp)
if schemaPointerRegexp.MatchString(validationError.SchemaPtr) {
return validationError.SchemaPtr
}

// The schema validator does not provide full pointer past logic inversion keywords to the lowest level keywords related to the validation error cause.
// Therefore the sub-keywords must be checked for matches in order to be able to interpret the exact cause of the failure.
if regexp.MustCompile("(/not)|(/oneOf)$").MatchString(validationError.SchemaPtr) {
return validationErrorSchemaSubPointerMatch(schemaPointerRegexp, validationError.SchemaPtr, validationErrorSchemaPointerValue(validationError, schemasPath))
}

return ""
}

// validationErrorSchemaSubPointerMatch recursively checks JSON pointers of all keywords under the parent pointer for match against a regular expression.
// The matching JSON pointer is returned.
func validationErrorSchemaSubPointerMatch(schemaPointerRegexp *regexp.Regexp, parentPointer string, pointerValueObject interface{}) string {
// Recurse through iterable objects.
switch assertedObject := pointerValueObject.(type) {
case []interface{}:
for index, element := range assertedObject {
// Append index to JSON pointer and check for match.
matchingPointer := validationErrorSchemaSubPointerMatch(schemaPointerRegexp, fmt.Sprintf("%s/%d", parentPointer, index), element)
if matchingPointer != "" {
return matchingPointer
}
}
case map[string]interface{}:
for key := range assertedObject {
// Append key to JSON pointer and check for match.
matchingPointer := validationErrorSchemaSubPointerMatch(schemaPointerRegexp, parentPointer+"/"+key, assertedObject[key])
if matchingPointer != "" {
return matchingPointer
}
// TODO: Follow references. For now, the schema code must be written so that the problematic keywords are after the reference.
}
}

// pointerValueObject is not further iterable. Check for match against the parent JSON pointer.
logrus.Tracef("Checking schema pointer: %s match with regexp: %s", parentPointer, schemaPointerRegexp)
if schemaPointerRegexp.MatchString(parentPointer) {
return parentPointer
}
return ""
}

// validationErrorSchemaPointerValueMatch marshalls the data in the schema at the given JSON pointer and returns whether
// it matches against the given regular expression.
func validationErrorSchemaPointerValueMatch(
schemaPointerValueRegexp *regexp.Regexp,
validationError *jsonschema.ValidationError,
schemaURL,
schemaPointer string,
schemasPath *paths.Path,
) bool {
marshalledSchemaPointerValue, err := json.Marshal(schemaPointerValue(validationError, schemasPath))
marshalledSchemaPointerValue, err := json.Marshal(schemaPointerValue(schemaURL, schemaPointer, schemasPath))
logrus.Tracef("Checking schema pointer value: %s match with regexp: %s", marshalledSchemaPointerValue, schemaPointerValueRegexp)
if err != nil {
panic(err)
Expand Down
13 changes: 9 additions & 4 deletions check/checkdata/schema/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ func logValidationError(validationError *jsonschema.ValidationError, schemasPath
logrus.Tracef("Instance pointer: %v", validationError.InstancePtr)
logrus.Tracef("Schema URL: %s", validationError.SchemaURL)
logrus.Tracef("Schema pointer: %s", validationError.SchemaPtr)
logrus.Tracef("Schema pointer value: %v", schemaPointerValue(validationError, schemasPath))
logrus.Tracef("Schema pointer value: %v", validationErrorSchemaPointerValue(validationError, schemasPath))
logrus.Tracef("Failure context: %v", validationError.Context)
logrus.Tracef("Failure context type: %T", validationError.Context)

Expand All @@ -165,10 +165,15 @@ func logValidationError(validationError *jsonschema.ValidationError, schemasPath
}
}

// validationErrorSchemaPointerValue returns the object identified by the validation error's schema JSON pointer.
func validationErrorSchemaPointerValue(validationError *jsonschema.ValidationError, schemasPath *paths.Path) interface{} {
return schemaPointerValue(validationError.SchemaURL, validationError.SchemaPtr, schemasPath)
}

// schemaPointerValue returns the object identified by the given JSON pointer from the schema file.
func schemaPointerValue(validationError *jsonschema.ValidationError, schemasPath *paths.Path) interface{} {
schemaPath := schemasPath.Join(path.Base(validationError.SchemaURL))
return jsonPointerValue(validationError.SchemaPtr, schemaPath)
func schemaPointerValue(schemaURL, schemaPointer string, schemasPath *paths.Path) interface{} {
schemaPath := schemasPath.Join(path.Base(schemaURL))
return jsonPointerValue(schemaPointer, schemaPath)
}

// jsonPointerValue returns the object identified by the given JSON pointer from the JSON file.
Expand Down
6 changes: 2 additions & 4 deletions check/checkdata/schema/schema_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package schema

import (
"fmt"
"os"
"regexp"
"runtime"
Expand Down Expand Up @@ -155,14 +154,13 @@ func Test_pathURI(t *testing.T) {
}
}

func Test_schemaPointerValue(t *testing.T) {
func Test_validationErrorSchemaPointerValue(t *testing.T) {
validationError := jsonschema.ValidationError{
SchemaURL: "https://raw.githubusercontent.com/arduino/arduino-check/main/check/checkdata/schema/testdata/referenced-schema-1.json",
SchemaPtr: "#/definitions/patternObject/pattern",
}

schemaPointerValueInterface := schemaPointerValue(&validationError, schemasPath)
fmt.Printf("%T", schemaPointerValueInterface)
schemaPointerValueInterface := validationErrorSchemaPointerValue(&validationError, schemasPath)
schemaPointerValue, ok := schemaPointerValueInterface.(string)
require.True(t, ok)
require.Equal(t, "^[a-z]+$", schemaPointerValue)
Expand Down