ripple 0.7.1 → 0.8.0.beta

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. data/Rakefile +5 -4
  2. data/lib/rails/generators/ripple/model/model_generator.rb +20 -0
  3. data/lib/rails/generators/ripple/model/templates/model.rb +10 -0
  4. data/lib/rails/generators/ripple_generator.rb +60 -0
  5. data/lib/ripple.rb +24 -6
  6. data/lib/ripple/associations.rb +70 -3
  7. data/lib/ripple/associations/linked.rb +17 -5
  8. data/lib/ripple/associations/many.rb +5 -4
  9. data/lib/ripple/associations/many_embedded_proxy.rb +2 -0
  10. data/lib/ripple/associations/many_linked_proxy.rb +35 -0
  11. data/lib/ripple/associations/one.rb +4 -0
  12. data/lib/ripple/associations/one_embedded_proxy.rb +1 -1
  13. data/lib/ripple/associations/one_linked_proxy.rb +29 -0
  14. data/lib/ripple/attribute_methods/dirty.rb +2 -2
  15. data/lib/ripple/core_ext.rb +1 -0
  16. data/lib/ripple/core_ext/casting.rb +6 -8
  17. data/lib/ripple/document.rb +1 -0
  18. data/lib/ripple/document/bucket_access.rb +1 -1
  19. data/lib/ripple/document/finders.rb +1 -1
  20. data/lib/ripple/document/persistence.rb +28 -8
  21. data/lib/ripple/embedded_document.rb +1 -0
  22. data/lib/ripple/embedded_document/persistence.rb +11 -1
  23. data/lib/ripple/inspection.rb +26 -0
  24. data/lib/ripple/locale/en.yml +8 -5
  25. data/lib/ripple/railtie.rb +2 -12
  26. data/lib/ripple/validations.rb +5 -0
  27. data/lib/ripple/validations/associated_validator.rb +1 -2
  28. data/spec/fixtures/config.yml +6 -1
  29. data/spec/integration/ripple/associations_spec.rb +21 -0
  30. data/spec/ripple/associations/many_embedded_proxy_spec.rb +6 -0
  31. data/spec/ripple/associations/many_linked_proxy_spec.rb +103 -0
  32. data/spec/ripple/associations/one_embedded_proxy_spec.rb +5 -0
  33. data/spec/ripple/associations/one_linked_proxy_spec.rb +76 -0
  34. data/spec/ripple/associations/proxy_spec.rb +1 -1
  35. data/spec/ripple/bucket_access_spec.rb +3 -4
  36. data/spec/ripple/core_ext_spec.rb +11 -0
  37. data/spec/ripple/embedded_document/persistence_spec.rb +12 -0
  38. data/spec/ripple/finders_spec.rb +1 -1
  39. data/spec/ripple/inspection_spec.rb +48 -0
  40. data/spec/ripple/persistence_spec.rb +52 -6
  41. data/spec/ripple/properties_spec.rb +6 -0
  42. data/spec/ripple/ripple_spec.rb +12 -1
  43. data/spec/support/models/tasks.rb +13 -0
  44. data/spec/support/models/widget.rb +5 -1
  45. metadata +43 -18
@@ -0,0 +1,29 @@
1
+ # Copyright 2010 Sean Cribbs, Sonian Inc., and Basho Technologies, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ require 'ripple'
15
+
16
+ module Ripple
17
+ module Associations
18
+ class OneLinkedProxy < Proxy
19
+ include One
20
+ include Linked
21
+
22
+ protected
23
+ def find_target
24
+ return nil if links.blank?
25
+ klass.send(:instantiate,robjects.first)
26
+ end
27
+ end
28
+ end
29
+ end
@@ -20,7 +20,7 @@ module Ripple
20
20
  include ActiveModel::Dirty
21
21
 
22
22
  # @private
23
- def save
23
+ def save(*args)
24
24
  if result = super
25
25
  changed_attributes.clear
26
26
  end
