api_resource 0.2.1

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 (59) hide show
  1. data/.document +5 -0
  2. data/.rspec +3 -0
  3. data/Gemfile +29 -0
  4. data/Gemfile.lock +152 -0
  5. data/Guardfile +22 -0
  6. data/LICENSE.txt +20 -0
  7. data/README.rdoc +19 -0
  8. data/Rakefile +49 -0
  9. data/VERSION +1 -0
  10. data/api_resource.gemspec +154 -0
  11. data/lib/api_resource.rb +129 -0
  12. data/lib/api_resource/association_activation.rb +19 -0
  13. data/lib/api_resource/associations.rb +169 -0
  14. data/lib/api_resource/associations/association_proxy.rb +115 -0
  15. data/lib/api_resource/associations/belongs_to_remote_object_proxy.rb +16 -0
  16. data/lib/api_resource/associations/dynamic_resource_scope.rb +23 -0
  17. data/lib/api_resource/associations/has_many_remote_object_proxy.rb +16 -0
  18. data/lib/api_resource/associations/has_one_remote_object_proxy.rb +24 -0
  19. data/lib/api_resource/associations/multi_argument_resource_scope.rb +15 -0
  20. data/lib/api_resource/associations/multi_object_proxy.rb +73 -0
  21. data/lib/api_resource/associations/related_object_hash.rb +12 -0
  22. data/lib/api_resource/associations/relation_scope.rb +30 -0
  23. data/lib/api_resource/associations/resource_scope.rb +34 -0
  24. data/lib/api_resource/associations/scope.rb +107 -0
  25. data/lib/api_resource/associations/single_object_proxy.rb +81 -0
  26. data/lib/api_resource/attributes.rb +162 -0
  27. data/lib/api_resource/base.rb +587 -0
  28. data/lib/api_resource/callbacks.rb +49 -0
  29. data/lib/api_resource/connection.rb +171 -0
  30. data/lib/api_resource/core_extensions.rb +7 -0
  31. data/lib/api_resource/custom_methods.rb +119 -0
  32. data/lib/api_resource/exceptions.rb +87 -0
  33. data/lib/api_resource/formats.rb +14 -0
  34. data/lib/api_resource/formats/json_format.rb +25 -0
  35. data/lib/api_resource/formats/xml_format.rb +36 -0
  36. data/lib/api_resource/local.rb +12 -0
  37. data/lib/api_resource/log_subscriber.rb +15 -0
  38. data/lib/api_resource/mocks.rb +269 -0
  39. data/lib/api_resource/model_errors.rb +86 -0
  40. data/lib/api_resource/observing.rb +29 -0
  41. data/lib/api_resource/railtie.rb +22 -0
  42. data/lib/api_resource/scopes.rb +45 -0
  43. data/spec/lib/associations_spec.rb +656 -0
  44. data/spec/lib/attributes_spec.rb +121 -0
  45. data/spec/lib/base_spec.rb +504 -0
  46. data/spec/lib/callbacks_spec.rb +68 -0
  47. data/spec/lib/connection_spec.rb +76 -0
  48. data/spec/lib/local_spec.rb +20 -0
  49. data/spec/lib/mocks_spec.rb +28 -0
  50. data/spec/lib/model_errors_spec.rb +29 -0
  51. data/spec/spec_helper.rb +36 -0
  52. data/spec/support/mocks/association_mocks.rb +46 -0
  53. data/spec/support/mocks/error_resource_mocks.rb +21 -0
  54. data/spec/support/mocks/test_resource_mocks.rb +43 -0
  55. data/spec/support/requests/association_requests.rb +14 -0
  56. data/spec/support/requests/error_resource_requests.rb +25 -0
  57. data/spec/support/requests/test_resource_requests.rb +31 -0
  58. data/spec/support/test_resource.rb +64 -0
  59. metadata +334 -0
