autoparse 0.1.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.
@@ -0,0 +1,422 @@
1
+ # Copyright 2010 Google Inc
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ require 'json'
17
+ require 'time'
18
+ require 'autoparse/inflection'
19
+ require 'addressable/uri'
20
+
21
+ module AutoParse
22
+ class Instance
23
+ def self.uri
24
+ return @uri ||= nil
25
+ end
26
+
27
+ def self.properties
28
+ return @properties ||= {}
29
+ end
30
+
31
+ def self.additional_properties_schema
32
+ return EMPTY_SCHEMA
33
+ end
34
+
35
+ def self.property_dependencies
36
+ return @property_dependencies ||= {}
37
+ end
38
+
39
+ def self.data
40
+ return @schema_data
41
+ end
42
+
43
+ def self.description
44
+ return @schema_data['description']
45
+ end
46
+
47
+ def self.validate_string_property(property_value, schema_data)
48
+ property_value = property_value.to_str rescue property_value
49
+ if !property_value.kind_of?(String)
50
+ return false
51
+ else
52
+ # TODO: implement more than type-checking
53
+ return true
54
+ end
55
+ end
56
+
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)
134
+ # TODO: implement more than type-checking
135
+ return true
136
+ end
137
+
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
+ def self.validate_integer_property(property_value, schema_data)
152
+ return false if !property_value.kind_of?(Integer)
153
+ if schema_data['minimum'] && schema_data['exclusiveMinimum']
154
+ return false if property_value <= schema_data['minimum']
155
+ elsif schema_data['minimum']
156
+ return false if property_value < schema_data['minimum']
157
+ end
158
+ if schema_data['maximum'] && schema_data['exclusiveMaximum']
159
+ return false if property_value >= schema_data['maximum']
160
+ elsif schema_data['maximum']
161
+ return false if property_value > schema_data['maximum']
162
+ end
163
+ return true
164
+ end
165
+
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
177
+ end
178
+
179
+ def self.validate_array_property(property_value, schema_data)
180
+ if property_value.respond_to?(:to_ary)
181
+ property_value = property_value.to_ary
182
+ else
183
+ return false
184
+ end
185
+ property_value.each do |item_value|
186
+ unless self.validate_property_value(item_value, schema_data['items'])
187
+ return false
188
+ end
189
+ end
190
+ return true
191
+ end
192
+
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)
222
+ if property_value.kind_of?(Instance)
223
+ return property_value.valid?
224
+ elsif schema != nil && schema.kind_of?(Class)
225
+ return schema.new(property_value).valid?
226
+ 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."
243
+ 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
+ end
251
+ end
252
+
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
259
+ end
260
+ end
261
+
262
+ ##
263
+ # @api private
264
+ def self.validate_property_value(property_value, schema_data)
265
+ if property_value == nil && schema_data['required'] == true
266
+ return false
267
+ elsif property_value == nil
268
+ # Value was omitted, but not required. Still valid.
269
+ return true
270
+ end
271
+
272
+ # Verify property values
273
+ if schema_data['$ref']
274
+ schema_uri = self.uri + Addressable::URI.parse(schema_data['$ref'])
275
+ schema = AutoParse.schemas[schema_uri]
276
+ if schema == nil
277
+ raise ArgumentError,
278
+ "Could not find schema: #{schema_data['$ref']} " +
279
+ "Referenced schema must be parsed first."
280
+ end
281
+ schema_data = schema.data
282
+ end
283
+ case schema_data['type']
284
+ when 'string'
285
+ return false unless self.validate_string_property(
286
+ property_value, schema_data
287
+ )
288
+ when 'boolean'
289
+ return false unless self.validate_boolean_property(
290
+ property_value, schema_data
291
+ )
292
+ when 'number'
293
+ return false unless self.validate_number_property(
294
+ property_value, schema_data
295
+ )
296
+ when 'integer'
297
+ return false unless self.validate_integer_property(
298
+ property_value, schema_data
299
+ )
300
+ when 'array'
301
+ return false unless self.validate_array_property(
302
+ property_value, schema_data
303
+ )
304
+ when 'object'
305
+ return false unless self.validate_object_property(
306
+ property_value, schema_data
307
+ )
308
+ else
309
+ # Either type 'any' or we don't know what this is,
310
+ # default to anything goes. Validation of an 'any' property always
311
+ # succeeds.
312
+ end
313
+ return true
314
+ end
315
+
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."
322
+ end
323
+ if data.respond_to?(:to_hash)
324
+ data = data.to_hash
325
+ elsif data.respond_to?(:to_json)
326
+ data = JSON.parse(data.to_json)
327
+ else
328
+ raise TypeError,
329
+ 'Unable to parse. ' +
330
+ 'Expected data to respond to either :to_hash or :to_json.'
331
+ end
332
+ @data = data
333
+ end
334
+
335
+ def [](key)
336
+ return @data[key]
337
+ end
338
+
339
+ def []=(key, value)
340
+ return @data[key] = value
341
+ end
342
+
343
+ ##
344
+ # Validates the parsed data against the schema.
345
+ def valid?
346
+ unvalidated_fields = @data.keys.dup
347
+ for property_key, property_schema in self.class.properties
348
+ property_value = self[property_key]
349
+ if !self.class.validate_property_value(property_value, property_schema)
350
+ return false
351
+ end
352
+ if property_value == nil && property_schema['required'] != true
353
+ # Value was omitted, but not required. Still valid. Skip dependency
354
+ # checks.
355
+ next
356
+ end
357
+
358
+ # Verify property dependencies
359
+ property_dependencies = self.class.property_dependencies[property_key]
360
+ case property_dependencies
361
+ when String, Array
362
+ property_dependencies = [property_dependencies].flatten
363
+ for dependency_key in property_dependencies
364
+ dependency_value = self[dependency_key]
365
+ return false if dependency_value == nil
366
+ end
367
+ when Class
368
+ if property_dependencies.ancestors.include?(Instance)
369
+ dependency_instance = property_dependencies.new(property_value)
370
+ return false unless dependency_instance.valid?
371
+ else
372
+ raise TypeError,
373
+ "Expected schema Class, got #{property_dependencies.class}."
374
+ end
375
+ end
376
+ end
377
+ if self.class.additional_properties_schema == nil
378
+ # No additional properties allowed
379
+ return false unless unvalidated_fields.empty?
380
+ elsif self.class.additional_properties_schema != EMPTY_SCHEMA
381
+ # Validate all remaining fields against this schema
382
+
383
+ # Make sure tests don't pass prematurely
384
+ return false
385
+ end
386
+ if self.class.superclass && self.class.superclass != Instance &&
387
+ self.class.ancestors.first != Instance
388
+ # The spec actually only defined the 'extends' semantics as children
389
+ # must also validate aainst the parent.
390
+ return false unless self.class.superclass.new(@data).valid?
391
+ end
392
+ return true
393
+ end
394
+
395
+ def to_hash
396
+ return @data
397
+ end
398
+
399
+ def to_json
400
+ return JSON.generate(self.to_hash)
401
+ end
402
+
403
+ ##
404
+ # Returns a <code>String</code> representation of the schema instance.
405
+ #
406
+ # @return [String] The instance's state, as a <code>String</code>.
407
+ def inspect
408
+ if self.class.respond_to?(:description)
409
+ sprintf(
410
+ "#<%s:%#0x DESC:'%s'>",
411
+ self.class.to_s, self.object_id, self.class.description
412
+ )
413
+ else
414
+ sprintf("#<%s:%#0x>", self.class.to_s, self.object_id)
415
+ end
416
+ end
417
+ end
418
+
419
+ ##
420
+ # The empty schema accepts all JSON.
421
+ EMPTY_SCHEMA = Instance
422
+ end
@@ -0,0 +1,26 @@
1
+ # Copyright 2010 Google Inc
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ # Used to prevent the class/module from being loaded more than once
16
+ unless defined? AutoParse::VERSION
17
+ module AutoParse
18
+ module VERSION
19
+ MAJOR = 0
20
+ MINOR = 1
21
+ TINY = 0
22
+
23
+ STRING = [MAJOR, MINOR, TINY].join('.')
24
+ end
25
+ end
26
+ end
data/lib/autoparse.rb ADDED
@@ -0,0 +1,203 @@
1
+ # Copyright 2010 Google Inc
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require 'autoparse/instance'
16
+ require 'autoparse/version'
17
+ require 'addressable/uri'
18
+
19
+ module AutoParse
20
+ def self.schemas
21
+ @schemas ||= {}
22
+ end
23
+
24
+ def self.generate(schema_data, uri=nil)
25
+ if schema_data["extends"]
26
+ super_uri = uri + Addressable::URI.parse(schema_data["extends"])
27
+ super_schema = self.schemas[super_uri]
28
+ if super_schema == nil
29
+ raise ArgumentError,
30
+ "Could not find schema to extend: #{schema_data["extends"]} " +
31
+ "Parent schema must be parsed before child schema."
32
+ end
33
+ else
34
+ super_schema = Instance
35
+ end
36
+ schema = Class.new(super_schema) do
37
+ @uri = Addressable::URI.parse(uri)
38
+ @uri.normalize! if @uri != nil
39
+ @schema_data = schema_data
40
+
41
+ def self.additional_properties_schema
42
+ # Override the superclass implementation so we're not always returning
43
+ # the empty schema.
44
+ return @additional_properties_schema
45
+ end
46
+
47
+ (@schema_data['properties'] || []).each do |(k, v)|
48
+ property_key, property_schema = k, v
49
+ property_name = INFLECTOR.underscore(property_key).gsub("-", "_")
50
+ property_super_schema = super_schema.properties[property_key]
51
+ if property_super_schema
52
+ # TODO: Not sure if this should be a recursive merge or not...
53
+ # TODO: Might need to raise an error if a schema is extended in
54
+ # a way that violates the requirement that all child instances also
55
+ # 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
69
+ 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
+ )
95
+ 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
+ )
101
+ end
102
+ end
103
+
104
+ if schema_data['additionalProperties'] == true ||
105
+ schema_data['additionalProperties'] == nil
106
+ # Schema-less unknown properties are allowed.
107
+ @additional_properties_schema = EMPTY_SCHEMA
108
+ define_method('method_missing') do |method, *params, &block|
109
+ # We need to convert from Ruby calling style to JavaScript calling
110
+ # style. If this fails, attempt to use JavaScript calling style
111
+ # directly.
112
+
113
+ # We can't modify the method in-place because this affects the call
114
+ # to super.
115
+ stripped_method = method.to_s
116
+ assignment = false
117
+ if stripped_method[-1..-1] == '='
118
+ assignment = true
119
+ stripped_method[-1..-1] = ''
120
+ end
121
+ key = INFLECTOR.camelize(stripped_method)
122
+ key[0..0] = key[0..0].downcase
123
+ if self[key] != nil
124
+ value = self[key]
125
+ elsif self[stripped_method] != nil
126
+ key = stripped_method
127
+ value = self[stripped_method]
128
+ else
129
+ # Method not found.
130
+ super
131
+ end
132
+ # If additionalProperties is simply set to true, no parsing takes
133
+ # place and all values are treated as 'any'.
134
+ if assignment
135
+ new_value = params[0]
136
+ self[key] = new_value
137
+ else
138
+ value
139
+ end
140
+ end
141
+
142
+ elsif schema_data['additionalProperties']
143
+ # Unknown properties follow the supplied schema.
144
+ ap_schema = Schema.generate(schema_data['additionalProperties'])
145
+ @additional_properties_schema = ap_schema
146
+ define_method('method_missing') do |method, *params, &block|
147
+ # We need to convert from Ruby calling style to JavaScript calling
148
+ # style. If this fails, attempt to use JavaScript calling style
149
+ # directly.
150
+
151
+ # We can't modify the method in-place because this affects the call
152
+ # to super.
153
+ stripped_method = method.to_s
154
+ assignment = false
155
+ if stripped_method[-1..-1] == '='
156
+ assignment = true
157
+ stripped_method[-1..-1] = ''
158
+ end
159
+ key = INFLECTOR.camelize(stripped_method)
160
+ key[0..0] = key[0..0].downcase
161
+ if self[key] != nil
162
+ value = self[key]
163
+ elsif self[stripped_method] != nil
164
+ key = stripped_method
165
+ value = self[stripped_method]
166
+ else
167
+ # Method not found.
168
+ super
169
+ end
170
+ if assignment
171
+ # In the case of assignment, it's very likely the developer is
172
+ # passing in an unparsed Hash value. This value must be parsed.
173
+ # Unfortunately, we may accidentally reparse something that's
174
+ # already in a parsed state because Schema.new(Schema.new(data))
175
+ # is completely valid. This will cause performance issues if
176
+ # developers are careless, but since there's no good reason to
177
+ # do assignment on parsed objects, hopefully this should not
178
+ # cause problems often.
179
+ new_value = params[0]
180
+ self[key] = ap_schema.new(new_value)
181
+ else
182
+ ap_schema.new(value)
183
+ end
184
+ end
185
+ else
186
+ @additional_properties_schema = nil
187
+ end
188
+
189
+ if schema_data['dependencies']
190
+ 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
+ self.property_dependencies[dependency_key] = dependency_data
195
+ end
196
+ end
197
+ end
198
+
199
+ # Register the new schema.
200
+ self.schemas[schema.uri] = schema
201
+ return schema
202
+ end
203
+ end