@@ -29,7 +29,7 @@ module Ripple
29
29
 
30
30
  # @private
31
31
  def reload
32
- returning super do
32
+ super.tap do
33
33
  changed_attributes.clear
34
34
  end
35
35
  end
@@ -0,0 +1 @@
1
+ require 'ripple/core_ext/casting'
@@ -57,13 +57,15 @@ end
57
57
  # @private
58
58
  class String
59
59
  def self.ripple_cast(value)
60
+ return nil if value.nil?
60
61
  value.respond_to?(:to_s) && value.to_s or raise Ripple::PropertyTypeMismatch.new(self, value)
61
62
  end
62
63
  end
63
64
 
64
65
  # Stand-in for true/false property types.
65
- module Boolean
66
- def self.ripple_cast(value)
66
+ module ::Boolean
67
+ extend self
68
+ def ripple_cast(value)
67
69
  case value
68
70
  when NilClass
69
71
  nil
@@ -83,16 +85,12 @@ end
83
85
 
84
86
  # @private
85
87
  class TrueClass
86
- def self.ripple_cast(value)
87
- Boolean.ripple_cast(value)
88
- end
88
+ extend Boolean
89
89
  end
90
90
 
91
91
  # @private
92
92
  class FalseClass
93
- def self.ripple_cast(value)
94
- Boolean.ripple_cast(value)
95
- end
93
+ extend Boolean
96
94
  end
97
95
 
98
96
  # @private
@@ -58,6 +58,7 @@ module Ripple
58
58
  include Ripple::Callbacks
59
59
  include Ripple::Conversion
60
60
  include Ripple::Document::Finders
61
+ include Ripple::Inspection
61
62
  end
62
63
 
63
64
  module ClassMethods
@@ -25,7 +25,7 @@ module Ripple
25
25
 
26
26
  # @return [Riak::Bucket] The bucket assigned to this class.
27
27
  def bucket
28
- Ripple.client[bucket_name, {:keys => false}]
28
+ Riak::Bucket.new(Ripple.client, bucket_name)
29
29
  end
30
30
 
31
31
  # Set the bucket name for this class and its subclasses.
@@ -110,7 +110,7 @@ module Ripple
110
110
 
111
111
  private
112
112
  def find_one(key)
113
- instantiate(bucket.get(key))
113
+ instantiate(bucket.get(key, quorums.slice(:r)))
114
114
  rescue Riak::FailedRequest => fr
115
115
  return nil if fr.code.to_i == 404
116
116
  raise fr
@@ -18,9 +18,9 @@ module Ripple
18
18
  module Persistence
19
19
  extend ActiveSupport::Concern
20
20
  extend ActiveSupport::Autoload
21
-
21
+
22
22
  module ClassMethods
23
-
23
+
24
24
  # Instantiates a new record, applies attributes from a block, and saves it
25
25
  def create(attrs={}, &block)
26
26
  new(attrs, &block).tap {|s| s.save}
@@ -31,7 +31,13 @@ module Ripple
31
31
  def destroy_all
32
32
  all(&:destroy)
33
33
  end
34
-
34
+
35
+ attr_writer :quorums
36
+ alias_method "set_quorums", "quorums="
37
+
38
+ def quorums
39
+ @quorums ||= {}
40
+ end
35
41
  end
36
42
 
37
43
  module InstanceMethods
@@ -46,12 +52,27 @@ module Ripple
46
52
  @new || false
47
53
  end
48
54
 
55
+ # Updates a single attribute and then saves the document
56
+ # NOTE: THIS SKIPS VALIDATIONS! Use with caution.
57
+ # @return [true,false] whether the document succeeded in saving
58
+ def update_attribute(attribute, value)
59
+ send("#{attribute}=", value)
60
+ save(:validate => false)
61
+ end
62
+
63
+ # Writes new attributes and then saves the document
64
+ # @return [true,false] whether the document succeeded in saving
65
+ def update_attributes(attrs)
66
+ self.attributes = attrs
67
+ save
68
+ end
69
+
49
70
  # Saves the document in Riak.
