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 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('.')