Skip to content

Commit 99f300a

Browse files
authored
Merge pull request #19 from arduino/per1234/new-schema-package
Switch to JSON schema package that provides machine readable output
2 parents 29d2569 + 2163397 commit 99f300a

14 files changed

+1029
-43
lines changed

Diff for: .prettierignore

+3
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,6 @@
33
.vscode/
44
.vs/
55
.ionide/
6+
7+
# Test files
8+
/check/checkdata/schema/testdata/invalid-schema.json

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+
}

0 commit comments

Comments
 (0)