Skip to content

Commit 48e82ca

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 48e82ca

File tree

6 files changed

+786
-44
lines changed

6 files changed

+786
-44
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-31
Original file line numberDiff line numberDiff line change
@@ -17,82 +17,162 @@
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.
43-
schemaPath := schemasPath.Join(schemaFilename)
44-
schemaURI := pathURI(schemaPath)
45-
compiledSchema, err := schemaLoader.Compile(gojsonschema.NewReferenceLoader(schemaURI))
46+
compiledSchema, err := compile(compiler, schemaFilename, schemasPath)
4647
if err != nil {
4748
panic(err)
4849
}
50+
4951
return compiledSchema
5052
}
5153

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)
54+
// Validate validates an instance against a JSON schema and returns nil if it was success, or the
55+
// jsonschema.ValidationError object otherwise.
56+
func Validate(instanceObject *properties.Map, schemaObject *jsonschema.Schema, schemasPath *paths.Path) *jsonschema.ValidationError {
57+
// Convert the instance data from the native properties.Map type to the interface type required by the schema
58+
// validation package.
59+
instanceObjectMap := instanceObject.AsMap()
60+
instanceInterface := make(map[string]interface{}, len(instanceObjectMap))
61+
for k, v := range instanceObjectMap {
62+
instanceInterface[k] = v
5763
}
5864

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

6278
// 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)
79+
func RequiredPropertyMissing(propertyName string, validationResult *jsonschema.ValidationError, schemasPath *paths.Path) bool {
80+
return ValidationErrorMatch("#", "/required$", "", "^#/"+propertyName+"$", validationResult, schemasPath)
6581
}
6682

6783
// 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)
84+
func PropertyPatternMismatch(propertyName string, validationResult *jsonschema.ValidationError, schemasPath *paths.Path) bool {
85+
return ValidationErrorMatch("#/"+propertyName, "/pattern$", "", "", validationResult, schemasPath)
7086
}
7187

7288
// 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() {
89+
// See: https://godoc.org/github.com/ory/jsonschema#ValidationError
90+
func ValidationErrorMatch(
91+
instancePointerQuery string,
92+
schemaPointerQuery string,
93+
schemaPointerValueQuery string,
94+
failureContextQuery string,
95+
validationResult *jsonschema.ValidationError,
96+
schemasPath *paths.Path,
97+
) bool {
98+
if validationResult == nil {
7699
// No error, so nothing to match
100+
logrus.Trace("Schema validation passed. No match is possible.")
77101
return false
78102
}
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-
}
103+
104+
instancePointerRegexp := regexp.MustCompile(instancePointerQuery)
105+
schemaPointerRegexp := regexp.MustCompile(schemaPointerQuery)
106+
schemaPointerValueRegexp := regexp.MustCompile(schemaPointerValueQuery)
107+
failureContextRegexp := regexp.MustCompile(failureContextQuery)
108+
109+
return validationErrorMatch(
110+
instancePointerRegexp,
111+
schemaPointerRegexp,
112+
schemaPointerValueRegexp,
113+
failureContextRegexp,
114+
validationResult,
115+
schemasPath)
116+
}
117+
118+
// loadReferencedSchema adds a schema that is referenced by the parent schema to the compiler object.
119+
func loadReferencedSchema(compiler *jsonschema.Compiler, schemaFilename string, schemasPath *paths.Path) error {
120+
schemaPath := schemasPath.Join(schemaFilename)
121+
schemaFile, err := schemaPath.Open()
122+
if err != nil {
123+
return err
86124
}
125+
defer schemaFile.Close()
87126

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

91171
// pathURI returns the URI representation of the path argument.
92172
func pathURI(path *paths.Path) string {
93173
absolutePath, err := path.Abs()
94174
if err != nil {
95-
panic(err.Error())
175+
panic(err)
96176
}
97177
uriFriendlyPath := filepath.ToSlash(absolutePath.String())
98178
// In order to be valid, the path in the URI must start with `/`, but Windows paths do not.
@@ -106,3 +186,129 @@ func pathURI(path *paths.Path) string {
106186

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

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)