old_api_resource 0.3.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.
Files changed (48) hide show
  1. data/.document +5 -0
  2. data/.rspec +3 -0
  3. data/Gemfile +26 -0
  4. data/Guardfile +22 -0
  5. data/LICENSE.txt +20 -0
  6. data/README.rdoc +19 -0
  7. data/Rakefile +49 -0
  8. data/VERSION +1 -0
  9. data/lib/old_api_resource.rb +70 -0
  10. data/lib/old_api_resource/associations.rb +192 -0
  11. data/lib/old_api_resource/associations/association_proxy.rb +92 -0
  12. data/lib/old_api_resource/associations/multi_object_proxy.rb +74 -0
  13. data/lib/old_api_resource/associations/related_object_hash.rb +12 -0
  14. data/lib/old_api_resource/associations/relation_scope.rb +24 -0
  15. data/lib/old_api_resource/associations/resource_scope.rb +25 -0
  16. data/lib/old_api_resource/associations/scope.rb +88 -0
  17. data/lib/old_api_resource/associations/single_object_proxy.rb +64 -0
  18. data/lib/old_api_resource/attributes.rb +162 -0
  19. data/lib/old_api_resource/base.rb +548 -0
  20. data/lib/old_api_resource/callbacks.rb +49 -0
  21. data/lib/old_api_resource/connection.rb +167 -0
  22. data/lib/old_api_resource/core_extensions.rb +7 -0
  23. data/lib/old_api_resource/custom_methods.rb +119 -0
  24. data/lib/old_api_resource/exceptions.rb +85 -0
  25. data/lib/old_api_resource/formats.rb +14 -0
  26. data/lib/old_api_resource/formats/json_format.rb +25 -0
  27. data/lib/old_api_resource/formats/xml_format.rb +36 -0
  28. data/lib/old_api_resource/log_subscriber.rb +15 -0
  29. data/lib/old_api_resource/mocks.rb +260 -0
  30. data/lib/old_api_resource/model_errors.rb +86 -0
  31. data/lib/old_api_resource/observing.rb +29 -0
  32. data/lib/old_api_resource/railtie.rb +18 -0
  33. data/old_api_resource.gemspec +134 -0
  34. data/spec/lib/associations_spec.rb +519 -0
  35. data/spec/lib/attributes_spec.rb +121 -0
  36. data/spec/lib/base_spec.rb +499 -0
  37. data/spec/lib/callbacks_spec.rb +68 -0
  38. data/spec/lib/mocks_spec.rb +28 -0
  39. data/spec/lib/model_errors_spec.rb +29 -0
  40. data/spec/spec_helper.rb +36 -0
  41. data/spec/support/mocks/association_mocks.rb +46 -0
  42. data/spec/support/mocks/error_resource_mocks.rb +21 -0
  43. data/spec/support/mocks/test_resource_mocks.rb +43 -0
  44. data/spec/support/requests/association_requests.rb +14 -0
  45. data/spec/support/requests/error_resource_requests.rb +25 -0
  46. data/spec/support/requests/test_resource_requests.rb +31 -0
  47. data/spec/support/test_resource.rb +50 -0
  48. metadata +286 -0
