json_schema 0.0.9 → 0.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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