seomoz-ripple 1.0.0.pre

Sign up to get free protection for your applications and to get access to all the features.
Files changed (149) hide show
  1. data/Gemfile +20 -0
  2. data/Guardfile +15 -0
  3. data/Rakefile +88 -0
  4. data/lib/rails/generators/ripple/configuration/configuration_generator.rb +13 -0
  5. data/lib/rails/generators/ripple/configuration/templates/ripple.yml +24 -0
  6. data/lib/rails/generators/ripple/js/js_generator.rb +13 -0
  7. data/lib/rails/generators/ripple/js/templates/js/contrib.js +63 -0
  8. data/lib/rails/generators/ripple/js/templates/js/iso8601.js +76 -0
  9. data/lib/rails/generators/ripple/js/templates/js/ripple.js +132 -0
  10. data/lib/rails/generators/ripple/model/model_generator.rb +20 -0
  11. data/lib/rails/generators/ripple/model/templates/model.rb +10 -0
  12. data/lib/rails/generators/ripple/observer/observer_generator.rb +16 -0
  13. data/lib/rails/generators/ripple/observer/templates/observer.rb +4 -0
  14. data/lib/rails/generators/ripple/test/templates/test_server.rb +46 -0
  15. data/lib/rails/generators/ripple/test/test_generator.rb +39 -0
  16. data/lib/rails/generators/ripple_generator.rb +78 -0
  17. data/lib/ripple.rb +79 -0
  18. data/lib/ripple/associations.rb +356 -0
  19. data/lib/ripple/associations/embedded.rb +35 -0
  20. data/lib/ripple/associations/instantiators.rb +26 -0
  21. data/lib/ripple/associations/linked.rb +65 -0
  22. data/lib/ripple/associations/many.rb +38 -0
  23. data/lib/ripple/associations/many_embedded_proxy.rb +38 -0
  24. data/lib/ripple/associations/many_linked_proxy.rb +66 -0
  25. data/lib/ripple/associations/many_reference_proxy.rb +93 -0
  26. data/lib/ripple/associations/many_stored_key_proxy.rb +76 -0
  27. data/lib/ripple/associations/one.rb +20 -0
  28. data/lib/ripple/associations/one_embedded_proxy.rb +35 -0
  29. data/lib/ripple/associations/one_key_proxy.rb +58 -0
  30. data/lib/ripple/associations/one_linked_proxy.rb +22 -0
  31. data/lib/ripple/associations/one_stored_key_proxy.rb +43 -0
  32. data/lib/ripple/associations/proxy.rb +118 -0
  33. data/lib/ripple/attribute_methods.rb +118 -0
  34. data/lib/ripple/attribute_methods/dirty.rb +50 -0
  35. data/lib/ripple/attribute_methods/query.rb +34 -0
  36. data/lib/ripple/attribute_methods/read.rb +26 -0
  37. data/lib/ripple/attribute_methods/write.rb +25 -0
  38. data/lib/ripple/callbacks.rb +74 -0
  39. data/lib/ripple/conflict/basic_resolver.rb +82 -0
  40. data/lib/ripple/conflict/document_hooks.rb +20 -0
  41. data/lib/ripple/conflict/resolver.rb +71 -0
  42. data/lib/ripple/conflict/test_helper.rb +33 -0
  43. data/lib/ripple/conversion.rb +28 -0
  44. data/lib/ripple/core_ext.rb +2 -0
  45. data/lib/ripple/core_ext/casting.rb +148 -0
  46. data/lib/ripple/core_ext/object.rb +8 -0
  47. data/lib/ripple/document.rb +104 -0
  48. data/lib/ripple/document/bucket_access.rb +25 -0
  49. data/lib/ripple/document/finders.rb +131 -0
  50. data/lib/ripple/document/key.rb +43 -0
  51. data/lib/ripple/document/link.rb +30 -0
  52. data/lib/ripple/document/persistence.rb +115 -0
  53. data/lib/ripple/embedded_document.rb +64 -0
  54. data/lib/ripple/embedded_document/around_callbacks.rb +18 -0
  55. data/lib/ripple/embedded_document/finders.rb +26 -0
  56. data/lib/ripple/embedded_document/persistence.rb +77 -0
  57. data/lib/ripple/i18n.rb +2 -0
  58. data/lib/ripple/inspection.rb +32 -0
  59. data/lib/ripple/locale/en.yml +21 -0
  60. data/lib/ripple/nested_attributes.rb +265 -0
  61. data/lib/ripple/observable.rb +28 -0
  62. data/lib/ripple/properties.rb +73 -0
  63. data/lib/ripple/property_type_mismatch.rb +12 -0
  64. data/lib/ripple/railtie.rb +13 -0
  65. data/lib/ripple/serialization.rb +84 -0
  66. data/lib/ripple/timestamps.rb +27 -0
  67. data/lib/ripple/translation.rb +14 -0
  68. data/lib/ripple/validations.rb +67 -0
  69. data/lib/ripple/validations/associated_validator.rb +43 -0
  70. data/ripple.gemspec +46 -0
  71. data/seomoz-ripple.gemspec +46 -0
  72. data/spec/fixtures/config.yml +8 -0
  73. data/spec/integration/ripple/associations_spec.rb +220 -0
  74. data/spec/integration/ripple/conflict_resolution_spec.rb +293 -0
  75. data/spec/integration/ripple/nested_attributes_spec.rb +264 -0
  76. data/spec/integration/ripple/persistence_spec.rb +57 -0
  77. data/spec/integration/ripple/search_associations_spec.rb +42 -0
  78. data/spec/ripple/associations/many_embedded_proxy_spec.rb +122 -0
  79. data/spec/ripple/associations/many_linked_proxy_spec.rb +191 -0
  80. data/spec/ripple/associations/many_reference_proxy_spec.rb +170 -0
  81. data/spec/ripple/associations/many_stored_key_proxy_spec.rb +158 -0
  82. data/spec/ripple/associations/one_embedded_proxy_spec.rb +125 -0
  83. data/spec/ripple/associations/one_key_proxy_spec.rb +82 -0
  84. data/spec/ripple/associations/one_linked_proxy_spec.rb +91 -0
  85. data/spec/ripple/associations/one_stored_key_proxy_spec.rb +72 -0
  86. data/spec/ripple/associations/proxy_spec.rb +84 -0
  87. data/spec/ripple/associations_spec.rb +129 -0
  88. data/spec/ripple/attribute_methods/dirty_spec.rb +80 -0
  89. data/spec/ripple/attribute_methods_spec.rb +230 -0
  90. data/spec/ripple/bucket_access_spec.rb +25 -0
  91. data/spec/ripple/callbacks_spec.rb +176 -0
  92. data/spec/ripple/conflict/resolver_spec.rb +42 -0
  93. data/spec/ripple/conversion_spec.rb +22 -0
  94. data/spec/ripple/core_ext_spec.rb +103 -0
  95. data/spec/ripple/document/link_spec.rb +67 -0
  96. data/spec/ripple/document_spec.rb +96 -0
  97. data/spec/ripple/embedded_document/finders_spec.rb +29 -0
  98. data/spec/ripple/embedded_document/persistence_spec.rb +80 -0
  99. data/spec/ripple/embedded_document_spec.rb +84 -0
  100. data/spec/ripple/finders_spec.rb +217 -0
  101. data/spec/ripple/inspection_spec.rb +51 -0
  102. data/spec/ripple/key_spec.rb +30 -0
  103. data/spec/ripple/observable_spec.rb +121 -0
  104. data/spec/ripple/persistence_spec.rb +326 -0
  105. data/spec/ripple/properties_spec.rb +262 -0
  106. data/spec/ripple/ripple_spec.rb +71 -0
  107. data/spec/ripple/serialization_spec.rb +51 -0
  108. data/spec/ripple/timestamps_spec.rb +76 -0
  109. data/spec/ripple/validations/associated_validator_spec.rb +77 -0
  110. data/spec/ripple/validations_spec.rb +104 -0
  111. data/spec/spec_helper.rb +26 -0
  112. data/spec/support/associations.rb +1 -0
  113. data/spec/support/associations/proxies.rb +17 -0
  114. data/spec/support/integration_setup.rb +11 -0
  115. data/spec/support/mocks.rb +4 -0
  116. data/spec/support/models.rb +4 -0
  117. data/spec/support/models/address.rb +12 -0
  118. data/spec/support/models/box.rb +13 -0
  119. data/spec/support/models/car.rb +16 -0
  120. data/spec/support/models/cardboard_box.rb +3 -0
  121. data/spec/support/models/clock.rb +12 -0
  122. data/spec/support/models/clock_observer.rb +3 -0
  123. data/spec/support/models/company.rb +23 -0
  124. data/spec/support/models/customer.rb +4 -0
  125. data/spec/support/models/driver.rb +6 -0
  126. data/spec/support/models/email.rb +4 -0
  127. data/spec/support/models/engine.rb +5 -0
  128. data/spec/support/models/family.rb +16 -0
  129. data/spec/support/models/favorite.rb +4 -0
  130. data/spec/support/models/invoice.rb +7 -0
  131. data/spec/support/models/late_invoice.rb +3 -0
  132. data/spec/support/models/ninja.rb +9 -0
  133. data/spec/support/models/note.rb +5 -0
  134. data/spec/support/models/page.rb +4 -0
  135. data/spec/support/models/paid_invoice.rb +4 -0
  136. data/spec/support/models/passenger.rb +6 -0
  137. data/spec/support/models/profile.rb +10 -0
  138. data/spec/support/models/seat.rb +5 -0
  139. data/spec/support/models/subscription.rb +27 -0
  140. data/spec/support/models/tasks.rb +14 -0
  141. data/spec/support/models/team.rb +11 -0
  142. data/spec/support/models/transactions.rb +17 -0
  143. data/spec/support/models/tree.rb +4 -0
  144. data/spec/support/models/user.rb +10 -0
  145. data/spec/support/models/wheel.rb +6 -0
  146. data/spec/support/models/widget.rb +22 -0
  147. data/spec/support/test_server.rb +18 -0
  148. data/spec/support/test_server.yml.example +2 -0
  149. metadata +362 -0
