api_resource 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
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