50
71
  # @return [true,false] whether the document succeeded in saving
51
- def save
72
+ def save(*args)
52
73
  robject.key = key if robject.key != key
53
74
  robject.data = attributes_for_persistence
54
- robject.store
75
+ robject.store(self.class.quorums.slice(:w,:dw))
55
76
  self.key = robject.key
56
77
  @new = false
57
78
  true
@@ -70,7 +91,7 @@ module Ripple
70
91
 
71
92
  # Deletes the document from Riak and freezes this instance
72
93
  def destroy
73
- robject.delete unless new?
94
+ robject.delete(self.class.quorums.slice(:rw)) unless new?
74
95
  freeze
75
96
  true
76
97
  rescue Riak::FailedRequest
@@ -81,8 +102,7 @@ module Ripple
81
102
  def freeze
82
103
  @attributes.freeze; super
83
104
  end
84
-
85
- protected
105
+
86
106
  attr_writer :robject
87
107
 
88
108
  def robject
@@ -34,6 +34,7 @@ module Ripple
34
34
  include Ripple::Callbacks
35
35
  include Ripple::Conversion
36
36
  include Finders
37
+ include Ripple::Inspection
37
38
  end
38
39
 
39
40
  module ClassMethods
@@ -42,6 +42,16 @@ module Ripple
42
42
  end
43
43
  end
44
44
 
45
+ def update_attributes(attrs)
46
+ self.attributes = attrs
47
+ save
48
+ end
49
+
50
+ def update_attribute(attribute, value)
51
+ send("#{attribute}=", value)
52
+ save(:validate => false)
53
+ end
54
+
45
55
  def save(*args)
46
56
  if _root_document
47
57
  _root_document.save(*args)
@@ -57,7 +67,7 @@ module Ripple
57
67
  def _root_document
58
68
  @_parent_document.try(:_root_document)
59
69
  end
60
-
70
+
61
71
  def _parent_document=(value)
62
72
  @_parent_document = value
63
73
  end
@@ -0,0 +1,26 @@
1
+ # Copyright 2010 Sean Cribbs, Sonian Inc., and Basho Technologies, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require 'ripple'
16
+
17
+ module Ripple
18
+ # Makes IRB and other inspect output a bit friendlier
19
+ module Inspection
20
+ def inspect
21
+ attribute_list = attributes_for_persistence.except("_type").map {|k,v| "#{k}=#{v.inspect}" }.join(' ')
22
+ identifier = self.class.embeddable? ? "" : ":#{key || '[new]'}"
23
+ "<#{self.class.name}#{identifier} #{attribute_list}>"
24
+ end
25
+ end
26
+ end
@@ -13,11 +13,14 @@
13
13
  # limitations under the License.
14
14
  en:
15
15
  ripple:
16
- property_type_mismatch: "Cannot cast {{value}} into a {{class}}"
17
16
  attribute_hash: "value of attributes must be a Hash"
18
- document_invalid: "Validation failed: {{errors}}"
17
+ document_invalid: "Validation failed: %{errors}"
19
18
  document_not_found:
20
19
  no_key: "Couldn't find document without a key"
21
- one_key: "Couldn't find document with key: {{key}}"
22
- many_keys: "Couldn't find documents with keys: {{keys}}"
23
- no_root_document: "You cannot call {{method}} on {{doc}} without a root document"
20
+ one_key: "Couldn't find document with key: %{key}"
21
+ many_keys: "Couldn't find documents with keys: %{keys}"
22
+ invalid_association_value: "Invalid value %{value} for association %{name} of type %{klass} on %{owner}"
23
+ missing_configuration: "You are missing your ripple configuration file that should be at %{file}"
24
+ no_root_document: "You cannot call %{method} on %{doc} without a root document"
25
+ property_type_mismatch: "Cannot cast %{value} into a %{class}"
26
+
@@ -12,21 +12,11 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
  require 'ripple'