@@ -0,0 +1,29 @@
1
+ module ApiResource
2
+
3
+ module Observing
4
+
5
+ extend ActiveSupport::Concern
6
+ include ActiveModel::Observing
7
+
8
+ # Redefine these methods to
9
+ included do
10
+ %w( create save update destroy ).each do |method|
11
+ alias_method_chain method, :observers
12
+ end
13
+
14
+ module InstanceMethods
15
+ %w( create save update destroy ).each do |method|
16
+ module_eval <<-EOE, __FILE__, __LINE__ + 1
17
+ def #{method}_with_observers(*args)
18
+ notify_observers(:before_#method)
19
+ if result = #{method}_without_observers(*args)
20
+ notify_observers(:after_#{method})
21
+ end
22
+ return result
23
+ end
24
+ EOE
25
+ end
26
+ end
27
+
28
+ end
29
+ end
@@ -0,0 +1,22 @@
1
+ require 'api_resource'
2
+ require 'rails'
3
+
4
+ module ApiResource
5
+
6
+ class Railtie < ::Rails::Railtie
7
+
8
+ config.api_resource = ActiveSupport::OrderedOptions.new
9
+
10
+ initializer "api_resource.set_configs" do |app|
11
+ app.config.api_resource.each do |k,v|
12
+ ApiResource::Base.send "#{k}=", v
13
+ end
14
+ end
15
+
16
+ initializer do
17
+ ApiResource::Associations.activate_active_record if defined?(ActiveRecord)
18
+ end
19
+
20
+ end
21
+
22
+ end
@@ -0,0 +1,45 @@
1
+ module ApiResource
2
+ module Scopes
3
+ extend ActiveSupport::Concern
4
+
5
+
6
+ module ClassMethods
7
+ def scopes
8
+ return self.related_objects[:scope]
9
+ end
10
+
11
+ def scope(name, hsh)
12
+ raise ArgumentError, "Expecting an attributes hash given #{hsh.inspect}" unless hsh.is_a?(Hash)
13
+ self.related_objects[:scope][name.to_sym] = hsh
14
+ # we also need to define a class method for each scope
15
+ self.instance_eval <<-EOE, __FILE__, __LINE__ + 1
16
+ def #{name}(*args)
17
+ return #{ApiResource::Associations::ResourceScope.class_factory(hsh)}.new(self, :#{name}, *args)
18
+ end
19
+ EOE
20
+ end
21
+
22
+ def scope?(name)
23
+ self.related_objects[:scope][name.to_sym].present?
24
+ end
25
+
26
+ def scope_attributes(name)
27
+ raise "No such scope #{name}" unless self.scope?(name)
28
+ self.related_objects[:scope][name.to_sym]
29
+ end
30
+ end
31
+ module InstanceMethods
32
+ def scopes
33
+ return self.class.scopes
34
+ end
35
+
36
+ def scope?(name)
37
+ return self.class.scope?(name)
38
+ end
39
+
40
+ def scope_attributes(name)
41
+ return self.class.scope_attributes(name)
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,656 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ include ApiResource
4
+
5
+ describe "Associations" do
6
+
7
+ after(:all) do
8
+ TestResource.reload_class_attributes
9
+ end
10
+
11
+ context "creating and testing for associations of various types" do
12
+
13
+ it "should be able to give a list of all associations" do
14
+ AllAssociations = Class.new(ApiResource::Base)
15
+ AllAssociations.class_eval do
16
+ has_many :has_many_objects
17
+ belongs_to :belongs_to_object
18
+ has_one :has_one_object
19
+ end
20
+ AllAssociations.association_names.sort.should eql [:has_many_objects, :belongs_to_object, :has_one_object].sort
21
+ AllAssociations.new.association_names.sort.should eql [:has_many_objects, :belongs_to_object, :has_one_object].sort
22
+ end
23
+
24
+ it "should be be able to define an asociation using a method named after that association type" do
25
+ TestResource.has_many :has_many_objects
26
+ TestResource.has_many?(:has_many_objects).should be_true
27
+ end
28
+
29
+ it "should be able to define associations with different class names" do
30
+ TestResource.has_many :test_name, :class_name => :has_many_objects
31
+ TestResource.has_many?(:test_name).should be_true
32
+ TestResource.has_many_class_name(:test_name).should eql("HasManyObject")
33
+ end
34
+
35
+ it "should be able to define multiple associations at the same time" do
36
+ TestResource.has_many :has_many_objects, :other_has_many_objects
37
+ TestResource.has_many?(:has_many_objects).should be_true
38
+ TestResource.has_many?(:other_has_many_objects).should be_true
39
+ end
40
+
41
+ it "should be able to tell if something is an association via the association? method" do
42
+ TestResource.belongs_to :belongs_to_object
43
+ TestResource.association?(:belongs_to_object).should be_true
44
+ end
45
+
46
+ it "should be able to get the class name of an association via the association_class_name method" do
47
+ TestResource.belongs_to :belongs_to_object
48
+ TestResource.association_class_name(:belongs_to_object).should eql("BelongsToObject")
49
+ TestResource.belongs_to :strange_name, :class_name => :belongs_to_object
50
+ TestResource.association_class_name(:strange_name).should eql("BelongsToObject")
51
+ end
52
+
53
+ it "should only define relationships for the given class - they should not cascade" do
54
+ TestResource.belongs_to :belongs_to_object
55
+ AnotherTestResource.association?(:belongs_to_object).should_not be true
56
+ end
57
+
58
+ it "should have its relationship cascade when sub-classed" do
59
+ TestResource.belongs_to :belongs_to_object
60
+ ChildTestResource.association?(:belongs_to_object).should be true
61
+ end
62
+
63
+ it "should have its relationship cascade when sub-classed after the relationship is defined" do
64
+ TestResource.belongs_to :belongs_to_object
65
+ class ChildTestResource2 < TestResource; end
66
+ ChildTestResource2.association?(:belongs_to_object).should be true
67
+ end
68
+
69
+ context "Determining associated classes with a namespace" do
70
+
71
+ it "should be able to find classes for associations that exist in the same module without a namespace" do
72
+ TestMod::TestClass.belongs_to :test_association
73
+ TestMod::TestClass.association_class_name(:test_association).should eql("TestMod::TestAssociation")
74
+ end
75
+
76
+ it "should be return a regular class name for a class defined at the root level" do
77
+ TestMod::TestClass.belongs_to :belongs_to_object
78
+ TestMod::TestClass.association_class_name(:belongs_to_object).should eql("BelongsToObject")
79
+ end
80
+
81
+ it "should work for a class name specified with a namespace module" do
82
+ TestMod::TestClass.belongs_to :nonsense, :class_name => "TestMod::TestAssociation"
83
+ TestMod::TestClass.association_class_name(:nonsense).should eql("TestMod::TestAssociation")
84
+ end
85
+
86
+ it "should work for nested module as well" do
87
+ TestMod::InnerMod::InnerClass.belongs_to :test_association
88
+ TestMod::InnerMod::InnerClass.association_class_name(:test_association).should eql("TestMod::TestAssociation")
89
+ end
90
+
91
+ it "should prefer to find classes within similar modules to ones in the root namespace" do
92
+ TestMod::InnerMod::InnerClass.belongs_to :test_resource
93
+ TestMod::InnerMod::InnerClass.association_class_name(:test_resource).should eql("TestMod::TestResource")
94
+ end
95
+
96
+ it "should be able to override into the root namespace by prefixing with ::" do
97
+ TestMod::InnerMod::InnerClass.belongs_to :test_resource, :class_name => "::TestResource"
98
+ TestMod::InnerMod::InnerClass.association_class_name(:test_resource).should eql("::TestResource")
99
+ end
100
+
101
+ end
102
+
103
+
104
+ end
105
+
106
+ context "Remote Definitions" do
107
+
108
+ before(:all) do
109
+ TestResource.reload_class_attributes
110
+ end
111
+
112
+ it "should be able define an association remotely" do
113
+ TestResource.belongs_to?(:belongs_to_object).should be true
114
+ TestResource.new.belongs_to_object.klass.should eql BelongsToObject
115
+ end
116
+
117
+ it "should be able define an association remotely" do
118
+ TestResource.belongs_to?(:custom_name).should be true
119
+ TestResource.new.custom_name.klass.should eql BelongsToObject
120
+ end
121
+
122
+ end
123
+
124
+
125
+ context "creating and testing for scopes" do
126
+
127
+ it "should be able to define scopes which require class names" do
128
+ lambda {
129
+ TestResource.scope :test_scope
130
+ }.should raise_error
131
+ TestResource.scope :test_scope, {:has_many_objects => "test"}
132
+ end
133
+
134
+ it "should be able to test if a scope exists" do
135
+ TestResource.scope :test_scope, {:item => "test"}
136
+ TestResource.scope?(:test_scope).should be_true
137
+ TestResource.scope_attributes(:test_scope).should eql({"item" => "test"})
138
+ end
139
+
140
+ end
141
+
142
+ context "testing for scopes and associations on an instance" do
143
+
144
+ it "should be able to define associations on a class and test for them on an instance" do
145
+ TestResource.has_many :has_many_objects, :class_name => :other_has_many_objects
146
+ tst = TestResource.new
147
+ tst.has_many?(:has_many_objects).should be_true
148
+ tst.has_many_class_name(:has_many_objects).should eql("OtherHasManyObject")
149
+ end
150
+
151
+ it "should be able to define scopes on a class and test for them on an instance" do
152
+ TestResource.scope :has_many_objects, {:item => "test"}
153
+ tst = TestResource.new
154
+ tst.scope?(:has_many_objects).should be_true
155
+ tst.scope_attributes(:has_many_objects).should eql({"item" => "test"})
156
+ end
157
+
158
+ end
159
+
160
+ describe "Single Object Associations" do
161
+
162
+ before(:all) do
163
+ TestResource.reload_class_attributes
164
+ end
165
+
166
+ after(:all) do
167
+ TestResource.reload_class_attributes
168
+ end
169
+
170
+ it "should return nil if its internal object is nil" do
171
+ ap = Associations::SingleObjectProxy.new("TestResource", {})
172
+ ap.instance_variable_set(:@internal_object, nil)
173
+ ap.blank?.should be_true
174
+ end
175
+
176
+ it "should not throw an error on serializable hash if its internal object is nil" do
177
+ ap = Associations::SingleObjectProxy.new("TestResource", {})
178
+ ap.instance_variable_set(:@internal_object, nil)
179
+ lambda {ap.serializable_hash}.should_not raise_error
180
+ end
181
+
182
+ it "should be able to create a SingleObjectProxy around a blank hash" do
183
+ ap = Associations::SingleObjectProxy.new("TestResource", {})
184
+ ap.remote_path.should be_blank
185
+ end
186
+
187
+ it "should be able to extract a service uri from the contents hash" do
188
+ ap = Associations::SingleObjectProxy.new("TestResource",{:service_uri => "/path"})
189
+ ap.remote_path.should eql("/path")
190
+ end
191
+
192
+ it "should be able to recognize the attributes of an object and not make them scopes" do
193
+ TestResource.define_attributes :test
194
+ ap = Associations::SingleObjectProxy.new("TestResource",{:service_uri => "/path", :test => "testval"})
195
+ ap.scope?("test").should be_false
196
+ ap.remote_path.should eql("/path")
197
+ end
198
+
199
+ it "should make all attributes except the service uri into scopes given the scopes_only option" do
200
+ ap = Associations::SingleObjectProxy.new("TestResource",{:service_uri => "/path", :test_scope => {"testval" => true}, :scopes_only => true})
201
+ ap.scope?("test_scope").should be_true
202
+ ap.remote_path.should eql("/path")
203
+ end
204
+
205
+ it "should pass the attributes that are not scopes and make them attributes of the object" do
206
+ TestResource.define_attributes :test
207
+ ap = Associations::SingleObjectProxy.new("TestResource",{:service_uri => "/path", :test => "testval"})
208
+ ap.internal_object.attributes.keys.should include("test")
209
+ end
210
+ end
211
+
212
+ describe "Multi Object Associations" do
213
+
214
+ before(:all) do
215
+ TestResource.related_objects[:scope].clear
216
+ end
217
+
218
+ after(:each) do
219
+ Associations::MultiObjectProxy.remote_path_element = :service_uri
220
+ end
221
+
222
+ describe "Loading settings" do
223
+
224
+ context "Loading array contents" do
225
+
226
+ it "should be able to load a blank array" do
227
+ ap = Associations::MultiObjectProxy.new("TestResource",[])
228
+ ap.remote_path.should be_nil
229
+ ap.scopes.keys.should eql([])
230
+ end
231
+
232
+ it "should be able to recognize a settings hash if it has a service_uri" do
233
+ ap = Associations::MultiObjectProxy.new("TestResource",[{:service_uri => "/route"}])
234
+ ap.remote_path.should eql("/route")
235
+ end
236
+
237
+ it "should be able to recognize a settings hash if it has a 'service_uri' with another preset name" do
238
+ Associations::MultiObjectProxy.remote_path_element = :the_element
239
+ ap = Associations::MultiObjectProxy.new("TestResource",[{:the_element => "/route"}])
240
+ ap.remote_path.should eql("/route")
241
+ end
242
+
243
+ end
244
+
245
+ context "Loading hash contents" do
246
+ it "should not be able to load a hash without a 'service_uri'" do
247
+ lambda {
248
+ Associations::MultiObjectProxy.new("TestResource", {:hi => 3})
249
+ }.should raise_error
250
+ end
251
+
252
+ it "should be able to recognize settings from a hash" do
253
+ ap = Associations::MultiObjectProxy.new("TestResource", {:service_uri => "/route"})
254
+ ap.remote_path.should eql("/route")
255
+ end
256
+
257
+ it "should recognize settings with differing 'service_uri' names" do
258
+ Associations::MultiObjectProxy.remote_path_element = :the_element
259
+ ap = Associations::MultiObjectProxy.new("TestResource",{:the_element => "/route"})
260
+ ap.remote_path.should eql("/route")
261
+ end
262
+ end
263
+
264
+ context "Defining scopes" do
265
+
266
+ it "should define scopes based on the other keys in a settings hash" do
267
+ ap = Associations::MultiObjectProxy.new("TestResource", [{:service_uri => "/route", :scope1 => {"scope1" => true}, :scope2 => {"scope2" => true}}])
268
+ [:scope1, :scope2].each{|s| ap.scopes.include?(s).should be_true }
269
+ end
270
+
271
+ it "should identify known scopes based on the scopes defined on the object it is a proxy to" do
272
+ TestResource.scope :class_scope, "class_scope" => "true"
273
+ ap = Associations::MultiObjectProxy.new("TestResource", [{:service_uri => "/route", :scope1 => {"scope1" => true}, :scope2 => {"scope2" => true}}])
274
+ [:scope1, :scope2, :class_scope].each{|s| ap.scopes.include?(s).should be_true}
275
+ end
276
+
277
+ it "scopes in the response should shadow class defined scopes" do
278
+ TestResource.scope :scope1, "scope1" => "true"
279
+ ap = Associations::MultiObjectProxy.new("TestResource", [{:service_uri => "/route", :scope1 => {"scope1" => true}, :scope2 => {"scope2" => true}}])
280
+ ap.scopes[:scope1].should eql({"scope1" => true})
281
+ end
282
+ end
283
+
284
+ end
285
+
286
+ describe "Selecting scopes" do
287
+
288
+ before(:all) do
289
+ ScopeResource.class_eval do
290
+ scope :one, :one => true
291
+ scope :two, :two => "test"
292
+ scope :three, :three => ["id"]
293
+ end
294
+ end
295
+
296
+
297
+ it "should be able to query scopes on the current model" do
298
+
299
+ ScopeResource.one.to_query.should eql "one=true"
300
+ ScopeResource.two("test").to_query.should eql "two=test"
301
+ ScopeResource.three(1,2,3).to_query.should eql "three[]=1&three[]=2&three[]=3"
302
+
303
+ ScopeResource.one.two("testing").three([2]).to_query.should eql "one=true&three[]=2&two=testing"
304
+ end
305
+
306
+
307
+ it "should be able to change scopes" do
308
+ ap = Associations::MultiObjectProxy.new("TestResource", [{:service_uri => "/route", :scope1 => {"scope1" => true}, :scope2 => {"scope2" => true}}])
309
+ ap.scope1.should be_a(Associations::RelationScope)
310
+ ap.scope1.current_scope.scope1_scope?.should be_true
311
+ end
312
+
313
+ it "should be able to chain scope calls together" do
314
+ ap = Associations::MultiObjectProxy.new("TestResource", [{:service_uri => "/route", :scope1 => {"scope1" => true}, :scope2 => {"scope2" => true}}])
315
+ ap.scope1.scope2.current_scope.scope1_and_scope2_scope?.should be_true
316
+ ap.scope1.scope2.to_query.should eql("scope1=true&scope2=true")
317
+ end
318
+
319
+ it "should support scopes that contain underscores" do
320
+ ap = Associations::MultiObjectProxy.new("TestResource", [{:service_uri => "/route", :scope_1 => {"scope_1" => true}, :scope_2 => {"scope_2" => true}}])
321
+ ap.scope_1.scope_2.current_scope.scope_1_and_scope_2_scope?.should be_true
322
+ end
323
+
324
+ it "should be able to return the current query string" do
325
+ ap = Associations::MultiObjectProxy.new("TestResource", [{:service_uri => "/route", :scope_1 => {"scope_1" => true}, :scope_2 => {"scope_2" => true}}])
326
+ ap.scope_1.scope_2.to_query.should eql("scope_1=true&scope_2=true")
327
+ end
328
+
329
+ it "should be able to substitute values into the scope query strings by passing a hash to the methods" do
330
+ ap = Associations::MultiObjectProxy.new("TestResource", [{:service_uri => "/route", :scope_1 => {"scope_1" => true, :test_sub => false}, :scope_2 => {"scope_2" => true}}])
331
+ obj = ap.scope_1(:test_sub => true).scope_2
332
+ obj.to_query.should eql("scope_1=true&scope_2=true&test_sub=true")
333
+ end
334
+ end
335
+
336
+
337
+ end
338
+
339
+ describe "Loading and Caching loaded data" do
340
+
341
+ context "Single Object" do
342
+
343
+ before(:all) do
344
+ TestResource.reload_class_attributes
345
+ end
346
+
347
+ it "should be able to force load an object" do
348
+ ap = Associations::SingleObjectProxy.new("TestResource",{:service_uri => '/single_object_association', :active => {:active => true}, :scopes_only => true})
349
+ ap.loaded.should be_blank
350
+ name = ap.name
351
+ name.should_not be_blank
352
+ ap.loaded.should_not be_blank
353
+ # Make sure it isn't reloaded
354
+ ap.name.should eql(name)
355
+ end
356
+
357
+ it "should be able to load a scope" do
358
+ ap = Associations::SingleObjectProxy.new("TestResource",{:service_uri => '/single_object_association', :active => {:active => true}, :scopes_only => true})
359
+ ap.internal_object.active.should be_false
360
+ ap.times_loaded.should eql(1)
361
+ ap.active.should be_a Associations::RelationScope
362
+ ap.active.name.should_not be_blank
363
+ ap.times_loaded.should eql(2)
364
+ ap.active.internal_object.active.should be_true
365
+ # another check that the resource wasn't reloaded
366
+ ap.times_loaded.should eql(2)
367
+ end
368
+
369
+ it "should be able to load a chain of scopes" do
370
+ ap = Associations::SingleObjectProxy.new("TestResource",{:service_uri => '/single_object_association', :active => {:active => true}, :with_birthday => {:birthday => true}, :scopes_only => true})
371
+ first = ap.active.with_birthday.id
372
+ ap.with_birthday.active.id.should eql(first)
373
+ ap.times_loaded.should eql(1)
374
+ ap.active.with_birthday.birthday.should_not be_blank
375
+ end
376
+
377
+ it "should be able to load a chain of scopes with substitution" do
378
+ ap = Associations::SingleObjectProxy.new("TestResource",{:service_uri => '/single_object_association', :active => {:active => false}, :with_birthday => {:birthday => true}, :scopes_only => true})
379
+ item = ap.active(:active => true).internal_object
380
+ item.active.should be_true
381
+ ap.times_loaded.should eql(1)
382
+ # This error and incrementing times_loaded means it tried to load again
383
+ lambda {
384
+ ap.active.internal_object
385
+ }.should raise_error
386
+ ap.times_loaded.should eql(2)
387
+ end
388
+
389
+ it "should proxy unknown methods to the object loading if it hasn't already" do
390
+ ap = Associations::SingleObjectProxy.new("TestResource",{:service_uri => '/single_object_association', :active => {:active => false}, :with_birthday => {:birthday => true}, :scopes_only => true})
391
+ ap.times_loaded.should eql(0)
392
+ ap.id.should_not be_blank
393
+ ap.times_loaded.should eql(1)
394
+ end
395
+
396
+ it "should only load each distinct set of scopes once" do
397
+ ap = Associations::SingleObjectProxy.new("TestResource",{:service_uri => '/single_object_association', :active => {:active => false}, :with_birthday => {:birthday => true}, :scopes_only => true})
398
+ ap.times_loaded.should eql(0)
399
+ ap.active(:active => true).with_birthday.internal_object
400
+ ap.with_birthday.active(:active => true).internal_object
401
+ ap.times_loaded.should eql(1)
402
+ end
403
+
404
+ it "should be able to clear it's loading cache" do
405
+ ap = Associations::SingleObjectProxy.new("TestResource",{:service_uri => '/single_object_association', :active => {:active => true}, :with_birthday => {:birthday => true}, :scopes_only => true})
406
+ ap.active.internal_object
407
+ ap.times_loaded.should eql(1)
408
+ ap.reload
409
+ ap.active.internal_object
410
+ ap.times_loaded.should eql(1)
411
+ end
412
+
413
+ end
414
+
415
+ context "Multi Object" do
416
+
417
+ it "should be able to load 'all'" do
418
+ ap = Associations::MultiObjectProxy.new("TestResource",{:service_uri => '/multi_object_association', :active => {:active => false}, :inactive => {:active => false}, :with_birthday => {:birthday => true}})
419
+ results = ap.all
420
+ results.size.should eql(5)
421
+ results.first.active.should be_false
422
+ end
423
+
424
+ it "should be able to load a scope" do
425
+ ap = Associations::MultiObjectProxy.new("TestResource",{:service_uri => '/multi_object_association', :active => {:active => false}, :inactive => {:active => false}, :with_birthday => {:birthday => true}})
426
+ results = ap.active.internal_object
427
+ results.size.should eql(5)
428
+ results.first.active.should be_true
429
+ end
430
+
431
+ it "should be able to load a chain of scopes" do
432
+ ap = Associations::MultiObjectProxy.new("TestResource",{:service_uri => '/multi_object_association', :active => {:active => true}, :inactive => {:active => false}, :with_birthday => {:birthday => true}})
433
+ results = ap.active.with_birthday.internal_object
434
+ results.first.active.should be_true
435
+ results.first.birthday.should_not be_blank
436
+ end
437
+
438
+ it "should be able to load a chain of scopes with substitution" do
439
+ ap = Associations::MultiObjectProxy.new("TestResource",{:service_uri => '/multi_object_association', :active => {:active => true}, :inactive => {:active => false}, :with_birthday => {:birthday => true}})
440
+ results = ap.inactive(:active => true).with_birthday.internal_object
441
+ results.first.active.should be_true
442
+ results.first.birthday.should_not be_blank
443
+ end
444
+
445
+ it "should proxy unknown methods to the object array loading if it hasn't already" do
446
+ ap = Associations::MultiObjectProxy.new("TestResource",{:service_uri => '/multi_object_association', :active => {:active => true}, :inactive => {:active => false}, :with_birthday => {:birthday => true}})
447
+ ap.first.should be_a TestResource
448
+ ap.active.first.should be_a TestResource
449
+ ap.times_loaded.should eql(2)
450
+ end
451
+
452
+ it "should only load each distinct set of scopes once" do
453
+ ap = Associations::MultiObjectProxy.new("TestResource",{:service_uri => '/multi_object_association', :active => {:active => true}, :inactive => {:active => false}, :with_birthday => {:birthday => true}})
454
+ ap.first
455
+ ap.active.first
456
+ ap.times_loaded.should eql(2)
457
+ ap.active.first
458
+ ap.times_loaded.should eql(2)
459
+ ap.active.with_birthday.first
460
+ ap.with_birthday.active.first
461
+ ap.times_loaded.should eql(3)
462
+ end
463
+
464
+ it "should be able to clear it's loading cache" do
465
+ ap = Associations::MultiObjectProxy.new("TestResource",{:service_uri => '/multi_object_association', :active => {:active => true}, :inactive => {:active => false}, :with_birthday => {:birthday => true}})
466
+ ap.active.first
467
+ ap.times_loaded.should eql(1)
468
+ ap.reload
469
+ ap.active.first
470
+ ap.times_loaded.should eql(1)
471
+ end
472
+
473
+ it "should be enumerable" do
474
+ ap = Associations::MultiObjectProxy.new("TestResource",{:service_uri => '/multi_object_association', :active => {:active => true}, :inactive => {:active => false}, :with_birthday => {:birthday => true}})
475
+ ap.each do |tr|
476
+ tr.name.should_not be_blank
477
+ end
478
+ end
479
+
480
+ end
481
+
482
+ context "Scopes" do
483
+
484
+ before(:all) do
485
+ TestResource.reload_class_attributes
486
+ end
487
+
488
+ it "should define class methods for the known scopes" do
489
+ TestResource.scopes.each do |key, _|
490
+ TestResource.should respond_to key
491
+ end
492
+ end
493
+
494
+ it "should return a ResourceScope when calling any scope on a class" do
495
+ TestResource.send(TestResource.scopes.first.first.to_sym).should be_a Associations::ResourceScope
496
+ end
497
+
498
+ it "should be able to chain scopes" do
499
+ scp = TestResource.active.paginate
500
+ scp.should be_a Associations::ResourceScope
501
+ scp.to_query.should eql("active=true&current_page=current_page&paginate=true&per_page=per_page")
502
+ end
503
+
504
+ it "should load when calling all" do
505
+ TestResource.active.should respond_to :all
506
+ TestResource.active.should respond_to :internal_object
507
+ results = TestResource.active.all
508
+ results.should be_a Array
509
+ results.size.should eql(5)
510
+ results.each do |res|
511
+ res.should be_a TestResource
512
+ end
513
+ end
514
+
515
+ it "should load when calling an enumerable method or an array method" do
516
+ TestResource.active.each do |result|
517
+ result.should be_a TestResource
518
+ end
519
+ end
520
+ end
521
+
522
+ context "Assigning Data" do
523
+ context "Single Object Association" do
524
+ before(:all) do
525
+ TestResource.has_one(:has_one_object)
526
+ TestResource.belongs_to(:belongs_to_object)
527
+ end
528
+ after(:all) do
529
+ TestResource.reload_class_attributes
530
+ end
531
+
532
+ it "should assign associations to the correct type on initialization" do
533
+ tr = TestResource.new(:has_one_object => {:name => "Dan"}, :belongs_to_object => {:name => "Dan"})
534
+
535
+ tr.has_one_object.internal_object.should be_instance_of HasOneObject
536
+ tr.belongs_to_object.internal_object.should be_instance_of BelongsToObject
537
+
538
+ end
539
+
540
+ it "should assign associations to the correct type when setting attributes directly" do
541
+ tr = TestResource.new()
542
+ tr.has_one_object = {:name => "Dan"}
543
+ tr.belongs_to_object = {:name => "Dan"}
544
+
545
+ tr.has_one_object.internal_object.should be_instance_of HasOneObject
546
+ tr.belongs_to_object.internal_object.should be_instance_of BelongsToObject
547
+ end
548
+
549
+
550
+ end
551
+
552
+ context "Single Object Association" do
553
+ before(:all) do
554
+ TestResource.has_many(:has_many_objects)
555
+ end
556
+ after(:all) do
557
+ TestResource.reload_class_attributes
558
+ end
559
+
560
+ it "should assign associations to the correct type on initialization" do
561
+ tr = TestResource.new(:has_many_objects => [{:name => "Dan"}])
562
+ tr.has_many_objects.internal_object.first.should be_instance_of HasManyObject
563
+
564
+ end
565
+
566
+ it "should assign associations to the correct type when setting attributes directly" do
567
+ tr = TestResource.new()
568
+ tr.has_many_objects = [{:name => "Dan"}]
569
+ tr.has_many_objects.internal_object.first.should be_instance_of HasManyObject
570
+ end
571
+
572
+
573
+ end
574
+
575
+ context "ActiveModel" do
576
+ before(:all) do
577
+ require 'active_record'
578
+ ActiveRecord::Base.establish_connection({"adapter" => "sqlite3", "database" => "/tmp/api_resource_test_db.sqlite"})
579
+ ActiveRecord::Base.connection.create_table(:test_ars, :force => true) do |t|
580
+ t.integer(:test_resource_id)
581
+ end
582
+ ApiResource::Associations.activate_active_record
583
+ TestAR = Class.new(ActiveRecord::Base)
584
+ TestAR.class_eval do
585
+ belongs_to_remote :my_favorite_thing, :class_name => "TestClassYay"
586
+ end
587
+ end
588
+ it "should define remote association types for AR" do
589
+ [:has_many_remote, :belongs_to_remote, :has_one_remote].each do |assoc|
590
+ ActiveRecord::Base.singleton_methods.should include assoc
591
+ end
592
+ end
593
+ it "should add remote associations to related objects" do
594
+ TestAR.related_objects.should eql({"has_many_remote"=>{}, "belongs_to_remote"=>{"my_favorite_thing"=>"TestClassYay"}, "has_one_remote"=>{}, "scope"=>{}})
595
+ end
596
+ context "Not Overriding Scopes" do
597
+ it "should not override scopes, which would raise an error with lambda-style scopes" do
598
+ lambda {
599
+ TestAR.class_eval do
600
+ scope :my_favorite_scope, lambda {
601
+ joins(:my_test)
602
+ }
603
+ end
604
+ }.should_not raise_error
605
+ end
606
+ end
607
+ context "Belongs To" do
608
+
609
+ before(:all) do
610
+ TestAR.class_eval do
611
+ belongs_to_remote :test_resource
612
+ end
613
+ end
614
+
615
+ it "should attempt to load a single remote object for a belongs_to relationship" do
616
+ tar = TestAR.new
617
+ tar.stubs(:test_resource_id).returns(1)
618
+ TestResource.connection.expects(:get).with("/test_resources/1.json").once.returns({"name" => "testing"})
619
+ # load the test resource
620
+ tar.test_resource.name.should eql "testing"
621
+ end
622
+
623
+ end
624
+ context "Has One" do
625
+ before(:all) do
626
+ TestAR.class_eval do
627
+ has_one_remote :test_resource
628
+ end
629
+ end
630
+ it "should attempt to load a single remote object for a has_one relationship" do
631
+ tar = TestAR.new
632
+ tar.stubs(:id).returns(1)
633
+ TestResource.connection.expects(:get).with("/test_resources.json?test_ar_id=1").once.returns([{"name" => "testing"}])
634
+ # load the test resource
635
+ tar.test_resource.name.should eql "testing"
636
+ end
637
+ end
638
+ context "Has Many" do
639
+ before(:all) do
640
+ TestAR.class_eval do
641
+ has_many_remote :has_many_objects
642
+ end
643
+ end
644
+ it "should attempt to load a collection of remote objects for a has_many relationship" do
645
+ tar = TestAR.new
646
+ tar.stubs(:id).returns(1)
647
+ HasManyObject.connection.expects(:get).with("/has_many_objects.json?test_ar_id=1").once.returns([{"name" => "testing"}])
648
+ # load the test resource
649
+ tar.has_many_objects.first.name.should eql "testing"
650
+ end
651
+
652
+ end
653
+ end
654
+ end
655
+ end
656
+ end