wyngle-ripple 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (170) hide show
  1. data/.gitignore +35 -0
  2. data/Gemfile +17 -0
  3. data/Gemfile.lock +133 -0
  4. data/Gemfile.rails30 +3 -0
  5. data/Gemfile.rails31 +3 -0
  6. data/Gemfile.rails32 +3 -0
  7. data/Guardfile +17 -0
  8. data/LICENSE +16 -0
  9. data/README.markdown +166 -0
  10. data/RELEASE_NOTES.textile +286 -0
  11. data/Rakefile +63 -0
  12. data/lib/rails/generators/ripple/configuration/configuration_generator.rb +13 -0
  13. data/lib/rails/generators/ripple/configuration/templates/ripple.yml +25 -0
  14. data/lib/rails/generators/ripple/js/js_generator.rb +13 -0
  15. data/lib/rails/generators/ripple/js/templates/js/contrib.js +63 -0
  16. data/lib/rails/generators/ripple/js/templates/js/iso8601.js +76 -0
  17. data/lib/rails/generators/ripple/js/templates/js/ripple.js +132 -0
  18. data/lib/rails/generators/ripple/model/model_generator.rb +20 -0
  19. data/lib/rails/generators/ripple/model/templates/model.rb.erb +10 -0
  20. data/lib/rails/generators/ripple/observer/observer_generator.rb +16 -0
  21. data/lib/rails/generators/ripple/observer/templates/observer.rb.erb +2 -0
  22. data/lib/rails/generators/ripple/test/templates/cucumber.rb.erb +7 -0
  23. data/lib/rails/generators/ripple/test/test_generator.rb +45 -0
  24. data/lib/rails/generators/ripple_generator.rb +79 -0
  25. data/lib/ripple.rb +86 -0
  26. data/lib/ripple/associations.rb +380 -0
  27. data/lib/ripple/associations/embedded.rb +35 -0
  28. data/lib/ripple/associations/instantiators.rb +26 -0
  29. data/lib/ripple/associations/linked.rb +65 -0
  30. data/lib/ripple/associations/many.rb +38 -0
  31. data/lib/ripple/associations/many_embedded_proxy.rb +39 -0
  32. data/lib/ripple/associations/many_linked_proxy.rb +66 -0
  33. data/lib/ripple/associations/many_reference_proxy.rb +95 -0
  34. data/lib/ripple/associations/many_stored_key_proxy.rb +76 -0
  35. data/lib/ripple/associations/one.rb +20 -0
  36. data/lib/ripple/associations/one_embedded_proxy.rb +35 -0
  37. data/lib/ripple/associations/one_key_proxy.rb +58 -0
  38. data/lib/ripple/associations/one_linked_proxy.rb +26 -0
  39. data/lib/ripple/associations/one_stored_key_proxy.rb +43 -0
  40. data/lib/ripple/associations/proxy.rb +118 -0
  41. data/lib/ripple/attribute_methods.rb +132 -0
  42. data/lib/ripple/attribute_methods/dirty.rb +59 -0
  43. data/lib/ripple/attribute_methods/query.rb +34 -0
  44. data/lib/ripple/attribute_methods/read.rb +28 -0
  45. data/lib/ripple/attribute_methods/write.rb +25 -0
  46. data/lib/ripple/callbacks.rb +71 -0
  47. data/lib/ripple/conflict/basic_resolver.rb +86 -0
  48. data/lib/ripple/conflict/document_hooks.rb +46 -0
  49. data/lib/ripple/conflict/resolver.rb +96 -0
  50. data/lib/ripple/conflict/test_helper.rb +34 -0
  51. data/lib/ripple/conversion.rb +29 -0
  52. data/lib/ripple/core_ext.rb +3 -0
  53. data/lib/ripple/core_ext/casting.rb +151 -0
  54. data/lib/ripple/core_ext/indexes.rb +89 -0
  55. data/lib/ripple/core_ext/object.rb +8 -0
  56. data/lib/ripple/document.rb +105 -0
  57. data/lib/ripple/document/bucket_access.rb +25 -0
  58. data/lib/ripple/document/finders.rb +131 -0
  59. data/lib/ripple/document/key.rb +35 -0
  60. data/lib/ripple/document/link.rb +30 -0
  61. data/lib/ripple/document/persistence.rb +130 -0
  62. data/lib/ripple/embedded_document.rb +63 -0
  63. data/lib/ripple/embedded_document/around_callbacks.rb +18 -0
  64. data/lib/ripple/embedded_document/finders.rb +26 -0
  65. data/lib/ripple/embedded_document/persistence.rb +75 -0
  66. data/lib/ripple/i18n.rb +5 -0
  67. data/lib/ripple/indexes.rb +151 -0
  68. data/lib/ripple/inspection.rb +32 -0
  69. data/lib/ripple/locale/en.yml +26 -0
  70. data/lib/ripple/locale/fr.yml +24 -0
  71. data/lib/ripple/nested_attributes.rb +275 -0
  72. data/lib/ripple/observable.rb +28 -0
  73. data/lib/ripple/properties.rb +74 -0
  74. data/lib/ripple/property_type_mismatch.rb +12 -0
  75. data/lib/ripple/railtie.rb +26 -0
  76. data/lib/ripple/railties/ripple.rake +68 -0
  77. data/lib/ripple/serialization.rb +82 -0
  78. data/lib/ripple/test_server.rb +35 -0
  79. data/lib/ripple/timestamps.rb +25 -0
  80. data/lib/ripple/translation.rb +18 -0
  81. data/lib/ripple/validations.rb +65 -0
  82. data/lib/ripple/validations/associated_validator.rb +43 -0
  83. data/lib/ripple/version.rb +3 -0
  84. data/ripple.gemspec +29 -0
  85. data/spec/fixtures/config.yml +8 -0
  86. data/spec/generators/ripple/configuration_generator_spec.rb +9 -0
  87. data/spec/generators/ripple/js_generator_spec.rb +14 -0
  88. data/spec/generators/ripple/model_generator_spec.rb +64 -0
  89. data/spec/generators/ripple/observer_generator_spec.rb +20 -0
  90. data/spec/generators/ripple/test_generator_spec.rb +118 -0
  91. data/spec/generators/ripple_generator_spec.rb +11 -0
  92. data/spec/integration/ripple/associations_spec.rb +164 -0
  93. data/spec/integration/ripple/conflict_resolution_spec.rb +329 -0
  94. data/spec/integration/ripple/indexes_spec.rb +47 -0
  95. data/spec/integration/ripple/nested_attributes_spec.rb +261 -0
  96. data/spec/integration/ripple/persistence_spec.rb +36 -0
  97. data/spec/integration/ripple/search_associations_spec.rb +31 -0
  98. data/spec/ripple/associations/many_embedded_proxy_spec.rb +119 -0
  99. data/spec/ripple/associations/many_linked_proxy_spec.rb +191 -0
  100. data/spec/ripple/associations/many_reference_proxy_spec.rb +170 -0
  101. data/spec/ripple/associations/many_stored_key_proxy_spec.rb +158 -0
  102. data/spec/ripple/associations/one_embedded_proxy_spec.rb +125 -0
  103. data/spec/ripple/associations/one_key_proxy_spec.rb +82 -0
  104. data/spec/ripple/associations/one_linked_proxy_spec.rb +91 -0
  105. data/spec/ripple/associations/one_stored_key_proxy_spec.rb +72 -0
  106. data/spec/ripple/associations/proxy_spec.rb +84 -0
  107. data/spec/ripple/associations_spec.rb +153 -0
  108. data/spec/ripple/attribute_methods/dirty_spec.rb +80 -0
  109. data/spec/ripple/attribute_methods_spec.rb +286 -0
  110. data/spec/ripple/bucket_access_spec.rb +25 -0
  111. data/spec/ripple/callbacks_spec.rb +195 -0
  112. data/spec/ripple/conflict/resolver_spec.rb +42 -0
  113. data/spec/ripple/conversion_spec.rb +14 -0
  114. data/spec/ripple/core_ext_spec.rb +181 -0
  115. data/spec/ripple/document/link_spec.rb +67 -0
  116. data/spec/ripple/document_spec.rb +96 -0
  117. data/spec/ripple/embedded_document/finders_spec.rb +29 -0
  118. data/spec/ripple/embedded_document/persistence_spec.rb +80 -0
  119. data/spec/ripple/embedded_document_spec.rb +84 -0
  120. data/spec/ripple/finders_spec.rb +220 -0
  121. data/spec/ripple/indexes_spec.rb +111 -0
  122. data/spec/ripple/inspection_spec.rb +51 -0
  123. data/spec/ripple/key_spec.rb +31 -0
  124. data/spec/ripple/observable_spec.rb +120 -0
  125. data/spec/ripple/persistence_spec.rb +351 -0
  126. data/spec/ripple/properties_spec.rb +262 -0
  127. data/spec/ripple/ripple_spec.rb +71 -0
  128. data/spec/ripple/serialization_spec.rb +51 -0
  129. data/spec/ripple/timestamps_spec.rb +83 -0
  130. data/spec/ripple/validations/associated_validator_spec.rb +77 -0
  131. data/spec/ripple/validations_spec.rb +102 -0
  132. data/spec/spec_helper.rb +39 -0
  133. data/spec/support/associations.rb +1 -0
  134. data/spec/support/associations/proxies.rb +17 -0
  135. data/spec/support/generator_setup.rb +26 -0
  136. data/spec/support/integration_setup.rb +11 -0
  137. data/spec/support/models.rb +35 -0
  138. data/spec/support/models/address.rb +10 -0
  139. data/spec/support/models/box.rb +13 -0
  140. data/spec/support/models/car.rb +41 -0
  141. data/spec/support/models/cardboard_box.rb +2 -0
  142. data/spec/support/models/clock.rb +10 -0
  143. data/spec/support/models/clock_observer.rb +2 -0
  144. data/spec/support/models/company.rb +23 -0
  145. data/spec/support/models/credit_card.rb +5 -0
  146. data/spec/support/models/customer.rb +3 -0
  147. data/spec/support/models/email.rb +3 -0
  148. data/spec/support/models/family.rb +16 -0
  149. data/spec/support/models/favorite.rb +3 -0
  150. data/spec/support/models/indexer.rb +26 -0
  151. data/spec/support/models/invoice.rb +6 -0
  152. data/spec/support/models/late_invoice.rb +2 -0
  153. data/spec/support/models/nested.rb +12 -0
  154. data/spec/support/models/ninja.rb +7 -0
  155. data/spec/support/models/note.rb +4 -0
  156. data/spec/support/models/page.rb +3 -0
  157. data/spec/support/models/paid_invoice.rb +3 -0
  158. data/spec/support/models/post.rb +13 -0
  159. data/spec/support/models/profile.rb +7 -0
  160. data/spec/support/models/subscription.rb +26 -0
  161. data/spec/support/models/tasks.rb +9 -0
  162. data/spec/support/models/team.rb +11 -0
  163. data/spec/support/models/transactions.rb +17 -0
  164. data/spec/support/models/tree.rb +3 -0
  165. data/spec/support/models/user.rb +19 -0
  166. data/spec/support/models/widget.rb +23 -0
  167. data/spec/support/search.rb +14 -0
  168. data/spec/support/test_server.rb +39 -0
  169. data/spec/support/test_server.yml.example +14 -0
  170. metadata +434 -0
