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
- schemaPath := schemasPath .Join (schemaFilename )
44
- schemaURI := pathURI (schemaPath )
45
- compiledSchema , err := schemaLoader .Compile (gojsonschema .NewReferenceLoader (schemaURI ))
46
+ compiledSchema , err := compile (compiler , schemaFilename , schemasPath )
46
47
if err != nil {
47
48
panic (err )
48
49
}
50
+
49
51
return compiledSchema
50
52
}
51
53
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
57
63
}
58
64
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
+ }
59
75
return result
60
76
}
61
77
62
78
// 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 )
65
81
}
66
82
67
83
// 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 )
70
86
}
71
87
72
88
// 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 {
76
99
// No error, so nothing to match
100
+ logrus .Trace ("Schema validation passed. No match is possible." )
77
101
return false
78
102
}
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
86
124
}
125
+ defer schemaFile .Close ()
87
126
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 )
89
169
}
90
170
91
171
// pathURI returns the URI representation of the path argument.
92
172
func pathURI (path * paths.Path ) string {
93
173
absolutePath , err := path .Abs ()
94
174
if err != nil {
95
- panic (err . Error () )
175
+ panic (err )
96
176
}
97
177
uriFriendlyPath := filepath .ToSlash (absolutePath .String ())
98
178
// 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 {
106
186
107
187
return pathURI .String ()
108
188
}
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
+ }
0 commit comments