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/lib/autoparse.rb CHANGED
@@ -1,11 +1,11 @@
1
1
  # Copyright 2010 Google Inc
2
- #
2
+ #
3
3
  # Licensed under the Apache License, Version 2.0 (the "License");
4
4
  # you may not use this file except in compliance with the License.
5
5
  # You may obtain a copy of the License at
6
- #
6
+ #
7
7
  # http://www.apache.org/licenses/LICENSE-2.0
8
- #
8
+ #
9
9
  # Unless required by applicable law or agreed to in writing, software
10
10
  # distributed under the License is distributed on an "AS IS" BASIS,
11
11
  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -50,54 +50,28 @@ module AutoParse
50
50
  property_super_schema = super_schema.properties[property_key]
51
51
  if property_super_schema
52
52
  # TODO: Not sure if this should be a recursive merge or not...
53
+
53
54
  # TODO: Might need to raise an error if a schema is extended in
54
55
  # a way that violates the requirement that all child instances also
55
56
  # validate against the parent schema.
56
- property_schema = property_super_schema.merge(property_schema)
57
- end
58
- self.properties[property_key] = property_schema
59
- if property_schema['$ref']
60
- schema_uri =
61
- self.uri + Addressable::URI.parse(property_schema['$ref'])
62
- schema = AutoParse.schemas[schema_uri]
63
- if schema == nil
64
- raise ArgumentError,
65
- "Could not find schema: #{property_schema['$ref']} " +
66
- "Referenced schema must be parsed first."
67
- end
68
- property_schema = schema.data
57
+ property_schema = property_super_schema.data.merge(property_schema)
69
58
  end
70
- case property_schema['type']
71
- when 'string'
72
- define_string_property(
73
- property_name, property_key, property_schema
74
- )
75
- when 'boolean'
76
- define_boolean_property(
77
- property_name, property_key, property_schema
78
- )
79
- when 'number'
80
- define_number_property(
81
- property_name, property_key, property_schema
82
- )
83
- when 'integer'
84
- define_integer_property(
85
- property_name, property_key, property_schema
86
- )
87
- when 'array'
88
- define_array_property(
89
- property_name, property_key, property_schema
90
- )
91
- when 'object'
92
- define_object_property(
93
- property_name, property_key, property_schema
94
- )
59
+
60
+ if schema_data.has_key?('id')
61
+ property_schema_class = AutoParse.generate(property_schema)
95
62
  else
96
- # Either type 'any' or we don't know what this is,
97
- # default to anything goes.
98
- define_any_property(
99
- property_name, property_key, property_schema
100
- )
63
+ # If the schema has no ID, it inherits the ID from the parent schema.
64
+ property_schema_class = AutoParse.generate(property_schema, @uri)
65
+ end
66
+
67
+ self.properties[property_key] = property_schema_class
68
+ self.keys[property_name] = property_key
69
+
70
+ define_method(property_name) do
71
+ __get__(property_name)
72
+ end
73
+ define_method(property_name + '=') do |value|
74
+ __set__(property_name, value)
101
75
  end
102
76
  end
103
77
 
@@ -141,7 +115,7 @@ module AutoParse
141
115
 
142
116
  elsif schema_data['additionalProperties']
143
117
  # Unknown properties follow the supplied schema.
144
- ap_schema = Schema.generate(schema_data['additionalProperties'])
118
+ ap_schema = AutoParse.generate(schema_data['additionalProperties'])
145
119
  @additional_properties_schema = ap_schema
146
120
  define_method('method_missing') do |method, *params, &block|
147
121
  # We need to convert from Ruby calling style to JavaScript calling
@@ -188,9 +162,6 @@ module AutoParse
188
162
 
189
163
  if schema_data['dependencies']
190
164
  for dependency_key, dependency_data in schema_data['dependencies']
191
- if dependency_data.kind_of?(Hash)
192
- dependency_data = AutoParse.generate(dependency_data)
193
- end
194
165
  self.property_dependencies[dependency_key] = dependency_data
195
166
  end
196
167
  end
@@ -200,4 +171,285 @@ module AutoParse
200
171
  self.schemas[schema.uri] = schema
201
172
  return schema
202
173
  end
