Skip to content

Commit 3767eb3

Browse files
committed
Switch to JSON schema package that provides machine readable output
The previous JSON schema validation package's output was not sufficient to be able to programmatically interpret validation results.
1 parent 29d2569 commit 3767eb3

File tree

6 files changed

+786
-42
lines changed

6 files changed

+786
-42
lines changed

Diff for: check/checkdata/library.go

+3-4
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import (
1919
"github.com/arduino/arduino-check/project"
2020
"github.com/arduino/arduino-check/project/library/libraryproperties"
2121
"github.com/arduino/go-properties-orderedmap"
22-
"github.com/xeipuuv/gojsonschema"
22+
"github.com/ory/jsonschema/v3"
2323
)
2424

2525
// Initialize gathers the library check data for the specified project.
@@ -47,10 +47,9 @@ func LibraryProperties() *properties.Map {
4747
return libraryProperties
4848
}
4949

50-
var libraryPropertiesSchemaValidationResult *gojsonschema.Result
50+
var libraryPropertiesSchemaValidationResult *jsonschema.ValidationError
5151

5252
// LibraryPropertiesSchemaValidationResult returns the result of validating library.properties against the JSON schema.
53-
// See: https://github.com/xeipuuv/gojsonschema
54-
func LibraryPropertiesSchemaValidationResult() *gojsonschema.Result {
53+
func LibraryPropertiesSchemaValidationResult() *jsonschema.ValidationError {
5554
return libraryPropertiesSchemaValidationResult
5655
}

Diff for: check/checkdata/schema/schema.go

+237-29
Original file line numberDiff line numberDiff line change
@@ -17,82 +17,164 @@
1717
package schema
1818

1919
import (
20+
"encoding/json"
21+
"fmt"
2022
"net/url"
23+
"path"
2124
"path/filepath"
2225
"regexp"
2326

2427
"github.com/arduino/go-paths-helper"
25-
"github.com/xeipuuv/gojsonschema"
28+
"github.com/arduino/go-properties-orderedmap"
29+
"github.com/ory/jsonschema/v3"
30+
"github.com/sirupsen/logrus"
31+
"github.com/xeipuuv/gojsonreference"
2632
)
2733

2834
// Compile compiles the schema files specified by the filename arguments and returns the compiled schema.
29-
func Compile(schemaFilename string, referencedSchemaFilenames []string, schemasPath *paths.Path) *gojsonschema.Schema {
30-
schemaLoader := gojsonschema.NewSchemaLoader()
35+
func Compile(schemaFilename string, referencedSchemaFilenames []string, schemasPath *paths.Path) *jsonschema.Schema {
36+
compiler := jsonschema.NewCompiler()
3137

3238
// Load the referenced schemas.
3339
for _, referencedSchemaFilename := range referencedSchemaFilenames {
34-
referencedSchemaPath := schemasPath.Join(referencedSchemaFilename)
35-
referencedSchemaURI := pathURI(referencedSchemaPath)
36-
err := schemaLoader.AddSchemas(gojsonschema.NewReferenceLoader(referencedSchemaURI))
37-
if err != nil {
40+
if err := loadReferencedSchema(compiler, referencedSchemaFilename, schemasPath); err != nil {
3841
panic(err)
3942
}
4043
}
4144

4245
// Compile the schema.
4346
schemaPath := schemasPath.Join(schemaFilename)
4447
schemaURI := pathURI(schemaPath)
45-
compiledSchema, err := schemaLoader.Compile(gojsonschema.NewReferenceLoader(schemaURI))
48+
compiledSchema, err := compiler.Compile(schemaURI)
4649
if err != nil {
4750
panic(err)
4851
}
52+
4953
return compiledSchema
5054
}
5155

52-
// Validate validates an instance against a JSON schema and returns the gojsonschema.Result object.
53-
func Validate(instanceObject interface{}, schemaObject *gojsonschema.Schema) *gojsonschema.Result {
54-
result, err := schemaObject.Validate(gojsonschema.NewGoLoader(instanceObject))
55-
if err != nil {
56-
panic(err)
56+
// Validate validates an instance against a JSON schema and returns nil if it was success, or the
57+
// jsonschema.ValidationError object otherwise.
58+
func Validate(instanceObject *properties.Map, schemaObject *jsonschema.Schema, schemasPath *paths.Path) *jsonschema.ValidationError {
59+
// Convert the instance data from the native properties.Map type to the interface type required by the schema
60+
// validation package.
61+
instanceObjectMap := instanceObject.AsMap()
62+
instanceInterface := make(map[string]interface{}, len(instanceObjectMap))
63+
for k, v := range instanceObjectMap {
64+
instanceInterface[k] = v
5765
}
5866

67+
validationError := schemaObject.ValidateInterface(instanceInterface)
68+
result, _ := validationError.(*jsonschema.ValidationError)
69+
if result == nil {
70+
logrus.Debug("Schema validation of instance document passed")
71+
72+
} else {
73+
logrus.Debug("Schema validation of instance document failed:")
74+
logValidationError(result, schemasPath)
75+
logrus.Trace("-----------------------------------------------")
76+
}
5977
return result
6078
}
6179

6280
// RequiredPropertyMissing returns whether the given required property is missing from the document.
63-
func RequiredPropertyMissing(propertyName string, validationResult *gojsonschema.Result) bool {
64-
return ValidationErrorMatch("required", "(root)", propertyName+" is required", validationResult)
81+
func RequiredPropertyMissing(propertyName string, validationResult *jsonschema.ValidationError, schemasPath *paths.Path) bool {
82+
return ValidationErrorMatch("#", "/required$", "", "^#/"+propertyName+"$", validationResult, schemasPath)
6583
}
6684

6785
// PropertyPatternMismatch returns whether the given property did not match the regular expression defined in the JSON schema.
68-
func PropertyPatternMismatch(propertyName string, validationResult *gojsonschema.Result) bool {
69-
return ValidationErrorMatch("pattern", propertyName, "", validationResult)
86+
func PropertyPatternMismatch(propertyName string, validationResult *jsonschema.ValidationError, schemasPath *paths.Path) bool {
87+
return ValidationErrorMatch("#/"+propertyName, "/pattern$", "", "", validationResult, schemasPath)
7088
}
7189

7290
// ValidationErrorMatch returns whether the given query matches against the JSON schema validation error.
73-
// See: https://github.com/xeipuuv/gojsonschema#working-with-errors
74-
func ValidationErrorMatch(typeQuery string, fieldQuery string, descriptionQueryRegexp string, validationResult *gojsonschema.Result) bool {
75-
if validationResult.Valid() {
91+
// See: https://godoc.org/github.com/ory/jsonschema#ValidationError
92+
func ValidationErrorMatch(
93+
instancePointerQuery,
94+
schemaPointerQuery,
95+
schemaPointerValueQuery,
96+
failureContextQuery string,
97+
validationResult *jsonschema.ValidationError,
98+
schemasPath *paths.Path,
99+
) bool {
100+
if validationResult == nil {
76101
// No error, so nothing to match
102+
logrus.Trace("Schema validation passed. No match is possible.")
77103
return false
78104
}
79-
for _, validationError := range validationResult.Errors() {
80-
if typeQuery == "" || typeQuery == validationError.Type() {
81-
if fieldQuery == "" || fieldQuery == validationError.Field() {
82-
descriptionQuery := regexp.MustCompile(descriptionQueryRegexp)
83-
return descriptionQuery.MatchString(validationError.Description())
84-
}
85-
}
105+
106+
instancePointerRegexp := regexp.MustCompile(instancePointerQuery)
107+
schemaPointerRegexp := regexp.MustCompile(schemaPointerQuery)
108+
schemaPointerValueRegexp := regexp.MustCompile(schemaPointerValueQuery)
109+
failureContextRegexp := regexp.MustCompile(failureContextQuery)
110+
111+
return validationErrorMatch(
112+
instancePointerRegexp,
113+
schemaPointerRegexp,
114+
schemaPointerValueRegexp,
115+
failureContextRegexp,
116+
validationResult,
117+
schemasPath)
118+
}
119+
120+
// loadReferencedSchema adds a schema that is referenced by the parent schema to the compiler object.
121+
func loadReferencedSchema(compiler *jsonschema.Compiler, schemaFilename string, schemasPath *paths.Path) error {
122+
schemaPath := schemasPath.Join(schemaFilename)
123+
schemaFile, err := schemaPath.Open()
124+
if err != nil {
125+
return err
86126
}
127+
defer schemaFile.Close()
87128

88-
return false
129+
// Get the $id value from the schema to use as the `url` argument for the `compiler.AddResource()` call.
130+
id, err := schemaID(schemaFilename, schemasPath)
131+
if err != nil {
132+
return err
133+
}
134+
135+
return compiler.AddResource(id, schemaFile)
136+
}
137+
138+
// schemaID returns the value of the schema's $id key.
139+
func schemaID(schemaFilename string, schemasPath *paths.Path) (string, error) {
140+
schemaPath := schemasPath.Join(schemaFilename)
141+
schemaInterface := unmarshalJSONFile(schemaPath)
142+
143+
id, ok := schemaInterface.(map[string]interface{})["$id"].(string)
144+
if !ok {
145+
return "", fmt.Errorf("Schema %s is missing an $id keyword", schemaPath)
146+
}
147+
148+
return id, nil
149+
}
150+
151+
// unmarshalJSONFile returns the data from a JSON file.
152+
func unmarshalJSONFile(filePath *paths.Path) interface{} {
153+
fileBuffer, err := filePath.ReadFile()
154+
if err != nil {
155+
panic(err)
156+
}
157+
158+
var dataInterface interface{}
159+
if err := json.Unmarshal(fileBuffer, &dataInterface); err != nil {
160+
panic(err)
161+
}
162+
163+
return dataInterface
164+
}
165+
166+
// compile compiles the parent schema and returns the resulting jsonschema.Schema object.
167+
func compile(compiler *jsonschema.Compiler, schemaFilename string, schemasPath *paths.Path) (*jsonschema.Schema, error) {
168+
schemaPath := schemasPath.Join(schemaFilename)
169+
schemaURI := pathURI(schemaPath)
170+
return compiler.Compile(schemaURI)
89171
}
90172

91173
// pathURI returns the URI representation of the path argument.
92174
func pathURI(path *paths.Path) string {
93175
absolutePath, err := path.Abs()
94176
if err != nil {
95-
panic(err.Error())
177+
panic(err)
96178
}
97179
uriFriendlyPath := filepath.ToSlash(absolutePath.String())
98180
// In order to be valid, the path in the URI must start with `/`, but Windows paths do not.
@@ -106,3 +188,129 @@ func pathURI(path *paths.Path) string {
106188

107189
return pathURI.String()
108190
}
191+
192+
// logValidationError logs the schema validation error data
193+
func logValidationError(validationError *jsonschema.ValidationError, schemasPath *paths.Path) {
194+
logrus.Trace("--------Schema validation failure cause--------")
195+
logrus.Tracef("Error message: %s", validationError.Error())
196+
logrus.Tracef("Instance pointer: %v", validationError.InstancePtr)
197+
logrus.Tracef("Schema URL: %s", validationError.SchemaURL)
198+
logrus.Tracef("Schema pointer: %s", validationError.SchemaPtr)
199+
logrus.Tracef("Schema pointer value: %v", schemaPointerValue(validationError, schemasPath))
200+
logrus.Tracef("Failure context: %v", validationError.Context)
201+
logrus.Tracef("Failure context type: %T", validationError.Context)
202+
203+
// Recursively log all causes.
204+
for _, validationErrorCause := range validationError.Causes {
205+
logValidationError(validationErrorCause, schemasPath)
206+
}
207+
}
208+
209+
// schemaPointerValue returns the object identified by the given JSON pointer from the schema file.
210+
func schemaPointerValue(validationError *jsonschema.ValidationError, schemasPath *paths.Path) interface{} {
211+
schemaPath := schemasPath.Join(path.Base(validationError.SchemaURL))
212+
return jsonPointerValue(validationError.SchemaPtr, schemaPath)
213+
}
214+
215+
// jsonPointerValue returns the object identified by the given JSON pointer from the JSON file.
216+
func jsonPointerValue(jsonPointer string, filePath *paths.Path) interface{} {
217+
jsonReference, err := gojsonreference.NewJsonReference(jsonPointer)
218+
if err != nil {
219+
panic(err)
220+
}
221+
jsonInterface := unmarshalJSONFile(filePath)
222+
jsonPointerValue, _, err := jsonReference.GetPointer().Get(jsonInterface)
223+
if err != nil {
224+
panic(err)
225+
}
226+
return jsonPointerValue
227+
}
228+
229+
func validationErrorMatch(
230+
instancePointerRegexp,
231+
schemaPointerRegexp,
232+
schemaPointerValueRegexp,
233+
failureContextRegexp *regexp.Regexp,
234+
validationError *jsonschema.ValidationError,
235+
schemasPath *paths.Path,
236+
) bool {
237+
logrus.Trace("--------Checking schema validation failure match--------")
238+
logrus.Tracef("Checking instance pointer: %s match with regexp: %s", validationError.InstancePtr, instancePointerRegexp)
239+
if instancePointerRegexp.MatchString(validationError.InstancePtr) {
240+
logrus.Tracef("Matched!")
241+
logrus.Tracef("Checking schema pointer: %s match with regexp: %s", validationError.SchemaPtr, schemaPointerRegexp)
242+
if schemaPointerRegexp.MatchString(validationError.SchemaPtr) {
243+
logrus.Tracef("Matched!")
244+
if validationErrorSchemaPointerValueMatch(schemaPointerValueRegexp, validationError, schemasPath) {
245+
logrus.Tracef("Matched!")
246+
logrus.Tracef("Checking failure context: %v match with regexp: %s", validationError.Context, failureContextRegexp)
247+
if validationErrorContextMatch(failureContextRegexp, validationError) {
248+
logrus.Tracef("Matched!")
249+
return true
250+
}
251+
}
252+
}
253+
}
254+
255+
// Recursively check all causes for a match.
256+
for _, validationErrorCause := range validationError.Causes {
257+
if validationErrorMatch(
258+
instancePointerRegexp,
259+
schemaPointerRegexp,
260+
schemaPointerValueRegexp,
261+
failureContextRegexp,
262+
validationErrorCause,
263+
schemasPath,
264+
) {
265+
return true
266+
}
267+
}
268+
269+
return false
270+
}
271+
272+
// validationErrorSchemaPointerValueMatch marshalls the data in the schema at the given JSON pointer and returns whether
273+
// it matches against the given regular expression.
274+
func validationErrorSchemaPointerValueMatch(
275+
schemaPointerValueRegexp *regexp.Regexp,
276+
validationError *jsonschema.ValidationError,
277+
schemasPath *paths.Path,
278+
) bool {
279+
marshalledSchemaPointerValue, err := json.Marshal(schemaPointerValue(validationError, schemasPath))
280+
logrus.Tracef("Checking schema pointer value: %s match with regexp: %s", marshalledSchemaPointerValue, schemaPointerValueRegexp)
281+
if err != nil {
282+
panic(err)
283+
}
284+
return schemaPointerValueRegexp.Match(marshalledSchemaPointerValue)
285+
}
286+
287+
// validationErrorContextMatch parses the validation error context data and returns whether it matches against the given
288+
// regular expression.
289+
func validationErrorContextMatch(failureContextRegexp *regexp.Regexp, validationError *jsonschema.ValidationError) bool {
290+
// This was added in the github.com/ory/jsonschema fork of github.com/santhosh-tekuri/jsonschema
291+
// It currently only provides context about the `required` keyword.
292+
switch contextObject := validationError.Context.(type) {
293+
case nil:
294+
return failureContextRegexp.MatchString("")
295+
case *jsonschema.ValidationErrorContextRequired:
296+
return validationErrorContextRequiredMatch(failureContextRegexp, contextObject)
297+
default:
298+
logrus.Errorf("Unhandled validation error context type: %T", validationError.Context)
299+
return failureContextRegexp.MatchString("")
300+
}
301+
}
302+
303+
// validationErrorContextRequiredMatch returns whether any of the JSON pointers of missing required properties match
304+
// against the given regular expression.
305+
func validationErrorContextRequiredMatch(
306+
failureContextRegexp *regexp.Regexp,
307+
contextObject *jsonschema.ValidationErrorContextRequired,
308+
) bool {
309+
// See: https://godoc.org/github.com/ory/jsonschema#ValidationErrorContextRequired
310+
for _, requiredPropertyPointer := range contextObject.Missing {
311+
if failureContextRegexp.MatchString(requiredPropertyPointer) {
312+
return true
313+
}
314+
}
315+
return false
316+
}

Diff for: check/checkfunctions/library.go

+4-3
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"github.com/arduino/arduino-check/check/checkdata"
2222
"github.com/arduino/arduino-check/check/checkdata/schema"
2323
"github.com/arduino/arduino-check/check/checkresult"
24+
"github.com/arduino/arduino-check/configuration"
2425
)
2526

2627
// LibraryPropertiesFormat checks for invalid library.properties format.
@@ -37,7 +38,7 @@ func LibraryPropertiesNameFieldMissing() (result checkresult.Type, output string
3738
return checkresult.NotRun, ""
3839
}
3940

40-
if schema.RequiredPropertyMissing("name", checkdata.LibraryPropertiesSchemaValidationResult()) {
41+
if schema.RequiredPropertyMissing("name", checkdata.LibraryPropertiesSchemaValidationResult(), configuration.SchemasPath()) {
4142
return checkresult.Fail, ""
4243
}
4344
return checkresult.Pass, ""
@@ -49,7 +50,7 @@ func LibraryPropertiesNameFieldDisallowedCharacters() (result checkresult.Type,
4950
return checkresult.NotRun, ""
5051
}
5152

52-
if schema.PropertyPatternMismatch("name", checkdata.LibraryPropertiesSchemaValidationResult()) {
53+
if schema.PropertyPatternMismatch("name", checkdata.LibraryPropertiesSchemaValidationResult(), configuration.SchemasPath()) {
5354
return checkresult.Fail, ""
5455
}
5556

@@ -62,7 +63,7 @@ func LibraryPropertiesVersionFieldMissing() (result checkresult.Type, output str
6263
return checkresult.NotRun, ""
6364
}
6465

65-
if schema.RequiredPropertyMissing("version", checkdata.LibraryPropertiesSchemaValidationResult()) {
66+
if schema.RequiredPropertyMissing("version", checkdata.LibraryPropertiesSchemaValidationResult(), configuration.SchemasPath()) {
6667
return checkresult.Fail, ""
6768
}
6869
return checkresult.Pass, ""

Diff for: go.mod

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ require (
66
github.com/arduino/arduino-cli v0.0.0-20200922073731-53e3230c4f71
77
github.com/arduino/go-paths-helper v1.3.2
88
github.com/arduino/go-properties-orderedmap v1.4.0
9+
github.com/ory/jsonschema/v3 v3.0.1
910
github.com/sirupsen/logrus v1.6.0
1011
github.com/stretchr/testify v1.6.1
11-
github.com/xeipuuv/gojsonschema v1.2.0
12+
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415
1213
)

0 commit comments

Comments
 (0)