@@ -0,0 +1,548 @@
1
+ require 'active_support'
2
+ require 'active_support/core_ext'
3
+ require 'active_support/string_inquirer'
4
+
5
+ module OldApiResource
6
+
7
+ class Base
8
+
9
+ class_inheritable_accessor :site, :proxy, :user, :password, :auth_type, :format, :timeout, :ssl_options
10
+
11
+ class_inheritable_accessor :include_root_in_json; self.include_root_in_json = true
12
+ class_inheritable_accessor :include_blank_attributes_on_create; self.include_blank_attributes_on_create = false
13
+ class_inheritable_accessor :include_all_attributes_on_update; self.include_blank_attributes_on_create = false
14
+
15
+ class_inheritable_accessor :primary_key; self.primary_key = "id"
16
+
17
+ attr_accessor :prefix_options
18
+
19
+ class << self
20
+
21
+ # writers - accessors with defaults were not working
22
+ attr_writer :element_name, :collection_name
23
+
24
+ def inherited(klass)
25
+ # Call the methods of the superclass to make sure inheritable accessors and the like have been inherited
26
+ super
27
+ # Now we need to define the inherited method on the klass that's doing the inheriting
28
+ # it calls super which will allow the chaining effect we need
29
+ klass.instance_eval <<-EOE, __FILE__, __LINE__ + 1
30
+ def inherited(klass)
31
+ super
32
+ end
33
+ EOE
34
+ # Now we can make a call to setup the inheriting klass with its attributes
35
+ klass.set_class_attributes_upon_load unless klass.instance_variable_defined?(:@class_data)
36
+ klass.instance_variable_set(:@class_data, true)
37
+ # we want to reset element_name and collection_name for the inherited class
38
+ true
39
+ end
40
+
41
+
42
+ # This makes a request to new_element_path
43
+ def set_class_attributes_upon_load
44
+ begin
45
+ class_data = self.connection.get(self.new_element_path)
46
+ # Attributes go first
47
+ if class_data["attributes"]
48
+ define_attributes *(class_data["attributes"]["public"] || [])
49
+ define_protected_attributes *(class_data["attributes"]["protected"] || [])
50
+ end
51
+ # Then scopes
52
+ if class_data["scopes"]
53
+ class_data["scopes"].each do |hash|
54
+ self.scope hash.first[0], hash.first[1]
55
+ end
56
+ end
57
+ # Then associations
58
+ if class_data["associations"]
59
+ class_data["associations"].each do |(key,hash)|
60
+ hash.each do |assoc_name, assoc_options|
61
+ self.send(key, assoc_name, assoc_options)
62
+ end
63
+ end
64
+ end
65
+ # Swallow up any loading errors because the site may be incorrect
66
+ rescue Exception => e
67
+ if OldApiResource.raise_missing_definition_error
68
+ raise e
69
+ end
70
+ OldApiResource.logger.warn("#{self}: #{e.message}")
71
+ OldApiResource.logger.debug(e.backtrace.pretty_inspect)
72
+ return e.respond_to?(:request) ? e.request : nil
73
+ end
74
+ end
75
+
76
+ def reload_class_attributes
77
+ # clear the public_attribute_names, protected_attribute_names
78
+ self.clear_attributes
79
+ self.clear_associations
80
+ self.set_class_attributes_upon_load
81
+ end
82
+
83
+ def site=(site)
84
+ # store so we can reload attributes if the site changed
85
+ old_site = self.site.to_s.clone
86
+ @connection = nil
87
+
88
+ if site.nil?
89
+ write_inheritable_attribute(:site, nil)
90
+ # no site, so we'll skip the reload
91
+ return site
92
+ else
93
+ write_inheritable_attribute(:site, create_site_uri_from(site))
94
+ end
95
+
96
+ # reset class attributes and try to reload them if the site changed
97
+ unless self.site.to_s == old_site
98
+ self.reload_class_attributes
99
+ end
100
+
101
+ return site
102
+ end
103
+
104
+
105
+ def format=(mime_type_or_format)
106
+ format = mime_type_or_format.is_a?(Symbol) ? OldApiResource::Formats[mime_type_or_format] : mime_type_or_format
107
+ write_inheritable_attribute(:format, format)
108
+ self.connection.format = format if self.site
109
+ end
110
+
111
+ # Default format is json
112
+ def format
113
+ read_inheritable_attribute(:format) || OldApiResource::Formats::JsonFormat
114
+ end
115
+
116
+ def timeout=(timeout)
117
+ @connection = nil
118
+ write_inheritable_attribute(:timeout, timeout)
119
+ end
120
+
121
+ def connection(refresh = false)
122
+ @connection = Connection.new(self.site, self.format) if refresh || @connection.nil?
123
+ @connection.timeout = self.timeout
124
+ @connection
125
+ end
126
+
127
+ def headers
128
+ @headers || {}
129
+ end
130
+
131
+ def prefix(options = {})
132
+ default = (self.site ? self.site.path : '/')
133
+ default << '/' unless default[-1..-1] == '/'
134
+ self.prefix = default
135
+ prefix(options)
136
+ end
137
+
138
+ def prefix_source
139
+ prefix
140
+ prefix_source
141
+ end
142
+
143
+ def prefix=(value = '/')
144
+ prefix_call = value.gsub(/:\w+/) { |key| "\#{URI.escape options[#{key}].to_s}"}
145
+ @prefix_parameters = nil
146
+ silence_warnings do
147
+ instance_eval <<-EOE, __FILE__, __LINE__ + 1
148
+ def prefix_source() "#{value}" end
149
+ def prefix(options={}) "#{prefix_call}" end
150
+ EOE
151
+ end
152
+ rescue Exception => e
153
+ logger.error "Couldn't set prefix: #{e}\n #{code}" if logger
154
+ raise
155
+ end
156
+
157
+ # element_name with default
158
+ def element_name
159
+ @element_name ||= self.model_name.element
160
+ end
161
+ # collection_name with default
162
+ def collection_name
163
+ @collection_name ||= ActiveSupport::Inflector.pluralize(self.element_name)
164
+ end
165
+
166
+ # alias_method :set_prefix, :prefix=
167
+ # alias_method :set_element_name, :element_name=
168
+ # alias_method :set_collection_name, :collection_name=
169
+
170
+ def element_path(id, prefix_options = {}, query_options = nil)
171
+ prefix_options, query_options = split_options(prefix_options) if query_options.nil?
172
+ "#{prefix(prefix_options)}#{collection_name}/#{URI.escape id.to_s}.#{format.extension}#{query_string(query_options)}"
173
+ end
174
+
175
+ def new_element_path(prefix_options = {})
176
+ "#{prefix(prefix_options)}#{collection_name}/new.#{format.extension}"
177
+ end
178
+
179
+ def collection_path(prefix_options = {}, query_options = nil)
180
+ prefix_options, query_options = split_options(prefix_options) if query_options.nil?
181
+ "#{prefix(prefix_options)}#{collection_name}.#{format.extension}#{query_string(query_options)}"
182
+ end
183
+
184
+ def build(attributes = {})
185
+ self.new(attributes)
186
+ end
187
+
188
+ def create(attributes = {})
189
+ self.new(attributes).tap{ |resource| resource.save }
190
+ end
191
+
192
+ def find(*arguments)
193
+ scope = arguments.slice!(0)
194
+ options = arguments.slice!(0) || {}
195
+
196
+ case scope
197
+ when :all then find_every(options)
198
+ when :first then find_every(options).first
199
+ when :last then find_every(options).last
200
+ when :one then find_one(options)
201
+ else find_single(scope, options)
202
+ end
203
+ end
204
+
205
+
206
+ # A convenience wrapper for <tt>find(:first, *args)</tt>. You can pass
207
+ # in all the same arguments to this method as you can to
208
+ # <tt>find(:first)</tt>.
209
+ def first(*args)
210
+ find(:first, *args)
211
+ end
212
+
213
+ # A convenience wrapper for <tt>find(:last, *args)</tt>. You can pass
214
+ # in all the same arguments to this method as you can to
215
+ # <tt>find(:last)</tt>.
216
+ def last(*args)
217
+ find(:last, *args)
218
+ end
219
+
220
+ # This is an alias for find(:all). You can pass in all the same
221
+ # arguments to this method as you can to <tt>find(:all)</tt>
222
+ def all(*args)
223
+ find(:all, *args)
224
+ end
225
+
226
+
227
+ # Deletes the resources with the ID in the +id+ parameter.
228
+ #
229
+ # ==== Options
230
+ # All options specify \prefix and query parameters.
231
+ #
232
+ # ==== Examples
233
+ # Event.delete(2) # sends DELETE /events/2
234
+ #
235
+ # Event.create(:name => 'Free Concert', :location => 'Community Center')
236
+ # my_event = Event.find(:first) # let's assume this is event with ID 7
237
+ # Event.delete(my_event.id) # sends DELETE /events/7
238
+ #
239
+ # # Let's assume a request to events/5/cancel.xml
240
+ # Event.delete(params[:id]) # sends DELETE /events/5
241
+ def delete(id, options = {})
242
+ connection.delete(element_path(id, options))
243
+ end
244
+
245
+ private
246
+ # Find every resource
247
+ def find_every(options)
248
+ begin
249
+ case from = options[:from]
250
+ when Symbol
251
+ instantiate_collection(get(from, options[:params]))
252
+ when String
253
+ path = "#{from}#{query_string(options[:params])}"
254
+ instantiate_collection(connection.get(path, headers) || [])
255
+ else
256
+ prefix_options, query_options = split_options(options[:params])
257
+ path = collection_path(prefix_options, query_options)
258
+ instantiate_collection( (connection.get(path, headers) || []), prefix_options )
259
+ end
260
+ rescue OldApiResource::ResourceNotFound
261
+ # Swallowing ResourceNotFound exceptions and return nil - as per
262
+ # ActiveRecord.
263
+ nil
264
+ end
265
+ end
266
+
267
+ # Find a single resource from a one-off URL
268
+ def find_one(options)
269
+ case from = options[:from]
270
+ when Symbol
271
+ instantiate_record(get(from, options[:params]))
272
+ when String
273
+ path = "#{from}#{query_string(options[:params])}"
274
+ instantiate_record(connection.get(path, headers))
275
+ end
276
+ end
277
+
278
+ # Find a single resource from the default URL
279
+ def find_single(scope, options)
280
+ prefix_options, query_options = split_options(options[:params])
281
+ path = element_path(scope, prefix_options, query_options)
282
+ instantiate_record(connection.get(path, headers), prefix_options)
283
+ end
284
+
285
+ def instantiate_collection(collection, prefix_options = {})
286
+ collection.collect! { |record| instantiate_record(record, prefix_options) }
287
+ end
288
+
289
+ def instantiate_record(record, prefix_options = {})
290
+ new(record).tap do |resource|
291
+ resource.prefix_options = prefix_options
292
+ end
293
+ end
294
+
295
+
296
+ # Accepts a URI and creates the site URI from that.
297
+ def create_site_uri_from(site)
298
+ site.is_a?(URI) ? site.dup : uri_parser.parse(site)
299
+ end
300
+
301
+ # Accepts a URI and creates the proxy URI from that.
302
+ def create_proxy_uri_from(proxy)
303
+ proxy.is_a?(URI) ? proxy.dup : uri_parser.parse(proxy)
304
+ end
305
+
306
+ # contains a set of the current prefix parameters.
307
+ def prefix_parameters
308
+ @prefix_parameters ||= prefix_source.scan(/:\w+/).map { |key| key[1..-1].to_sym }.to_set
309
+ end
310
+
311
+ # Builds the query string for the request.
312
+ def query_string(options)
313
+ "?#{options.to_query}" unless options.nil? || options.empty?
314
+ end
315
+
316
+ # split an option hash into two hashes, one containing the prefix options,
317
+ # and the other containing the leftovers.
318
+ def split_options(options = {})
319
+ prefix_options, query_options = {}, {}
320
+ (options || {}).each do |key, value|
321
+ next if key.blank?
322
+ (prefix_parameters.include?(key.to_sym) ? prefix_options : query_options)[key.to_sym] = value
323
+ end
324
+
325
+ [ prefix_options, query_options ]
326
+ end
327
+
328
+ def uri_parser
329
+ @uri_parser ||= URI.const_defined?(:Parser) ? URI::Parser.new : URI
330
+ end
331
+
332
+ end
333
+
334
+ def initialize(attributes = {})
335
+ @prefix_options = {}
336
+ load(attributes)
337
+ end
338
+
339
+ def new?
340
+ id.nil?
341
+ end
342
+ alias :new_record? :new?
343
+
344
+ def persisted?
345
+ !new?
346
+ end
347
+
348
+ def id
349
+ self.attributes[self.class.primary_key]
350
+ end
351
+
352
+ # Bypass dirty tracking for this field
353
+ def id=(id)
354
+ attributes[self.class.primary_key] = id
355
+ end
356
+
357
+ def ==(other)
358
+ other.equal?(self) || (other.instance_of?(self.class) && other.id == self.id && other.prefix_options == self.prefix_options)
359
+ end
360
+
361
+ def eql?(other)
362
+ self == other
363
+ end
364
+
365
+ def hash
366
+ id.hash
367
+ end
368
+
369
+ def dup
370
+ self.class.new.tap do |resource|
371
+ resource.attributes = self.attributes
372
+ resource.prefix_options = @prefix_options
373
+ end
374
+ end
375
+
376
+ def update_attributes(attrs)
377
+ self.attributes = attrs
378
+ self.save
379
+ end
380
+
381
+ def save(*args)
382
+ new? ? create(*args) : update(*args)
383
+ end
384
+
385
+ def save!(*args)
386
+ save(*args) || raise(OldApiResource::ResourceInvalid.new(self))
387
+ end
388
+
389
+ def destroy
390
+ connection.delete(element_path(self.id), self.class.headers)
391
+ end
392
+
393
+ def encode(options = {})
394
+ self.send("to_#{self.class.format.extension}", options)
395
+ end
396
+
397
+ def reload
398
+ self.load(self.class.find(to_param, :params => @prefix_options).attributes)
399
+ end
400
+
401
+ def load(attributes)
402
+ return if attributes.nil?
403
+ raise ArgumentError, "expected an attributes Hash, got #{attributes.inspect}" unless attributes.is_a?(Hash)
404
+ @prefix_options, attributes = split_options(attributes)
405
+
406
+ attributes.symbolize_keys.each do |key, value|
407
+ # If this attribute doesn't exist define it as a protected attribute
408
+ self.class.define_protected_attributes(key) unless self.respond_to?(key)
409
+ self.attributes[key] =
410
+ case value
411
+ when Array
412
+ if self.has_many?(key)
413
+ MultiObjectProxy.new(self.has_many_class_name(key), value)
414
+ elsif self.association?(key)
415
+ raise ArgumentError, "Expected a hash value or nil, got: #{value.inspect}"
416
+ else
417
+ value.dup rescue value
418
+ end
419
+ when Hash
420
+ if self.has_many?(key)
421
+ MultiObjectProxy.new(self.has_many_class_name(key), value)
422
+ elsif self.association?(key)
423
+ SingleObjectProxy.new(self.association_class_name(key), value)
424
+ else
425
+ value.dup rescue value
426
+ end
427
+ when NilClass
428
+ # If it's nil and an association then create a blank object
429
+ if self.has_many?(key)
430
+ return MultiObjectProxy.new(self.has_many_class_name(key), [])
431
+ elsif self.association?(key)
432
+ SingleObjectProxy.new(self.association_class_name(key), value)
433
+ end
434
+ else
435
+ raise ArgumentError, "expected an array or a hash for the association #{key}, got: #{value.inspect}" if self.association?(key)
436
+ value.dup rescue value
437
+ end
438
+ end
439
+ return self
440
+ end
441
+
442
+ # Override to_s and inspect so they only show attributes
443
+ # and not associations, this prevents force loading of associations
444
+ # when we call to_s or inspect on a descendent of base but allows it if we
445
+ # try to evaluate an association directly
446
+ def to_s
447
+ return "#<#{self.class}:0x%08x @attributes=#{self.attributes.inject({}){|accum,(k,v)| self.association?(k) ? accum : accum.merge(k => v)}}" % (self.object_id * 2)
448
+ end
449
+
450
+ alias_method :inspect, :to_s
451
+
452
+ # Methods for serialization as json or xml, relying on the serializable_hash method
453
+ def to_xml(options = {})
454
+ self.serializable_hash(options).to_xml(:root => self.class.element_name)
455
+ end
456
+
457
+ def to_json(options = {})
458
+ self.class.include_root_in_json ? {self.class.element_name => self.serializable_hash(options)}.to_json : self.serializable_hash(options).to_json
459
+ end
460
+
461
+ def serializable_hash(options = {})
462
+ options[:include_associations] = options[:include_associations] ? options[:include_associations].symbolize_array : []
463
+ options[:include_extras] = options[:include_extras] ? options[:include_extras].symbolize_array : []
464
+ options[:except] ||= []
465
+ ret = self.attributes.inject({}) do |accum, (key,val)|
466
+ # If this is an association and it's in include_associations then include it
467
+ if self.association?(key) && options[:include_associations].include?(key.to_sym)
468
+ accum.merge(key => val.serializable_hash({:include_id => true}))
469
+ elsif options[:include_extras].include?(key.to_sym)
470
+ accum.merge(key => val)
471
+ elsif options[:except].include?(key.to_sym)
472
+ accum
473
+ else
474
+ self.association?(key) || !self.attribute?(key) || self.protected_attribute?(key) ? accum : accum.merge(key => val)
475
+ end
476
+ end
477
+ # include id - this is for nested updates
478
+ ret[:id] = self.id if options[:include_id] && !self.id.nil?
479
+ ret
480
+ end
481
+
482
+ protected
483
+ def connection(refresh = false)
484
+ self.class.connection(refresh)
485
+ end
486
+
487
+ def load_attributes_from_response(response)
488
+ load(response)
489
+ end
490
+
491
+ def element_path(id, prefix_options = {}, query_options = nil)
492
+ self.class.element_path(id, prefix_options, query_options)
493
+ end
494
+
495
+ def new_element_path(prefix_options = {})
496
+ self.class.new_element_path(prefix_options)
497
+ end
498
+
499
+ def collection_path(prefix_options = {},query_options = nil)
500
+ self.class.collection_path(prefix_options, query_options)
501
+ end
502
+
503
+ def create(*args)
504
+ opts = args.extract_options!
505
+ # When we create we should not include any blank attributes unless they are associations
506
+ except = self.class.include_blank_attributes_on_create ? {} : self.attributes.select{|k,v| v.blank?}
507
+ opts[:except] = opts[:except] ? opts[:except].concat(except.keys).uniq.symbolize_array : except.keys.symbolize_array
508
+ opts[:include_associations] = opts[:include_associations] ? opts[:include_associations].concat(args) : []
509
+ opts[:include_extras] ||= []
510
+ body = RestClient::Payload.has_file?(self.attributes) ? self.serializable_hash(opts) : encode(opts)
511
+ connection.post(collection_path, body, self.class.headers).tap do |response|
512
+ load_attributes_from_response(response)
513
+ end
514
+ end
515
+
516
+ def update(*args)
517
+ opts = args.extract_options!
518
+ # When we create we should not include any blank attributes
519
+ except = self.class.attribute_names - self.changed.symbolize_array
520
+ changed_associations = self.changed.symbolize_array.select{|item| self.association?(item)}
521
+ opts[:except] = opts[:except] ? opts[:except].concat(except).uniq.symbolize_array : except.symbolize_array
522
+ opts[:include_associations] = opts[:include_associations] ? opts[:include_associations].concat(args).concat(changed_associations).uniq : changed_associations.concat(args)
523
+ opts[:include_extras] ||= []
524
+ opts[:except] = [:id] if self.class.include_all_attributes_on_update
525
+ body = RestClient::Payload.has_file?(self.attributes) ? self.serializable_hash(opts) : encode(opts)
526
+ # We can just ignore the response
527
+ connection.put(element_path(self.id, prefix_options), body, self.class.headers).tap do |response|
528
+ load_attributes_from_response(response)
529
+ end
530
+ end
531
+
532
+ private
533
+
534
+ def split_options(options = {})
535
+ self.class.__send__(:split_options, options)
536
+ end
537
+
538
+ end
539
+
540
+ class Base
541
+ extend ActiveModel::Naming
542
+ # Order is important here
543
+ # It should be Validations, Dirty Tracking, Callbacks so the include order is the opposite
544
+ include Associations, Callbacks, Attributes, ModelErrors
545
+
546
+ end
547
+
548
+ end