wyngle-ripple 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +35 -0
- data/Gemfile +17 -0
- data/Gemfile.lock +133 -0
- data/Gemfile.rails30 +3 -0
- data/Gemfile.rails31 +3 -0
- data/Gemfile.rails32 +3 -0
- data/Guardfile +17 -0
- data/LICENSE +16 -0
- data/README.markdown +166 -0
- data/RELEASE_NOTES.textile +286 -0
- data/Rakefile +63 -0
- data/lib/rails/generators/ripple/configuration/configuration_generator.rb +13 -0
- data/lib/rails/generators/ripple/configuration/templates/ripple.yml +25 -0
- data/lib/rails/generators/ripple/js/js_generator.rb +13 -0
- data/lib/rails/generators/ripple/js/templates/js/contrib.js +63 -0
- data/lib/rails/generators/ripple/js/templates/js/iso8601.js +76 -0
- data/lib/rails/generators/ripple/js/templates/js/ripple.js +132 -0
- data/lib/rails/generators/ripple/model/model_generator.rb +20 -0
- data/lib/rails/generators/ripple/model/templates/model.rb.erb +10 -0
- data/lib/rails/generators/ripple/observer/observer_generator.rb +16 -0
- data/lib/rails/generators/ripple/observer/templates/observer.rb.erb +2 -0
- data/lib/rails/generators/ripple/test/templates/cucumber.rb.erb +7 -0
- data/lib/rails/generators/ripple/test/test_generator.rb +45 -0
- data/lib/rails/generators/ripple_generator.rb +79 -0
- data/lib/ripple.rb +86 -0
- data/lib/ripple/associations.rb +380 -0
- data/lib/ripple/associations/embedded.rb +35 -0
- data/lib/ripple/associations/instantiators.rb +26 -0
- data/lib/ripple/associations/linked.rb +65 -0
- data/lib/ripple/associations/many.rb +38 -0
- data/lib/ripple/associations/many_embedded_proxy.rb +39 -0
- data/lib/ripple/associations/many_linked_proxy.rb +66 -0
- data/lib/ripple/associations/many_reference_proxy.rb +95 -0
- data/lib/ripple/associations/many_stored_key_proxy.rb +76 -0
- data/lib/ripple/associations/one.rb +20 -0
- data/lib/ripple/associations/one_embedded_proxy.rb +35 -0
- data/lib/ripple/associations/one_key_proxy.rb +58 -0
- data/lib/ripple/associations/one_linked_proxy.rb +26 -0
- data/lib/ripple/associations/one_stored_key_proxy.rb +43 -0
- data/lib/ripple/associations/proxy.rb +118 -0
- data/lib/ripple/attribute_methods.rb +132 -0
- data/lib/ripple/attribute_methods/dirty.rb +59 -0
- data/lib/ripple/attribute_methods/query.rb +34 -0
- data/lib/ripple/attribute_methods/read.rb +28 -0
- data/lib/ripple/attribute_methods/write.rb +25 -0
- data/lib/ripple/callbacks.rb +71 -0
- data/lib/ripple/conflict/basic_resolver.rb +86 -0
- data/lib/ripple/conflict/document_hooks.rb +46 -0
- data/lib/ripple/conflict/resolver.rb +96 -0
- data/lib/ripple/conflict/test_helper.rb +34 -0
- data/lib/ripple/conversion.rb +29 -0
- data/lib/ripple/core_ext.rb +3 -0
- data/lib/ripple/core_ext/casting.rb +151 -0
- data/lib/ripple/core_ext/indexes.rb +89 -0
- data/lib/ripple/core_ext/object.rb +8 -0
- data/lib/ripple/document.rb +105 -0
- data/lib/ripple/document/bucket_access.rb +25 -0
- data/lib/ripple/document/finders.rb +131 -0
- data/lib/ripple/document/key.rb +35 -0
- data/lib/ripple/document/link.rb +30 -0
- data/lib/ripple/document/persistence.rb +130 -0
- data/lib/ripple/embedded_document.rb +63 -0
- data/lib/ripple/embedded_document/around_callbacks.rb +18 -0
- data/lib/ripple/embedded_document/finders.rb +26 -0
- data/lib/ripple/embedded_document/persistence.rb +75 -0
- data/lib/ripple/i18n.rb +5 -0
- data/lib/ripple/indexes.rb +151 -0
- data/lib/ripple/inspection.rb +32 -0
- data/lib/ripple/locale/en.yml +26 -0
- data/lib/ripple/locale/fr.yml +24 -0
- data/lib/ripple/nested_attributes.rb +275 -0
- data/lib/ripple/observable.rb +28 -0
- data/lib/ripple/properties.rb +74 -0
- data/lib/ripple/property_type_mismatch.rb +12 -0
- data/lib/ripple/railtie.rb +26 -0
- data/lib/ripple/railties/ripple.rake +68 -0
- data/lib/ripple/serialization.rb +82 -0
- data/lib/ripple/test_server.rb +35 -0
- data/lib/ripple/timestamps.rb +25 -0
- data/lib/ripple/translation.rb +18 -0
- data/lib/ripple/validations.rb +65 -0
- data/lib/ripple/validations/associated_validator.rb +43 -0
- data/lib/ripple/version.rb +3 -0
- data/ripple.gemspec +29 -0
- data/spec/fixtures/config.yml +8 -0
- data/spec/generators/ripple/configuration_generator_spec.rb +9 -0
- data/spec/generators/ripple/js_generator_spec.rb +14 -0
- data/spec/generators/ripple/model_generator_spec.rb +64 -0
- data/spec/generators/ripple/observer_generator_spec.rb +20 -0
- data/spec/generators/ripple/test_generator_spec.rb +118 -0
- data/spec/generators/ripple_generator_spec.rb +11 -0
- data/spec/integration/ripple/associations_spec.rb +164 -0
- data/spec/integration/ripple/conflict_resolution_spec.rb +329 -0
- data/spec/integration/ripple/indexes_spec.rb +47 -0
- data/spec/integration/ripple/nested_attributes_spec.rb +261 -0
- data/spec/integration/ripple/persistence_spec.rb +36 -0
- data/spec/integration/ripple/search_associations_spec.rb +31 -0
- data/spec/ripple/associations/many_embedded_proxy_spec.rb +119 -0
- data/spec/ripple/associations/many_linked_proxy_spec.rb +191 -0
- data/spec/ripple/associations/many_reference_proxy_spec.rb +170 -0
- data/spec/ripple/associations/many_stored_key_proxy_spec.rb +158 -0
- data/spec/ripple/associations/one_embedded_proxy_spec.rb +125 -0
- data/spec/ripple/associations/one_key_proxy_spec.rb +82 -0
- data/spec/ripple/associations/one_linked_proxy_spec.rb +91 -0
- data/spec/ripple/associations/one_stored_key_proxy_spec.rb +72 -0
- data/spec/ripple/associations/proxy_spec.rb +84 -0
- data/spec/ripple/associations_spec.rb +153 -0
- data/spec/ripple/attribute_methods/dirty_spec.rb +80 -0
- data/spec/ripple/attribute_methods_spec.rb +286 -0
- data/spec/ripple/bucket_access_spec.rb +25 -0
- data/spec/ripple/callbacks_spec.rb +195 -0
- data/spec/ripple/conflict/resolver_spec.rb +42 -0
- data/spec/ripple/conversion_spec.rb +14 -0
- data/spec/ripple/core_ext_spec.rb +181 -0
- data/spec/ripple/document/link_spec.rb +67 -0
- data/spec/ripple/document_spec.rb +96 -0
- data/spec/ripple/embedded_document/finders_spec.rb +29 -0
- data/spec/ripple/embedded_document/persistence_spec.rb +80 -0
- data/spec/ripple/embedded_document_spec.rb +84 -0
- data/spec/ripple/finders_spec.rb +220 -0
- data/spec/ripple/indexes_spec.rb +111 -0
- data/spec/ripple/inspection_spec.rb +51 -0
- data/spec/ripple/key_spec.rb +31 -0
- data/spec/ripple/observable_spec.rb +120 -0
- data/spec/ripple/persistence_spec.rb +351 -0
- data/spec/ripple/properties_spec.rb +262 -0
- data/spec/ripple/ripple_spec.rb +71 -0
- data/spec/ripple/serialization_spec.rb +51 -0
- data/spec/ripple/timestamps_spec.rb +83 -0
- data/spec/ripple/validations/associated_validator_spec.rb +77 -0
- data/spec/ripple/validations_spec.rb +102 -0
- data/spec/spec_helper.rb +39 -0
- data/spec/support/associations.rb +1 -0
- data/spec/support/associations/proxies.rb +17 -0
- data/spec/support/generator_setup.rb +26 -0
- data/spec/support/integration_setup.rb +11 -0
- data/spec/support/models.rb +35 -0
- data/spec/support/models/address.rb +10 -0
- data/spec/support/models/box.rb +13 -0
- data/spec/support/models/car.rb +41 -0
- data/spec/support/models/cardboard_box.rb +2 -0
- data/spec/support/models/clock.rb +10 -0
- data/spec/support/models/clock_observer.rb +2 -0
- data/spec/support/models/company.rb +23 -0
- data/spec/support/models/credit_card.rb +5 -0
- data/spec/support/models/customer.rb +3 -0
- data/spec/support/models/email.rb +3 -0
- data/spec/support/models/family.rb +16 -0
- data/spec/support/models/favorite.rb +3 -0
- data/spec/support/models/indexer.rb +26 -0
- data/spec/support/models/invoice.rb +6 -0
- data/spec/support/models/late_invoice.rb +2 -0
- data/spec/support/models/nested.rb +12 -0
- data/spec/support/models/ninja.rb +7 -0
- data/spec/support/models/note.rb +4 -0
- data/spec/support/models/page.rb +3 -0
- data/spec/support/models/paid_invoice.rb +3 -0
- data/spec/support/models/post.rb +13 -0
- data/spec/support/models/profile.rb +7 -0
- data/spec/support/models/subscription.rb +26 -0
- data/spec/support/models/tasks.rb +9 -0
- data/spec/support/models/team.rb +11 -0
- data/spec/support/models/transactions.rb +17 -0
- data/spec/support/models/tree.rb +3 -0
- data/spec/support/models/user.rb +19 -0
- data/spec/support/models/widget.rb +23 -0
- data/spec/support/search.rb +14 -0
- data/spec/support/test_server.rb +39 -0
- data/spec/support/test_server.yml.example +14 -0
- 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
|