json_schema 0.0.9 → 0.0.10
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/commands/validate_schema.rb +11 -7
- data/lib/json_schema/parser.rb +37 -28
- data/lib/json_schema/reference_expander.rb +10 -1
- data/lib/json_schema/schema.rb +12 -0
- data/lib/json_schema/validator.rb +5 -4
- data/schemas/heroku-hyper-schema.json +96 -0
- data/test/json_schema/parser_test.rb +15 -6
- data/test/json_schema/reference_expander_test.rb +30 -0
- data/test/json_schema/validator_test.rb +4 -2
- metadata +3 -2
@@ -48,8 +48,8 @@ module Commands
|
|
48
48
|
if valid
|
49
49
|
@messages += ["#{data_file} is valid."]
|
50
50
|
else
|
51
|
-
errors = ["Invalid."] +
|
52
|
-
|
51
|
+
@errors = ["#{data_file}: Invalid."] +
|
52
|
+
map_schema_errors(data_file, errors)
|
53
53
|
end
|
54
54
|
end
|
55
55
|
|
@@ -78,21 +78,25 @@ module Commands
|
|
78
78
|
true
|
79
79
|
end
|
80
80
|
|
81
|
+
# Builds a JSON Reference + message like "/path/to/file#/path/to/data".
|
82
|
+
def map_schema_errors(file, errors)
|
83
|
+
errors.map { |e| "#{file}#{e.schema.pointer}: #{e.message}" }
|
84
|
+
end
|
85
|
+
|
81
86
|
def parse(file)
|
82
87
|
return nil if !check_file(file)
|
83
88
|
|
84
89
|
parser = JsonSchema::Parser.new
|
85
90
|
if !(schema = parser.parse(JSON.parse(File.read(file))))
|
86
|
-
@errors = ["Schema is invalid."] +
|
87
|
-
|
91
|
+
@errors = ["#{file}: Schema is invalid."] +
|
92
|
+
map_schema_errors(file, parser.errors)
|
88
93
|
return nil
|
89
94
|
end
|
90
95
|
|
91
96
|
expander = JsonSchema::ReferenceExpander.new
|
92
97
|
if !expander.expand(schema, store: @store)
|
93
|
-
@errors = ["Could not expand schema references."] +
|
94
|
-
expander.errors
|
95
|
-
@errors.map! { |e| "#{file}: #{e}" }
|
98
|
+
@errors = ["#{file}: Could not expand schema references."] +
|
99
|
+
map_schema_errors(file, expander.errors)
|
96
100
|
return nil
|
97
101
|
end
|
98
102
|
|
data/lib/json_schema/parser.rb
CHANGED
@@ -24,7 +24,7 @@ module JsonSchema
|
|
24
24
|
# object, the @errors array is an instance-wide accumulator
|
25
25
|
@errors = []
|
26
26
|
|
27
|
-
schema = parse_data(data, parent)
|
27
|
+
schema = parse_data(data, parent, "#")
|
28
28
|
if @errors.count == 0
|
29
29
|
schema
|
30
30
|
else
|
@@ -73,69 +73,75 @@ module JsonSchema
|
|
73
73
|
# an object indicates a schema that will be used to parse any
|
74
74
|
# properties not listed in `properties`
|
75
75
|
if schema.additional_properties.is_a?(Hash)
|
76
|
-
schema.additional_properties =
|
77
|
-
|
76
|
+
schema.additional_properties = parse_data(
|
77
|
+
schema.additional_properties,
|
78
|
+
schema,
|
79
|
+
"additionalProperties"
|
80
|
+
)
|
78
81
|
end
|
79
82
|
# otherwise, leave as boolean
|
80
83
|
end
|
81
84
|
end
|
82
85
|
|
83
86
|
def parse_all_of(schema)
|
84
|
-
if schema.all_of
|
85
|
-
schema.all_of = schema.all_of.
|
87
|
+
if schema.all_of
|
88
|
+
schema.all_of = schema.all_of.each_with_index.
|
89
|
+
map { |s, i| parse_data(s, schema, "allOf/#{i}") }
|
86
90
|
end
|
87
91
|
end
|
88
92
|
|
89
93
|
def parse_any_of(schema)
|
90
|
-
if schema.any_of
|
91
|
-
schema.any_of = schema.any_of.
|
94
|
+
if schema.any_of
|
95
|
+
schema.any_of = schema.any_of.each_with_index.
|
96
|
+
map { |s, i| parse_data(s, schema, "anyOf/#{i}") }
|
92
97
|
end
|
93
98
|
end
|
94
99
|
|
95
100
|
def parse_one_of(schema)
|
96
|
-
if schema.one_of
|
97
|
-
schema.one_of = schema.one_of.
|
101
|
+
if schema.one_of
|
102
|
+
schema.one_of = schema.one_of.each_with_index.
|
103
|
+
map { |s, i| parse_data(s, schema, "oneOf/#{i}") }
|
98
104
|
end
|
99
105
|
end
|
100
106
|
|
101
|
-
def parse_data(data, parent
|
102
|
-
schema = Schema.new
|
103
|
-
|
107
|
+
def parse_data(data, parent, fragment)
|
104
108
|
if !data.is_a?(Hash)
|
105
109
|
# it would be nice to make this message more specific/nicer (at best it
|
106
110
|
# points to the wrong schema)
|
107
111
|
message = %{Expected schema; value was: #{data.inspect}.}
|
108
112
|
@errors << SchemaError.new(parent, message)
|
109
113
|
elsif ref = data["$ref"]
|
114
|
+
schema = Schema.new
|
115
|
+
schema.fragment = fragment
|
116
|
+
schema.parent = parent
|
110
117
|
schema.reference = JsonReference::Reference.new(ref)
|
111
118
|
else
|
112
|
-
schema = parse_schema(data, parent)
|
119
|
+
schema = parse_schema(data, parent, fragment)
|
113
120
|
end
|
114
121
|
|
115
|
-
schema.parent = parent
|
116
122
|
schema
|
117
123
|
end
|
118
124
|
|
119
125
|
def parse_definitions(schema)
|
120
|
-
if schema.definitions
|
126
|
+
if schema.definitions
|
121
127
|
# leave the original data reference intact
|
122
128
|
schema.definitions = schema.definitions.dup
|
123
129
|
schema.definitions.each do |key, definition|
|
124
|
-
subschema = parse_data(definition, schema)
|
130
|
+
subschema = parse_data(definition, schema, "definitions/#{key}")
|
125
131
|
schema.definitions[key] = subschema
|
126
132
|
end
|
127
133
|
end
|
128
134
|
end
|
129
135
|
|
130
136
|
def parse_dependencies(schema)
|
131
|
-
if schema.dependencies
|
137
|
+
if schema.dependencies
|
132
138
|
# leave the original data reference intact
|
133
139
|
schema.dependencies = schema.dependencies.dup
|
134
140
|
schema.dependencies.each do |k, s|
|
135
141
|
# may be Array, String (simple dependencies), or Hash (schema
|
136
142
|
# dependency)
|
137
143
|
if s.is_a?(Hash)
|
138
|
-
schema.dependencies[k] = parse_data(s, schema)
|
144
|
+
schema.dependencies[k] = parse_data(s, schema, "dependencies")
|
139
145
|
elsif s.is_a?(String)
|
140
146
|
# just normalize all simple dependencies to arrays
|
141
147
|
schema.dependencies[k] = [s]
|
@@ -148,17 +154,18 @@ module JsonSchema
|
|
148
154
|
if schema.items
|
149
155
|
# tuple validation: an array of schemas
|
150
156
|
if schema.items.is_a?(Array)
|
151
|
-
schema.items = schema.items.
|
157
|
+
schema.items = schema.items.each_with_index.
|
158
|
+
map { |s, i| parse_data(s, schema, "items/#{i}") }
|
152
159
|
# list validation: a single schema
|
153
160
|
else
|
154
|
-
schema.items = parse_data(schema.items, schema)
|
161
|
+
schema.items = parse_data(schema.items, schema, "items")
|
155
162
|
end
|
156
163
|
end
|
157
164
|
end
|
158
165
|
|
159
166
|
def parse_links(schema)
|
160
167
|
if schema.links
|
161
|
-
schema.links = schema.links.map { |l|
|
168
|
+
schema.links = schema.links.each_with_index.map { |l, i|
|
162
169
|
link = Schema::Link.new
|
163
170
|
link.parent = schema
|
164
171
|
|
@@ -169,7 +176,7 @@ module JsonSchema
|
|
169
176
|
link.title = l["title"]
|
170
177
|
|
171
178
|
if l["schema"]
|
172
|
-
link.schema = parse_data(l["schema"], schema)
|
179
|
+
link.schema = parse_data(l["schema"], schema, "links/#{i}/schema")
|
173
180
|
end
|
174
181
|
|
175
182
|
link
|
@@ -186,17 +193,17 @@ module JsonSchema
|
|
186
193
|
end
|
187
194
|
|
188
195
|
def parse_not(schema)
|
189
|
-
if schema.not
|
190
|
-
schema.not = parse_data(schema.not, schema)
|
196
|
+
if schema.not
|
197
|
+
schema.not = parse_data(schema.not, schema, "not")
|
191
198
|
end
|
192
199
|
end
|
193
200
|
|
194
201
|
def parse_pattern_properties(schema)
|
195
|
-
if schema.pattern_properties
|
202
|
+
if schema.pattern_properties
|
196
203
|
# leave the original data reference intact
|
197
204
|
properties = schema.pattern_properties.dup
|
198
205
|
properties = properties.map do |k, s|
|
199
|
-
[Regexp.new(k), parse_data(s, schema)]
|
206
|
+
[Regexp.new(k), parse_data(s, schema, "patternProperties/#{k}")]
|
200
207
|
end
|
201
208
|
schema.pattern_properties = Hash[*properties.flatten]
|
202
209
|
end
|
@@ -207,14 +214,16 @@ module JsonSchema
|
|
207
214
|
schema.properties = schema.properties.dup
|
208
215
|
if schema.properties && schema.properties.is_a?(Hash)
|
209
216
|
schema.properties.each do |key, definition|
|
210
|
-
subschema = parse_data(definition, schema)
|
217
|
+
subschema = parse_data(definition, schema, "properties/#{key}")
|
211
218
|
schema.properties[key] = subschema
|
212
219
|
end
|
213
220
|
end
|
214
221
|
end
|
215
222
|
|
216
|
-
def parse_schema(data, parent
|
223
|
+
def parse_schema(data, parent, fragment)
|
217
224
|
schema = Schema.new
|
225
|
+
schema.fragment = fragment
|
226
|
+
schema.parent = parent
|
218
227
|
|
219
228
|
schema.data = data
|
220
229
|
schema.id = validate_type(schema, [String], "id")
|
@@ -52,11 +52,20 @@ module JsonSchema
|
|
52
52
|
return false unless success
|
53
53
|
end
|
54
54
|
|
55
|
-
# copy new schema into existing one while preserving parent
|
55
|
+
# copy new schema into existing one while preserving parent, fragment,
|
56
|
+
# and reference
|
56
57
|
parent = ref_schema.parent
|
57
58
|
ref_schema.copy_from(new_schema)
|
58
59
|
ref_schema.parent = parent
|
59
60
|
|
61
|
+
# correct all parent references to point back to ref_schema instead of
|
62
|
+
# new_schema
|
63
|
+
if ref_schema.original?
|
64
|
+
schema_children(ref_schema).each do |schema|
|
65
|
+
schema.parent = ref_schema
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
60
69
|
true
|
61
70
|
end
|
62
71
|
|
data/lib/json_schema/schema.rb
CHANGED
@@ -19,6 +19,10 @@ module JsonSchema
|
|
19
19
|
@clones = Set.new
|
20
20
|
end
|
21
21
|
|
22
|
+
# Fragment of a JSON Pointer that can help us build a pointer back to this
|
23
|
+
# schema for debugging.
|
24
|
+
attr_accessor :fragment
|
25
|
+
|
22
26
|
# Rather than a normal schema, the node may be a JSON Reference. In this
|
23
27
|
# case, no other attributes will be filled in except for #parent.
|
24
28
|
attr_accessor :reference
|
@@ -255,6 +259,14 @@ module JsonSchema
|
|
255
259
|
!clones.include?(self)
|
256
260
|
end
|
257
261
|
|
262
|
+
def pointer
|
263
|
+
if parent
|
264
|
+
parent.pointer + "/" + fragment
|
265
|
+
else
|
266
|
+
fragment
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
258
270
|
def validate(data)
|
259
271
|
validator = Validator.new(self)
|
260
272
|
valid = validator.validate(data)
|
@@ -123,8 +123,10 @@ module JsonSchema
|
|
123
123
|
valid = schema.any_of.any? do |subschema|
|
124
124
|
validate_data(subschema, data, [])
|
125
125
|
end
|
126
|
-
|
127
|
-
|
126
|
+
if !valid
|
127
|
+
message = %{Data did not match any subschema of "anyOf" condition.}
|
128
|
+
errors << SchemaError.new(schema, message)
|
129
|
+
end
|
128
130
|
valid
|
129
131
|
end
|
130
132
|
|
@@ -361,7 +363,6 @@ module JsonSchema
|
|
361
363
|
return true if schema.properties.empty?
|
362
364
|
valid = true
|
363
365
|
schema.properties.each do |key, subschema|
|
364
|
-
|
365
366
|
if value = data[key]
|
366
367
|
valid = strict_and valid, validate_data(subschema, value, errors)
|
367
368
|
end
|
@@ -374,7 +375,7 @@ module JsonSchema
|
|
374
375
|
if (missing = required - data.keys).empty?
|
375
376
|
true
|
376
377
|
else
|
377
|
-
message = %{Missing required keys in object
|
378
|
+
message = %{Missing required keys "#{missing.sort.join(", ")}" in object; keys are "#{data.keys.sort.join(", ")}".}
|
378
379
|
errors << SchemaError.new(schema, message)
|
379
380
|
false
|
380
381
|
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
{
|
2
|
+
"$schema": "http://json-schema.org/draft-04/heroku-hyper-schema#",
|
3
|
+
"id": "http://json-schema.org/draft-04/heroku-hyper-schema#",
|
4
|
+
"title": "Heroku JSON Hyper-Schema",
|
5
|
+
"allOf": [
|
6
|
+
{
|
7
|
+
"$ref": "http://json-schema.org/draft-04/hyper-schema#"
|
8
|
+
}
|
9
|
+
],
|
10
|
+
"definitions": {
|
11
|
+
"entity": {
|
12
|
+
"anyOf": [
|
13
|
+
{
|
14
|
+
"$ref": "#/definitions/entityWithPatternProperties"
|
15
|
+
},
|
16
|
+
{
|
17
|
+
"$ref": "#/definitions/entityWithProperties"
|
18
|
+
}
|
19
|
+
]
|
20
|
+
},
|
21
|
+
"entityWithPatternProperties": {
|
22
|
+
"required": [
|
23
|
+
"definitions",
|
24
|
+
"description",
|
25
|
+
"id",
|
26
|
+
"links",
|
27
|
+
"patternProperties",
|
28
|
+
"title",
|
29
|
+
"type"
|
30
|
+
]
|
31
|
+
},
|
32
|
+
"entityWithProperties": {
|
33
|
+
"properties": {
|
34
|
+
"definitions": {
|
35
|
+
"additionalProperties": {
|
36
|
+
"$ref": "#/definitions/property"
|
37
|
+
},
|
38
|
+
"properties": {
|
39
|
+
"identity": {
|
40
|
+
"$ref": "#/definitions/identity"
|
41
|
+
}
|
42
|
+
},
|
43
|
+
"required": ["identity"]
|
44
|
+
}
|
45
|
+
},
|
46
|
+
"required": [
|
47
|
+
"definitions",
|
48
|
+
"description",
|
49
|
+
"id",
|
50
|
+
"links",
|
51
|
+
"properties",
|
52
|
+
"title",
|
53
|
+
"type"
|
54
|
+
]
|
55
|
+
},
|
56
|
+
"identity": {
|
57
|
+
"additionalProperties": false,
|
58
|
+
"properties": {
|
59
|
+
"anyOf": {
|
60
|
+
"additionalProperties": {
|
61
|
+
"$ref": "#/definitions/ref"
|
62
|
+
}
|
63
|
+
}
|
64
|
+
},
|
65
|
+
"required": ["anyOf"]
|
66
|
+
},
|
67
|
+
"property": {
|
68
|
+
"required": ["description", "type"]
|
69
|
+
},
|
70
|
+
"ref": {
|
71
|
+
"required": ["$ref"]
|
72
|
+
}
|
73
|
+
},
|
74
|
+
"properties": {
|
75
|
+
"definitions": {
|
76
|
+
"additionalProperties": {
|
77
|
+
"$ref": "#/definitions/entity"
|
78
|
+
}
|
79
|
+
},
|
80
|
+
"properties": {
|
81
|
+
"additionalProperties": {
|
82
|
+
"$ref": "#/definitions/ref"
|
83
|
+
}
|
84
|
+
}
|
85
|
+
},
|
86
|
+
"required": [
|
87
|
+
"$schema",
|
88
|
+
"definitions",
|
89
|
+
"description",
|
90
|
+
"id",
|
91
|
+
"links",
|
92
|
+
"properties",
|
93
|
+
"title",
|
94
|
+
"type"
|
95
|
+
]
|
96
|
+
}
|
@@ -198,37 +198,46 @@ describe JsonSchema::Parser do
|
|
198
198
|
assert_equal true, schema.read_only
|
199
199
|
end
|
200
200
|
|
201
|
+
it "builds appropriate JSON Pointers" do
|
202
|
+
schema = parse.definitions["app"].definitions["name"]
|
203
|
+
assert_equal "#/definitions/app/definitions/name", schema.pointer
|
204
|
+
end
|
205
|
+
|
201
206
|
it "errors on non-string ids" do
|
202
207
|
schema_sample["id"] = 4
|
203
208
|
refute parse
|
204
|
-
assert_includes
|
209
|
+
assert_includes errors,
|
210
|
+
%{Expected "id" to be of type "string"; value was: 4.}
|
205
211
|
end
|
206
212
|
|
207
213
|
it "errors on non-string titles" do
|
208
214
|
schema_sample["title"] = 4
|
209
215
|
refute parse
|
210
|
-
assert_includes
|
216
|
+
assert_includes errors,
|
217
|
+
%{Expected "title" to be of type "string"; value was: 4.}
|
211
218
|
end
|
212
219
|
|
213
220
|
it "errors on non-string descriptions" do
|
214
221
|
schema_sample["description"] = 4
|
215
222
|
refute parse
|
216
|
-
assert_includes
|
223
|
+
assert_includes errors,
|
224
|
+
%{Expected "description" to be of type "string"; value was: 4.}
|
217
225
|
end
|
218
226
|
|
219
227
|
it "errors on non-array and non-string types" do
|
220
228
|
schema_sample["type"] = 4
|
221
229
|
refute parse
|
222
|
-
assert_includes
|
230
|
+
assert_includes errors,
|
231
|
+
%{Expected "type" to be of type "array/string"; value was: 4.}
|
223
232
|
end
|
224
233
|
|
225
234
|
it "errors on unknown types" do
|
226
235
|
schema_sample["type"] = ["float", "double"]
|
227
236
|
refute parse
|
228
|
-
assert_includes
|
237
|
+
assert_includes errors, %{Unknown types: double, float.}
|
229
238
|
end
|
230
239
|
|
231
|
-
def
|
240
|
+
def errors
|
232
241
|
@parser.errors.map { |e| e.message }
|
233
242
|
end
|
234
243
|
|
@@ -138,6 +138,36 @@ describe JsonSchema::ReferenceExpander do
|
|
138
138
|
assert_equal ["object"], schema.type
|
139
139
|
end
|
140
140
|
|
141
|
+
it "builds appropriate JSON Pointers for expanded references" do
|
142
|
+
expand
|
143
|
+
assert_equal [], errors
|
144
|
+
|
145
|
+
# the *referenced* schema should still have a proper pointer
|
146
|
+
schema = @schema.definitions["app"].definitions["name"]
|
147
|
+
assert_equal "#/definitions/app/definitions/name", schema.pointer
|
148
|
+
|
149
|
+
# the *reference* schema should have expanded a pointer
|
150
|
+
schema = @schema.properties["app"].properties["name"]
|
151
|
+
assert_equal "#/properties/app/properties/name", schema.pointer
|
152
|
+
end
|
153
|
+
|
154
|
+
# clones are special in that they retain their original pointer despite where
|
155
|
+
# they've been nested
|
156
|
+
it "builds appropriate JSON Pointers for circular dependencies" do
|
157
|
+
pointer("#/properties").merge!(
|
158
|
+
"app" => { "$ref" => "#" }
|
159
|
+
)
|
160
|
+
expand
|
161
|
+
|
162
|
+
# the first self reference has the standard pointer as expected
|
163
|
+
schema = @schema.properties["app"]
|
164
|
+
assert_equal "#/properties/app", schema.pointer
|
165
|
+
|
166
|
+
# but diving deeper results in the same pointer again
|
167
|
+
schema = schema.properties["app"]
|
168
|
+
assert_equal "#/properties/app", schema.pointer
|
169
|
+
end
|
170
|
+
|
141
171
|
it "errors on a JSON Pointer that can't be resolved" do
|
142
172
|
pointer("#/properties").merge!(
|
143
173
|
"app" => { "$ref" => "#/definitions/nope" }
|
@@ -336,7 +336,8 @@ describe JsonSchema::Validator do
|
|
336
336
|
)
|
337
337
|
data_sample["production"] = true
|
338
338
|
refute validate
|
339
|
-
assert_includes error_messages,
|
339
|
+
assert_includes error_messages,
|
340
|
+
%{Missing required keys "ssl" in object; keys are "name, production".}
|
340
341
|
end
|
341
342
|
|
342
343
|
it "validates schema dependencies" do
|
@@ -396,7 +397,8 @@ describe JsonSchema::Validator do
|
|
396
397
|
)
|
397
398
|
data_sample.delete("name")
|
398
399
|
refute validate
|
399
|
-
assert_includes error_messages,
|
400
|
+
assert_includes error_messages,
|
401
|
+
%{Missing required keys "name" in object; keys are "".}
|
400
402
|
end
|
401
403
|
|
402
404
|
it "validates allOf" do
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: json_schema
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.10
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2014-05-
|
12
|
+
date: 2014-05-22 00:00:00.000000000 Z
|
13
13
|
dependencies: []
|
14
14
|
description:
|
15
15
|
email:
|
@@ -21,6 +21,7 @@ extra_rdoc_files: []
|
|
21
21
|
files:
|
22
22
|
- README.md
|
23
23
|
- bin/validate-schema
|
24
|
+
- schemas/heroku-hyper-schema.json
|
24
25
|
- schemas/hyper-schema.json
|
25
26
|
- schemas/schema.json
|
26
27
|
- lib/commands/validate_schema.rb
|