@@ -0,0 +1,191 @@
1
+ require 'spec_helper'
2
+
3
+ describe Ripple::Associations::ManyLinkedProxy do
4
+ require 'support/models/tasks'
5
+
6
+ before :each do
7
+ @person = Person.new {|p| p.key = "riak-user" }
8
+ @task = Task.new {|t| t.key = "one" }
9
+ @other_task = Task.new {|t| t.key = "two" }
10
+ @third_task = Task.new {|t| t.key = "three" }
11
+ [@person, @task, @other_task].each do |doc|
12
+ doc.stub!(:new?).and_return(false)
13
+ end
14
+ end
15
+
16
+ it "should be empty before any associated documents are set" do
17
+ @person.tasks.should be_empty
18
+ end
19
+
20
+ it "should accept an array of documents" do
21
+ @person.tasks = [@task]
22
+ end
23
+
24
+ it "should set the links on the RObject when assigning" do
25
+ @person.tasks = [@task]
26
+ @person.robject.links.should include(@task.to_link("tasks"))
27
+ end
28
+
29
+ it "should be able to replace the entire collection of documents (even appended ones)" do
30
+ @person.tasks << @task
31
+ @person.tasks = [@other_task]
32
+ @person.tasks.should == [@other_task]
33
+ end
34
+
35
+ it "should return the assigned documents when assigning" do
36
+ t = (@person.tasks = [@task])
37
+ t.should == [@task]
38
+ end
39
+
40
+ it "should link-walk to the associated documents when accessing" do
41
+ @person.robject.links << @task.to_link("tasks")
42
+ @person.robject.should_receive(:walk).with(Riak::WalkSpec.new(:bucket => "tasks", :tag => "tasks")).and_return([])
43
+ @person.tasks.should == []
44
+ end
45
+
46
+ it "handles conflict appropriately by selecting the linked-walk robjects that match the links" do
47
+ @person.robject.links << @task.to_link("tasks") << @other_task.to_link("tasks")
48
+ @person.robject.
49
+ should_receive(:walk).
50
+ with(Riak::WalkSpec.new(:bucket => "tasks", :tag => "tasks")).
51
+ and_return([[@task.robject, @other_task.robject, @third_task.robject]])
52
+
53
+ @person.tasks.should == [@task, @other_task]
54
+ end
55
+
56
+ it "allows the links to be replaced directly" do
57
+ @person.tasks = [@task]
58
+ @person.tasks.__send__(:should_receive, :reset)
59
+ @person.tasks.__send__(:links).should == [@task.robject.to_link("tasks")]
60
+ @person.tasks.replace_links([@other_task, @third_task].map { |t| t.robject.to_link("tasks") })
61
+ @person.tasks.__send__(:links).should == [@other_task, @third_task].map { |t| t.robject.to_link("tasks") }
62
+ end
63
+
64
+ it "should replace associated documents with a new set" do
65
+ @person.tasks = [@task]
66
+ @person.tasks = [@other_task]
67
+ @person.tasks.should == [@other_task]
68
+ end
69
+
70
+ it "asks the keys set for the count to avoid having to unnecessarily load all documents" do
71
+ @person.tasks.keys.stub(:size => 17)
72
+ @person.tasks.count.should == 17
73
+ end
74
+
75
+ # it "should be able to build a new associated document" do
76
+ # pending "Need unsaved document support"
77
+ # end
78
+
79
+ it "should return an array from to_ary" do
80
+ @person.tasks << @task
81
+ @person.tasks.to_ary.should == [@task]
82
+ end
83
+
84
+ it "should refuse assigning a collection of the wrong type" do
85
+ lambda { @person.tasks = nil }.should raise_error
86
+ lambda { @person.tasks = @task }.should raise_error
87
+ lambda { @person.tasks = [@person] }.should raise_error
88
+ end
89
+
90
+ describe "#<< (when the target has not already been loaded)" do
91
+ it "avoids link-walking when adding a record to an unloaded association" do
92
+ @person.robject.should_not_receive(:walk)
93
+ @person.tasks << @task
94
+ end
95
+
96
+ it "should be able to count the associated documents" do
97
+ @person.tasks << @task
98
+ @person.tasks.count.should == 1
99
+ @person.tasks << @other_task
100
+ @person.tasks.count.should == 2
101
+ end
102
+
103
+ it "maintains the list of keys properly as new documents are appended" do
104
+ @person.tasks << @task
105
+ @person.tasks.should have(1).key
106
+ @person.tasks << @other_task
107
+ @person.tasks.should have(2).keys
108
+ end
109
+
110
+ it "should be able to append documents to the associated set" do
111
+ @person.tasks << @task
112
+ @person.tasks << @other_task
113
+ @person.should have(2).tasks
114
+ end
115
+
116
+ it "should be able to chain calls to adding documents" do
117
+ @person.tasks << @task << @other_task
118
+ @person.should have(2).tasks
119
+ end
120
+
121
+ it "should set the links on the RObject when appending" do
122
+ @person.tasks << @task << @other_task
123
+ [@task, @other_task].each do |t|
124
+ @person.robject.links.should include(t.to_link("tasks"))
125
+ end
126
+ end
127
+
128
+ it "does not return duplicates (for when the object has been appended and it's robject is found while walking the links)" do
129
+ @person.tasks.stub(:robjects => [@task.robject])
130
+ @person.tasks.reset
131
+ @person.tasks << @task
132
+ @person.tasks.should == [@task]
133
+ end
134
+ end
135
+
136
+ describe "#reset" do
137
+ it "clears appended documents" do
138
+ @person.tasks << @task
139
+ @person.tasks.reset
140
+ @person.tasks.should == []
141
+ end
142
+ end
143
+
144
+ describe "#keys" do
145
+ let(:link_keys) { %w[ 1 2 3 ] }
146
+ let(:links) { link_keys.map { |k| Riak::Link.new('tasks', k, 'task') } }
147
+
148
+ before(:each) do
149
+ @person.tasks.stub(:links => links)
150
+ end
151
+
152
+ it "returns a set of keys" do
153
+ @person.tasks.keys.should be_a(Set)
154
+ @person.tasks.keys.to_a.should == link_keys
155
+ end
156
+
157
+ it "is memoized between calls" do
158
+ @person.tasks.keys.should equal(@person.tasks.keys)
159
+ end
160
+
161
+ it "is cleared when the association is reset" do
162
+ orig_set = @person.tasks.keys
163
+ @person.tasks.reset
164
+ @person.tasks.keys.should_not equal(orig_set)
165
+ end
166
+
167
+ it "is cleared when the association is replaced" do
168
+ orig_set = @person.tasks.keys
169
+ @person.tasks.replace([@task])
170
+ @person.tasks.keys.should_not equal(orig_set)
171
+ end
172
+ end
173
+
174
+ describe "#include?" do
175
+ it "delegates to the set of keys so as not to unnecessarily load the associated documents" do
176
+ @person.tasks.keys.should_receive(:include?).with(@task.key).and_return(true)
177
+ @person.tasks.include?(@task).should be_true
178
+ end
179
+
180
+ it "short-circuits and returns false if the given object is not a ripple document" do
181
+ @person.tasks.keys.should_not_receive(:include?)
182
+ @person.tasks.include?(Object.new).should be_false
183
+ end
184
+
185
+ it "returns false if the document's bucket is different from the associations bucket, even if the keys are the same" do
186
+ @person.tasks << @task
187
+ other_person = Person.new { |p| p.key = @task.key }
188
+ @person.tasks.include?(other_person).should be_false
189
+ end
190
+ end
191
+ end
@@ -0,0 +1,170 @@
1
+ require 'spec_helper'
2
+
3
+ describe Ripple::Associations::ManyReferenceProxy do
4
+ require 'support/models/transactions'
5
+
6
+ before :each do
7
+ @account = Account.new {|e| e.key = "accounty"}
8
+ @payment_method = PaymentMethod.new {|e| e.key = "paymadoo"}
9
+ @other_payment_method = PaymentMethod.new {|e| e.key = "otherpaym"}
10
+ Ripple.client.stub(:search => {"response" => {"docs" => []}})
11
+ end
12
+
13
+ it "should be empty before any associated documents are set" do
14
+ @account.payment_methods.should be_empty
15
+ end
16
+
17
+ it "should accept an array of documents" do
18
+ @account.payment_methods = [@payment_method]
19
+ end
20
+
21
+ it "should set the key on the sub object when assigning" do
22
+ @account.payment_methods = [@payment_method]
23
+ @payment_method.account_key.should == "accounty"
24
+ end
25
+
26
+ it "should be able to replace the entire collection of documents (even appended ones)" do
27
+ @account.payment_methods << @payment_method
28
+ @account.payment_methods = [@other_payment_method]
29
+ @account.payment_methods.should == [@other_payment_method]
30
+ end
31
+
32
+ it "should return the assigned documents when assigning" do
33
+ t = (@account.payment_methods = [@payment_method])
34
+ t.should == [@payment_method]
35
+ end
36
+
37
+ it "should find the associated documents when accessing" do
38
+ Ripple.client.should_receive(:search).with("payment_methods", "account_key: accounty").and_return({"response" => {"docs" => ["id" => "paymadoo"]}})
39
+ PaymentMethod.should_receive(:find).with(["paymadoo"]).and_return([@payment_method])
40
+ @account.payment_methods.should == [@payment_method]
41
+ end
42
+
43
+ it "should replace associated documents with a new set" do
44
+ @account.payment_methods = [@payment_method]
45
+ @account.payment_methods = [@other_payment_method]
46
+ @account.payment_methods.should == [@other_payment_method]
47
+ end
48
+
49
+ it "should return an array from to_ary" do
50
+ @account.payment_methods << @payment_method
51
+ @account.payment_methods.to_ary.should == [@payment_method]
52
+ end
53
+
54
+ it "should refuse assigning a collection of the wrong type" do
55
+ lambda { @account.payment_methods = nil }.should raise_error
56
+ lambda { @account.payment_methods = @payment_method }.should raise_error
57
+ lambda { @account.payment_methods = [@account] }.should raise_error
58
+ end
59
+
60
+ describe "#<< (when the target has not already been loaded)" do
61
+ it "avoids searching when adding a record to an unloaded association" do
62
+ PaymentMethod.should_not_receive(:search)
63
+ @account.payment_methods << @payment_method
64
+ end
65
+
66
+ it "should be able to count the associated documents" do
67
+ @account.payment_methods << @payment_method
68
+ @account.payment_methods.count.should == 1
69
+ @account.payment_methods << @other_payment_method
70
+ @account.payment_methods.count.should == 2
71
+ end
72
+
73
+ it "should be able to count without loading documents" do
74
+ Ripple.client.stub(:search => {"response" => {"docs" => [{"id" => @payment_method.key}, {"id" => @other_payment_method.key}]}})
75
+ PaymentMethod.should_not_receive(:find)
76
+ @account.payment_methods.count.should == 2
77
+ end
78
+
79
+ it "should be able to append documents to the associated set" do
80
+ @account.payment_methods << @payment_method
81
+ @account.payment_methods << @other_payment_method
82
+ @account.should have(2).payment_methods
83
+ end
84
+
85
+ it "should be able to chain calls to adding documents" do
86
+ @account.payment_methods << @payment_method << @other_payment_method
87
+ @account.should have(2).payment_methods
88
+ end
89
+
90
+ it "should assign the keys on the sub object when appending" do
91
+ @account.payment_methods << @payment_method << @other_payment_method
92
+ [@payment_method, @other_payment_method].each do |t|
93
+ t.account_key.should == "accounty"
94
+ end
95
+ end
96
+
97
+ it "does not return duplicates (for when the object has been appended and it's robject is found while walking the links)" do
98
+ @account.payment_methods.stub(:find_target => Set.new([@payment_method]))
99
+ @account.payment_methods.reset
100
+ @account.payment_methods << @payment_method
101
+ @account.payment_methods.should == [@payment_method]
102
+ end
103
+ end
104
+
105
+ describe "#reset" do
106
+ it "clears appended documents" do
107
+ @account.payment_methods << @payment_method
108
+ @account.payment_methods.reset
109
+ @account.payment_methods.should == []
110
+ end
111
+ end
112
+
113
+ describe "#keys" do
114
+ let(:ze_keys) { %w(1 2 3) }
115
+ let(:search_results) do
116
+ {"response" => {"docs" => ze_keys.map { |k| {"id" => k} }}}
117
+ end
118
+
119
+ before(:each) do
120
+ Ripple.client.stub(:search => search_results)
121
+ PaymentMethod.stub(:find => [@payment_method])
122
+ end
123
+
124
+ it "returns a set of keys" do
125
+ @account.payment_methods.keys.should be_a(Set)
126
+ @account.payment_methods.keys.to_a.should == ze_keys
127
+ end
128
+
129
+ it "is memoized between calls" do
130
+ @account.payment_methods.keys.should equal(@account.payment_methods.keys)
131
+ end
132
+
133
+ it "is cleared when the association is reset" do
134
+ orig_set = @account.payment_methods.keys
135
+ @account.payment_methods.reset
136
+ @account.payment_methods.keys.should_not equal(orig_set)
137
+ end
138
+
139
+ it "is cleared when the association is replaced" do
140
+ orig_set = @account.payment_methods.keys
141
+ @account.payment_methods.replace([@payment_method])
142
+ @account.payment_methods.keys.should_not equal(orig_set)
143
+ end
144
+
145
+ it "maintains the list of keys properly as new documents are appended" do
146
+ @account.payment_methods << @payment_method
147
+ @account.payment_methods.should have(1).key
148
+ @account.payment_methods << @other_payment_method
149
+ @account.payment_methods.should have(2).keys
150
+ end
151
+ end
152
+
153
+ describe "#include?" do
154
+ it "delegates to the set of keys so as not to unnecessarily load the associated documents" do
155
+ @account.payment_methods.keys.should_receive(:include?).with(@payment_method.key).and_return(true)
156
+ @account.payment_methods.include?(@payment_method).should be_true
157
+ end
158
+
159
+ it "short-circuits and returns false if the given object is not a ripple document" do
160
+ @account.payment_methods.keys.should_not_receive(:include?)
161
+ @account.payment_methods.include?(Object.new).should be_false
162
+ end
163
+
164
+ it "returns false if the document's bucket is different from the associations bucket, even if the keys are the same" do
165
+ @account.payment_methods << @payment_method
166
+ other_account = Account.new { |p| p.key = @payment_method.key }
167
+ @account.payment_methods.include?(other_account).should be_false
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,158 @@
1
+ require 'spec_helper'
2
+
3
+ describe Ripple::Associations::ManyStoredKeyProxy do
4
+ require 'support/models/transactions'
5
+
6
+ before :each do
7
+ @account = Account.new {|t| t.key = "accounty" }
8
+ @transaction_one = Transaction.new {|t| t.key = "one" }
9
+ @transaction_two = Transaction.new {|t| t.key = "two" }
10
+ @transaction_three = Transaction.new {|t| t.key = "three" }
11
+ @transaction_one.stub(:new_record?).and_return(false)
12
+ @transaction_two.stub(:new_record?).and_return(false)
13
+ @transaction_three.stub(:new_record?).and_return(false)
14
+ end
15
+
16
+ it "should be empty before any associated documents are set" do
17
+ @account.transactions.should be_empty
18
+ end
19
+
20
+ it "should allow appending" do
21
+ @account.transactions << @transaction_one
22
+ @account.transactions.should == [@transaction_one]
23
+ @account.transaction_keys.should == ["one"]
24
+ end
25
+
26
+ it "should be able to chain calls to adding documents" do
27
+ @account.transactions << @transaction_one << @transaction_two
28
+ @account.transactions.should == [@transaction_one, @transaction_two]
29
+ end
30
+
31
+ it "creates the right type of key collection" do
32
+ Object.module_eval do
33
+ class DifferentlyKeyedAccount
34
+ include Ripple::Document
35
+ property :transaction_keys, SortedSet
36
+ many :transactions, :using => :stored_key
37
+ end
38
+ end
39
+
40
+ account = DifferentlyKeyedAccount.new
41
+ account.transactions << @transaction_one
42
+ account.transaction_keys.should == SortedSet.new(["one"])
43
+ end
44
+
45
+ it "should accept an array of documents" do
46
+ @account.transactions = [@transaction_one]
47
+ @account.transactions.should == [@transaction_one]
48
+ @account.transaction_keys.should == %w(one)
49
+ end
50
+
51
+ it "should be able to replace the entire collection of documents (even appended ones)" do
52
+ @account.transactions << @transaction_one
53
+ @account.transactions = [@transaction_two]
54
+ @account.transactions.should == [@transaction_two]
55
+ @account.transaction_keys.should == %w(two)
56
+ end
57
+
58
+ it "should return the assigned documents when assigning" do
59
+ t = (@account.transactions = [@transaction_one])
60
+ t.should == [@transaction_one]
61
+ end
62
+
63
+ it "asks the keys set for the count to avoid having to unnecessarily load all documents" do
64
+ @account.transactions << @transaction_one
65
+ @account.transaction_keys.stub(:size => 17)
66
+ @account.transactions.count.should == 17
67
+ end
68
+
69
+ it "should return an array from to_ary" do
70
+ @account.transactions << @transaction_one
71
+ @account.transactions.to_ary.should == [@transaction_one]
72
+ end
73
+
74
+ it "should refuse assigning a collection of the wrong type" do
75
+ lambda { @account.transactions = nil }.should raise_error
76
+ lambda { @account.transactions = @transaction_one }.should raise_error
77
+ lambda { @account.transactions = [@account] }.should raise_error
78
+ end
79
+
80
+ it "should refuse appending a document of the wrong type" do
81
+ lambda { @account.transactions << Account.new }.should raise_error
82
+ end
83
+
84
+ describe "#reset" do
85
+ it "clears appended documents" do
86
+ @account.transactions << @transaction_one
87
+ @account.transactions.reset
88
+ @account.transactions.should == []
89
+ end
90
+
91
+ it "resets to the saved state of the proxy" do
92
+ Transaction.stub(:find).and_return([@transaction_one])
93
+ @account.transactions << @transaction_two
94
+ @account.transactions.reset
95
+ Transaction.stub(:find).and_return([@transaction_one])
96
+ @account.transactions.should == [ @transaction_one ]
97
+ end
98
+ end
99
+
100
+ describe "#include?" do
101
+ it "delegates to the set of keys so as not to unnecessarily load the associated documents" do
102
+ @account.transactions.keys.should_receive(:include?).with(@transaction_two.key).and_return(true)
103
+ @account.transactions.include?(@transaction_two).should be_true
104
+ end
105
+
106
+ it "short-circuits and returns false if the given object is not a ripple document" do
107
+ @account.transactions.keys.should_not_receive(:include?)
108
+ @account.transactions.include?(Object.new).should be_false
109
+ end
110
+
111
+ it "returns false if the document's bucket is different from the associations bucket, even if the keys are the same" do
112
+ @account.transactions << @transaction_one
113
+ other_account = Account.new { |p| p.key = @transaction_one.key }
114
+ @account.transactions.include?(other_account).should be_false
115
+ end
116
+ end
117
+
118
+ describe "#keys" do
119
+ before do
120
+ @account.transactions << @transaction_one << @transaction_two
121
+ end
122
+
123
+ it "returns a set of keys" do
124
+ @account.transactions.keys.should be_a(Array)
125
+ @account.transactions.keys.to_a.should == %w(one two)
126
+ end
127
+
128
+ it "is memoized between calls" do
129
+ @account.transactions.keys.should equal(@account.transactions.keys)
130
+ end
131
+
132
+ it "is cleared when the association is reset" do
133
+ orig_set = @account.transactions.keys
134
+ @account.transactions.reset
135
+ @account.transactions.keys.should_not equal(orig_set)
136
+ @account.transactions.keys.should == []
137
+ end
138
+
139
+ it "is cleared when the association is replaced" do
140
+ orig_set = @account.transactions.keys
141
+ @account.transactions.replace([@transaction_one])
142
+ @account.transactions.keys.should_not equal(orig_set)
143
+ @account.transactions.keys.to_a.should == %w(one)
144
+ end
145
+
146
+ it "maintains the list of keys properly as new documents are appended" do
147
+ @account.transactions.keys.size.should == 2
148
+ @account.transactions << @transaction_three
149
+ @account.transactions.keys.size.should == 3
150
+ end
151
+
152
+ end
153
+
154
+ it "temporarily bombs if the document you're appending isn't saved. This behavior shouldn't last long." do
155
+ lambda { @account.transactions << Transaction.new }.should raise_error
156
+ end
157
+
158
+ end