174
+
175
+ def self.import_string(value, schema_class)
176
+ if value != nil
177
+ format = schema_class.data['format']
178
+ if format == 'byte'
179
+ Base64.decode64(value)
180
+ elsif format == 'date-time'
181
+ Time.parse(value)
182
+ elsif format == 'url'
183
+ Addressable::URI.parse(value)
184
+ elsif format =~ /^u?int(32|64)$/
185
+ value.to_i
186
+ else
187
+ value
188
+ end
189
+ else
190
+ nil
191
+ end
192
+ end
193
+
194
+ def self.export_string(value, schema_class)
195
+ format = schema_class.data['format']
196
+ if format == 'byte'
197
+ Base64.encode64(value)
198
+ elsif format == 'date-time'
199
+ if value.respond_to?(:to_str)
200
+ value = Time.parse(value.to_str)
201
+ elsif !value.respond_to?(:xmlschema)
202
+ raise TypeError,
203
+ "Could not obtain RFC 3339 timestamp from #{value.class}."
204
+ end
205
+ value.xmlschema
206
+ elsif format == 'url'
207
+ # This effectively does limited URI validation.
208
+ Addressable::URI.parse(value).to_str
209
+ elsif format =~ /^u?int(32|64)$/
210
+ value.to_s
211
+ elsif value.respond_to?(:to_str)
212
+ value.to_str
213
+ elsif value.kind_of?(Symbol)
214
+ value.to_s
215
+ else
216
+ raise TypeError,
217
+ "Expected String or Symbol, got #{value.class}."
218
+ end
219
+ end
220
+
221
+ def self.import_boolean(value, schema_class)
222
+ case value.to_s.downcase
223
+ when 'true', 'yes', 'y', 'on', '1'
224
+ true
225
+ when 'false', 'no', 'n', 'off', '0'
226
+ false
227
+ when 'nil', 'null', 'undefined'
228
+ nil
229
+ else
230
+ raise TypeError,
231
+ "Expected boolean, got #{value.class}."
232
+ end
233
+ end
234
+
235
+ def self.export_boolean(value, schema_class)
236
+ case value.to_s.downcase
237
+ when 'true', 'yes', 'y', 'on', '1'
238
+ true
239
+ when 'false', 'no', 'n', 'off', '0'
240
+ false
241
+ when 'nil', 'null', 'undefined'
242
+ nil
243
+ else
244
+ raise TypeError, "Expected boolean, got #{value.class}."
245
+ end
246
+ end
247
+
248
+ def self.import_number(value, schema_class)
249
+ if value == nil
250
+ value
251
+ else
252
+ Float(value)
253
+ end
254
+ end
255
+
256
+ def self.export_number(value, schema_class)
257
+ if value == nil
258
+ value
259
+ else
260
+ Float(value)
261
+ end
262
+ end
263
+
264
+ def self.import_integer(value, schema_class)
265
+ if value == nil
266
+ value
267
+ else
268
+ Integer(value)
269
+ end
270
+ end
271
+
272
+ def self.export_integer(value, schema_class)
273
+ if value == nil
274
+ value
275
+ else
276
+ Integer(value)
277
+ end
278
+ end
279
+
280
+ def self.import_array(value, schema_class)
281
+ array = (if value != nil && !value.respond_to?(:to_ary)
282
+ raise TypeError,
283
+ "Expected Array, got #{value.class}."
284
+ else
285
+ value.to_ary
286
+ end)
287
+ items_data = schema_class.data['items']
288
+ if items_data && items_data['$ref']
289
+ items_uri = schema_class.uri + Addressable::URI.parse(items_data['$ref'])
290
+ items_schema = AutoParse.schemas[items_uri]
291
+ if items_schema
292
+ array.map! do |item|
293
+ items_schema.new(item)
294
+ end
295
+ else
296
+ raise ArgumentError,
297
+ "Could not find schema: #{items_uri}."
298
+ end
299
+ end
300
+ array
301
+ end
302
+
303
+ def self.export_array(value, schema_class)
304
+ # FIXME: Each item in the Array needs to be exported as well.
305
+ if value == nil
306
+ value
307
+ elsif value.respond_to?(:to_ary)
308
+ value.to_ary
309
+ else
310
+ raise TypeError, "Expected Array, got #{value.class}."
311
+ end
312
+ end
313
+
314
+ def self.import_object(value, schema_class)
315
+ value ? schema_class.new(value) : nil
316
+ end
317
+
318
+ def self.export_object(value, schema_class)
319
+ # FIXME: Every field must be exported as well.
320
+ if value.nil?
321
+ nil
322
+ elsif value.respond_to?(:to_hash)
323
+ value.to_hash
324
+ elsif value.respond_to?(:to_json)
325
+ ::JSON.parse(value.to_json)
326
+ else
327
+ raise TypeError, "Expected Hash, got #{value.class}."
328
+ end
329
+ end
330
+
331
+ def self.import_union(value, schema_class)
332
+ import_type = match_type(
333
+ value, schema_class.data['type'], schema_class.uri
334
+ )
335
+ case import_type
336
+ when 'string'
337
+ AutoParse.import_string(value, schema_class)
338
+ when 'boolean'
339
+ AutoParse.import_boolean(value, schema_class)
340
+ when 'integer'
341
+ AutoParse.import_integer(value, schema_class)
342
+ when 'number'
343
+ AutoParse.import_number(value, schema_class)
344
+ when 'array'
345
+ AutoParse.import_array(value, schema_class)
346
+ when 'object'
347
+ AutoParse.import_object(value, schema_class)
348
+ when 'null'
349
+ nil
350
+ when Class
351
+ AutoParse.import_object(value, import_type)
352
+ else
353
+ AutoParse.import_any(value, schema_class)
354
+ end
355
+ end
356
+
357
+ def self.export_union(value, schema_class)
358
+ export_type = match_type(
359
+ value, schema_class.data['type'], schema_class.uri
360
+ )
361
+ case export_type
362
+ when 'string'
363
+ AutoParse.export_string(value, schema_class)
364
+ when 'boolean'
365
+ AutoParse.export_boolean(value, schema_class)
366
+ when 'integer'
367
+ AutoParse.export_integer(value, schema_class)
368
+ when 'number'
369
+ AutoParse.export_number(value, schema_class)
370
+ when 'array'
371
+ AutoParse.export_array(value, schema_class)
372
+ when 'object'
373
+ AutoParse.export_object(value, schema_class)
374
+ when 'null'
375
+ nil
376
+ when Class
377
+ AutoParse.export_object(value, export_type)
378
+ else
379
+ AutoParse.export_any(value, schema_class)
380
+ end
381
+ end
382
+
383
+ def self.import_any(value, schema_class)
384
+ value
385
+ end
386
+
387
+ def self.export_any(value, schema_class)
388
+ value
389
+ end
390
+
391
+ ##
392
+ # Given a value and a union of types, selects the type which is the best
393
+ # match for the given value. More than one type may match the value, in which
394
+ # case, the first type in the union will be returned.
395
+ def self.match_type(value, union, base_uri=nil)
396
+ possible_types = [union].flatten.compact
397
+ # Strict pass
398
+ for type in possible_types
399
+ # We import as the first type in the list that validates.
400
+ case type
401
+ when 'string'
402
+ return 'string' if value.kind_of?(String)
403
+ when 'boolean'
404
+ return 'boolean' if value == true or value == false
405
+ when 'integer'
406
+ return 'integer' if value.kind_of?(Integer)
407
+ when 'number'
408
+ return 'number' if value.kind_of?(Numeric)
409
+ when 'array'
410
+ return 'array' if value.kind_of?(Array)
411
+ when 'object'
412
+ return 'object' if value.kind_of?(Hash) || value.kind_of?(Instance)
413
+ when 'null'
414
+ return 'null' if value.nil?
415
+ when Hash
416
+ # Schema embedded directly.
417
+ unless base_uri
418
+ schema_class = AutoParse.generate(type)
419
+ else
420
+ schema_class = AutoParse.generate(type, base_uri)
421
+ end
422
+ if type['$ref']
423
+ schema_class = schema_class.dereference
424
+ end
425
+ return schema_class if schema_class.new(value).valid?
426
+ end
427
+ end
428
+ # Lenient pass
429
+ for type in possible_types
430
+ # We import as the first type in the list that validates.
431
+ case type
432
+ when 'string'
433
+ return 'string' if value.respond_to?(:to_str) || value.kind_of?(Symbol)
434
+ when 'boolean'
435
+ if ['true', 'yes', 'y', 'on', '1',
436
+ 'false', 'no', 'n', 'off', '0'].include?(value.to_s.downcase)
437
+ return 'boolean'
438
+ end
439
+ when 'integer'
440
+ return 'integer' if value.to_i != 0 || value == "0"
441
+ when 'number'
442
+ return 'number' if value.to_f != 0.0 || value == "0" || value == "0.0"
443
+ when 'array'
444
+ return 'array' if value.respond_to?(:to_ary)
445
+ when 'object'
446
+ if value.respond_to?(:to_hash) || value.respond_to?(:to_json)
447
+ return 'object'
448
+ end
449
+ when 'any'
450
+ return 'any'
451
+ end
452
+ end
453
+ return nil
454
+ end
203
455
  end