@@ -0,0 +1,14 @@
1
+ require 'spec_helper'
2
+ require 'rails/generators/ripple/js/js_generator'
3
+
4
+ describe Ripple::Generators::JsGenerator do
5
+ before { run_generator }
6
+
7
+ it "should create an app/mapreduce directory" do
8
+ file('app/mapreduce').should exist
9
+ end
10
+
11
+ it "should copy all standard JS files into the mapreduce directory" do
12
+ Dir[file('app/mapreduce/*')].sort.map{|f| File.basename(f) }.should == %w{contrib.js iso8601.js ripple.js}
13
+ end
14
+ end
@@ -0,0 +1,64 @@
1
+ require 'spec_helper'
2
+ require 'rails/generators/ripple/model/model_generator'
3
+
4
+ shared_examples_for :model_generator do
5
+ it("should create the model file"){ model_file.should exist }
6
+ it { should contain(class_decl) }
7
+ it("should create the attribute declarations") do
8
+ attributes.each do |name, type|
9
+ should contain("property :#{name}, #{type}")
10
+ end
11
+ end
12
+ end
13
+
14
+ shared_examples_for :subclass_model_generator do
15
+ it_behaves_like :model_generator
16
+ it { should_not contain("include Ripple::Document") }
17
+ it { should_not contain("include Ripple::EmbeddedDocument") }
18
+ end
19
+
20
+ shared_examples_for :embedded_document_generator do
21
+ it_behaves_like :model_generator
22
+ it { should contain("include Ripple::EmbeddedDocument") }
23
+ end
24
+
25
+ shared_examples_for :document_generator do
26
+ it_behaves_like :model_generator
27
+ it { should contain("include Ripple::Document") }
28
+ end
29
+
30
+ describe Ripple::Generators::ModelGenerator do
31
+ let(:cli){ %w{general_model} }
32
+ let(:model_file){ file('app/models/general_model.rb') }
33
+ let(:class_decl){ "class GeneralModel" }
34
+ let(:attributes){ {} }
35
+ subject { model_file }
36
+ before { run_generator cli }
37
+
38
+ describe "generating a bare model" do
39
+ it_behaves_like :document_generator
40
+ end
41
+
42
+ describe "generating with attributes" do
43
+ let(:cli){ %w{general_model name:string shipped:datetime size:integer} }
44
+ let(:attributes) { {:name => String, :shipped => Time, :size => Integer } }
45
+ it_behaves_like :document_generator
46
+ end
47
+
48
+ describe "generating a model with a parent class" do
49
+ let(:cli){ %w{general_model --parent=widget} }
50
+ let(:class_decl){ "class GeneralModel < Widget" }
51
+ it_behaves_like :subclass_model_generator
52
+ end
53
+
54
+ describe "generating an embedded model" do
55
+ let(:cli){ %w{general_model --embedded} }
56
+ it_behaves_like :embedded_document_generator
57
+ end
58
+
59
+ describe "generating a model embedded in a parent document" do
60
+ let(:cli){ %w{general_model --embedded-in=widget} }
61
+ it_behaves_like :embedded_document_generator
62
+ it { should contain("embedded_in :widget") }
63
+ end
64
+ end
@@ -0,0 +1,20 @@
1
+ require 'spec_helper'
2
+ require 'rails/generators/ripple/observer/observer_generator'
3
+
4
+ describe Ripple::Generators::ObserverGenerator do
5
+ context "in the top-level scope" do
6
+ before { run_generator %w{person} }
7
+ subject{ file('app/models/person_observer.rb') }
8
+
9
+ it { should exist }
10
+ it { should contain("class PersonObserver < ActiveModel::Observer") }
11
+ end
12
+
13
+ context "in a nested scope" do
14
+ before { run_generator %w{profiles/social} }
15
+ subject { file('app/models/profiles/social_observer.rb') }
16
+
17
+ it { should exist }
18
+ it { should contain("class Profiles::SocialObserver < ActiveModel::Observer") }
19
+ end
20
+ end
@@ -0,0 +1,118 @@
1
+ require 'spec_helper'
2
+ require 'rails/generators/ripple/test/test_generator'
3
+
4
+ describe Ripple::Generators::TestGenerator do
5
+ context "when Cucumber is present" do
6
+ before { mkdir_p file('features/support') }
7
+ before { run_generator }
8
+
9
+ it "should create a support file for the test server" do
10
+ file('features/support/ripple.rb').should exist
11
+ end
12
+ end
13
+
14
+ context "when RSpec is present" do
15
+ let(:original_contents) do
16
+ [
17
+ "require 'rspec'",
18
+ "RSpec.configure do |config|",
19
+ " config.mock_with :rspec",
20
+ "end"
21
+ ]
22
+ end
23
+ let(:helper) { file('spec/spec_helper.rb') }
24
+ let(:contents) { File.read(helper) }
25
+ before do
26
+ mkdir_p file('spec')
27
+ File.open(helper,'w') do |f|
28
+ f.write original_contents.join("\n")
29
+ end
30
+ run_generator
31
+ end
32
+
33
+ it "should insert the test server require" do
34
+ contents.should include("require 'ripple/test_server'")
35
+ end
36
+
37
+ it "should insert the test server setup" do
38
+ contents.should include(" config.before(:suite) { Ripple::TestServer.setup }")
39
+ contents.should include(" config.after(:each) { Ripple::TestServer.clear }")
40
+ contents.should include(" config.after(:suite) { Ripple::TestServer.instance.stop }")
41
+ end
42
+
43
+ context "when the configuration block is indented" do
44
+ let(:original_contents) do
45
+ [
46
+ "require 'rspec'",
47
+ "require 'spork'",
48
+ "Spork.prefork do",
49
+ " RSpec.configure do |config|",
50
+ " config.mock_with :rspec",
51
+ " end",
52
+ "end"
53
+ ]
54
+ end
55
+
56
+ it "should insert the test server require with additional indentation" do
57
+ contents.should include(" require 'ripple/test_server'")
58
+ end
59
+
60
+ it "should insert the test server setup with additional indentation" do
61
+ contents.should include(" config.before(:suite) { Ripple::TestServer.setup }")
62
+ contents.should include(" config.after(:each) { Ripple::TestServer.clear }")
63
+ contents.should include(" config.after(:suite) { Ripple::TestServer.instance.stop }")
64
+ end
65
+ end
66
+ end
67
+
68
+ context "when Test::Unit is present" do
69
+ let(:original_contents) do
70
+ [
71
+ "require 'active_support/test_case'",
72
+ "class ActiveSupport::TestCase",
73
+ " setup :load_fixtures",
74
+ "end"
75
+ ]
76
+ end
77
+ let(:helper) { file('test/test_helper.rb') }
78
+ let(:contents) { File.read(helper) }
79
+ before do
80
+ mkdir_p file('test')
81
+ File.open(helper,'w') do |f|
82
+ f.write original_contents.join("\n")
83
+ end
84
+ run_generator
85
+ end
86
+
87
+ it "should insert the test server require" do
88
+ contents.should include("require 'ripple/test_server'")
89
+ end
90
+
91
+ it "should insert the test server setup and teardown" do
92
+ contents.should include(" setup { Ripple::TestServer.setup }")
93
+ contents.should include(" teardown { Ripple::TestServer.clear }")
94
+ end
95
+
96
+ context "when the test case class is indented" do
97
+ let(:original_contents) do
98
+ [
99
+ "require 'active_support/test_case'",
100
+ "module MyApp",
101
+ " class ActiveSupport::TestCase",
102
+ " setup :load_fixtures",
103
+ " end",
104
+ "end"
105
+ ]
106
+ end
107
+
108
+ it "should insert the test server require with additional indentation" do
109
+ contents.should include(" require 'ripple/test_server'")
110
+ end
111
+
112
+ it "should insert the test server setup and teardown with additional indentation" do
113
+ contents.should include(" setup { Ripple::TestServer.setup }")
114
+ contents.should include(" teardown { Ripple::TestServer.clear }")
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,11 @@
1
+ require 'spec_helper'
2
+ require 'rails/generators/ripple_generator'
3
+
4
+ describe RippleGenerator do
5
+ it "should invoke the sub-generators" do
6
+ %w{configuration js test}.each do |gen|
7
+ generator.should_receive(:invoke).with("ripple:#{gen}")
8
+ end
9
+ run_generator
10
+ end
11
+ end
@@ -0,0 +1,164 @@
1
+ require 'spec_helper'
2
+
3
+ describe "Ripple Associations" do
4
+ before :each do
5
+ @user = User.new(:email => 'riak@ripple.com')
6
+ @profile = UserProfile.new(:name => 'Ripple')
7
+ @billing = Address.new(:street => '123 Somewhere Dr', :kind => 'billing')
8
+ @shipping = Address.new(:street => '321 Anywhere Pl', :kind => 'shipping')
9
+ @friend1 = User.create(:email => "friend@ripple.com")
10
+ @friend2 = User.create(:email => "friend2@ripple.com")
11
+ @cc = CreditCard.new(:number => '12345')
12
+ @post = Post.new(:title => "Hello, world!")
13
+ @comment_one = Comment.new.tap{|c| c.key = "one"; c.save! }
14
+ @comment_two = Comment.new.tap{|c| c.key = "two"; c.save! }
15
+ end
16
+
17
+ it "should save and restore a many stored key association" do
18
+ @post.comments << @comment_one << @comment_two
19
+ @post.save!
20
+
21
+ post = Post.find(@post.key)
22
+ post.comment_keys.should == [ 'one', 'two' ]
23
+ post.comments.keys.should == [ 'one', 'two' ]
24
+ post.comments.should == [ @comment_one, @comment_two ]
25
+ end
26
+
27
+ it "should remove a document from a many stored key association" do
28
+ @post.comments << @comment_one
29
+ @post.comments << @comment_two
30
+ @post.save!
31
+ @post.comments.delete(@comment_one)
32
+ @post.save!
33
+
34
+ @post = Post.find(@post.key)
35
+ @post.comment_keys.should == [ @comment_two.key ]
36
+ @post.comments.should == [ @comment_two ]
37
+ end
38
+
39
+ it "should save one embedded associations" do
40
+ @user.user_profile = @profile
41
+ @user.save
42
+ @found = User.find(@user.key)
43
+ @found.user_profile.name.should == 'Ripple'
44
+ @found.user_profile.should be_a(UserProfile)
45
+ @found.user_profile.user.should == @found
46
+ end
47
+
48
+ it "should not raise an error when a one linked associated record has been deleted" do
49
+ @user.emergency_contact = @friend1
50
+ @user.save
51
+
52
+ @friend1.destroy
53
+ @found = User.find(@user.key)
54
+ @found.emergency_contact.should be_nil
55
+ end
56
+
57
+ it "should allow a many linked record to be deleted from the association but kept in the datastore" do
58
+ @user.friends << @friend1
59
+ @user.save!
60
+
61
+ @user.friends.delete(@friend1)
62
+ @user.save!
63
+
64
+ found_user = User.find(@user.key)
65
+ found_user.friends.should be_empty
66
+ User.find(@friend1.key).should be
67
+ end
68
+
69
+ it "should allow a many embedded record to be deleted from the association" do
70
+ @user.addresses << @billing << @shipping
71
+ @user.save!
72
+
73
+ @user.addresses.delete(@billing)
74
+ @user.save!
75
+ User.find(@user.key).addresses.should == [@shipping]
76
+ end
77
+
78
+ it "should save many embedded associations" do
79
+ @user.addresses << @billing << @shipping
80
+ @user.save
81
+ @found = User.find(@user.key)
82
+ @found.addresses.count.should == 2
83
+ @bill = @found.addresses.detect {|a| a.kind == 'billing'}
84
+ @ship = @found.addresses.detect {|a| a.kind == 'shipping'}
85
+ @bill.street.should == '123 Somewhere Dr'
86
+ @ship.street.should == '321 Anywhere Pl'
87
+ @bill.user.should == @found
88
+ @ship.user.should == @found
89
+ @bill.should be_a(Address)
90
+ @ship.should be_a(Address)
91
+ end
92
+
93
+ it "should save a many linked association" do
94
+ @user.friends << @friend1 << @friend2
95
+ @user.save
96
+ @user.should_not be_new_record
97
+ @found = User.find(@user.key)
98
+ @found.friends.should include(@friend1)
99
+ @found.friends.should include(@friend2)
100
+ end
101
+
102
+ it "should save a one linked association" do
103
+ @user.emergency_contact = @friend1
104
+ @user.save
105
+ @user.should_not be_new_record
106
+ @found = User.find(@user.key)
107
+ @found.emergency_contact.should == @friend1
108
+ end
109
+
110
+ it "should reload associations" do
111
+ @user.friends << @friend1
112
+ @user.save!
113
+
114
+ friend1_new_instance = User.find(@friend1.key)
115
+ friend1_new_instance.email = 'new-address@ripple.com'
116
+ friend1_new_instance.save!
117
+
118
+ @user.reload
119
+ @user.friends.map(&:email).should == ['new-address@ripple.com']
120
+ end
121
+
122
+ it "allows and autosaves transitive linked associations" do
123
+ friend = User.new(:email => 'user-friend@example.com')
124
+ friend.key = 'main-user-friend'
125
+ @user.key = 'main-user'
126
+ @user.friends << friend
127
+ friend.friends << @user
128
+
129
+ @user.save! # should save both since friend is new
130
+
131
+ found_user = User.find!(@user.key)
132
+ found_friend = User.find!(friend.key)
133
+
134
+ found_user.friends.should == [found_friend]
135
+ found_friend.friends.should == [found_user]
136
+ end
137
+
138
+ it "should find the object associated by key after saving" do
139
+ @user.key = 'paying-user'
140
+ @user.credit_card = @cc
141
+ @user.save && @cc.save
142
+ @found = User.find(@user.key)
143
+ @found.reload
144
+ @found.credit_card.should eq(@cc)
145
+ end
146
+
147
+ it "should assign the generated riak key to the associated object using key" do
148
+ @user.key.should be_nil
149
+ @user.credit_card = @cc
150
+ @user.save
151
+ @cc.key.should_not be_blank
152
+ @cc.key.should eq(@user.key)
153
+ end
154
+
155
+ it "should save one association by storing key" do
156
+ @user.save!
157
+ @post.user = @user
158
+ @post.save!
159
+ @post.user_key.should == @user.key
160
+ @found = Post.find(@post.key)
161
+ @found.user.email.should == 'riak@ripple.com'
162
+ @found.user.should be_a(User)
163
+ end
164
+ end
@@ -0,0 +1,329 @@
1
+ require 'spec_helper'
2
+
3
+ describe "Ripple conflict resolution", :integration => true do
4
+ class ConflictedPerson
5
+ include Ripple::Document
6
+
7
+ property :name, String
8
+ property :age, Integer, :numericality => { :greater_than => 0, :allow_nil => true }
9
+ property :gender, String
10
+ property :favorite_colors, Set, :default => lambda { Set.new }
11
+ property :created_at, DateTime
12
+ property :updated_at, DateTime
13
+ property :coworker_keys, Array
14
+ property :mother_key, String
15
+ key_on :name
16
+
17
+ # embedded
18
+ one :address, :class_name => 'ConflictedAddress'
19
+ many :jobs, :class_name => 'ConflictedJob'
20
+
21
+ # linked
22
+ one :spouse, :class_name => 'ConflictedPerson'
23
+ many :friends, :class_name => 'ConflictedPerson'
24
+
25
+ #stored_key
26
+ one :mother, :using => :stored_key, :class_name => 'ConflictedPerson'
27
+ many :coworkers, :using => :stored_key, :class_name => 'ConflictedPerson'
28
+ end
29
+
30
+ class ConflictedAddress
31
+ include Ripple::EmbeddedDocument
32
+ property :city, String
33
+ end
34
+
35
+ class ConflictedJob
36
+ include Ripple::EmbeddedDocument
37
+ property :title, String
38
+ end
39
+
40
+ before(:each) do
41
+ ConflictedPerson.bucket.allow_mult ||= true
42
+ ConflictedPerson.on_conflict { } # reset to no-op
43
+ end
44
+
45
+ context 'when there is no conflict' do
46
+ it 'does not invoke the on_conflict hook' do
47
+ ConflictedPerson.on_conflict { raise "This conflict hook should not be invoked" }
48
+
49
+ # no errors should be raised by the hook above
50
+ ConflictedPerson.find('Noone')
51
+ ConflictedPerson.create!(:name => 'John')
52
+ ConflictedPerson.find('John').should_not be_nil
53
+ end
54
+ end
55
+
56
+ let(:created_at) { DateTime.new(2011, 5, 12, 8, 30, 0) }
57
+ let(:updated_at) { DateTime.new(2011, 5, 12, 8, 30, 0) }
58
+
59
+ let(:original_person) do
60
+ ConflictedPerson.create!(
61
+ :name => 'John',
62
+ :age => 25,
63
+ :gender => 'male',
64
+ :favorite_colors => ['green'],
65
+ :address => ConflictedAddress.new(:city => 'Seattle'),
66
+ :jobs => [ConflictedJob.new(:title => 'Engineer')],
67
+ :spouse => ConflictedPerson.create!(:name => 'Jill', :gender => 'female'),
68
+ :friends => [ConflictedPerson.create!(:name => 'Quinn', :gender => 'male')],
69
+ :coworkers => [ConflictedPerson.create!(:name => 'Horace', :gender => 'male')],
70
+ :mother => ConflictedPerson.create!(:name => 'Serena', :gender => 'female'),
71
+ :created_at => created_at,
72
+ :updated_at => updated_at
73
+ )
74
+ end
75
+
76
+ context 'for a document that has a deleted sibling' do
77
+ before(:each) do
78
+ create_conflict original_person,
79
+ lambda { |p| p.destroy! },
80
+ lambda { |p| p.age = 20 },
81
+ lambda { |p| p.age = 30 }
82
+ end
83
+
84
+ it 'indicates that one of the siblings was deleted' do
85
+ siblings = nil
86
+ ConflictedPerson.on_conflict { |s, c| siblings = s }
87
+ ConflictedPerson.find('John')
88
+
89
+ siblings.should have(3).sibling_records
90
+ deleted, undeleted = siblings.partition(&:deleted?)
91
+ deleted.should have(1).record
92
+ undeleted.should have(2).records
93
+ deleted = deleted.first
94
+
95
+ # the deleted record should be totally blank except for the name (since it is the key)
96
+ deleted.attributes.reject { |k, v| v.blank? }.should == {"name" => "John"}
97
+ end
98
+
99
+ it 'does not consider the deleted sibling when trying basic resolution of attributes that siblings are in agreement about' do
100
+ record = conflicts = nil
101
+ ConflictedPerson.on_conflict { |s, c| conflicts = c; record = self }
102
+ ConflictedPerson.find('John')
103
+
104
+ conflicts.should == [:age]
105
+ record.gender.should == 'male'
106
+ record.favorite_colors.should == ['green'].to_set
107
+ end
108
+ end
109
+
110
+ context 'for a document that has conflicted attributes' do
111
+ let(:most_recent_updated_at) { DateTime.new(2011, 6, 4, 12, 30) }
112
+ let(:earliest_created_at) { DateTime.new(2010, 5, 3, 12, 30) }
113
+
114
+ before(:each) do
115
+ create_conflict original_person,
116
+ lambda { |p| p.age = 20; p.created_at = earliest_created_at },
117
+ lambda { |p| p.age = 30; p.updated_at = most_recent_updated_at },
118
+ lambda { |p| p.favorite_colors << 'red' }
119
+ end
120
+
121
+ it 'raises a NotImplementedError when there is no on_conflict handler' do
122
+ ConflictedPerson.instance_variable_get(:@on_conflict_block).should_not be_nil
123
+ ConflictedPerson.instance_variable_set(:@on_conflict_block, nil)
124
+
125
+ expect {
126
+ ConflictedPerson.find('John')
127
+ }.to raise_error(NotImplementedError)
128
+ end
129
+
130
+ it 'invokes the on_conflict block with the siblings and the list of conflicted attributes' do
131
+ siblings = conflicts = nil
132
+ ConflictedPerson.on_conflict { |s, c| siblings = s; conflicts = c }
133
+ ConflictedPerson.find('John')
134
+
135
+ siblings.should have(3).sibling_records
136
+ siblings.map(&:class).uniq.should == [ConflictedPerson]
137
+ siblings.map(&:age).should =~ [20, 25, 30]
138
+
139
+ conflicts.should =~ [:age, :favorite_colors]
140
+ end
141
+
142
+ it 'automatically resolves any attributes that are in agreement among all siblings' do
143
+ record = nil
144
+ ConflictedPerson.on_conflict { record = self }
145
+ ConflictedPerson.find('John')
146
+ record.name.should == 'John'
147
+ record.gender.should == 'male'
148
+ end
149
+
150
+ it 'automatically resolves updated_at to the most recent timestamp' do
151
+ record = nil
152
+ ConflictedPerson.on_conflict { record = self }
153
+ ConflictedPerson.find('John')
154
+ record.updated_at.should == most_recent_updated_at
155
+ end
156
+
157
+ it 'automatically resolves created_at to the earliest timestamp' do
158
+ record = nil
159
+ ConflictedPerson.on_conflict { record = self }
160
+ ConflictedPerson.find('John')
161
+ record.created_at.should == earliest_created_at
162
+ end
163
+
164
+ it 'automatically sets conflicted attributes to their default values' do
165
+ record = nil
166
+ ConflictedPerson.on_conflict { record = self }
167
+ ConflictedPerson.find('John')
168
+ record.age.should be_nil
169
+ record.favorite_colors.should == Set.new
170
+ end
171
+
172
+ it 'returns the resolved record with the changes made by the on_conflict hook' do
173
+ ConflictedPerson.on_conflict do |siblings, _|
174
+ self.age = siblings.map(&:age).inject(&:+)
175
+ end
176
+
177
+ person = ConflictedPerson.find('John')
178
+ person.age.should == (20 + 25 + 30)
179
+ end
180
+
181
+ it "reloads a document with conflicts" do
182
+ record = original_person.reload
183
+ record.updated_at.should == most_recent_updated_at
184
+ end
185
+
186
+ context 'when .on_conflict is given a list of attributes' do
187
+ it 'raises an error if attributes not mentioned in the list are in conflict' do
188
+ ConflictedPerson.on_conflict(:age) { }
189
+ expect {
190
+ ConflictedPerson.find('John')
191
+ }.to raise_error(NotImplementedError) # since favorite_colors is also in conflict
192
+ end
193
+
194
+ it 'does not raise an error if all conflicted attributes are in the list' do
195
+ ConflictedPerson.on_conflict(:age, :favorite_colors) { }
196
+ ConflictedPerson.find('John')
197
+ end
198
+ end
199
+ end
200
+
201
+ context 'when there are conflicts on a one embedded association' do
202
+ before(:each) do
203
+ create_conflict original_person,
204
+ lambda { |p| p.address.city = 'San Francisco' },
205
+ lambda { |p| p.address.city = 'Portland' }
206
+ end
207
+
208
+ it 'sets the association to nil and includes its name in the list of conflicts passed to the on_conflict block' do
209
+ siblings = conflicts = record = nil
210
+ ConflictedPerson.on_conflict { |*a| siblings, conflicts = *a; record = self }
211
+ ConflictedPerson.find('John')
212
+ record.address.should be_nil
213
+ conflicts.should == [:address]
214
+ siblings.map { |s| s.address.city }.should =~ ['Portland', 'San Francisco']
215
+ end
216
+ end
217
+
218
+ context 'when there are conflicts on a many embedded association' do
219
+ before(:each) do
220
+ create_conflict original_person,
221
+ lambda { |p| p.jobs << ConflictedJob.new(:title => 'CEO') },
222
+ lambda { |p| p.jobs << ConflictedJob.new(:title => 'CTO') }
223
+ end
224
+
225
+ it 'sets the association to an empty array and includes its name in the list of conflicts passed to the on_conflict block' do
226
+ siblings = conflicts = record = nil
227
+ ConflictedPerson.on_conflict { |*a| siblings, conflicts = *a; record = self }
228
+ ConflictedPerson.find('John')
229
+ record.jobs.should == []
230
+ conflicts.should == [:jobs]
231
+ siblings.map { |s| s.jobs.map(&:title) }.should =~ [["Engineer", "CEO"], ["Engineer", "CTO"]]
232
+ end
233
+ end
234
+
235
+ context 'when there are conflicts on a one linked association' do
236
+ before(:each) do
237
+ create_conflict original_person,
238
+ lambda { |p| p.spouse = ConflictedPerson.create!(:name => 'Renee', :gender => 'female') },
239
+ lambda { |p| p.spouse = ConflictedPerson.create!(:name => 'Sharon', :gender => 'female') }
240
+ end
241
+
242
+ it 'sets the association to nil and includes its name in the list of conflicts passed to the on_conflict block' do
243
+ record_spouse = conflicts = sibling_spouse_names = nil
244
+
245
+ ConflictedPerson.on_conflict do |siblings, c|
246
+ record_spouse = spouse
247
+ conflicts = c
248
+ sibling_spouse_names = siblings.map { |s| s.spouse.name }
249
+ end
250
+
251
+ ConflictedPerson.find('John')
252
+ record_spouse.should be_nil
253
+ conflicts.should == [:spouse]
254
+ sibling_spouse_names.should =~ %w[ Sharon Renee ]
255
+ end
256
+ end
257
+
258
+ context 'when there are conflicts on a many linked association' do
259
+ before(:each) do
260
+ create_conflict original_person,
261
+ lambda { |p| p.friends << ConflictedPerson.new(:name => 'Luna', :gender => 'female') },
262
+ lambda { |p| p.friends << ConflictedPerson.new(:name => 'Molly', :gender => 'female') }
263
+ end
264
+
265
+ it 'sets the association to a blank array and includes its name in the list of conflicts passed to the on_conflict block' do
266
+ record_friends = conflicts = sibling_friend_names = nil
267
+
268
+ ConflictedPerson.on_conflict do |siblings, c|
269
+ record_friends = friends
270
+ conflicts = c
271
+ sibling_friend_names = siblings.map { |s| s.friends.map(&:name) }
272
+ end
273
+
274
+ ConflictedPerson.find('John')
275
+ record_friends.should == []
276
+ conflicts.should == [:friends]
277
+ sibling_friend_names.map(&:sort).should =~ [['Luna', 'Quinn'], ['Molly', 'Quinn']]
278
+ end
279
+ end
280
+
281
+ context 'when there are conflicts on a many stored_key association' do
282
+ before(:each) do
283
+ create_conflict original_person,
284
+ lambda { |p| p.coworkers << ConflictedPerson.create!(:name => 'Colleen', :gender => 'female') },
285
+ lambda { |p| p.coworkers = [ ConflictedPerson.create!(:name => 'Russ', :gender => 'male'),
286
+ ConflictedPerson.create!(:name => 'Denise', :gender => 'female') ] }
287
+ end
288
+
289
+ it 'sets the association to a blank array and includes the owner_keys in the list of conflicts passed to the on_conflict block' do
290
+ record_coworkers = record_coworker_keys = conflicts = sibling_coworker_keys = nil
291
+
292
+ ConflictedPerson.on_conflict do |siblings, c|
293
+ record_coworkers = coworkers
294
+ record_coworker_keys = coworker_keys
295
+ conflicts = c
296
+ sibling_coworker_keys = siblings.map { |s| s.coworker_keys }
297
+ end
298
+
299
+ ConflictedPerson.find('John')
300
+ record_coworker_keys.should == []
301
+ record_coworkers.should == []
302
+ conflicts.should == [:coworker_keys]
303
+ sibling_coworker_keys.map(&:sort).should =~ [['Colleen', 'Horace'], ['Denise', 'Russ']]
304
+ end
305
+ end
306
+
307
+ context 'when there are conflicts on a one stored_key association' do
308
+ before(:each) do
309
+ create_conflict original_person,
310
+ lambda { |p| p.mother = ConflictedPerson.new(:name => 'Nancy', :gender => 'female') },
311
+ lambda { |p| p.mother = ConflictedPerson.new(:name => 'Sherry', :gender => 'male') }
312
+ end
313
+
314
+ it 'sets the association to nil and includes its name in the list of conflicts passed to the on_conflict block' do
315
+ record_mother = conflicts = sibling_mother_keys = nil
316
+
317
+ ConflictedPerson.on_conflict do |siblings, c|
318
+ record_mother = mother
319
+ conflicts = c
320
+ sibling_mother_keys = siblings.map { |s| s.mother_key }
321
+ end
322
+
323
+ ConflictedPerson.find('John')
324
+ record_mother.should be_nil
325
+ conflicts.should == [:mother_key]
326
+ sibling_mother_keys.sort.should == %w(Nancy Sherry)
327
+ end
328
+ end
329
+ end