15
- require 'rails'
16
15
 
17
- # require in gemfile using
18
- # <tt>gem "ripple", :require_as => ["ripple", "ripple/railtie"]</tt>
19
16
  module Ripple
20
17
  class Railtie < Rails::Railtie
21
- railtie_name :ripple
22
-
23
18
  initializer "ripple.configure_rails_initialization" do
24
- Ripple.load_configuration
19
+ Ripple.load_configuration Rails.root.join('config', 'ripple.yml'), [Rails.env]
25
20
  end
26
21
  end
27
-
28
- def self.load_configuration
29
- config_file = Rails.root.join('config', 'database.yml')
30
- self.config = YAML.load_file(File.expand_path config_file).with_indifferent_access[:ripple][Rails.env]
31
- end
32
- end
22
+ end
@@ -64,6 +64,11 @@ module Ripple
64
64
  def save!
65
65
  (raise Ripple::DocumentInvalid.new(self) unless save) || true
66
66
  end
67
+
68
+ def update_attributes!(attrs)
69
+ self.attributes = attrs
70
+ save!
71
+ end
67
72
  end
68
73
  end
69
74
  end
@@ -14,14 +14,13 @@
14
14
  #
15
15
  # Taken from ActiveRecord::Validations::AssociatedValidators
16
16
  #
17
-
18
17
  require 'ripple'
19
18
 
20
19
  module Ripple
21
20
  module Validations
22
21
  class AssociatedValidator < ActiveModel::EachValidator
23
22
  def validate_each(record, attribute, value)
24
- unless Array(value).all? {|r| r.nil? || r.valid? }
23
+ unless Array(value).map {|r| r.nil? || r.valid? }.all?
25
24
  record.errors.add(attribute, :invalid, :default => options[:message], :value => value)
26
25
  end
27
26
  end
@@ -1,3 +1,8 @@
1
1
  ripple:
2
2
  port: 9000
3
- host: localhost
3
+ host: localhost
4
+
5
+ ripple_rails:
6
+ development:
7
+ port: 9001
8
+ host: 127.0.0.1
@@ -21,6 +21,8 @@ describe "Ripple Associations" do
21
21
  one :profile
22
22
  many :addresses
23
23
  property :email, String, :presence => true
24
+ many :friends, :class_name => "User"
25
+ one :emergency_contact, :class_name => "User"
24
26
  end
25
27
  class Profile
26
28
  include Ripple::EmbeddedDocument
@@ -41,6 +43,8 @@ describe "Ripple Associations" do
41
43
  @profile = Profile.new(:name => 'Ripple')
42
44
  @billing = Address.new(:street => '123 Somewhere Dr', :kind => 'billing')
43
45
  @shipping = Address.new(:street => '321 Anywhere Pl', :kind => 'shipping')
46
+ @friend1 = User.create(:email => "friend@ripple.com")
47
+ @friend2 = User.create(:email => "friend2@ripple.com")
44
48
  end
45
49
 
46
50
  it "should save one embedded associations" do
@@ -66,6 +70,23 @@ describe "Ripple Associations" do
66
70
  @bill.should be_a(Address)
67
71
  @ship.should be_a(Address)
68
72
  end
73
+
74
+ it "should save a many linked association" do
75
+ @user.friends << @friend1 << @friend2
76
+ @user.save
77
+ @user.should_not be_new_record
78
+ @found = User.find(@user.key)
79
+ @found.friends.map(&:key).should include(@friend1.key)
80
+ @found.friends.map(&:key).should include(@friend2.key)
81
+ end
82
+
83
+ it "should save a one linked association" do
84
+ @user.emergency_contact = @friend1
85
+ @user.save
86
+ @user.should_not be_new_record
87
+ @found = User.find(@user.key)
88
+ @found.emergency_contact.key.should == @friend1.key
89
+ end
69
90
 
70
91
  after :each do
71
92
  User.destroy_all
