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.
@@ -48,8 +48,8 @@ module Commands
48
48
  if valid
49
49
  @messages += ["#{data_file} is valid."]
50
50
  else
51
- errors = ["Invalid."] + errors.map { |e| e.message }
52
- @errors += errors.map { |e| "#{data_file}: #{e}" }
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."] + parser.errors.map { |e| e.message }
87
- @errors.map! { |e| "#{file}: #{e}" }
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.map { |e| e.message }
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
 
@@ -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
- parse_data(schema.additional_properties, schema)
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 && schema.all_of.is_a?(Array)
85
- schema.all_of = schema.all_of.map { |s| parse_data(s, schema) }
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 && schema.any_of.is_a?(Array)
91
- schema.any_of = schema.any_of.map { |s| parse_data(s, schema) }
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 && schema.one_of.is_a?(Array)
97
- schema.one_of = schema.one_of.map { |s| parse_data(s, schema) }
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 = nil)
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 && schema.definitions.is_a?(Hash)
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 && schema.dependencies.is_a?(Hash)
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.map { |s| parse_data(s, schema) }
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 && schema.not.is_a?(Hash)
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 && schema.pattern_properties.is_a?(Hash)
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 = nil)
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
 
@@ -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
- message = %{Data did not match any subschema of "anyOf" condition.}
127
- errors << SchemaError.new(schema, message) if !valid
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: #{missing.sort.join(", ")}.}
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 error_messages, %{Expected "id" to be of type "string"; value was: 4.}
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 error_messages, %{Expected "title" to be of type "string"; value was: 4.}
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 error_messages, %{Expected "description" to be of type "string"; value was: 4.}
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 error_messages, %{Expected "type" to be of type "array/string"; value was: 4.}
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 error_messages, %{Unknown types: double, float.}
237
+ assert_includes errors, %{Unknown types: double, float.}
229
238
  end
230
239
 
231
- def error_messages
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, %{Missing required keys in object: ssl.}
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, %{Missing required keys in object: name.}
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.9
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-21 00:00:00.000000000 Z
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