17
17
package schema
18
18
19
19
import (
20
+ "encoding/json"
21
+ "fmt"
20
22
"net/url"
23
+ "path"
21
24
"path/filepath"
22
25
"regexp"
23
26
24
27
"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"
26
32
)
27
33
28
34
// 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 ()
31
37
32
38
// Load the referenced schemas.
33
39
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 {
38
41
panic (err )
39
42
}
40
43
}
41
44
42
45
// Compile the schema.
43
46
schemaPath := schemasPath .Join (schemaFilename )
44
47
schemaURI := pathURI (schemaPath )
45
- compiledSchema , err := schemaLoader .Compile (gojsonschema . NewReferenceLoader ( schemaURI ) )
48
+ compiledSchema , err := compiler .Compile (schemaURI )
46
49
if err != nil {
47
50
panic (err )
48
51
}
52
+
49
53
return compiledSchema
50
54
}
51
55
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
57
65
}
58
66
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
+ }
59
77
return result
60
78
}
61
79
62
80
// 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 )
65
83
}
66
84
67
85
// 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 )
70
88
}
71
89
72
90
// 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 {
76
101
// No error, so nothing to match
102
+ logrus .Trace ("Schema validation passed. No match is possible." )
77
103
return false
78
104
}
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
86
126
}
127
+ defer schemaFile .Close ()
87
128
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 )
89
171
}
90
172
91
173
// pathURI returns the URI representation of the path argument.
92
174
func pathURI (path * paths.Path ) string {
93
175
absolutePath , err := path .Abs ()
94
176
if err != nil {
95
- panic (err . Error () )
177
+ panic (err )
96
178
}
97
179
uriFriendlyPath := filepath .ToSlash (absolutePath .String ())
98
180
// 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 {
106
188
107
189
return pathURI .String ()
108
190
}
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