autoparse 0.1.0 → 0.2.0
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.
- data/CHANGELOG +8 -0
- data/lib/autoparse/instance.rb +233 -177
- data/lib/autoparse/version.rb +1 -1
- data/lib/autoparse.rb +302 -50
- data/spec/autoparse/instance_spec.rb +347 -1
- data/spec/data/chaos.json +22 -0
- data/spec/data/node.json +9 -0
- metadata +6 -4
data/CHANGELOG
CHANGED
@@ -1,3 +1,11 @@
|
|
1
|
+
== 0.2.0
|
2
|
+
|
3
|
+
* added support for union types
|
4
|
+
* added support for recursive references
|
5
|
+
* fixed vestigial code from refactoring extraction
|
6
|
+
* fixed issue with references when schema URI is not supplied
|
7
|
+
* fixed issue with missing gem dependencies
|
8
|
+
|
1
9
|
== 0.1.0
|
2
10
|
|
3
11
|
* initial release
|
data/lib/autoparse/instance.rb
CHANGED
@@ -21,11 +21,47 @@ require 'addressable/uri'
|
|
21
21
|
module AutoParse
|
22
22
|
class Instance
|
23
23
|
def self.uri
|
24
|
-
return @uri ||=
|
24
|
+
return (@uri ||=
|
25
|
+
(@schema_data ? Addressable::URI.parse(@schema_data['id']) : nil)
|
26
|
+
)
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.dereference
|
30
|
+
if @schema_data['$ref']
|
31
|
+
# Dereference the schema if necessary.
|
32
|
+
schema_uri =
|
33
|
+
self.uri + Addressable::URI.parse(@schema_data['$ref'])
|
34
|
+
schema_class = AutoParse.schemas[schema_uri]
|
35
|
+
if schema_class == nil
|
36
|
+
raise ArgumentError,
|
37
|
+
"Could not find schema: #{@schema_data['$ref']}. " +
|
38
|
+
"Referenced schema must be parsed first."
|
39
|
+
else
|
40
|
+
return schema_class
|
41
|
+
end
|
42
|
+
else
|
43
|
+
return self
|
44
|
+
end
|
25
45
|
end
|
26
46
|
|
27
47
|
def self.properties
|
28
|
-
return @properties ||=
|
48
|
+
return @properties ||= (
|
49
|
+
if self.superclass.ancestors.include?(::AutoParse::Instance)
|
50
|
+
self.superclass.properties.dup
|
51
|
+
else
|
52
|
+
{}
|
53
|
+
end
|
54
|
+
)
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.keys
|
58
|
+
return @keys ||= (
|
59
|
+
if self.superclass.ancestors.include?(::AutoParse::Instance)
|
60
|
+
self.superclass.keys.dup
|
61
|
+
else
|
62
|
+
{}
|
63
|
+
end
|
64
|
+
)
|
29
65
|
end
|
30
66
|
|
31
67
|
def self.additional_properties_schema
|
@@ -54,100 +90,12 @@ module AutoParse
|
|
54
90
|
end
|
55
91
|
end
|
56
92
|
|
57
|
-
def self.
|
58
|
-
|
59
|
-
value = self[key] || schema_data['default']
|
60
|
-
if value != nil
|
61
|
-
if schema_data['format'] == 'byte'
|
62
|
-
Base64.decode64(value)
|
63
|
-
elsif schema_data['format'] == 'date-time'
|
64
|
-
Time.parse(value)
|
65
|
-
elsif schema_data['format'] == 'url'
|
66
|
-
Addressable::URI.parse(value)
|
67
|
-
elsif schema_data['format'] =~ /^u?int(32|64)$/
|
68
|
-
value.to_i
|
69
|
-
else
|
70
|
-
value
|
71
|
-
end
|
72
|
-
else
|
73
|
-
nil
|
74
|
-
end
|
75
|
-
end
|
76
|
-
define_method(property_name + '=') do |value|
|
77
|
-
if schema_data['format'] == 'byte'
|
78
|
-
self[key] = Base64.encode64(value)
|
79
|
-
elsif schema_data['format'] == 'date-time'
|
80
|
-
if value.respond_to?(:to_str)
|
81
|
-
value = Time.parse(value.to_str)
|
82
|
-
elsif !value.respond_to?(:xmlschema)
|
83
|
-
raise TypeError,
|
84
|
-
"Could not obtain RFC 3339 timestamp from #{value.class}."
|
85
|
-
end
|
86
|
-
self[key] = value.xmlschema
|
87
|
-
elsif schema_data['format'] == 'url'
|
88
|
-
# This effectively does limited URI validation.
|
89
|
-
self[key] = Addressable::URI.parse(value).to_str
|
90
|
-
elsif schema_data['format'] =~ /^u?int(32|64)$/
|
91
|
-
self[key] = value.to_s
|
92
|
-
elsif value.respond_to?(:to_str)
|
93
|
-
self[key] = value.to_str
|
94
|
-
elsif value.kind_of?(Symbol)
|
95
|
-
self[key] = value.to_s
|
96
|
-
else
|
97
|
-
raise TypeError,
|
98
|
-
"Expected String or Symbol, got #{value.class}."
|
99
|
-
end
|
100
|
-
end
|
101
|
-
end
|
102
|
-
|
103
|
-
def self.define_boolean_property(property_name, key, schema_data)
|
104
|
-
define_method(property_name) do
|
105
|
-
value = self[key] || schema_data['default']
|
106
|
-
case value.to_s.downcase
|
107
|
-
when 'true', 'yes', 'y', 'on', '1'
|
108
|
-
true
|
109
|
-
when 'false', 'no', 'n', 'off', '0'
|
110
|
-
false
|
111
|
-
when 'nil', 'null'
|
112
|
-
nil
|
113
|
-
else
|
114
|
-
raise TypeError,
|
115
|
-
"Expected boolean, got #{value.class}."
|
116
|
-
end
|
117
|
-
end
|
118
|
-
define_method(property_name + '=') do |value|
|
119
|
-
case value.to_s.downcase
|
120
|
-
when 'true', 'yes', 'y', 'on', '1'
|
121
|
-
self[key] = true
|
122
|
-
when 'false', 'no', 'n', 'off', '0'
|
123
|
-
self[key] = false
|
124
|
-
when 'nil', 'null'
|
125
|
-
self[key] = nil
|
126
|
-
else
|
127
|
-
raise TypeError, "Expected boolean, got #{value.class}."
|
128
|
-
end
|
129
|
-
end
|
130
|
-
end
|
131
|
-
|
132
|
-
def self.validate_number_property(property_value, schema_data)
|
133
|
-
return false if !property_value.kind_of?(Numeric)
|
93
|
+
def self.validate_boolean_property(property_value, schema_data)
|
94
|
+
return false if property_value != true && property_value != false
|
134
95
|
# TODO: implement more than type-checking
|
135
96
|
return true
|
136
97
|
end
|
137
98
|
|
138
|
-
def self.define_number_property(property_name, key, schema_data)
|
139
|
-
define_method(property_name) do
|
140
|
-
Float(self[key] || schema_data['default'])
|
141
|
-
end
|
142
|
-
define_method(property_name + '=') do |value|
|
143
|
-
if value == nil
|
144
|
-
self[key] = value
|
145
|
-
else
|
146
|
-
self[key] = Float(value)
|
147
|
-
end
|
148
|
-
end
|
149
|
-
end
|
150
|
-
|
151
99
|
def self.validate_integer_property(property_value, schema_data)
|
152
100
|
return false if !property_value.kind_of?(Integer)
|
153
101
|
if schema_data['minimum'] && schema_data['exclusiveMinimum']
|
@@ -163,17 +111,10 @@ module AutoParse
|
|
163
111
|
return true
|
164
112
|
end
|
165
113
|
|
166
|
-
def self.
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
define_method(property_name + '=') do |value|
|
171
|
-
if value == nil
|
172
|
-
self[key] = value
|
173
|
-
else
|
174
|
-
self[key] = Integer(value)
|
175
|
-
end
|
176
|
-
end
|
114
|
+
def self.validate_number_property(property_value, schema_data)
|
115
|
+
return false if !property_value.kind_of?(Numeric)
|
116
|
+
# TODO: implement more than type-checking
|
117
|
+
return true
|
177
118
|
end
|
178
119
|
|
179
120
|
def self.validate_array_property(property_value, schema_data)
|
@@ -190,73 +131,65 @@ module AutoParse
|
|
190
131
|
return true
|
191
132
|
end
|
192
133
|
|
193
|
-
def self.
|
194
|
-
define_method(property_name) do
|
195
|
-
# The default value of an empty Array obviates a mutator method.
|
196
|
-
value = self[key] || []
|
197
|
-
array = if value != nil && !value.respond_to?(:to_ary)
|
198
|
-
raise TypeError,
|
199
|
-
"Expected Array, got #{value.class}."
|
200
|
-
else
|
201
|
-
value.to_ary
|
202
|
-
end
|
203
|
-
if schema_data['items'] && schema_data['items']['$ref']
|
204
|
-
schema_name = schema_data['items']['$ref']
|
205
|
-
# FIXME: Vestigial bits need to be replaced with a more viable
|
206
|
-
# lookup system.
|
207
|
-
if AutoParse.schemas[schema_name]
|
208
|
-
schema_class = AutoParse.schemas[schema_name]
|
209
|
-
array.map! do |item|
|
210
|
-
schema_class.new(item)
|
211
|
-
end
|
212
|
-
else
|
213
|
-
raise ArgumentError,
|
214
|
-
"Could not find schema: #{schema_uri}."
|
215
|
-
end
|
216
|
-
end
|
217
|
-
array
|
218
|
-
end
|
219
|
-
end
|
220
|
-
|
221
|
-
def self.validate_object_property(property_value, schema_data, schema=nil)
|
134
|
+
def self.validate_object_property(property_value, schema_data)
|
222
135
|
if property_value.kind_of?(Instance)
|
223
136
|
return property_value.valid?
|
224
|
-
elsif schema != nil && schema.kind_of?(Class)
|
225
|
-
return schema.new(property_value).valid?
|
226
137
|
else
|
227
|
-
# This is highly ineffecient, but hard to avoid given the
|
228
|
-
# anonymous.
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
raise ArgumentError,
|
241
|
-
"Could not find schema: #{schema_data['$ref']} " +
|
242
|
-
"Referenced schema must be parsed first."
|
138
|
+
# This is highly ineffecient, but currently hard to avoid given the
|
139
|
+
# schema is anonymous, making lookups very difficult.
|
140
|
+
if schema_data.has_key?('id')
|
141
|
+
schema = AutoParse.generate(schema_data)
|
142
|
+
else
|
143
|
+
# If the schema has no ID, it inherits the ID from the parent schema,
|
144
|
+
# which should be `self`.
|
145
|
+
schema = AutoParse.generate(schema_data, self.uri)
|
146
|
+
end
|
147
|
+
begin
|
148
|
+
return schema.new(property_value).valid?
|
149
|
+
rescue TypeError, ArgumentError, ::JSON::ParserError
|
150
|
+
return false
|
243
151
|
end
|
244
|
-
else
|
245
|
-
# Anonymous schema
|
246
|
-
schema = AutoParse.generate(schema_data)
|
247
|
-
end
|
248
|
-
define_method(property_name) do
|
249
|
-
schema.new(self[key] || schema_data['default'])
|
250
152
|
end
|
251
153
|
end
|
252
154
|
|
253
|
-
def self.
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
155
|
+
def self.validate_union_property(property_value, schema_data)
|
156
|
+
union = schema_data['type']
|
157
|
+
possible_types = [union].flatten.compact
|
158
|
+
for type in possible_types
|
159
|
+
case type
|
160
|
+
when 'string'
|
161
|
+
return true if self.validate_string_property(
|
162
|
+
property_value, schema_data
|
163
|
+
)
|
164
|
+
when 'boolean'
|
165
|
+
return true if self.validate_boolean_property(
|
166
|
+
property_value, schema_data
|
167
|
+
)
|
168
|
+
when 'integer'
|
169
|
+
return true if self.validate_integer_property(
|
170
|
+
property_value, schema_data
|
171
|
+
)
|
172
|
+
when 'number'
|
173
|
+
return true if self.validate_number_property(
|
174
|
+
property_value, schema_data
|
175
|
+
)
|
176
|
+
when 'array'
|
177
|
+
return true if self.validate_array_property(
|
178
|
+
property_value, schema_data
|
179
|
+
)
|
180
|
+
when 'object'
|
181
|
+
return true if self.validate_object_property(
|
182
|
+
property_value, schema_data
|
183
|
+
)
|
184
|
+
when 'null'
|
185
|
+
return true if property_value.nil?
|
186
|
+
when 'any'
|
187
|
+
return true
|
188
|
+
end
|
259
189
|
end
|
190
|
+
# None of the union types validated.
|
191
|
+
# An empty union will fail to validate anything.
|
192
|
+
return false
|
260
193
|
end
|
261
194
|
|
262
195
|
##
|
@@ -275,7 +208,7 @@ module AutoParse
|
|
275
208
|
schema = AutoParse.schemas[schema_uri]
|
276
209
|
if schema == nil
|
277
210
|
raise ArgumentError,
|
278
|
-
"Could not find schema: #{schema_data['$ref']} " +
|
211
|
+
"Could not find schema: #{schema_data['$ref']}. " +
|
279
212
|
"Referenced schema must be parsed first."
|
280
213
|
end
|
281
214
|
schema_data = schema.data
|
@@ -289,14 +222,14 @@ module AutoParse
|
|
289
222
|
return false unless self.validate_boolean_property(
|
290
223
|
property_value, schema_data
|
291
224
|
)
|
292
|
-
when 'number'
|
293
|
-
return false unless self.validate_number_property(
|
294
|
-
property_value, schema_data
|
295
|
-
)
|
296
225
|
when 'integer'
|
297
226
|
return false unless self.validate_integer_property(
|
298
227
|
property_value, schema_data
|
299
228
|
)
|
229
|
+
when 'number'
|
230
|
+
return false unless self.validate_number_property(
|
231
|
+
property_value, schema_data
|
232
|
+
)
|
300
233
|
when 'array'
|
301
234
|
return false unless self.validate_array_property(
|
302
235
|
property_value, schema_data
|
@@ -305,6 +238,12 @@ module AutoParse
|
|
305
238
|
return false unless self.validate_object_property(
|
306
239
|
property_value, schema_data
|
307
240
|
)
|
241
|
+
when 'null'
|
242
|
+
return false unless property_value.nil?
|
243
|
+
when Array
|
244
|
+
return false unless self.validate_union_property(
|
245
|
+
property_value, schema_data
|
246
|
+
)
|
308
247
|
else
|
309
248
|
# Either type 'any' or we don't know what this is,
|
310
249
|
# default to anything goes. Validation of an 'any' property always
|
@@ -313,12 +252,16 @@ module AutoParse
|
|
313
252
|
return true
|
314
253
|
end
|
315
254
|
|
316
|
-
def initialize(data)
|
317
|
-
if self.class.data
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
255
|
+
def initialize(data={})
|
256
|
+
if (self.class.data || {})['type'] == nil
|
257
|
+
# Type is omitted, default value is any.
|
258
|
+
else
|
259
|
+
type_set = [(self.class.data || {})['type']].flatten.compact
|
260
|
+
if !type_set.include?('object')
|
261
|
+
raise TypeError,
|
262
|
+
"Only schemas of type 'object' are instantiable:\n" +
|
263
|
+
"#{self.class.data.inspect}"
|
264
|
+
end
|
322
265
|
end
|
323
266
|
if data.respond_to?(:to_hash)
|
324
267
|
data = data.to_hash
|
@@ -329,9 +272,121 @@ module AutoParse
|
|
329
272
|
'Unable to parse. ' +
|
330
273
|
'Expected data to respond to either :to_hash or :to_json.'
|
331
274
|
end
|
275
|
+
if data['$ref']
|
276
|
+
raise TypeError,
|
277
|
+
"Cannot instantiate a reference schema. Must be dereferenced first."
|
278
|
+
end
|
332
279
|
@data = data
|
333
280
|
end
|
334
281
|
|
282
|
+
def method_missing(method, *params, &block)
|
283
|
+
schema_data = self.class.data
|
284
|
+
unless schema_data['additionalProperties']
|
285
|
+
# Do nothing special if additionalProperties is not set.
|
286
|
+
super
|
287
|
+
else
|
288
|
+
# We can't modify the method in-place because this affects the call
|
289
|
+
# to super.
|
290
|
+
property_name = method.to_s
|
291
|
+
assignment = false
|
292
|
+
# Property names simply identify the property and thus don't
|
293
|
+
# include the assignment operator.
|
294
|
+
if property_name[-1..-1] == '='
|
295
|
+
assignment = true
|
296
|
+
property_name[-1..-1] = ''
|
297
|
+
end
|
298
|
+
property_key = self.class.keys[property_name]
|
299
|
+
property_schema = self.class.properties[property_key]
|
300
|
+
# TODO: Properly support additionalProperties.
|
301
|
+
if property_key == nil || property_schema == nil
|
302
|
+
# Method not found.
|
303
|
+
return super
|
304
|
+
end
|
305
|
+
# If additionalProperties is simply set to true, no parsing takes
|
306
|
+
# place and all values are treated as 'any'.
|
307
|
+
if assignment
|
308
|
+
new_value = params[0]
|
309
|
+
__set__(property_name, new_value)
|
310
|
+
else
|
311
|
+
__get__(property_name)
|
312
|
+
end
|
313
|
+
end
|
314
|
+
end
|
315
|
+
|
316
|
+
def __get__(property_name)
|
317
|
+
property_key = self.class.keys[property_name]
|
318
|
+
|
319
|
+
schema_class = self.class.properties[property_key]
|
320
|
+
if !schema_class
|
321
|
+
raise TypeError,
|
322
|
+
"Missing property schema for '#{property_key}'."
|
323
|
+
end
|
324
|
+
if schema_class.data['$ref']
|
325
|
+
# Dereference the schema if necessary.
|
326
|
+
schema_class = schema_class.dereference
|
327
|
+
# Avoid this dereference in the future.
|
328
|
+
self.class.properties[property_key] = schema_class
|
329
|
+
end
|
330
|
+
|
331
|
+
value = self[property_key] || schema_class.data['default']
|
332
|
+
|
333
|
+
case schema_class.data['type']
|
334
|
+
when 'string'
|
335
|
+
AutoParse.import_string(value, schema_class)
|
336
|
+
when 'boolean'
|
337
|
+
AutoParse.import_boolean(value, schema_class)
|
338
|
+
when 'integer'
|
339
|
+
AutoParse.import_integer(value, schema_class)
|
340
|
+
when 'number'
|
341
|
+
AutoParse.import_number(value, schema_class)
|
342
|
+
when 'array'
|
343
|
+
AutoParse.import_array(value, schema_class)
|
344
|
+
when 'object'
|
345
|
+
AutoParse.import_object(value, schema_class)
|
346
|
+
when 'null'
|
347
|
+
nil
|
348
|
+
when Array
|
349
|
+
AutoParse.import_union(value, schema_class)
|
350
|
+
else
|
351
|
+
AutoParse.import_any(value, schema_class)
|
352
|
+
end
|
353
|
+
end
|
354
|
+
protected :__get__
|
355
|
+
|
356
|
+
def __set__(property_name, value)
|
357
|
+
property_key = self.class.keys[property_name]
|
358
|
+
|
359
|
+
schema_class = self.class.properties[property_key]
|
360
|
+
if schema_class.data['$ref']
|
361
|
+
# Dereference the schema if necessary.
|
362
|
+
schema_class = schema_class.dereference
|
363
|
+
# Avoid this dereference in the future.
|
364
|
+
self.class.properties[property_key] = schema_class
|
365
|
+
end
|
366
|
+
|
367
|
+
case schema_class.data['type']
|
368
|
+
when 'string'
|
369
|
+
self[property_key] = AutoParse.export_string(value, schema_class)
|
370
|
+
when 'boolean'
|
371
|
+
self[property_key] = AutoParse.export_boolean(value, schema_class)
|
372
|
+
when 'integer'
|
373
|
+
self[property_key] = AutoParse.export_integer(value, schema_class)
|
374
|
+
when 'number'
|
375
|
+
self[property_key] = AutoParse.export_number(value, schema_class)
|
376
|
+
when 'array'
|
377
|
+
self[property_key] = AutoParse.export_array(value, schema_class)
|
378
|
+
when 'object'
|
379
|
+
self[property_key] = AutoParse.export_object(value, schema_class)
|
380
|
+
when 'null'
|
381
|
+
self[property_key] = nil
|
382
|
+
when Array
|
383
|
+
self[property_key] = AutoParse.export_union(value, schema_class)
|
384
|
+
else
|
385
|
+
self[property_key] = AutoParse.export_any(value, schema_class)
|
386
|
+
end
|
387
|
+
end
|
388
|
+
protected :__set__
|
389
|
+
|
335
390
|
def [](key)
|
336
391
|
return @data[key]
|
337
392
|
end
|
@@ -344,12 +399,13 @@ module AutoParse
|
|
344
399
|
# Validates the parsed data against the schema.
|
345
400
|
def valid?
|
346
401
|
unvalidated_fields = @data.keys.dup
|
347
|
-
for property_key,
|
402
|
+
for property_key, schema_class in self.class.properties
|
348
403
|
property_value = self[property_key]
|
349
|
-
if !self.class.validate_property_value(
|
404
|
+
if !self.class.validate_property_value(
|
405
|
+
property_value, schema_class.data)
|
350
406
|
return false
|
351
407
|
end
|
352
|
-
if property_value == nil &&
|
408
|
+
if property_value == nil && schema_class.data['required'] != true
|
353
409
|
# Value was omitted, but not required. Still valid. Skip dependency
|
354
410
|
# checks.
|
355
411
|
next
|
@@ -397,7 +453,7 @@ module AutoParse
|
|
397
453
|
end
|
398
454
|
|
399
455
|
def to_json
|
400
|
-
return JSON.generate(self.to_hash)
|
456
|
+
return ::JSON.generate(self.to_hash)
|
401
457
|
end
|
402
458
|
|
403
459
|
##
|