autoparse 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -21,11 +21,47 @@ require 'addressable/uri'
21
21
  module AutoParse
22
22
  class Instance
23
23
  def self.uri
24
- return @uri ||= nil
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.define_string_property(property_name, key, schema_data)
58
- define_method(property_name) do
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.define_integer_property(property_name, key, schema_data)
167
- define_method(property_name) do
168
- Integer(self[key] || schema_data['default'])
169
- end
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.define_array_property(property_name, key, schema_data)
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 schema is
228
- # anonymous.
229
- schema = AutoParse.generate(schema_data)
230
- return schema.new(property_value).valid?
231
- end
232
- end
233
-
234
- def self.define_object_property(property_name, key, schema_data)
235
- # TODO finish this up...
236
- if schema_data['$ref']
237
- schema_uri = self.uri + Addressable::URI.parse(schema_data['$ref'])
238
- schema = AutoParse.schemas[schema_uri]
239
- if schema == nil
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.define_any_property(property_name, key, schema_data)
254
- define_method(property_name) do
255
- self[key] || schema_data['default']
256
- end
257
- define_method(property_name + '=') do |value|
258
- self[key] = value
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
- self.class.data['type'] &&
319
- self.class.data['type'] != 'object'
320
- raise TypeError,
321
- "Only schemas of type 'object' are instantiable."
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, property_schema in self.class.properties
402
+ for property_key, schema_class in self.class.properties
348
403
  property_value = self[property_key]
349
- if !self.class.validate_property_value(property_value, property_schema)
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 && property_schema['required'] != true
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
  ##
@@ -17,7 +17,7 @@ unless defined? AutoParse::VERSION
17
17
  module AutoParse
18
18
  module VERSION
19
19
  MAJOR = 0
20
- MINOR = 1
20
+ MINOR = 2
21
21
  TINY = 0
22
22
 
23
23
  STRING = [MAJOR, MINOR, TINY].join('.')