From cd34d32fcb966b024f568e88cf8d634bb45eb426 Mon Sep 17 00:00:00 2001 From: per1234 Date: Wed, 11 Nov 2020 03:28:04 -0800 Subject: [PATCH] Add full schema pointer matching capability to schema.ValidationErrorMatch() 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. --- .../checkdata/schema/parsevalidationresult.go | 64 +++++++++++++++++-- check/checkdata/schema/schema.go | 13 ++-- check/checkdata/schema/schema_test.go | 6 +- 3 files changed, 70 insertions(+), 13 deletions(-) diff --git a/check/checkdata/schema/parsevalidationresult.go b/check/checkdata/schema/parsevalidationresult.go index ea59f97b..f518fcf8 100644 --- a/check/checkdata/schema/parsevalidationresult.go +++ b/check/checkdata/schema/parsevalidationresult.go @@ -17,6 +17,7 @@ package schema import ( "encoding/json" + "fmt" "regexp" "github.com/arduino/go-paths-helper" @@ -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) { @@ -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) diff --git a/check/checkdata/schema/schema.go b/check/checkdata/schema/schema.go index 56af6150..ab2cc8be 100644 --- a/check/checkdata/schema/schema.go +++ b/check/checkdata/schema/schema.go @@ -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) @@ -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. diff --git a/check/checkdata/schema/schema_test.go b/check/checkdata/schema/schema_test.go index 7bf65ea2..42cce8ad 100644 --- a/check/checkdata/schema/schema_test.go +++ b/check/checkdata/schema/schema_test.go @@ -1,7 +1,6 @@ package schema import ( - "fmt" "os" "regexp" "runtime" @@ -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)