@@ -121,4 +121,10 @@ describe Ripple::Associations::ManyEmbeddedProxy do
121
121
  @user.addresses << @address
122
122
  @user.addresses.to_ary.should == [@address]
123
123
  end
124
+
125
+ it "should refuse assigning documents of the wrong type" do
126
+ lambda { @user.addresses = nil }.should raise_error
127
+ lambda { @user.addresses = @address }.should raise_error
128
+ lambda { @user.addresses = [@note] }.should raise_error
129
+ end
124
130
  end
@@ -0,0 +1,103 @@
1
+ # Copyright 2010 Sean Cribbs, Sonian Inc., and Basho Technologies, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ require File.expand_path("../../../spec_helper", __FILE__)
15
+
16
+ describe Ripple::Associations::ManyLinkedProxy do
17
+ require 'support/models/tasks'
18
+
19
+ before :each do
20
+ @person = Person.new {|p| p.key = "riak-user" }
21
+ @task = Task.new {|t| t.key = "one" }
22
+ @other_task = Task.new {|t| t.key = "two" }
23
+ [@person, @task, @other_task].each do |doc|
24
+ doc.stub!(:new?).and_return(false)
25
+ end
26
+ end
27
+
28
+ it "should be empty before any associated documents are set" do
29
+ @person.tasks.should be_empty
30
+ end
31
+
32
+ it "should accept an array of documents" do
33
+ @person.tasks = [@task]
34
+ end
35
+
36
+ it "should set the links on the RObject when assigning" do
37
+ @person.tasks = [@task]
38
+ @person.robject.links.should include(@task.robject.to_link("tasks"))
39
+ end
40
+
41
+ it "should return the assigned documents when assigning" do
42
+ t = (@person.tasks = [@task])
43
+ t.should == [@task]
44
+ end
45
+
46
+ it "should save unsaved documents when assigning" do
47
+ @task.should_receive(:new?).and_return(true)
48
+ @task.should_receive(:save).and_return(true)
49
+ @person.tasks = [@task]
50
+ end
51
+
52
+ it "should link-walk to the associated documents when accessing" do
53
+ @person.robject.links << @task.robject.to_link("tasks")
54
+ @person.robject.should_receive(:walk).with(Riak::WalkSpec.new(:bucket => "tasks", :tag => "tasks")).and_return([])
55
+ @person.tasks.should == []
56
+ end
57
+
58
+ it "should replace associated documents with a new set" do
59
+ @person.tasks = [@task]
60
+ @person.tasks = [@other_task]
61
+ @person.tasks.should == [@other_task]
62
+ end
63
+
64
+ it "should be able to append documents to the associated set" do
65
+ @person.tasks << @task
66
+ @person.tasks << @other_task
67
+ @person.should have(2).tasks
68
+ end
69
+
70
+ it "should be able to chain calls to adding documents" do
71
+ @person.tasks << @task << @other_task
72
+ @person.should have(2).tasks
73
+ end
74
+
75
+ it "should set the links on the RObject when appending" do
76
+ @person.tasks << @task << @other_task
77
+ [@task, @other_task].each do |t|
78
+ @person.robject.links.should include(t.robject.to_link("tasks"))
79
+ end
80
+ end
81
+
82
+ it "should be able to count the associated documents" do
83
+ @person.tasks << @task
84
+ @person.tasks.count.should == 1
85
+ @person.tasks << @other_task
86
+ @person.tasks.count.should == 2
87
+ end
88
+
89
+ # it "should be able to build a new associated document" do
90
+ # pending "Need unsaved document support"
91
+ # end
92
+
93
+ it "should return an array from to_ary" do
94
+ @person.tasks << @task
95
+ @person.tasks.to_ary.should == [@task]
96
+ end
97
+
98
+ it "should refuse assigning a collection of the wrong type" do
99
+ lambda { @person.tasks = nil }.should raise_error
100
+ lambda { @person.tasks = @task }.should raise_error
101
+ lambda { @person.tasks = [@person] }.should raise_error
102
+ end
103
+ end